first working POC

This commit is contained in:
Siyuan 2025-11-12 11:53:06 +00:00
parent ffc26ac4e7
commit bca9afd1d7
16 changed files with 4351 additions and 1834 deletions

View File

@ -13,14 +13,8 @@ import (
"github.com/erikdubbelboer/gspt"
"github.com/jetkvm/kvm"
)
const (
envChildID = "JETKVM_CHILD_ID"
envSubcomponent = "JETKVM_SUBCOMPONENT"
errorDumpDir = "/userdata/jetkvm/crashdump"
errorDumpLastFile = "last-crash.log"
errorDumpTemplate = "jetkvm-%s.log"
"github.com/jetkvm/kvm/internal/native"
"github.com/jetkvm/kvm/internal/supervisor"
)
var (
@ -28,14 +22,14 @@ var (
)
func program() {
subcomponentOverride := os.Getenv(envSubcomponent)
subcomponentOverride := os.Getenv(supervisor.EnvSubcomponent)
if subcomponentOverride != "" {
subcomponent = subcomponentOverride
}
switch subcomponent {
case "native":
gspt.SetProcTitle(os.Args[0] + " [native]")
kvm.RunNativeProcess()
native.RunNativeProcess(os.Args[0])
default:
gspt.SetProcTitle(os.Args[0] + " [app]")
kvm.Main()
@ -58,7 +52,7 @@ func main() {
return
}
childID := os.Getenv(envChildID)
childID := os.Getenv(supervisor.EnvChildID)
switch childID {
case "":
doSupervise()
@ -90,11 +84,11 @@ func supervise() error {
// run the child binary
cmd := exec.Command(binPath)
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile)
cmd.Env = append(os.Environ(), []string{
fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()),
fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath),
fmt.Sprintf("%s=%s", supervisor.EnvChildID, kvm.GetBuiltAppVersion()),
fmt.Sprintf("%s=%s", supervisor.ErrorDumpLastFile, lastFilePath),
}...)
cmd.Args = os.Args
@ -202,11 +196,11 @@ func renameFile(f *os.File, newName string) error {
func ensureErrorDumpDir() error {
// TODO: check if the directory is writable
f, err := os.Stat(errorDumpDir)
f, err := os.Stat(supervisor.ErrorDumpDir)
if err == nil && f.IsDir() {
return nil
}
if err := os.MkdirAll(errorDumpDir, 0755); err != nil {
if err := os.MkdirAll(supervisor.ErrorDumpDir, 0755); err != nil {
return fmt.Errorf("failed to create error dump directory: %w", err)
}
return nil
@ -216,7 +210,7 @@ func createErrorDump(logFile *os.File) {
fmt.Println()
fileName := fmt.Sprintf(
errorDumpTemplate,
supervisor.ErrorDumpTemplate,
time.Now().Format("20060102-150405"),
)
@ -226,7 +220,7 @@ func createErrorDump(logFile *os.File) {
return
}
filePath := filepath.Join(errorDumpDir, fileName)
filePath := filepath.Join(supervisor.ErrorDumpDir, fileName)
if err := renameFile(logFile, filePath); err != nil {
fmt.Printf("failed to rename file: %v\n", err)
return
@ -234,7 +228,7 @@ func createErrorDump(logFile *os.File) {
fmt.Printf("error dump copied: %s\n", filePath)
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile)
if err := ensureSymlink(filePath, lastFilePath); err != nil {
fmt.Printf("failed to create symlink: %v\n", err)

2
go.mod
View File

@ -44,6 +44,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/creack/goselect v0.1.2 // indirect
@ -87,6 +88,7 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect

4
go.sum
View File

@ -12,6 +12,8 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
@ -173,6 +175,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@ -0,0 +1,418 @@
package native
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
pb "github.com/jetkvm/kvm/internal/native/proto"
)
// GRPCClient wraps the gRPC client for the native service
type GRPCClient struct {
conn *grpc.ClientConn
client pb.NativeServiceClient
logger *zerolog.Logger
eventStream pb.NativeService_StreamEventsClient
eventM sync.RWMutex
eventCh chan *pb.Event
eventDone chan struct{}
closed bool
closeM sync.Mutex
}
// NewGRPCClient creates a new gRPC client connected to the native service
func NewGRPCClient(socketPath string, logger *zerolog.Logger) (*GRPCClient, error) {
// Connect to the Unix domain socket
conn, err := grpc.NewClient(
fmt.Sprintf("unix-abstract:%v", socketPath),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
}
client := pb.NewNativeServiceClient(conn)
grpcClient := &GRPCClient{
conn: conn,
client: client,
logger: logger,
eventCh: make(chan *pb.Event, 100),
eventDone: make(chan struct{}),
}
// Start event stream
go grpcClient.startEventStream()
return grpcClient, nil
}
func (c *GRPCClient) startEventStream() {
// for {
// return
// c.closeM.Lock()
// if c.closed {
// c.closeM.Unlock()
// return
// }
// c.closeM.Unlock()
// ctx := context.Background()
// stream, err := c.client.StreamEvents(ctx, &pb.Empty{})
// if err != nil {
// c.logger.Warn().Err(err).Msg("failed to start event stream, retrying...")
// time.Sleep(1 * time.Second)
// continue
// }
// c.eventM.Lock()
// c.eventStream = stream
// c.eventM.Unlock()
// for {
// event, err := stream.Recv()
// if err == io.EOF {
// c.logger.Debug().Msg("event stream closed")
// break
// }
// if err != nil {
// c.logger.Warn().Err(err).Msg("event stream error")
// break
// }
// select {
// case c.eventCh <- event:
// default:
// c.logger.Warn().Msg("event channel full, dropping event")
// }
// }
// c.eventM.Lock()
// c.eventStream = nil
// c.eventM.Unlock()
// // Wait before retrying
// time.Sleep(1 * time.Second)
// }
}
func (c *GRPCClient) checkIsReady(ctx context.Context) error {
c.logger.Info().Msg("connection is idle, connecting...")
resp, err := c.client.IsReady(ctx, &pb.IsReadyRequest{})
if err != nil {
if errors.Is(err, status.Error(codes.Unavailable, "")) {
return fmt.Errorf("timeout waiting for ready: %w", err)
}
return fmt.Errorf("failed to check if ready: %w", err)
}
if resp.Ready {
return nil
}
return nil
}
// WaitReady waits for the gRPC connection to be ready
func (c *GRPCClient) WaitReady() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
for {
state := c.conn.GetState()
if state == connectivity.Idle || state == connectivity.Ready {
if err := c.checkIsReady(ctx); err != nil {
time.Sleep(1 * time.Second)
continue
}
}
c.logger.Info().Str("state", state.String()).Msg("waiting for connection to be ready")
if state == connectivity.Ready {
return nil
}
if state == connectivity.Shutdown {
return fmt.Errorf("connection failed: %v", state)
}
if !c.conn.WaitForStateChange(ctx, state) {
return ctx.Err()
}
}
}
// OnEvent registers an event handler
func (c *GRPCClient) OnEvent(eventType string, handler func(data interface{})) {
return
// go func() {
// for {
// select {
// case event := <-c.eventCh:
// if event.Type == eventType {
// var data interface{}
// switch eventType {
// case "video_state_change":
// if event.VideoState != nil {
// data = VideoState{
// Ready: event.VideoState.Ready,
// Error: event.VideoState.Error,
// Width: int(event.VideoState.Width),
// Height: int(event.VideoState.Height),
// FramePerSecond: event.VideoState.FramePerSecond,
// }
// }
// case "indev_event":
// data = event.IndevEvent
// case "rpc_event":
// data = event.RpcEvent
// case "video_frame":
// if event.VideoFrame != nil {
// data = map[string]interface{}{
// "frame": event.VideoFrame.Frame,
// "duration": time.Duration(event.VideoFrame.DurationNs),
// }
// }
// }
// if data != nil {
// handler(data)
// }
// }
// case <-c.eventDone:
// return
// }
// }
// }()
}
// Close closes the gRPC client
func (c *GRPCClient) Close() error {
c.closeM.Lock()
defer c.closeM.Unlock()
if c.closed {
return nil
}
c.closed = true
close(c.eventDone)
c.eventM.Lock()
if c.eventStream != nil {
c.eventStream.CloseSend()
}
c.eventM.Unlock()
return c.conn.Close()
}
// Video methods
func (c *GRPCClient) VideoSetSleepMode(enabled bool) error {
_, err := c.client.VideoSetSleepMode(context.Background(), &pb.VideoSetSleepModeRequest{Enabled: enabled})
return err
}
func (c *GRPCClient) VideoGetSleepMode() (bool, error) {
resp, err := c.client.VideoGetSleepMode(context.Background(), &pb.Empty{})
if err != nil {
return false, err
}
return resp.Enabled, nil
}
func (c *GRPCClient) VideoSleepModeSupported() bool {
resp, err := c.client.VideoSleepModeSupported(context.Background(), &pb.Empty{})
if err != nil {
return false
}
return resp.Supported
}
func (c *GRPCClient) VideoSetQualityFactor(factor float64) error {
_, err := c.client.VideoSetQualityFactor(context.Background(), &pb.VideoSetQualityFactorRequest{Factor: factor})
return err
}
func (c *GRPCClient) VideoGetQualityFactor() (float64, error) {
resp, err := c.client.VideoGetQualityFactor(context.Background(), &pb.Empty{})
if err != nil {
return 0, err
}
return resp.Factor, nil
}
func (c *GRPCClient) VideoSetEDID(edid string) error {
_, err := c.client.VideoSetEDID(context.Background(), &pb.VideoSetEDIDRequest{Edid: edid})
return err
}
func (c *GRPCClient) VideoGetEDID() (string, error) {
resp, err := c.client.VideoGetEDID(context.Background(), &pb.Empty{})
if err != nil {
return "", err
}
return resp.Edid, nil
}
func (c *GRPCClient) VideoLogStatus() (string, error) {
resp, err := c.client.VideoLogStatus(context.Background(), &pb.Empty{})
if err != nil {
return "", err
}
return resp.Status, nil
}
func (c *GRPCClient) VideoStop() error {
_, err := c.client.VideoStop(context.Background(), &pb.Empty{})
return err
}
func (c *GRPCClient) VideoStart() error {
_, err := c.client.VideoStart(context.Background(), &pb.Empty{})
return err
}
// UI methods
func (c *GRPCClient) GetLVGLVersion() (string, error) {
resp, err := c.client.GetLVGLVersion(context.Background(), &pb.Empty{})
if err != nil {
return "", err
}
return resp.Version, nil
}
func (c *GRPCClient) UIObjHide(objName string) (bool, error) {
resp, err := c.client.UIObjHide(context.Background(), &pb.UIObjHideRequest{ObjName: objName})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjShow(objName string) (bool, error) {
resp, err := c.client.UIObjShow(context.Background(), &pb.UIObjShowRequest{ObjName: objName})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UISetVar(name string, value string) {
_, _ = c.client.UISetVar(context.Background(), &pb.UISetVarRequest{Name: name, Value: value})
}
func (c *GRPCClient) UIGetVar(name string) string {
resp, err := c.client.UIGetVar(context.Background(), &pb.UIGetVarRequest{Name: name})
if err != nil {
return ""
}
return resp.Value
}
func (c *GRPCClient) UIObjAddState(objName string, state string) (bool, error) {
resp, err := c.client.UIObjAddState(context.Background(), &pb.UIObjAddStateRequest{ObjName: objName, State: state})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjClearState(objName string, state string) (bool, error) {
resp, err := c.client.UIObjClearState(context.Background(), &pb.UIObjClearStateRequest{ObjName: objName, State: state})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjAddFlag(objName string, flag string) (bool, error) {
resp, err := c.client.UIObjAddFlag(context.Background(), &pb.UIObjAddFlagRequest{ObjName: objName, Flag: flag})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjClearFlag(objName string, flag string) (bool, error) {
resp, err := c.client.UIObjClearFlag(context.Background(), &pb.UIObjClearFlagRequest{ObjName: objName, Flag: flag})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjSetOpacity(objName string, opacity int) (bool, error) {
resp, err := c.client.UIObjSetOpacity(context.Background(), &pb.UIObjSetOpacityRequest{ObjName: objName, Opacity: int32(opacity)})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjFadeIn(objName string, duration uint32) (bool, error) {
resp, err := c.client.UIObjFadeIn(context.Background(), &pb.UIObjFadeInRequest{ObjName: objName, Duration: duration})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjFadeOut(objName string, duration uint32) (bool, error) {
resp, err := c.client.UIObjFadeOut(context.Background(), &pb.UIObjFadeOutRequest{ObjName: objName, Duration: duration})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjSetLabelText(objName string, text string) (bool, error) {
resp, err := c.client.UIObjSetLabelText(context.Background(), &pb.UIObjSetLabelTextRequest{ObjName: objName, Text: text})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UIObjSetImageSrc(objName string, image string) (bool, error) {
resp, err := c.client.UIObjSetImageSrc(context.Background(), &pb.UIObjSetImageSrcRequest{ObjName: objName, Image: image})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) DisplaySetRotation(rotation uint16) (bool, error) {
resp, err := c.client.DisplaySetRotation(context.Background(), &pb.DisplaySetRotationRequest{Rotation: uint32(rotation)})
if err != nil {
return false, err
}
return resp.Success, nil
}
func (c *GRPCClient) UpdateLabelIfChanged(objName string, newText string) {
_, _ = c.client.UpdateLabelIfChanged(context.Background(), &pb.UpdateLabelIfChangedRequest{ObjName: objName, NewText: newText})
}
func (c *GRPCClient) UpdateLabelAndChangeVisibility(objName string, newText string) {
_, _ = c.client.UpdateLabelAndChangeVisibility(context.Background(), &pb.UpdateLabelAndChangeVisibilityRequest{ObjName: objName, NewText: newText})
}
func (c *GRPCClient) SwitchToScreenIf(screenName string, shouldSwitch []string) {
_, _ = c.client.SwitchToScreenIf(context.Background(), &pb.SwitchToScreenIfRequest{ScreenName: screenName, ShouldSwitch: shouldSwitch})
}
func (c *GRPCClient) SwitchToScreenIfDifferent(screenName string) {
_, _ = c.client.SwitchToScreenIfDifferent(context.Background(), &pb.SwitchToScreenIfDifferentRequest{ScreenName: screenName})
}
func (c *GRPCClient) DoNotUseThisIsForCrashTestingOnly() {
_, _ = c.client.DoNotUseThisIsForCrashTestingOnly(context.Background(), &pb.Empty{})
}

View File

@ -4,8 +4,6 @@ import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"sync"
"time"
@ -47,6 +45,7 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
}
event := &pb.Event{
Type: "video_state_change",
Data: &pb.Event_VideoState{
VideoState: &pb.VideoState{
Ready: state.Ready,
Error: state.Error,
@ -54,6 +53,7 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
Height: int32(state.Height),
FramePerSecond: state.FramePerSecond,
},
},
}
s.broadcastEvent(event)
}
@ -64,7 +64,9 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
}
s.broadcastEvent(&pb.Event{
Type: "indev_event",
Data: &pb.Event_IndevEvent{
IndevEvent: event,
},
})
}
@ -74,7 +76,9 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
}
s.broadcastEvent(&pb.Event{
Type: "rpc_event",
Data: &pb.Event_RpcEvent{
RpcEvent: event,
},
})
}
@ -84,10 +88,12 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
}
s.broadcastEvent(&pb.Event{
Type: "video_frame",
Data: &pb.Event_VideoFrame{
VideoFrame: &pb.VideoFrame{
Frame: frame,
DurationNs: duration.Nanoseconds(),
},
},
})
}
@ -107,6 +113,10 @@ func (s *grpcServer) broadcastEvent(event *pb.Event) {
}
}
func (s *grpcServer) IsReady(ctx context.Context, req *pb.IsReadyRequest) (*pb.IsReadyResponse, error) {
return &pb.IsReadyResponse{Ready: true, VideoReady: true}, nil
}
// Video methods
func (s *grpcServer) VideoSetSleepMode(ctx context.Context, req *pb.VideoSetSleepModeRequest) (*pb.Empty, error) {
if err := s.native.VideoSetSleepMode(req.Enabled); err != nil {
@ -356,17 +366,6 @@ func (s *grpcServer) StreamEvents(req *pb.Empty, stream pb.NativeService_StreamE
// StartGRPCServer starts the gRPC server on a Unix domain socket
func StartGRPCServer(server *grpcServer, socketPath string, logger *zerolog.Logger) (*grpc.Server, net.Listener, error) {
// Remove socket if it exists
if _, err := os.Stat(socketPath); err == nil {
if err := os.Remove(socketPath); err != nil {
return nil, nil, fmt.Errorf("failed to remove existing socket: %w", err)
}
}
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {
return nil, nil, fmt.Errorf("failed to create socket directory: %w", err)
}
lis, err := net.Listen("unix", socketPath)
if err != nil {

View File

@ -1,293 +0,0 @@
package native
import (
"bufio"
"encoding/json"
"fmt"
"io"
"sync"
"time"
"github.com/rs/zerolog"
)
// Request represents a JSON-RPC request
type Request struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// Response represents a JSON-RPC response
type Response struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id,omitempty"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
}
// RPCError represents a JSON-RPC error
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Event represents a JSON-RPC notification/event
type Event struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params map[string]interface{} `json:"params,omitempty"`
}
// ProcessConfig is the configuration for the native process
type ProcessConfig struct {
Disable bool `json:"disable"`
SystemVersion string `json:"system_version"` // Serialized as string
AppVersion string `json:"app_version"` // Serialized as string
DisplayRotation uint16 `json:"display_rotation"`
DefaultQualityFactor float64 `json:"default_quality_factor"`
}
// IPCClient handles communication with the native process
type IPCClient struct {
stdin io.WriteCloser
stdout *bufio.Scanner
logger *zerolog.Logger
requestID int64
requestIDM sync.Mutex
pendingRequests map[interface{}]chan *Response
pendingM sync.Mutex
eventHandlers map[string][]func(data interface{})
eventM sync.RWMutex
readyCh chan struct{}
ready bool
closed bool
closeM sync.Mutex
}
type processCmd interface {
Start() error
Wait() error
GetProcess() interface {
Kill() error
Signal(sig interface{}) error
}
StdinPipe() (io.WriteCloser, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
}
func NewIPCClient(cmd processCmd, logger *zerolog.Logger) (*IPCClient, error) {
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("failed to get stdin pipe: %w", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
stdin.Close()
return nil, fmt.Errorf("failed to get stdout pipe: %w", err)
}
client := &IPCClient{
stdin: stdin,
stdout: bufio.NewScanner(stdout),
logger: logger,
pendingRequests: make(map[interface{}]chan *Response),
eventHandlers: make(map[string][]func(data interface{})),
readyCh: make(chan struct{}),
}
// Start reading responses
go client.readLoop()
return client, nil
}
func (c *IPCClient) readLoop() {
for c.stdout.Scan() {
line := c.stdout.Bytes()
if len(line) == 0 {
continue
}
// Try to parse as response
var resp Response
if err := json.Unmarshal(line, &resp); err == nil {
// Check if it's a ready signal (result is "ready" and no ID)
if resp.Result == "ready" && resp.ID == nil && !c.ready {
c.ready = true
close(c.readyCh)
continue
}
if resp.Result != nil || resp.Error != nil {
c.handleResponse(&resp)
continue
}
}
// Try to parse as event
var event Event
if err := json.Unmarshal(line, &event); err == nil && event.Method == "event" {
c.handleEvent(&event)
continue
}
c.logger.Warn().Bytes("line", line).Msg("unexpected message from native process")
}
c.closeM.Lock()
if !c.closed {
c.closed = true
c.closeM.Unlock()
c.logger.Warn().Msg("native process stdout closed")
// Cancel all pending requests
c.pendingM.Lock()
for id, ch := range c.pendingRequests {
close(ch)
delete(c.pendingRequests, id)
}
c.pendingM.Unlock()
} else {
c.closeM.Unlock()
}
}
func (c *IPCClient) handleResponse(resp *Response) {
c.pendingM.Lock()
ch, ok := c.pendingRequests[resp.ID]
if ok {
delete(c.pendingRequests, resp.ID)
}
c.pendingM.Unlock()
if ok {
select {
case ch <- resp:
default:
}
}
}
func (c *IPCClient) handleEvent(event *Event) {
if event.Method != "event" || event.Params == nil {
return
}
eventType, ok := event.Params["type"].(string)
if !ok {
return
}
data := event.Params["data"]
c.eventM.RLock()
handlers := c.eventHandlers[eventType]
c.eventM.RUnlock()
for _, handler := range handlers {
handler(data)
}
}
func (c *IPCClient) Call(method string, params interface{}) (*Response, error) {
c.closeM.Lock()
if c.closed {
c.closeM.Unlock()
return nil, fmt.Errorf("client is closed")
}
c.closeM.Unlock()
c.requestIDM.Lock()
c.requestID++
id := c.requestID
c.requestIDM.Unlock()
req := Request{
JSONRPC: "2.0",
ID: id,
Method: method,
}
if params != nil {
paramsBytes, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("failed to marshal params: %w", err)
}
req.Params = paramsBytes
}
ch := make(chan *Response, 1)
c.pendingM.Lock()
c.pendingRequests[id] = ch
c.pendingM.Unlock()
reqBytes, err := json.Marshal(req)
if err != nil {
c.pendingM.Lock()
delete(c.pendingRequests, id)
c.pendingM.Unlock()
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := c.stdin.Write(append(reqBytes, '\n')); err != nil {
c.pendingM.Lock()
delete(c.pendingRequests, id)
c.pendingM.Unlock()
return nil, fmt.Errorf("failed to write request: %w", err)
}
select {
case resp := <-ch:
if resp.Error != nil {
return nil, fmt.Errorf("RPC error: %s (code: %d)", resp.Error.Message, resp.Error.Code)
}
return resp, nil
case <-time.After(30 * time.Second):
c.pendingM.Lock()
delete(c.pendingRequests, id)
c.pendingM.Unlock()
return nil, fmt.Errorf("request timeout")
}
}
func (c *IPCClient) OnEvent(eventType string, handler func(data interface{})) {
c.eventM.Lock()
defer c.eventM.Unlock()
c.eventHandlers[eventType] = append(c.eventHandlers[eventType], handler)
}
func (c *IPCClient) WaitReady() error {
select {
case <-c.readyCh:
return nil
case <-time.After(10 * time.Second):
return fmt.Errorf("timeout waiting for ready signal")
}
}
func (c *IPCClient) Close() error {
c.closeM.Lock()
defer c.closeM.Unlock()
if c.closed {
return nil
}
c.closed = true
c.pendingM.Lock()
for id, ch := range c.pendingRequests {
close(ch)
delete(c.pendingRequests, id)
}
c.pendingM.Unlock()
return c.stdin.Close()
}

View File

@ -1,11 +1,14 @@
package native
import (
"os"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
var nativeLogger = logging.GetSubsystemLogger("native")
var nativeL = logging.GetSubsystemLogger("native").With().Int("pid", os.Getpid()).Logger()
var nativeLogger = &nativeL
var displayLogger = logging.GetSubsystemLogger("display")
type nativeLogMessage struct {

View File

@ -28,11 +28,11 @@ type Native struct {
}
type NativeOptions struct {
Disable bool
SystemVersion *semver.Version
AppVersion *semver.Version
DisplayRotation uint16
DefaultQualityFactor float64
Disable bool `env:"JETKVM_NATIVE_DISABLE"`
SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"`
AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"`
DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"`
DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"`
OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string)
@ -50,7 +50,7 @@ func NewNative(opts NativeOptions) *Native {
onVideoFrameReceived := opts.OnVideoFrameReceived
if onVideoFrameReceived == nil {
onVideoFrameReceived = func(frame []byte, duration time.Duration) {
nativeLogger.Info().Interface("frame", frame).Dur("duration", duration).Msg("video frame received")
nativeLogger.Trace().Interface("frame", frame).Dur("duration", duration).Msg("video frame received")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,8 @@ option go_package = "github.com/jetkvm/kvm/internal/native/proto";
// NativeService provides methods to interact with the native layer
service NativeService {
// Init
rpc Init(InitRequest) returns (InitResponse);
// Ready check
rpc IsReady(IsReadyRequest) returns (IsReadyResponse);
// Video methods
rpc VideoSetSleepMode(VideoSetSleepModeRequest) returns (Empty);
@ -52,16 +52,12 @@ service NativeService {
// Messages
message Empty {}
message InitRequest {
string system_version = 1;
string app_version = 2;
uint32 display_rotation = 3;
double default_quality_factor = 4;
}
message IsReadyRequest {}
message InitResponse {
bool success = 1;
message IsReadyResponse {
bool ready = 1;
string error = 2;
bool video_ready = 3;
}
message VideoState {

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,22 @@
package native
import (
"encoding/json"
"fmt"
"net"
"os"
"os/exec"
"sync"
"syscall"
"time"
"github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/supervisor"
"github.com/rs/zerolog"
)
const (
maxFrameSize = 1920 * 1080 / 2
)
// cmdWrapper wraps exec.Cmd to implement processCmd interface
type cmdWrapper struct {
*exec.Cmd
@ -43,94 +46,116 @@ func (p *processWrapper) Signal(sig interface{}) error {
// NativeProxy is a proxy that communicates with a separate native process
type NativeProxy struct {
client *IPCClient
cmd *exec.Cmd
wrapped *cmdWrapper
nativeUnixSocket string
videoStreamUnixSocket string
videoStreamListener net.Listener
binaryPath string
client *GRPCClient
cmd *cmdWrapper
logger *zerolog.Logger
ready chan struct{}
options *NativeOptions
restartM sync.Mutex
stopped bool
opts NativeProxyOptions
binaryPath string
configJSON []byte
processWait chan error
}
// NativeProxyOptions are options for creating a NativeProxy
type NativeProxyOptions struct {
Disable bool
SystemVersion *semver.Version
AppVersion *semver.Version
DisplayRotation uint16
DefaultQualityFactor float64
OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string)
OnRpcEvent func(event string)
Logger *zerolog.Logger
func ensureDirectoryExists(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0600)
}
return nil
}
// NewNativeProxy creates a new NativeProxy that spawns a separate process
func NewNativeProxy(opts NativeProxyOptions) (*NativeProxy, error) {
if opts.Logger == nil {
opts.Logger = nativeLogger
}
func NewNativeProxy(opts NativeOptions) (*NativeProxy, error) {
nativeUnixSocket := "jetkvm-native-grpc"
videoStreamUnixSocket := "@jetkvm-native-video-stream"
// Get the current executable path to spawn itself
exePath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
binaryPath := exePath
config := ProcessConfig{
Disable: opts.Disable,
SystemVersion: "",
AppVersion: "",
DisplayRotation: opts.DisplayRotation,
DefaultQualityFactor: opts.DefaultQualityFactor,
proxy := &NativeProxy{
nativeUnixSocket: nativeUnixSocket,
videoStreamUnixSocket: videoStreamUnixSocket,
binaryPath: exePath,
logger: nativeLogger,
ready: make(chan struct{}),
options: &opts,
processWait: make(chan error, 1),
}
if opts.SystemVersion != nil {
config.SystemVersion = opts.SystemVersion.String()
}
if opts.AppVersion != nil {
config.AppVersion = opts.AppVersion.String()
}
configJSON, err := json.Marshal(config)
proxy.cmd, err = proxy.spawnProcess()
nativeLogger.Info().Msg("spawned process")
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %w", err)
return nil, fmt.Errorf("failed to spawn process: %w", err)
}
cmd := exec.Command(binaryPath, string(configJSON))
// create unix packet
listener, err := net.Listen("unixpacket", videoStreamUnixSocket)
if err != nil {
nativeLogger.Warn().Err(err).Msg("failed to start server")
return nil, fmt.Errorf("failed to start server: %w", err)
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
nativeLogger.Warn().Err(err).Msg("failed to accept socket")
continue
}
nativeLogger.Info().Str("socket", conn.RemoteAddr().String()).Msg("accepted socket")
go proxy.handleVideoFrame(conn)
}
}()
return proxy, nil
}
func (p *NativeProxy) spawnProcess() (*cmdWrapper, error) {
cmd := exec.Command(
p.binaryPath,
"-subcomponent=native",
)
cmd.Stdout = os.Stdout // Forward stdout to parent
cmd.Stderr = os.Stderr // Forward stderr to parent
// Set environment variable to indicate native process mode
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=native", supervisor.EnvSubcomponent))
cmd.Env = append(
os.Environ(),
fmt.Sprintf("%s=native", supervisor.EnvSubcomponent),
fmt.Sprintf("%s=%s", "JETKVM_NATIVE_SOCKET", p.nativeUnixSocket),
fmt.Sprintf("%s=%s", "JETKVM_VIDEO_STREAM_SOCKET", p.videoStreamUnixSocket),
fmt.Sprintf("%s=%s", "JETKVM_NATIVE_SYSTEM_VERSION", p.options.SystemVersion),
fmt.Sprintf("%s=%s", "JETKVM_NATIVE_APP_VERSION", p.options.AppVersion),
fmt.Sprintf("%s=%d", "JETKVM_NATIVE_DISPLAY_ROTATION", p.options.DisplayRotation),
fmt.Sprintf("%s=%f", "JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR", p.options.DefaultQualityFactor),
)
// Wrap cmd to implement processCmd interface
wrappedCmd := &cmdWrapper{Cmd: cmd}
client, err := NewIPCClient(wrappedCmd, opts.Logger)
return wrappedCmd, nil
}
func (p *NativeProxy) handleVideoFrame(conn net.Conn) {
defer conn.Close()
inboundPacket := make([]byte, maxFrameSize)
lastFrame := time.Now()
for {
n, err := conn.Read(inboundPacket)
if err != nil {
return nil, fmt.Errorf("failed to create IPC client: %w", err)
nativeLogger.Warn().Err(err).Msg("failed to accept socket")
break
}
proxy := &NativeProxy{
client: client,
cmd: cmd,
wrapped: wrappedCmd,
logger: opts.Logger,
ready: make(chan struct{}),
opts: opts,
binaryPath: binaryPath,
configJSON: configJSON,
processWait: make(chan error, 1),
now := time.Now()
sinceLastFrame := now.Sub(lastFrame)
lastFrame = now
p.options.OnVideoFrameReceived(inboundPacket[:n], sinceLastFrame)
}
// Set up event handlers
proxy.setupEventHandlers(client)
return proxy, nil
}
// Start starts the native process
@ -146,6 +171,15 @@ func (p *NativeProxy) Start() error {
return fmt.Errorf("failed to start native process: %w", err)
}
nativeLogger.Info().Msg("process ready")
client, err := NewGRPCClient(p.nativeUnixSocket, nativeLogger)
nativeLogger.Info().Str("socket_path", p.nativeUnixSocket).Msg("created client")
if err != nil {
return fmt.Errorf("failed to create IPC client: %w", err)
}
p.client = client
// Wait for ready signal from the native process
if err := p.client.WaitReady(); err != nil {
// Clean up if ready failed
@ -153,9 +187,12 @@ func (p *NativeProxy) Start() error {
_ = p.cmd.Process.Kill()
_ = p.cmd.Wait()
}
return err
return fmt.Errorf("failed to wait for ready: %w", err)
}
// Set up event handlers
p.setupEventHandlers(client)
// Start monitoring process for crashes
go p.monitorProcess()
@ -216,14 +253,10 @@ func (p *NativeProxy) restartProcess() error {
return fmt.Errorf("proxy is stopped")
}
// Create new command
cmd := exec.Command(p.binaryPath, string(p.configJSON))
cmd.Stderr = os.Stderr
// Set environment variable to indicate native process mode
cmd.Env = append(os.Environ(), "JETKVM_NATIVE_PROCESS=1")
// Wrap cmd to implement processCmd interface
wrappedCmd := &cmdWrapper{Cmd: cmd}
wrappedCmd, err := p.spawnProcess()
if err != nil {
return fmt.Errorf("failed to spawn process: %w", err)
}
// Close old client
if p.client != nil {
@ -231,7 +264,7 @@ func (p *NativeProxy) restartProcess() error {
}
// Create new client
client, err := NewIPCClient(wrappedCmd, p.logger)
client, err := NewGRPCClient(p.nativeUnixSocket, p.logger)
if err != nil {
return fmt.Errorf("failed to create IPC client: %w", err)
}
@ -240,90 +273,89 @@ func (p *NativeProxy) restartProcess() error {
p.setupEventHandlers(client)
// Start the process
if err := cmd.Start(); err != nil {
if err := wrappedCmd.Start(); err != nil {
return fmt.Errorf("failed to start native process: %w", err)
}
// Wait for ready
if err := client.WaitReady(); err != nil {
if cmd.Process != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
if wrappedCmd.Process != nil {
_ = wrappedCmd.Process.Kill()
_ = wrappedCmd.Wait()
}
return fmt.Errorf("timeout waiting for ready: %w", err)
}
p.cmd = cmd
p.wrapped = wrappedCmd
p.cmd = wrappedCmd
p.client = client
p.logger.Info().Msg("native process restarted successfully")
return nil
}
func (p *NativeProxy) setupEventHandlers(client *IPCClient) {
if p.opts.OnVideoStateChange != nil {
client.OnEvent("video_state_change", func(data interface{}) {
dataBytes, err := json.Marshal(data)
if err != nil {
p.logger.Warn().Err(err).Msg("failed to marshal video state event")
return
}
var state VideoState
if err := json.Unmarshal(dataBytes, &state); err != nil {
p.logger.Warn().Err(err).Msg("failed to unmarshal video state event")
return
}
p.opts.OnVideoStateChange(state)
})
}
func (p *NativeProxy) setupEventHandlers(client *GRPCClient) {
// if p.opts.OnVideoStateChange != nil {
// client.OnEvent("video_state_change", func(data interface{}) {
// dataBytes, err := json.Marshal(data)
// if err != nil {
// p.logger.Warn().Err(err).Msg("failed to marshal video state event")
// return
// }
// var state VideoState
// if err := json.Unmarshal(dataBytes, &state); err != nil {
// p.logger.Warn().Err(err).Msg("failed to unmarshal video state event")
// return
// }
// p.opts.OnVideoStateChange(state)
// })
// }
if p.opts.OnIndevEvent != nil {
client.OnEvent("indev_event", func(data interface{}) {
if event, ok := data.(string); ok {
p.opts.OnIndevEvent(event)
}
})
}
// if p.opts.OnIndevEvent != nil {
// client.OnEvent("indev_event", func(data interface{}) {
// if event, ok := data.(string); ok {
// p.opts.OnIndevEvent(event)
// }
// })
// }
if p.opts.OnRpcEvent != nil {
client.OnEvent("rpc_event", func(data interface{}) {
if event, ok := data.(string); ok {
p.opts.OnRpcEvent(event)
}
})
}
// if p.opts.OnRpcEvent != nil {
// client.OnEvent("rpc_event", func(data interface{}) {
// if event, ok := data.(string); ok {
// p.opts.OnRpcEvent(event)
// }
// })
// }
if p.opts.OnVideoFrameReceived != nil {
client.OnEvent("video_frame", func(data interface{}) {
dataMap, ok := data.(map[string]interface{})
if !ok {
p.logger.Warn().Msg("invalid video frame event data")
return
}
// if p.opts.OnVideoFrameReceived != nil {
// client.OnEvent("video_frame", func(data interface{}) {
// dataMap, ok := data.(map[string]interface{})
// if !ok {
// p.logger.Warn().Msg("invalid video frame event data")
// return
// }
frameData, ok := dataMap["frame"].([]interface{})
if !ok {
p.logger.Warn().Msg("invalid frame data in event")
return
}
// frameData, ok := dataMap["frame"].([]interface{})
// if !ok {
// p.logger.Warn().Msg("invalid frame data in event")
// return
// }
frame := make([]byte, len(frameData))
for i, v := range frameData {
if b, ok := v.(float64); ok {
frame[i] = byte(b)
}
}
// frame := make([]byte, len(frameData))
// for i, v := range frameData {
// if b, ok := v.(float64); ok {
// frame[i] = byte(b)
// }
// }
durationNs, ok := dataMap["duration"].(float64)
if !ok {
p.logger.Warn().Msg("invalid duration in event")
return
}
// durationNs, ok := dataMap["duration"].(float64)
// if !ok {
// p.logger.Warn().Msg("invalid duration in event")
// return
// }
p.opts.OnVideoFrameReceived(frame, time.Duration(durationNs))
})
}
// p.opts.OnVideoFrameReceived(frame, time.Duration(durationNs))
// })
// }
}
// Stop stops the native process
@ -347,336 +379,123 @@ func (p *NativeProxy) Stop() error {
return nil
}
// Implement all Native methods by forwarding to IPC
// Implement all Native methods by forwarding to gRPC client
func (p *NativeProxy) VideoSetSleepMode(enabled bool) error {
_, err := p.client.Call("VideoSetSleepMode", map[string]interface{}{
"enabled": enabled,
})
return err
return p.client.VideoSetSleepMode(enabled)
}
func (p *NativeProxy) VideoGetSleepMode() (bool, error) {
resp, err := p.client.Call("VideoGetSleepMode", nil)
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.VideoGetSleepMode()
}
func (p *NativeProxy) VideoSleepModeSupported() bool {
resp, err := p.client.Call("VideoSleepModeSupported", nil)
if err != nil {
return false
}
result, ok := resp.Result.(bool)
if !ok {
return false
}
return result
return p.client.VideoSleepModeSupported()
}
func (p *NativeProxy) VideoSetQualityFactor(factor float64) error {
_, err := p.client.Call("VideoSetQualityFactor", map[string]interface{}{
"factor": factor,
})
return err
return p.client.VideoSetQualityFactor(factor)
}
func (p *NativeProxy) VideoGetQualityFactor() (float64, error) {
resp, err := p.client.Call("VideoGetQualityFactor", nil)
if err != nil {
return 0, err
}
result, ok := resp.Result.(float64)
if !ok {
return 0, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.VideoGetQualityFactor()
}
func (p *NativeProxy) VideoSetEDID(edid string) error {
_, err := p.client.Call("VideoSetEDID", map[string]interface{}{
"edid": edid,
})
return err
return p.client.VideoSetEDID(edid)
}
func (p *NativeProxy) VideoGetEDID() (string, error) {
resp, err := p.client.Call("VideoGetEDID", nil)
if err != nil {
return "", err
}
result, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("invalid response type")
}
return result, nil
return p.client.VideoGetEDID()
}
func (p *NativeProxy) VideoLogStatus() (string, error) {
resp, err := p.client.Call("VideoLogStatus", nil)
if err != nil {
return "", err
}
result, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("invalid response type")
}
return result, nil
return p.client.VideoLogStatus()
}
func (p *NativeProxy) VideoStop() error {
_, err := p.client.Call("VideoStop", nil)
return err
return p.client.VideoStop()
}
func (p *NativeProxy) VideoStart() error {
_, err := p.client.Call("VideoStart", nil)
return err
return p.client.VideoStart()
}
func (p *NativeProxy) GetLVGLVersion() (string, error) {
resp, err := p.client.Call("GetLVGLVersion", nil)
if err != nil {
return "", err
}
result, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("invalid response type")
}
return result, nil
return p.client.GetLVGLVersion()
}
func (p *NativeProxy) UIObjHide(objName string) (bool, error) {
resp, err := p.client.Call("UIObjHide", map[string]interface{}{
"obj_name": objName,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjHide(objName)
}
func (p *NativeProxy) UIObjShow(objName string) (bool, error) {
resp, err := p.client.Call("UIObjShow", map[string]interface{}{
"obj_name": objName,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjShow(objName)
}
func (p *NativeProxy) UISetVar(name string, value string) {
_, _ = p.client.Call("UISetVar", map[string]interface{}{
"name": name,
"value": value,
})
p.client.UISetVar(name, value)
}
func (p *NativeProxy) UIGetVar(name string) string {
resp, err := p.client.Call("UIGetVar", map[string]interface{}{
"name": name,
})
if err != nil {
return ""
}
result, ok := resp.Result.(string)
if !ok {
return ""
}
return result
return p.client.UIGetVar(name)
}
func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) {
resp, err := p.client.Call("UIObjAddState", map[string]interface{}{
"obj_name": objName,
"state": state,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjAddState(objName, state)
}
func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) {
resp, err := p.client.Call("UIObjClearState", map[string]interface{}{
"obj_name": objName,
"state": state,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjClearState(objName, state)
}
func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) {
resp, err := p.client.Call("UIObjAddFlag", map[string]interface{}{
"obj_name": objName,
"flag": flag,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjAddFlag(objName, flag)
}
func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) {
resp, err := p.client.Call("UIObjClearFlag", map[string]interface{}{
"obj_name": objName,
"flag": flag,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjClearFlag(objName, flag)
}
func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) {
resp, err := p.client.Call("UIObjSetOpacity", map[string]interface{}{
"obj_name": objName,
"opacity": opacity,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjSetOpacity(objName, opacity)
}
func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) {
resp, err := p.client.Call("UIObjFadeIn", map[string]interface{}{
"obj_name": objName,
"duration": duration,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjFadeIn(objName, duration)
}
func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) {
resp, err := p.client.Call("UIObjFadeOut", map[string]interface{}{
"obj_name": objName,
"duration": duration,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjFadeOut(objName, duration)
}
func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) {
resp, err := p.client.Call("UIObjSetLabelText", map[string]interface{}{
"obj_name": objName,
"text": text,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjSetLabelText(objName, text)
}
func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) {
resp, err := p.client.Call("UIObjSetImageSrc", map[string]interface{}{
"obj_name": objName,
"image": image,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.UIObjSetImageSrc(objName, image)
}
func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) {
resp, err := p.client.Call("DisplaySetRotation", map[string]interface{}{
"rotation": rotation,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
return p.client.DisplaySetRotation(rotation)
}
func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) {
_, _ = p.client.Call("UpdateLabelIfChanged", map[string]interface{}{
"obj_name": objName,
"new_text": newText,
})
p.client.UpdateLabelIfChanged(objName, newText)
}
func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) {
_, _ = p.client.Call("UpdateLabelAndChangeVisibility", map[string]interface{}{
"obj_name": objName,
"new_text": newText,
})
p.client.UpdateLabelAndChangeVisibility(objName, newText)
}
func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) {
_, _ = p.client.Call("SwitchToScreenIf", map[string]interface{}{
"screen_name": screenName,
"should_switch": shouldSwitch,
})
p.client.SwitchToScreenIf(screenName, shouldSwitch)
}
func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) {
_, _ = p.client.Call("SwitchToScreenIfDifferent", map[string]interface{}{
"screen_name": screenName,
})
p.client.SwitchToScreenIfDifferent(screenName)
}
func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() {
_, _ = p.client.Call("DoNotUseThisIsForCrashTestingOnly", nil)
p.client.DoNotUseThisIsForCrashTestingOnly()
}

90
internal/native/server.go Normal file
View File

@ -0,0 +1,90 @@
package native
import (
"fmt"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/caarlos0/env/v11"
"github.com/erikdubbelboer/gspt"
"github.com/rs/zerolog"
)
// Native Process
// stdout - exchange messages with the parent process
// stderr - logging and error messages
// RunNativeProcess runs the native process mode
func RunNativeProcess(binaryName string) {
// Initialize logger
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
gspt.SetProcTitle(binaryName + " [native]")
// Determine socket path
socketPath := os.Getenv("JETKVM_NATIVE_SOCKET")
videoStreamSocketPath := os.Getenv("JETKVM_VIDEO_STREAM_SOCKET")
if socketPath == "" || videoStreamSocketPath == "" {
logger.Fatal().Str("socket_path", socketPath).Str("video_stream_socket_path", videoStreamSocketPath).Msg("socket path or video stream socket path is not set")
}
// connect to video stream socket
conn, err := net.Dial("unixpacket", videoStreamSocketPath)
if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to video stream socket")
}
logger.Info().Str("video_stream_socket_path", videoStreamSocketPath).Msg("connected to video stream socket")
var nativeOptions NativeOptions
if err := env.Parse(&nativeOptions); err != nil {
logger.Fatal().Err(err).Msg("failed to parse native options")
}
nativeOptions.OnVideoFrameReceived = func(frame []byte, duration time.Duration) {
_, err := conn.Write(frame)
if err != nil {
logger.Fatal().Err(err).Msg("failed to write frame to video stream socket")
}
}
// Create native instance
nativeInstance := NewNative(nativeOptions)
// Start native instance
if err := nativeInstance.Start(); err != nil {
logger.Fatal().Err(err).Msg("failed to start native instance")
}
// Create gRPC server
grpcServer := NewGRPCServer(nativeInstance, &logger)
logger.Info().Msg("starting gRPC server")
// Start gRPC server
server, lis, err := StartGRPCServer(grpcServer, fmt.Sprintf("@%v", socketPath), &logger)
if err != nil {
logger.Fatal().Err(err).Msg("failed to start gRPC server")
}
gspt.SetProcTitle(binaryName + " [native] ready")
// Signal that we're ready by writing socket path to stdout (for parent to read)
fmt.Fprintf(os.Stdout, "%s\n", socketPath)
defer os.Stdout.Close()
// Set up signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
// Wait for signal
<-sigChan
logger.Info().Msg("received termination signal")
// Graceful shutdown
server.GracefulStop()
lis.Close()
logger.Info().Msg("native process exiting")
}

View File

@ -40,8 +40,8 @@ func Main() {
go runWatchdog()
go confirmCurrentSystem()
initDisplay()
initNative(systemVersionLocal, appVersionLocal)
initDisplay()
http.DefaultClient.Timeout = 1 * time.Minute

View File

@ -16,14 +16,14 @@ var (
)
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeLogger.Info().Msg("initializing native")
var err error
nativeInstance, err = native.NewNativeProxy(native.NativeProxyOptions{
nativeInstance, err = native.NewNativeProxy(native.NativeOptions{
Disable: failsafeModeActive,
SystemVersion: systemVersion,
AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(),
DefaultQualityFactor: config.VideoQualityFactor,
Logger: nativeLogger,
OnVideoStateChange: func(state native.VideoState) {
lastVideoState = state
triggerVideoStateUpdate()

View File

@ -1,528 +0,0 @@
package kvm
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/native"
"github.com/rs/zerolog"
)
// RunNativeProcess runs the native process mode
func RunNativeProcess() {
// Initialize logger
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
// Parse command line arguments (config is passed as first arg)
if len(os.Args) < 2 {
logger.Fatal().Msg("usage: native process requires config_json argument")
}
var config native.ProcessConfig
if err := json.Unmarshal([]byte(os.Args[1]), &config); err != nil {
logger.Fatal().Err(err).Msg("failed to parse config")
}
// Parse version strings
var systemVersion *semver.Version
if config.SystemVersion != "" {
v, err := semver.NewVersion(config.SystemVersion)
if err != nil {
logger.Warn().Err(err).Str("version", config.SystemVersion).Msg("failed to parse system version")
} else {
systemVersion = v
}
}
var appVersion *semver.Version
if config.AppVersion != "" {
v, err := semver.NewVersion(config.AppVersion)
if err != nil {
logger.Warn().Err(err).Str("version", config.AppVersion).Msg("failed to parse app version")
} else {
appVersion = v
}
}
// Create native instance
nativeInstance := native.NewNative(native.NativeOptions{
Disable: config.Disable,
SystemVersion: systemVersion,
AppVersion: appVersion,
DisplayRotation: config.DisplayRotation,
DefaultQualityFactor: config.DefaultQualityFactor,
OnVideoStateChange: func(state native.VideoState) {
sendEvent("video_state_change", state)
},
OnIndevEvent: func(event string) {
sendEvent("indev_event", event)
},
OnRpcEvent: func(event string) {
sendEvent("rpc_event", event)
},
OnVideoFrameReceived: func(frame []byte, duration time.Duration) {
sendEvent("video_frame", map[string]interface{}{
"frame": frame,
"duration": duration.Nanoseconds(),
})
},
})
// Start native instance
if err := nativeInstance.Start(); err != nil {
logger.Fatal().Err(err).Msg("failed to start native instance")
}
// Create gRPC server
grpcServer := native.NewGRPCServer(nativeInstance, &logger)
// Determine socket path
socketPath := os.Getenv("JETKVM_NATIVE_SOCKET")
if socketPath == "" {
// Default to a socket in /tmp
socketPath = filepath.Join("/tmp", fmt.Sprintf("jetkvm-native-%d.sock", os.Getpid()))
}
// Start gRPC server
server, lis, err := native.StartGRPCServer(grpcServer, socketPath, &logger)
if err != nil {
logger.Fatal().Err(err).Msg("failed to start gRPC server")
}
// Signal that we're ready by writing socket path to stdout (for parent to read)
fmt.Fprintf(os.Stdout, "%s\n", socketPath)
os.Stdout.Close()
// Set up signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
// Wait for signal
<-sigChan
logger.Info().Msg("received termination signal")
// Graceful shutdown
server.GracefulStop()
lis.Close()
logger.Info().Msg("native process exiting")
}
// All JSON-RPC handlers have been removed - now using gRPC
case "VideoSetSleepMode":
var params struct {
Enabled bool `json:"enabled"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
err = n.VideoSetSleepMode(params.Enabled)
case "VideoGetSleepMode":
var result bool
result, err = n.VideoGetSleepMode()
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "VideoSleepModeSupported":
result = n.VideoSleepModeSupported()
sendResponse(encoder, req.ID, "result", result)
return
case "VideoSetQualityFactor":
var params struct {
Factor float64 `json:"factor"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
err = n.VideoSetQualityFactor(params.Factor)
case "VideoGetQualityFactor":
var result float64
result, err = n.VideoGetQualityFactor()
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "VideoSetEDID":
var params struct {
EDID string `json:"edid"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
err = n.VideoSetEDID(params.EDID)
case "VideoGetEDID":
var result string
result, err = n.VideoGetEDID()
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "VideoLogStatus":
var result string
result, err = n.VideoLogStatus()
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "VideoStop":
err = n.VideoStop()
case "VideoStart":
err = n.VideoStart()
case "GetLVGLVersion":
var result string
result, err = n.GetLVGLVersion()
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjHide":
var params struct {
ObjName string `json:"obj_name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjHide(params.ObjName)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjShow":
var params struct {
ObjName string `json:"obj_name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjShow(params.ObjName)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UISetVar":
var params struct {
Name string `json:"name"`
Value string `json:"value"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
n.UISetVar(params.Name, params.Value)
sendResponse(encoder, req.ID, "result", nil)
return
case "UIGetVar":
var params struct {
Name string `json:"name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
result = n.UIGetVar(params.Name)
sendResponse(encoder, req.ID, "result", result)
return
case "UIObjAddState":
var params struct {
ObjName string `json:"obj_name"`
State string `json:"state"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjAddState(params.ObjName, params.State)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjClearState":
var params struct {
ObjName string `json:"obj_name"`
State string `json:"state"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjClearState(params.ObjName, params.State)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjAddFlag":
var params struct {
ObjName string `json:"obj_name"`
Flag string `json:"flag"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjAddFlag(params.ObjName, params.Flag)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjClearFlag":
var params struct {
ObjName string `json:"obj_name"`
Flag string `json:"flag"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjClearFlag(params.ObjName, params.Flag)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjSetOpacity":
var params struct {
ObjName string `json:"obj_name"`
Opacity int `json:"opacity"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjSetOpacity(params.ObjName, params.Opacity)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjFadeIn":
var params struct {
ObjName string `json:"obj_name"`
Duration uint32 `json:"duration"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjFadeIn(params.ObjName, params.Duration)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjFadeOut":
var params struct {
ObjName string `json:"obj_name"`
Duration uint32 `json:"duration"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjFadeOut(params.ObjName, params.Duration)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjSetLabelText":
var params struct {
ObjName string `json:"obj_name"`
Text string `json:"text"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjSetLabelText(params.ObjName, params.Text)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UIObjSetImageSrc":
var params struct {
ObjName string `json:"obj_name"`
Image string `json:"image"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.UIObjSetImageSrc(params.ObjName, params.Image)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "DisplaySetRotation":
var params struct {
Rotation uint16 `json:"rotation"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
var result bool
result, err = n.DisplaySetRotation(params.Rotation)
if err == nil {
sendResponse(encoder, req.ID, "result", result)
return
}
case "UpdateLabelIfChanged":
var params struct {
ObjName string `json:"obj_name"`
NewText string `json:"new_text"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
n.UpdateLabelIfChanged(params.ObjName, params.NewText)
sendResponse(encoder, req.ID, "result", nil)
return
case "UpdateLabelAndChangeVisibility":
var params struct {
ObjName string `json:"obj_name"`
NewText string `json:"new_text"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
n.UpdateLabelAndChangeVisibility(params.ObjName, params.NewText)
sendResponse(encoder, req.ID, "result", nil)
return
case "SwitchToScreenIf":
var params struct {
ScreenName string `json:"screen_name"`
ShouldSwitch []string `json:"should_switch"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
n.SwitchToScreenIf(params.ScreenName, params.ShouldSwitch)
sendResponse(encoder, req.ID, "result", nil)
return
case "SwitchToScreenIfDifferent":
var params struct {
ScreenName string `json:"screen_name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
sendError(encoder, req.ID, -32602, "Invalid params", nil)
return
}
n.SwitchToScreenIfDifferent(params.ScreenName)
sendResponse(encoder, req.ID, "result", nil)
return
case "DoNotUseThisIsForCrashTestingOnly":
n.DoNotUseThisIsForCrashTestingOnly()
sendResponse(encoder, req.ID, "result", nil)
return
default:
sendError(encoder, req.ID, -32601, fmt.Sprintf("Method not found: %s", req.Method), nil)
return
}
if err != nil {
sendError(encoder, req.ID, -32000, err.Error(), nil)
return
}
sendResponse(encoder, req.ID, "result", result)
}
func sendResponse(encoder *json.Encoder, id interface{}, key string, result interface{}) {
response := map[string]interface{}{
"jsonrpc": "2.0",
key: result,
}
if id != nil {
response["id"] = id
}
if err := encoder.Encode(response); err != nil {
fmt.Fprintf(os.Stderr, "failed to send response: %v\n", err)
}
}
func sendError(encoder *json.Encoder, id interface{}, code int, message string, data interface{}) {
response := map[string]interface{}{
"jsonrpc": "2.0",
"error": map[string]interface{}{
"code": code,
"message": message,
},
}
if id != nil {
response["id"] = id
}
if data != nil {
response["error"].(map[string]interface{})["data"] = data
}
if err := encoder.Encode(response); err != nil {
fmt.Fprintf(os.Stderr, "failed to send error: %v\n", err)
}
}
func sendEvent(eventType string, data interface{}) {
event := map[string]interface{}{
"jsonrpc": "2.0",
"method": "event",
"params": map[string]interface{}{
"type": eventType,
"data": data,
},
}
encoder := json.NewEncoder(os.Stdout)
if err := encoder.Encode(event); err != nil {
fmt.Fprintf(os.Stderr, "failed to send event: %v\n", err)
}
}