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/erikdubbelboer/gspt"
|
||||||
"github.com/jetkvm/kvm"
|
"github.com/jetkvm/kvm"
|
||||||
)
|
"github.com/jetkvm/kvm/internal/native"
|
||||||
|
"github.com/jetkvm/kvm/internal/supervisor"
|
||||||
const (
|
|
||||||
envChildID = "JETKVM_CHILD_ID"
|
|
||||||
envSubcomponent = "JETKVM_SUBCOMPONENT"
|
|
||||||
errorDumpDir = "/userdata/jetkvm/crashdump"
|
|
||||||
errorDumpLastFile = "last-crash.log"
|
|
||||||
errorDumpTemplate = "jetkvm-%s.log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -28,14 +22,14 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func program() {
|
func program() {
|
||||||
subcomponentOverride := os.Getenv(envSubcomponent)
|
subcomponentOverride := os.Getenv(supervisor.EnvSubcomponent)
|
||||||
if subcomponentOverride != "" {
|
if subcomponentOverride != "" {
|
||||||
subcomponent = subcomponentOverride
|
subcomponent = subcomponentOverride
|
||||||
}
|
}
|
||||||
switch subcomponent {
|
switch subcomponent {
|
||||||
case "native":
|
case "native":
|
||||||
gspt.SetProcTitle(os.Args[0] + " [native]")
|
gspt.SetProcTitle(os.Args[0] + " [native]")
|
||||||
kvm.RunNativeProcess()
|
native.RunNativeProcess(os.Args[0])
|
||||||
default:
|
default:
|
||||||
gspt.SetProcTitle(os.Args[0] + " [app]")
|
gspt.SetProcTitle(os.Args[0] + " [app]")
|
||||||
kvm.Main()
|
kvm.Main()
|
||||||
|
|
@ -58,7 +52,7 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
childID := os.Getenv(envChildID)
|
childID := os.Getenv(supervisor.EnvChildID)
|
||||||
switch childID {
|
switch childID {
|
||||||
case "":
|
case "":
|
||||||
doSupervise()
|
doSupervise()
|
||||||
|
|
@ -90,11 +84,11 @@ func supervise() error {
|
||||||
// run the child binary
|
// run the child binary
|
||||||
cmd := exec.Command(binPath)
|
cmd := exec.Command(binPath)
|
||||||
|
|
||||||
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
|
lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile)
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(), []string{
|
cmd.Env = append(os.Environ(), []string{
|
||||||
fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()),
|
fmt.Sprintf("%s=%s", supervisor.EnvChildID, kvm.GetBuiltAppVersion()),
|
||||||
fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath),
|
fmt.Sprintf("%s=%s", supervisor.ErrorDumpLastFile, lastFilePath),
|
||||||
}...)
|
}...)
|
||||||
cmd.Args = os.Args
|
cmd.Args = os.Args
|
||||||
|
|
||||||
|
|
@ -202,11 +196,11 @@ func renameFile(f *os.File, newName string) error {
|
||||||
|
|
||||||
func ensureErrorDumpDir() error {
|
func ensureErrorDumpDir() error {
|
||||||
// TODO: check if the directory is writable
|
// TODO: check if the directory is writable
|
||||||
f, err := os.Stat(errorDumpDir)
|
f, err := os.Stat(supervisor.ErrorDumpDir)
|
||||||
if err == nil && f.IsDir() {
|
if err == nil && f.IsDir() {
|
||||||
return nil
|
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 fmt.Errorf("failed to create error dump directory: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -216,7 +210,7 @@ func createErrorDump(logFile *os.File) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
fileName := fmt.Sprintf(
|
fileName := fmt.Sprintf(
|
||||||
errorDumpTemplate,
|
supervisor.ErrorDumpTemplate,
|
||||||
time.Now().Format("20060102-150405"),
|
time.Now().Format("20060102-150405"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -226,7 +220,7 @@ func createErrorDump(logFile *os.File) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := filepath.Join(errorDumpDir, fileName)
|
filePath := filepath.Join(supervisor.ErrorDumpDir, fileName)
|
||||||
if err := renameFile(logFile, filePath); err != nil {
|
if err := renameFile(logFile, filePath); err != nil {
|
||||||
fmt.Printf("failed to rename file: %v\n", err)
|
fmt.Printf("failed to rename file: %v\n", err)
|
||||||
return
|
return
|
||||||
|
|
@ -234,7 +228,7 @@ func createErrorDump(logFile *os.File) {
|
||||||
|
|
||||||
fmt.Printf("error dump copied: %s\n", filePath)
|
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 {
|
if err := ensureSymlink(filePath, lastFilePath); err != nil {
|
||||||
fmt.Printf("failed to create symlink: %v\n", err)
|
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/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.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/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/creack/goselect v0.1.2 // 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/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // 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 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 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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=
|
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/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 h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -47,12 +45,14 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
||||||
}
|
}
|
||||||
event := &pb.Event{
|
event := &pb.Event{
|
||||||
Type: "video_state_change",
|
Type: "video_state_change",
|
||||||
VideoState: &pb.VideoState{
|
Data: &pb.Event_VideoState{
|
||||||
Ready: state.Ready,
|
VideoState: &pb.VideoState{
|
||||||
Error: state.Error,
|
Ready: state.Ready,
|
||||||
Width: int32(state.Width),
|
Error: state.Error,
|
||||||
Height: int32(state.Height),
|
Width: int32(state.Width),
|
||||||
FramePerSecond: state.FramePerSecond,
|
Height: int32(state.Height),
|
||||||
|
FramePerSecond: state.FramePerSecond,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
s.broadcastEvent(event)
|
s.broadcastEvent(event)
|
||||||
|
|
@ -63,8 +63,10 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
||||||
originalIndevEvent(event)
|
originalIndevEvent(event)
|
||||||
}
|
}
|
||||||
s.broadcastEvent(&pb.Event{
|
s.broadcastEvent(&pb.Event{
|
||||||
Type: "indev_event",
|
Type: "indev_event",
|
||||||
IndevEvent: event,
|
Data: &pb.Event_IndevEvent{
|
||||||
|
IndevEvent: event,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,8 +75,10 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
||||||
originalRpcEvent(event)
|
originalRpcEvent(event)
|
||||||
}
|
}
|
||||||
s.broadcastEvent(&pb.Event{
|
s.broadcastEvent(&pb.Event{
|
||||||
Type: "rpc_event",
|
Type: "rpc_event",
|
||||||
RpcEvent: event,
|
Data: &pb.Event_RpcEvent{
|
||||||
|
RpcEvent: event,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,9 +88,11 @@ func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
||||||
}
|
}
|
||||||
s.broadcastEvent(&pb.Event{
|
s.broadcastEvent(&pb.Event{
|
||||||
Type: "video_frame",
|
Type: "video_frame",
|
||||||
VideoFrame: &pb.VideoFrame{
|
Data: &pb.Event_VideoFrame{
|
||||||
Frame: frame,
|
VideoFrame: &pb.VideoFrame{
|
||||||
DurationNs: duration.Nanoseconds(),
|
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
|
// Video methods
|
||||||
func (s *grpcServer) VideoSetSleepMode(ctx context.Context, req *pb.VideoSetSleepModeRequest) (*pb.Empty, error) {
|
func (s *grpcServer) VideoSetSleepMode(ctx context.Context, req *pb.VideoSetSleepModeRequest) (*pb.Empty, error) {
|
||||||
if err := s.native.VideoSetSleepMode(req.Enabled); err != nil {
|
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
|
// StartGRPCServer starts the gRPC server on a Unix domain socket
|
||||||
func StartGRPCServer(server *grpcServer, socketPath string, logger *zerolog.Logger) (*grpc.Server, net.Listener, error) {
|
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)
|
lis, err := net.Listen("unix", socketPath)
|
||||||
if err != nil {
|
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
|
package native
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
"github.com/rs/zerolog"
|
"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")
|
var displayLogger = logging.GetSubsystemLogger("display")
|
||||||
|
|
||||||
type nativeLogMessage struct {
|
type nativeLogMessage struct {
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,11 @@ type Native struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type NativeOptions struct {
|
type NativeOptions struct {
|
||||||
Disable bool
|
Disable bool `env:"JETKVM_NATIVE_DISABLE"`
|
||||||
SystemVersion *semver.Version
|
SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"`
|
||||||
AppVersion *semver.Version
|
AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"`
|
||||||
DisplayRotation uint16
|
DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"`
|
||||||
DefaultQualityFactor float64
|
DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"`
|
||||||
OnVideoStateChange func(state VideoState)
|
OnVideoStateChange func(state VideoState)
|
||||||
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||||
OnIndevEvent func(event string)
|
OnIndevEvent func(event string)
|
||||||
|
|
@ -50,7 +50,7 @@ func NewNative(opts NativeOptions) *Native {
|
||||||
onVideoFrameReceived := opts.OnVideoFrameReceived
|
onVideoFrameReceived := opts.OnVideoFrameReceived
|
||||||
if onVideoFrameReceived == nil {
|
if onVideoFrameReceived == nil {
|
||||||
onVideoFrameReceived = func(frame []byte, duration time.Duration) {
|
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
|
// NativeService provides methods to interact with the native layer
|
||||||
service NativeService {
|
service NativeService {
|
||||||
// Init
|
// Ready check
|
||||||
rpc Init(InitRequest) returns (InitResponse);
|
rpc IsReady(IsReadyRequest) returns (IsReadyResponse);
|
||||||
|
|
||||||
// Video methods
|
// Video methods
|
||||||
rpc VideoSetSleepMode(VideoSetSleepModeRequest) returns (Empty);
|
rpc VideoSetSleepMode(VideoSetSleepModeRequest) returns (Empty);
|
||||||
|
|
@ -52,16 +52,12 @@ service NativeService {
|
||||||
// Messages
|
// Messages
|
||||||
message Empty {}
|
message Empty {}
|
||||||
|
|
||||||
message InitRequest {
|
message IsReadyRequest {}
|
||||||
string system_version = 1;
|
|
||||||
string app_version = 2;
|
|
||||||
uint32 display_rotation = 3;
|
|
||||||
double default_quality_factor = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message InitResponse {
|
message IsReadyResponse {
|
||||||
bool success = 1;
|
bool ready = 1;
|
||||||
string error = 2;
|
string error = 2;
|
||||||
|
bool video_ready = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VideoState {
|
message VideoState {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,19 +1,22 @@
|
||||||
package native
|
package native
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
|
||||||
"github.com/jetkvm/kvm/internal/supervisor"
|
"github.com/jetkvm/kvm/internal/supervisor"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxFrameSize = 1920 * 1080 / 2
|
||||||
|
)
|
||||||
|
|
||||||
// cmdWrapper wraps exec.Cmd to implement processCmd interface
|
// cmdWrapper wraps exec.Cmd to implement processCmd interface
|
||||||
type cmdWrapper struct {
|
type cmdWrapper struct {
|
||||||
*exec.Cmd
|
*exec.Cmd
|
||||||
|
|
@ -43,94 +46,116 @@ func (p *processWrapper) Signal(sig interface{}) error {
|
||||||
|
|
||||||
// NativeProxy is a proxy that communicates with a separate native process
|
// NativeProxy is a proxy that communicates with a separate native process
|
||||||
type NativeProxy struct {
|
type NativeProxy struct {
|
||||||
client *IPCClient
|
nativeUnixSocket string
|
||||||
cmd *exec.Cmd
|
videoStreamUnixSocket string
|
||||||
wrapped *cmdWrapper
|
videoStreamListener net.Listener
|
||||||
|
binaryPath string
|
||||||
|
|
||||||
|
client *GRPCClient
|
||||||
|
cmd *cmdWrapper
|
||||||
logger *zerolog.Logger
|
logger *zerolog.Logger
|
||||||
ready chan struct{}
|
ready chan struct{}
|
||||||
|
options *NativeOptions
|
||||||
restartM sync.Mutex
|
restartM sync.Mutex
|
||||||
stopped bool
|
stopped bool
|
||||||
opts NativeProxyOptions
|
|
||||||
binaryPath string
|
|
||||||
configJSON []byte
|
|
||||||
processWait chan error
|
processWait chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NativeProxyOptions are options for creating a NativeProxy
|
func ensureDirectoryExists(path string) error {
|
||||||
type NativeProxyOptions struct {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
Disable bool
|
return os.MkdirAll(path, 0600)
|
||||||
SystemVersion *semver.Version
|
}
|
||||||
AppVersion *semver.Version
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNativeProxy creates a new NativeProxy that spawns a separate process
|
// NewNativeProxy creates a new NativeProxy that spawns a separate process
|
||||||
func NewNativeProxy(opts NativeProxyOptions) (*NativeProxy, error) {
|
func NewNativeProxy(opts NativeOptions) (*NativeProxy, error) {
|
||||||
if opts.Logger == nil {
|
nativeUnixSocket := "jetkvm-native-grpc"
|
||||||
opts.Logger = nativeLogger
|
videoStreamUnixSocket := "@jetkvm-native-video-stream"
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current executable path to spawn itself
|
// Get the current executable path to spawn itself
|
||||||
exePath, err := os.Executable()
|
exePath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||||
}
|
}
|
||||||
binaryPath := exePath
|
|
||||||
|
|
||||||
config := ProcessConfig{
|
proxy := &NativeProxy{
|
||||||
Disable: opts.Disable,
|
nativeUnixSocket: nativeUnixSocket,
|
||||||
SystemVersion: "",
|
videoStreamUnixSocket: videoStreamUnixSocket,
|
||||||
AppVersion: "",
|
binaryPath: exePath,
|
||||||
DisplayRotation: opts.DisplayRotation,
|
logger: nativeLogger,
|
||||||
DefaultQualityFactor: opts.DefaultQualityFactor,
|
ready: make(chan struct{}),
|
||||||
|
options: &opts,
|
||||||
|
processWait: make(chan error, 1),
|
||||||
}
|
}
|
||||||
if opts.SystemVersion != nil {
|
proxy.cmd, err = proxy.spawnProcess()
|
||||||
config.SystemVersion = opts.SystemVersion.String()
|
nativeLogger.Info().Msg("spawned process")
|
||||||
}
|
|
||||||
if opts.AppVersion != nil {
|
|
||||||
config.AppVersion = opts.AppVersion.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSON, err := json.Marshal(config)
|
|
||||||
if err != nil {
|
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
|
cmd.Stderr = os.Stderr // Forward stderr to parent
|
||||||
// Set environment variable to indicate native process mode
|
// 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
|
// Wrap cmd to implement processCmd interface
|
||||||
wrappedCmd := &cmdWrapper{Cmd: cmd}
|
wrappedCmd := &cmdWrapper{Cmd: cmd}
|
||||||
|
|
||||||
client, err := NewIPCClient(wrappedCmd, opts.Logger)
|
return wrappedCmd, nil
|
||||||
if err != nil {
|
}
|
||||||
return nil, fmt.Errorf("failed to create IPC client: %w", err)
|
|
||||||
|
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
|
// Start starts the native process
|
||||||
|
|
@ -146,6 +171,15 @@ func (p *NativeProxy) Start() error {
|
||||||
return fmt.Errorf("failed to start native process: %w", err)
|
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
|
// Wait for ready signal from the native process
|
||||||
if err := p.client.WaitReady(); err != nil {
|
if err := p.client.WaitReady(); err != nil {
|
||||||
// Clean up if ready failed
|
// Clean up if ready failed
|
||||||
|
|
@ -153,9 +187,12 @@ func (p *NativeProxy) Start() error {
|
||||||
_ = p.cmd.Process.Kill()
|
_ = p.cmd.Process.Kill()
|
||||||
_ = p.cmd.Wait()
|
_ = 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
|
// Start monitoring process for crashes
|
||||||
go p.monitorProcess()
|
go p.monitorProcess()
|
||||||
|
|
||||||
|
|
@ -216,14 +253,10 @@ func (p *NativeProxy) restartProcess() error {
|
||||||
return fmt.Errorf("proxy is stopped")
|
return fmt.Errorf("proxy is stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new command
|
wrappedCmd, err := p.spawnProcess()
|
||||||
cmd := exec.Command(p.binaryPath, string(p.configJSON))
|
if err != nil {
|
||||||
cmd.Stderr = os.Stderr
|
return fmt.Errorf("failed to spawn process: %w", err)
|
||||||
// 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}
|
|
||||||
|
|
||||||
// Close old client
|
// Close old client
|
||||||
if p.client != nil {
|
if p.client != nil {
|
||||||
|
|
@ -231,7 +264,7 @@ func (p *NativeProxy) restartProcess() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new client
|
// Create new client
|
||||||
client, err := NewIPCClient(wrappedCmd, p.logger)
|
client, err := NewGRPCClient(p.nativeUnixSocket, p.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create IPC client: %w", err)
|
return fmt.Errorf("failed to create IPC client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -240,90 +273,89 @@ func (p *NativeProxy) restartProcess() error {
|
||||||
p.setupEventHandlers(client)
|
p.setupEventHandlers(client)
|
||||||
|
|
||||||
// Start the process
|
// 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)
|
return fmt.Errorf("failed to start native process: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for ready
|
// Wait for ready
|
||||||
if err := client.WaitReady(); err != nil {
|
if err := client.WaitReady(); err != nil {
|
||||||
if cmd.Process != nil {
|
if wrappedCmd.Process != nil {
|
||||||
_ = cmd.Process.Kill()
|
_ = wrappedCmd.Process.Kill()
|
||||||
_ = cmd.Wait()
|
_ = wrappedCmd.Wait()
|
||||||
}
|
}
|
||||||
return fmt.Errorf("timeout waiting for ready: %w", err)
|
return fmt.Errorf("timeout waiting for ready: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.cmd = cmd
|
p.cmd = wrappedCmd
|
||||||
p.wrapped = wrappedCmd
|
|
||||||
p.client = client
|
p.client = client
|
||||||
|
|
||||||
p.logger.Info().Msg("native process restarted successfully")
|
p.logger.Info().Msg("native process restarted successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) setupEventHandlers(client *IPCClient) {
|
func (p *NativeProxy) setupEventHandlers(client *GRPCClient) {
|
||||||
if p.opts.OnVideoStateChange != nil {
|
// if p.opts.OnVideoStateChange != nil {
|
||||||
client.OnEvent("video_state_change", func(data interface{}) {
|
// client.OnEvent("video_state_change", func(data interface{}) {
|
||||||
dataBytes, err := json.Marshal(data)
|
// dataBytes, err := json.Marshal(data)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
p.logger.Warn().Err(err).Msg("failed to marshal video state event")
|
// p.logger.Warn().Err(err).Msg("failed to marshal video state event")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
var state VideoState
|
// var state VideoState
|
||||||
if err := json.Unmarshal(dataBytes, &state); err != nil {
|
// if err := json.Unmarshal(dataBytes, &state); err != nil {
|
||||||
p.logger.Warn().Err(err).Msg("failed to unmarshal video state event")
|
// p.logger.Warn().Err(err).Msg("failed to unmarshal video state event")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
p.opts.OnVideoStateChange(state)
|
// p.opts.OnVideoStateChange(state)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if p.opts.OnIndevEvent != nil {
|
// if p.opts.OnIndevEvent != nil {
|
||||||
client.OnEvent("indev_event", func(data interface{}) {
|
// client.OnEvent("indev_event", func(data interface{}) {
|
||||||
if event, ok := data.(string); ok {
|
// if event, ok := data.(string); ok {
|
||||||
p.opts.OnIndevEvent(event)
|
// p.opts.OnIndevEvent(event)
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if p.opts.OnRpcEvent != nil {
|
// if p.opts.OnRpcEvent != nil {
|
||||||
client.OnEvent("rpc_event", func(data interface{}) {
|
// client.OnEvent("rpc_event", func(data interface{}) {
|
||||||
if event, ok := data.(string); ok {
|
// if event, ok := data.(string); ok {
|
||||||
p.opts.OnRpcEvent(event)
|
// p.opts.OnRpcEvent(event)
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if p.opts.OnVideoFrameReceived != nil {
|
// if p.opts.OnVideoFrameReceived != nil {
|
||||||
client.OnEvent("video_frame", func(data interface{}) {
|
// client.OnEvent("video_frame", func(data interface{}) {
|
||||||
dataMap, ok := data.(map[string]interface{})
|
// dataMap, ok := data.(map[string]interface{})
|
||||||
if !ok {
|
// if !ok {
|
||||||
p.logger.Warn().Msg("invalid video frame event data")
|
// p.logger.Warn().Msg("invalid video frame event data")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
frameData, ok := dataMap["frame"].([]interface{})
|
// frameData, ok := dataMap["frame"].([]interface{})
|
||||||
if !ok {
|
// if !ok {
|
||||||
p.logger.Warn().Msg("invalid frame data in event")
|
// p.logger.Warn().Msg("invalid frame data in event")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
frame := make([]byte, len(frameData))
|
// frame := make([]byte, len(frameData))
|
||||||
for i, v := range frameData {
|
// for i, v := range frameData {
|
||||||
if b, ok := v.(float64); ok {
|
// if b, ok := v.(float64); ok {
|
||||||
frame[i] = byte(b)
|
// frame[i] = byte(b)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
durationNs, ok := dataMap["duration"].(float64)
|
// durationNs, ok := dataMap["duration"].(float64)
|
||||||
if !ok {
|
// if !ok {
|
||||||
p.logger.Warn().Msg("invalid duration in event")
|
// p.logger.Warn().Msg("invalid duration in event")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
p.opts.OnVideoFrameReceived(frame, time.Duration(durationNs))
|
// p.opts.OnVideoFrameReceived(frame, time.Duration(durationNs))
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the native process
|
// Stop stops the native process
|
||||||
|
|
@ -347,336 +379,123 @@ func (p *NativeProxy) Stop() error {
|
||||||
return nil
|
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 {
|
func (p *NativeProxy) VideoSetSleepMode(enabled bool) error {
|
||||||
_, err := p.client.Call("VideoSetSleepMode", map[string]interface{}{
|
return p.client.VideoSetSleepMode(enabled)
|
||||||
"enabled": enabled,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoGetSleepMode() (bool, error) {
|
func (p *NativeProxy) VideoGetSleepMode() (bool, error) {
|
||||||
resp, err := p.client.Call("VideoGetSleepMode", nil)
|
return p.client.VideoGetSleepMode()
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(bool)
|
|
||||||
if !ok {
|
|
||||||
return false, fmt.Errorf("invalid response type")
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoSleepModeSupported() bool {
|
func (p *NativeProxy) VideoSleepModeSupported() bool {
|
||||||
resp, err := p.client.Call("VideoSleepModeSupported", nil)
|
return p.client.VideoSleepModeSupported()
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(bool)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoSetQualityFactor(factor float64) error {
|
func (p *NativeProxy) VideoSetQualityFactor(factor float64) error {
|
||||||
_, err := p.client.Call("VideoSetQualityFactor", map[string]interface{}{
|
return p.client.VideoSetQualityFactor(factor)
|
||||||
"factor": factor,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoGetQualityFactor() (float64, error) {
|
func (p *NativeProxy) VideoGetQualityFactor() (float64, error) {
|
||||||
resp, err := p.client.Call("VideoGetQualityFactor", nil)
|
return p.client.VideoGetQualityFactor()
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(float64)
|
|
||||||
if !ok {
|
|
||||||
return 0, fmt.Errorf("invalid response type")
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoSetEDID(edid string) error {
|
func (p *NativeProxy) VideoSetEDID(edid string) error {
|
||||||
_, err := p.client.Call("VideoSetEDID", map[string]interface{}{
|
return p.client.VideoSetEDID(edid)
|
||||||
"edid": edid,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoGetEDID() (string, error) {
|
func (p *NativeProxy) VideoGetEDID() (string, error) {
|
||||||
resp, err := p.client.Call("VideoGetEDID", nil)
|
return p.client.VideoGetEDID()
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid response type")
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoLogStatus() (string, error) {
|
func (p *NativeProxy) VideoLogStatus() (string, error) {
|
||||||
resp, err := p.client.Call("VideoLogStatus", nil)
|
return p.client.VideoLogStatus()
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid response type")
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoStop() error {
|
func (p *NativeProxy) VideoStop() error {
|
||||||
_, err := p.client.Call("VideoStop", nil)
|
return p.client.VideoStop()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) VideoStart() error {
|
func (p *NativeProxy) VideoStart() error {
|
||||||
_, err := p.client.Call("VideoStart", nil)
|
return p.client.VideoStart()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) GetLVGLVersion() (string, error) {
|
func (p *NativeProxy) GetLVGLVersion() (string, error) {
|
||||||
resp, err := p.client.Call("GetLVGLVersion", nil)
|
return p.client.GetLVGLVersion()
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid response type")
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjHide(objName string) (bool, error) {
|
func (p *NativeProxy) UIObjHide(objName string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjHide", map[string]interface{}{
|
return p.client.UIObjHide(objName)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjShow(objName string) (bool, error) {
|
func (p *NativeProxy) UIObjShow(objName string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjShow", map[string]interface{}{
|
return p.client.UIObjShow(objName)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UISetVar(name string, value string) {
|
func (p *NativeProxy) UISetVar(name string, value string) {
|
||||||
_, _ = p.client.Call("UISetVar", map[string]interface{}{
|
p.client.UISetVar(name, value)
|
||||||
"name": name,
|
|
||||||
"value": value,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIGetVar(name string) string {
|
func (p *NativeProxy) UIGetVar(name string) string {
|
||||||
resp, err := p.client.Call("UIGetVar", map[string]interface{}{
|
return p.client.UIGetVar(name)
|
||||||
"name": name,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
result, ok := resp.Result.(string)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) {
|
func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjAddState", map[string]interface{}{
|
return p.client.UIObjAddState(objName, state)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) {
|
func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjClearState", map[string]interface{}{
|
return p.client.UIObjClearState(objName, state)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) {
|
func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjAddFlag", map[string]interface{}{
|
return p.client.UIObjAddFlag(objName, flag)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) {
|
func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjClearFlag", map[string]interface{}{
|
return p.client.UIObjClearFlag(objName, flag)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) {
|
func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjSetOpacity", map[string]interface{}{
|
return p.client.UIObjSetOpacity(objName, opacity)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) {
|
func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjFadeIn", map[string]interface{}{
|
return p.client.UIObjFadeIn(objName, duration)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) {
|
func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjFadeOut", map[string]interface{}{
|
return p.client.UIObjFadeOut(objName, duration)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) {
|
func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjSetLabelText", map[string]interface{}{
|
return p.client.UIObjSetLabelText(objName, text)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) {
|
func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) {
|
||||||
resp, err := p.client.Call("UIObjSetImageSrc", map[string]interface{}{
|
return p.client.UIObjSetImageSrc(objName, image)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) {
|
func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) {
|
||||||
resp, err := p.client.Call("DisplaySetRotation", map[string]interface{}{
|
return p.client.DisplaySetRotation(rotation)
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) {
|
func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) {
|
||||||
_, _ = p.client.Call("UpdateLabelIfChanged", map[string]interface{}{
|
p.client.UpdateLabelIfChanged(objName, newText)
|
||||||
"obj_name": objName,
|
|
||||||
"new_text": newText,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) {
|
func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) {
|
||||||
_, _ = p.client.Call("UpdateLabelAndChangeVisibility", map[string]interface{}{
|
p.client.UpdateLabelAndChangeVisibility(objName, newText)
|
||||||
"obj_name": objName,
|
|
||||||
"new_text": newText,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) {
|
func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) {
|
||||||
_, _ = p.client.Call("SwitchToScreenIf", map[string]interface{}{
|
p.client.SwitchToScreenIf(screenName, shouldSwitch)
|
||||||
"screen_name": screenName,
|
|
||||||
"should_switch": shouldSwitch,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) {
|
func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) {
|
||||||
_, _ = p.client.Call("SwitchToScreenIfDifferent", map[string]interface{}{
|
p.client.SwitchToScreenIfDifferent(screenName)
|
||||||
"screen_name": screenName,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() {
|
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 runWatchdog()
|
||||||
go confirmCurrentSystem()
|
go confirmCurrentSystem()
|
||||||
|
|
||||||
initDisplay()
|
|
||||||
initNative(systemVersionLocal, appVersionLocal)
|
initNative(systemVersionLocal, appVersionLocal)
|
||||||
|
initDisplay()
|
||||||
|
|
||||||
http.DefaultClient.Timeout = 1 * time.Minute
|
http.DefaultClient.Timeout = 1 * time.Minute
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||||
|
nativeLogger.Info().Msg("initializing native")
|
||||||
var err error
|
var err error
|
||||||
nativeInstance, err = native.NewNativeProxy(native.NativeProxyOptions{
|
nativeInstance, err = native.NewNativeProxy(native.NativeOptions{
|
||||||
Disable: failsafeModeActive,
|
Disable: failsafeModeActive,
|
||||||
SystemVersion: systemVersion,
|
SystemVersion: systemVersion,
|
||||||
AppVersion: appVersion,
|
AppVersion: appVersion,
|
||||||
DisplayRotation: config.GetDisplayRotation(),
|
DisplayRotation: config.GetDisplayRotation(),
|
||||||
DefaultQualityFactor: config.VideoQualityFactor,
|
DefaultQualityFactor: config.VideoQualityFactor,
|
||||||
Logger: nativeLogger,
|
|
||||||
OnVideoStateChange: func(state native.VideoState) {
|
OnVideoStateChange: func(state native.VideoState) {
|
||||||
lastVideoState = state
|
lastVideoState = state
|
||||||
triggerVideoStateUpdate()
|
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