kvm/internal/native/proxy.go

683 lines
16 KiB
Go

package native
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"sync"
"syscall"
"time"
"github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/supervisor"
"github.com/rs/zerolog"
)
// cmdWrapper wraps exec.Cmd to implement processCmd interface
type cmdWrapper struct {
*exec.Cmd
}
func (c *cmdWrapper) GetProcess() interface {
Kill() error
Signal(sig interface{}) error
} {
return &processWrapper{Process: c.Cmd.Process}
}
type processWrapper struct {
*os.Process
}
func (p *processWrapper) Signal(sig interface{}) error {
if sig == nil {
// Check if process is alive by sending signal 0
return p.Process.Signal(os.Signal(syscall.Signal(0)))
}
if s, ok := sig.(os.Signal); ok {
return p.Process.Signal(s)
}
return fmt.Errorf("invalid signal type")
}
// NativeProxy is a proxy that communicates with a separate native process
type NativeProxy struct {
client *IPCClient
cmd *exec.Cmd
wrapped *cmdWrapper
logger *zerolog.Logger
ready chan struct{}
restartM sync.Mutex
stopped bool
opts NativeProxyOptions
binaryPath string
configJSON []byte
processWait chan error
}
// NativeProxyOptions are options for creating a NativeProxy
type NativeProxyOptions struct {
Disable bool
SystemVersion *semver.Version
AppVersion *semver.Version
DisplayRotation uint16
DefaultQualityFactor float64
OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string)
OnRpcEvent func(event string)
Logger *zerolog.Logger
}
// NewNativeProxy creates a new NativeProxy that spawns a separate process
func NewNativeProxy(opts NativeProxyOptions) (*NativeProxy, error) {
if opts.Logger == nil {
opts.Logger = nativeLogger
}
// Get the current executable path to spawn itself
exePath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
binaryPath := exePath
config := ProcessConfig{
Disable: opts.Disable,
SystemVersion: "",
AppVersion: "",
DisplayRotation: opts.DisplayRotation,
DefaultQualityFactor: opts.DefaultQualityFactor,
}
if opts.SystemVersion != nil {
config.SystemVersion = opts.SystemVersion.String()
}
if opts.AppVersion != nil {
config.AppVersion = opts.AppVersion.String()
}
configJSON, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %w", err)
}
cmd := exec.Command(binaryPath, string(configJSON))
cmd.Stderr = os.Stderr // Forward stderr to parent
// Set environment variable to indicate native process mode
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=native", supervisor.EnvSubcomponent))
// Wrap cmd to implement processCmd interface
wrappedCmd := &cmdWrapper{Cmd: cmd}
client, err := NewIPCClient(wrappedCmd, opts.Logger)
if err != nil {
return nil, fmt.Errorf("failed to create IPC client: %w", err)
}
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
func (p *NativeProxy) Start() error {
p.restartM.Lock()
defer p.restartM.Unlock()
if p.stopped {
return fmt.Errorf("proxy is stopped")
}
if err := p.cmd.Start(); err != nil {
return fmt.Errorf("failed to start native process: %w", err)
}
// Wait for ready signal from the native process
if err := p.client.WaitReady(); err != nil {
// Clean up if ready failed
if p.cmd.Process != nil {
_ = p.cmd.Process.Kill()
_ = p.cmd.Wait()
}
return err
}
// Start monitoring process for crashes
go p.monitorProcess()
close(p.ready)
return nil
}
// monitorProcess monitors the native process and restarts it if it crashes
func (p *NativeProxy) monitorProcess() {
for {
p.restartM.Lock()
cmd := p.cmd
stopped := p.stopped
p.restartM.Unlock()
if stopped {
return
}
if cmd == nil {
return
}
err := cmd.Wait()
select {
case p.processWait <- err:
default:
}
p.restartM.Lock()
if p.stopped {
p.restartM.Unlock()
return
}
p.restartM.Unlock()
p.logger.Warn().Err(err).Msg("native process exited, restarting...")
// Wait a bit before restarting
time.Sleep(1 * time.Second)
// Restart the process
if err := p.restartProcess(); err != nil {
p.logger.Error().Err(err).Msg("failed to restart native process")
// Wait longer before retrying
time.Sleep(5 * time.Second)
continue
}
}
}
// restartProcess restarts the native process
func (p *NativeProxy) restartProcess() error {
p.restartM.Lock()
defer p.restartM.Unlock()
if p.stopped {
return fmt.Errorf("proxy is stopped")
}
// Create new command
cmd := exec.Command(p.binaryPath, string(p.configJSON))
cmd.Stderr = os.Stderr
// Set environment variable to indicate native process mode
cmd.Env = append(os.Environ(), "JETKVM_NATIVE_PROCESS=1")
// Wrap cmd to implement processCmd interface
wrappedCmd := &cmdWrapper{Cmd: cmd}
// Close old client
if p.client != nil {
_ = p.client.Close()
}
// Create new client
client, err := NewIPCClient(wrappedCmd, p.logger)
if err != nil {
return fmt.Errorf("failed to create IPC client: %w", err)
}
// Set up event handlers again
p.setupEventHandlers(client)
// Start the process
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start native process: %w", err)
}
// Wait for ready
if err := client.WaitReady(); err != nil {
if cmd.Process != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}
return fmt.Errorf("timeout waiting for ready: %w", err)
}
p.cmd = cmd
p.wrapped = wrappedCmd
p.client = client
p.logger.Info().Msg("native process restarted successfully")
return nil
}
func (p *NativeProxy) setupEventHandlers(client *IPCClient) {
if p.opts.OnVideoStateChange != nil {
client.OnEvent("video_state_change", func(data interface{}) {
dataBytes, err := json.Marshal(data)
if err != nil {
p.logger.Warn().Err(err).Msg("failed to marshal video state event")
return
}
var state VideoState
if err := json.Unmarshal(dataBytes, &state); err != nil {
p.logger.Warn().Err(err).Msg("failed to unmarshal video state event")
return
}
p.opts.OnVideoStateChange(state)
})
}
if p.opts.OnIndevEvent != nil {
client.OnEvent("indev_event", func(data interface{}) {
if event, ok := data.(string); ok {
p.opts.OnIndevEvent(event)
}
})
}
if p.opts.OnRpcEvent != nil {
client.OnEvent("rpc_event", func(data interface{}) {
if event, ok := data.(string); ok {
p.opts.OnRpcEvent(event)
}
})
}
if p.opts.OnVideoFrameReceived != nil {
client.OnEvent("video_frame", func(data interface{}) {
dataMap, ok := data.(map[string]interface{})
if !ok {
p.logger.Warn().Msg("invalid video frame event data")
return
}
frameData, ok := dataMap["frame"].([]interface{})
if !ok {
p.logger.Warn().Msg("invalid frame data in event")
return
}
frame := make([]byte, len(frameData))
for i, v := range frameData {
if b, ok := v.(float64); ok {
frame[i] = byte(b)
}
}
durationNs, ok := dataMap["duration"].(float64)
if !ok {
p.logger.Warn().Msg("invalid duration in event")
return
}
p.opts.OnVideoFrameReceived(frame, time.Duration(durationNs))
})
}
}
// Stop stops the native process
func (p *NativeProxy) Stop() error {
p.restartM.Lock()
defer p.restartM.Unlock()
p.stopped = true
if err := p.client.Close(); err != nil {
p.logger.Warn().Err(err).Msg("failed to close IPC client")
}
if p.cmd.Process != nil {
if err := p.cmd.Process.Kill(); err != nil {
return fmt.Errorf("failed to kill native process: %w", err)
}
_ = p.cmd.Wait()
}
return nil
}
// Implement all Native methods by forwarding to IPC
func (p *NativeProxy) VideoSetSleepMode(enabled bool) error {
_, err := p.client.Call("VideoSetSleepMode", map[string]interface{}{
"enabled": enabled,
})
return err
}
func (p *NativeProxy) VideoGetSleepMode() (bool, error) {
resp, err := p.client.Call("VideoGetSleepMode", nil)
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) VideoSleepModeSupported() bool {
resp, err := p.client.Call("VideoSleepModeSupported", nil)
if err != nil {
return false
}
result, ok := resp.Result.(bool)
if !ok {
return false
}
return result
}
func (p *NativeProxy) VideoSetQualityFactor(factor float64) error {
_, err := p.client.Call("VideoSetQualityFactor", map[string]interface{}{
"factor": factor,
})
return err
}
func (p *NativeProxy) VideoGetQualityFactor() (float64, error) {
resp, err := p.client.Call("VideoGetQualityFactor", nil)
if err != nil {
return 0, err
}
result, ok := resp.Result.(float64)
if !ok {
return 0, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) VideoSetEDID(edid string) error {
_, err := p.client.Call("VideoSetEDID", map[string]interface{}{
"edid": edid,
})
return err
}
func (p *NativeProxy) VideoGetEDID() (string, error) {
resp, err := p.client.Call("VideoGetEDID", nil)
if err != nil {
return "", err
}
result, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) VideoLogStatus() (string, error) {
resp, err := p.client.Call("VideoLogStatus", nil)
if err != nil {
return "", err
}
result, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) VideoStop() error {
_, err := p.client.Call("VideoStop", nil)
return err
}
func (p *NativeProxy) VideoStart() error {
_, err := p.client.Call("VideoStart", nil)
return err
}
func (p *NativeProxy) GetLVGLVersion() (string, error) {
resp, err := p.client.Call("GetLVGLVersion", nil)
if err != nil {
return "", err
}
result, ok := resp.Result.(string)
if !ok {
return "", fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjHide(objName string) (bool, error) {
resp, err := p.client.Call("UIObjHide", map[string]interface{}{
"obj_name": objName,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjShow(objName string) (bool, error) {
resp, err := p.client.Call("UIObjShow", map[string]interface{}{
"obj_name": objName,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UISetVar(name string, value string) {
_, _ = p.client.Call("UISetVar", map[string]interface{}{
"name": name,
"value": value,
})
}
func (p *NativeProxy) UIGetVar(name string) string {
resp, err := p.client.Call("UIGetVar", map[string]interface{}{
"name": name,
})
if err != nil {
return ""
}
result, ok := resp.Result.(string)
if !ok {
return ""
}
return result
}
func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) {
resp, err := p.client.Call("UIObjAddState", map[string]interface{}{
"obj_name": objName,
"state": state,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) {
resp, err := p.client.Call("UIObjClearState", map[string]interface{}{
"obj_name": objName,
"state": state,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) {
resp, err := p.client.Call("UIObjAddFlag", map[string]interface{}{
"obj_name": objName,
"flag": flag,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) {
resp, err := p.client.Call("UIObjClearFlag", map[string]interface{}{
"obj_name": objName,
"flag": flag,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) {
resp, err := p.client.Call("UIObjSetOpacity", map[string]interface{}{
"obj_name": objName,
"opacity": opacity,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) {
resp, err := p.client.Call("UIObjFadeIn", map[string]interface{}{
"obj_name": objName,
"duration": duration,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) {
resp, err := p.client.Call("UIObjFadeOut", map[string]interface{}{
"obj_name": objName,
"duration": duration,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) {
resp, err := p.client.Call("UIObjSetLabelText", map[string]interface{}{
"obj_name": objName,
"text": text,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) {
resp, err := p.client.Call("UIObjSetImageSrc", map[string]interface{}{
"obj_name": objName,
"image": image,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) {
resp, err := p.client.Call("DisplaySetRotation", map[string]interface{}{
"rotation": rotation,
})
if err != nil {
return false, err
}
result, ok := resp.Result.(bool)
if !ok {
return false, fmt.Errorf("invalid response type")
}
return result, nil
}
func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) {
_, _ = p.client.Call("UpdateLabelIfChanged", map[string]interface{}{
"obj_name": objName,
"new_text": newText,
})
}
func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) {
_, _ = p.client.Call("UpdateLabelAndChangeVisibility", map[string]interface{}{
"obj_name": objName,
"new_text": newText,
})
}
func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) {
_, _ = p.client.Call("SwitchToScreenIf", map[string]interface{}{
"screen_name": screenName,
"should_switch": shouldSwitch,
})
}
func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) {
_, _ = p.client.Call("SwitchToScreenIfDifferent", map[string]interface{}{
"screen_name": screenName,
})
}
func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() {
_, _ = p.client.Call("DoNotUseThisIsForCrashTestingOnly", nil)
}