mirror of https://github.com/jetkvm/kvm.git
first working POC
This commit is contained in:
parent
ffc26ac4e7
commit
bca9afd1d7
32
cmd/main.go
32
cmd/main.go
|
|
@ -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
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -47,12 +45,14 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
|||
}
|
||||
event := &pb.Event{
|
||||
Type: "video_state_change",
|
||||
VideoState: &pb.VideoState{
|
||||
Ready: state.Ready,
|
||||
Error: state.Error,
|
||||
Width: int32(state.Width),
|
||||
Height: int32(state.Height),
|
||||
FramePerSecond: state.FramePerSecond,
|
||||
Data: &pb.Event_VideoState{
|
||||
VideoState: &pb.VideoState{
|
||||
Ready: state.Ready,
|
||||
Error: state.Error,
|
||||
Width: int32(state.Width),
|
||||
Height: int32(state.Height),
|
||||
FramePerSecond: state.FramePerSecond,
|
||||
},
|
||||
},
|
||||
}
|
||||
s.broadcastEvent(event)
|
||||
|
|
@ -63,8 +63,10 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
|||
originalIndevEvent(event)
|
||||
}
|
||||
s.broadcastEvent(&pb.Event{
|
||||
Type: "indev_event",
|
||||
IndevEvent: event,
|
||||
Type: "indev_event",
|
||||
Data: &pb.Event_IndevEvent{
|
||||
IndevEvent: event,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -73,8 +75,10 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
|||
originalRpcEvent(event)
|
||||
}
|
||||
s.broadcastEvent(&pb.Event{
|
||||
Type: "rpc_event",
|
||||
RpcEvent: event,
|
||||
Type: "rpc_event",
|
||||
Data: &pb.Event_RpcEvent{
|
||||
RpcEvent: event,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -84,9 +88,11 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
|||
}
|
||||
s.broadcastEvent(&pb.Event{
|
||||
Type: "video_frame",
|
||||
VideoFrame: &pb.VideoFrame{
|
||||
Frame: frame,
|
||||
DurationNs: duration.Nanoseconds(),
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IPC client: %w", err)
|
||||
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 {
|
||||
nativeLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||
break
|
||||
}
|
||||
now := time.Now()
|
||||
sinceLastFrame := now.Sub(lastFrame)
|
||||
lastFrame = now
|
||||
p.options.OnVideoFrameReceived(inboundPacket[:n], sinceLastFrame)
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
2
main.go
2
main.go
|
|
@ -40,8 +40,8 @@ func Main() {
|
|||
go runWatchdog()
|
||||
go confirmCurrentSystem()
|
||||
|
||||
initDisplay()
|
||||
initNative(systemVersionLocal, appVersionLocal)
|
||||
initDisplay()
|
||||
|
||||
http.DefaultClient.Timeout = 1 * time.Minute
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue