package native import ( "context" "crypto/rand" "encoding/binary" "encoding/hex" "fmt" "io" "net" "os" "os/exec" "runtime" "strings" "sync" "syscall" "time" "github.com/Masterminds/semver/v3" "github.com/jetkvm/kvm/internal/supervisor" "github.com/jetkvm/kvm/internal/utils" "github.com/rs/zerolog" ) const ( maxFrameSize = 1920 * 1080 / 2 defaultMaxRestartAttempts uint = 5 ) type nativeProxyOptions struct { Disable bool `env:"JETKVM_NATIVE_DISABLE"` SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"` AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"` DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"` DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"` CtrlUnixSocket string `env:"JETKVM_NATIVE_CTRL_UNIX_SOCKET"` VideoStreamUnixSocket string `env:"JETKVM_NATIVE_VIDEO_STREAM_UNIX_SOCKET"` BinaryPath string `env:"JETKVM_NATIVE_BINARY_PATH"` LoggerLevel zerolog.Level `env:"JETKVM_NATIVE_LOGGER_LEVEL"` HandshakeMessage string `env:"JETKVM_NATIVE_HANDSHAKE_MESSAGE"` MaxRestartAttempts uint OnVideoFrameReceived func(frame []byte, duration time.Duration) OnIndevEvent func(event string) OnRpcEvent func(event string) OnVideoStateChange func(state VideoState) OnNativeRestart func() } func randomId(binaryLength int) string { s := make([]byte, binaryLength) _, err := rand.Read(s) if err != nil { nativeLogger.Error().Err(err).Msg("failed to generate random ID") return strings.Repeat("0", binaryLength*2) // return all zeros if error } return hex.EncodeToString(s) } func (n *NativeOptions) toProxyOptions() *nativeProxyOptions { // random 16 bytes hex string handshakeMessage := randomId(16) maxRestartAttempts := defaultMaxRestartAttempts if n.MaxRestartAttempts > 0 { maxRestartAttempts = n.MaxRestartAttempts } return &nativeProxyOptions{ SystemVersion: n.SystemVersion, AppVersion: n.AppVersion, DisplayRotation: n.DisplayRotation, DefaultQualityFactor: n.DefaultQualityFactor, OnVideoFrameReceived: n.OnVideoFrameReceived, OnIndevEvent: n.OnIndevEvent, OnRpcEvent: n.OnRpcEvent, OnVideoStateChange: n.OnVideoStateChange, OnNativeRestart: n.OnNativeRestart, HandshakeMessage: handshakeMessage, MaxRestartAttempts: maxRestartAttempts, } } func (p *nativeProxyOptions) toNativeOptions() *NativeOptions { return &NativeOptions{ SystemVersion: p.SystemVersion, AppVersion: p.AppVersion, DisplayRotation: p.DisplayRotation, DefaultQualityFactor: p.DefaultQualityFactor, } } // cmdWrapper wraps exec.Cmd to implement processCmd interface type cmdWrapper struct { *exec.Cmd stdoutHandler *nativeProxyStdoutHandler } func (c *cmdWrapper) GetProcess() interface { Kill() error Signal(sig interface{}) error } { return &processWrapper{Process: c.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 { nativeUnixSocket string videoStreamUnixSocket string videoStreamListener net.Listener binaryPath string startMu sync.Mutex // mutex for the start process (context and isStopped) ctx context.Context cancel context.CancelFunc client *GRPCClient clientMu sync.RWMutex // mutex for the client cmd *cmdWrapper cmdMu sync.Mutex // mutex for the cmd logger *zerolog.Logger options *nativeProxyOptions restarts uint stopped bool } // NewNativeProxy creates a new NativeProxy that spawns a separate process func NewNativeProxy(opts NativeOptions) (*NativeProxy, error) { proxyOptions := opts.toProxyOptions() proxyOptions.VideoStreamUnixSocket = fmt.Sprintf("@jetkvm/native/video-stream/%s", randomId(4)) // 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) } proxy := &NativeProxy{ nativeUnixSocket: proxyOptions.CtrlUnixSocket, videoStreamUnixSocket: proxyOptions.VideoStreamUnixSocket, binaryPath: exePath, logger: nativeLogger, options: proxyOptions, restarts: 0, } return proxy, nil } func (p *NativeProxy) startVideoStreamListener() error { if p.videoStreamListener != nil { return nil } logger := p.logger.With().Str("socketPath", p.videoStreamUnixSocket).Logger() listener, err := net.Listen("unix", p.videoStreamUnixSocket) if err != nil { logger.Warn().Err(err).Msg("failed to start video stream listener") return fmt.Errorf("failed to start video stream listener: %w", err) } logger.Info().Msg("video stream listener started") p.videoStreamListener = listener go func() { for { conn, err := listener.Accept() if err != nil { logger.Warn().Err(err).Msg("failed to accept socket") continue } logger.Info().Msg("video stream socket accepted") go p.handleVideoFrame(conn) } }() return nil } type nativeProxyStdoutHandler struct { mu *sync.Mutex handshakeCh chan bool handshakeMessage string handshakeDone bool bytesReceived int writeCount int createdAt time.Time } func (w *nativeProxyStdoutHandler) Write(p []byte) (n int, err error) { w.mu.Lock() defer w.mu.Unlock() w.writeCount++ w.bytesReceived += len(p) // Log every write to stdout from the child process for debugging dataPreview := string(p) if len(dataPreview) > 200 { dataPreview = dataPreview[:200] + "...(truncated)" } // Replace newlines for cleaner logging dataPreview = strings.ReplaceAll(dataPreview, "\n", "\\n") dataPreview = strings.ReplaceAll(dataPreview, "\r", "\\r") nativeLogger.Info(). Str("phase", "handshake"). Int("writeNum", w.writeCount). Int("bytesThisWrite", len(p)). Int("totalBytesReceived", w.bytesReceived). Dur("sinceCreated", time.Since(w.createdAt)). Bool("handshakeDone", w.handshakeDone). Str("dataPreview", dataPreview). Msg("[HANDSHAKE] stdout handler received data from child process") if !w.handshakeDone && strings.Contains(string(p), w.handshakeMessage) { nativeLogger.Info(). Str("phase", "handshake"). Dur("sinceCreated", time.Since(w.createdAt)). Int("totalBytesReceived", w.bytesReceived). Msg("[HANDSHAKE] handshake message FOUND in stdout - signaling parent") w.handshakeDone = true w.handshakeCh <- true return len(p), nil } os.Stdout.Write(p) return len(p), nil } func (p *NativeProxy) toProcessCommand() (*cmdWrapper, error) { // generate a new random ID for the gRPC socket on each restart // sometimes the socket is not closed properly when the process exits // this is a workaround to avoid the issue p.nativeUnixSocket = fmt.Sprintf("jetkvm/native/grpc/%s", randomId(4)) p.options.CtrlUnixSocket = p.nativeUnixSocket envArgs, err := utils.MarshalEnv(p.options) if err != nil { return nil, fmt.Errorf("failed to marshal environment variables: %w", err) } cmd := &cmdWrapper{ Cmd: exec.Command( p.binaryPath, "-subcomponent=native", ), stdoutHandler: &nativeProxyStdoutHandler{ mu: &sync.Mutex{}, handshakeCh: make(chan bool), handshakeMessage: p.options.HandshakeMessage, createdAt: time.Now(), }, } p.logger.Info(). Str("phase", "handshake"). Int("handshakeMsgLen", len(p.options.HandshakeMessage)). Str("binaryPath", p.binaryPath). Msg("[HANDSHAKE] created stdout handler, waiting for handshake message from child") cmd.Stdout = cmd.stdoutHandler cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, Pdeathsig: syscall.SIGTERM, } // Set environment variable to indicate native process mode cmd.Env = append( os.Environ(), envArgs..., ) return cmd, nil } func (p *NativeProxy) handleVideoFrame(conn net.Conn) { defer conn.Close() inboundPacket := make([]byte, maxFrameSize) var frameSizeBuffer [4]byte lastFrame := time.Now() for { // Read 4-byte frame length prefix _, err := io.ReadFull(conn, frameSizeBuffer[:]) if err != nil { if err != io.EOF { p.logger.Warn().Err(err).Msg("failed to read frame size from socket") } break } frameSize := binary.LittleEndian.Uint32(frameSizeBuffer[:]) if frameSize == 0 || frameSize > maxFrameSize { p.logger.Error().Uint32("frameSize", frameSize).Uint32("maxFrameSize", maxFrameSize). Msg("received invalid frame size") break } // Read the actual frame data _, err = io.ReadFull(conn, inboundPacket[:frameSize]) if err != nil { p.logger.Warn().Err(err).Msg("failed to read video frame from socket") break } now := time.Now() sinceLastFrame := now.Sub(lastFrame) lastFrame = now p.options.OnVideoFrameReceived(inboundPacket[:frameSize], sinceLastFrame) } } // it should be only called by start() method, as it isn't thread-safe func (p *NativeProxy) setUpGRPCClient() error { handshakeStartTime := time.Now() p.logger.Info(). Str("phase", "handshake"). Str("socketPath", "@"+p.nativeUnixSocket). Msg("[HANDSHAKE] waiting for handshake from child process (10 second timeout)") // Create a ticker for periodic status updates while waiting statusTicker := time.NewTicker(2 * time.Second) defer statusTicker.Stop() timeout := time.After(10 * time.Second) // wait until handshake completed with periodic status logging handshakeLoop: for { select { case <-p.cmd.stdoutHandler.handshakeCh: elapsed := time.Since(handshakeStartTime) p.logger.Info(). Str("phase", "handshake"). Dur("elapsed", elapsed). Int("totalBytesReceived", p.cmd.stdoutHandler.bytesReceived). Int("writeCount", p.cmd.stdoutHandler.writeCount). Msg("[HANDSHAKE] handshake completed successfully!") break handshakeLoop case <-statusTicker.C: elapsed := time.Since(handshakeStartTime) p.cmd.stdoutHandler.mu.Lock() bytesReceived := p.cmd.stdoutHandler.bytesReceived writeCount := p.cmd.stdoutHandler.writeCount handshakeDone := p.cmd.stdoutHandler.handshakeDone p.cmd.stdoutHandler.mu.Unlock() p.logger.Warn(). Str("phase", "handshake"). Dur("elapsed", elapsed). Int("bytesReceivedFromChild", bytesReceived). Int("writeCountFromChild", writeCount). Bool("handshakeDone", handshakeDone). Msg("[HANDSHAKE] still waiting for handshake message from child process...") case <-timeout: elapsed := time.Since(handshakeStartTime) p.cmd.stdoutHandler.mu.Lock() bytesReceived := p.cmd.stdoutHandler.bytesReceived writeCount := p.cmd.stdoutHandler.writeCount p.cmd.stdoutHandler.mu.Unlock() p.logger.Error(). Str("phase", "handshake"). Dur("elapsed", elapsed). Int("bytesReceivedFromChild", bytesReceived). Int("writeCountFromChild", writeCount). Msg("[HANDSHAKE] TIMEOUT - handshake not completed within 10 seconds. Child process may be stuck during initialization.") // Run device diagnostics on timeout p.logger.Error().Str("phase", "handshake").Msg("[DIAG] Running device diagnostics...") // Check device files deviceFiles := []string{"/dev/fb0", "/dev/input/event0", "/dev/input/event1", "/dev/v4l-subdev2", "/dev/video0"} for _, path := range deviceFiles { if info, err := os.Stat(path); err != nil { p.logger.Error().Str("phase", "handshake").Str("path", path).Err(err).Msg("[DIAG] device NOT found") } else { p.logger.Info().Str("phase", "handshake").Str("path", path).Str("mode", info.Mode().String()).Msg("[DIAG] device exists") } } // Check I2C device names if data, err := exec.Command("sh", "-c", "cat /sys/bus/i2c/devices/*/name 2>/dev/null").Output(); err == nil { names := strings.TrimSpace(string(data)) p.logger.Info().Str("phase", "handshake").Str("devices", names).Msg("[DIAG] I2C device names") } // Check HDMI capture chip sleep_mode sysfs sleepModePath := "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode" if data, err := os.ReadFile(sleepModePath); err != nil { p.logger.Error().Str("phase", "handshake").Err(err).Msg("[DIAG] sleep_mode sysfs NOT found - tc358743 may not be detected") } else { p.logger.Info().Str("phase", "handshake").Str("value", strings.TrimSpace(string(data))).Msg("[DIAG] sleep_mode sysfs exists") } // Run i2cdetect on bus 4 (HDMI capture chip) if output, err := exec.Command("i2cdetect", "-y", "4").Output(); err == nil { p.logger.Info().Str("phase", "handshake").Str("output", string(output)).Msg("[DIAG] i2cdetect bus 4") } // Dump dmesg tail if output, err := exec.Command("dmesg").Output(); err == nil { lines := strings.Split(string(output), "\n") if len(lines) > 20 { lines = lines[len(lines)-20:] } for _, line := range lines { if line != "" { p.logger.Info().Str("phase", "handshake").Str("line", line).Msg("[DIAG] dmesg") } } } return fmt.Errorf("handshake not completed within 10 seconds") } } p.logger.Info().Str("phase", "handshake").Msg("[HANDSHAKE] proceeding to create gRPC client") logger := p.logger.With().Str("socketPath", "@"+p.nativeUnixSocket).Logger() grpcClientStart := time.Now() client, err := NewGRPCClient(grpcClientOptions{ SocketPath: p.nativeUnixSocket, Logger: &logger, OnIndevEvent: p.options.OnIndevEvent, OnRpcEvent: p.options.OnRpcEvent, OnVideoStateChange: p.options.OnVideoStateChange, }) if err != nil { logger.Error().Err(err).Dur("elapsed", time.Since(grpcClientStart)).Msg("[HANDSHAKE] failed to create gRPC client") return fmt.Errorf("failed to create gRPC client: %w", err) } logger.Info().Dur("elapsed", time.Since(grpcClientStart)).Msg("[HANDSHAKE] created gRPC client successfully") p.client = client // Wait for ready signal from the native process logger.Info().Msg("[HANDSHAKE] waiting for gRPC ready signal from native process") readyStart := time.Now() if err := p.client.WaitReady(); err != nil { logger.Error().Err(err).Dur("elapsed", time.Since(readyStart)).Msg("[HANDSHAKE] failed to wait for gRPC ready") // Clean up if ready failed if p.cmd.Process != nil { _ = p.cmd.Process.Kill() _ = p.cmd.Wait() } return fmt.Errorf("failed to wait for ready: %w", err) } logger.Info().Dur("elapsed", time.Since(readyStart)).Msg("[HANDSHAKE] gRPC ready signal received - native process fully initialized") // Call on native restart callback if it exists and restarts are greater than 0 if p.options.OnNativeRestart != nil && p.restarts > 0 { go p.options.OnNativeRestart() } return nil } func (p *NativeProxy) doStart() error { doStartTime := time.Now() p.logger.Info().Str("phase", "proxy-start").Msg("[PROXY-START] doStart beginning") p.cmdMu.Lock() defer p.cmdMu.Unlock() // lock OS thread to prevent the process from being moved to a different thread // see also https://go.dev/issue/27505 runtime.LockOSThread() defer runtime.UnlockOSThread() p.logger.Info().Str("phase", "proxy-start").Msg("[PROXY-START] creating process command") cmd, err := p.toProcessCommand() if err != nil { return fmt.Errorf("failed to create process: %w", err) } p.cmd = cmd p.logger.Info().Str("phase", "proxy-start").Str("binary", p.binaryPath).Msg("[PROXY-START] starting native child process") processStartTime := time.Now() if err := p.cmd.Start(); err != nil { p.logger.Error().Err(err).Dur("elapsed", time.Since(processStartTime)).Msg("[PROXY-START] failed to start native process") return fmt.Errorf("failed to start native process: %w", err) } // here we'll replace the logger with a new one that includes the process ID // there's no need to lock the mutex here as the side effect is acceptable newLogger := p.logger.With().Int("pid", p.cmd.Process.Pid).Logger() p.logger = &newLogger p.logger.Info(). Str("phase", "proxy-start"). Dur("processStartElapsed", time.Since(processStartTime)). Msg("[PROXY-START] native child process started, now waiting for handshake") if err := p.setUpGRPCClient(); err != nil { p.logger.Error().Err(err).Dur("totalElapsed", time.Since(doStartTime)).Msg("[PROXY-START] failed to set up gRPC client") return fmt.Errorf("failed to set up gRPC client: %w", err) } p.logger.Info(). Str("phase", "proxy-start"). Dur("totalElapsed", time.Since(doStartTime)). Msg("[PROXY-START] doStart completed successfully - native process is ready") return nil } // Start starts the native process func (p *NativeProxy) Start() error { p.startMu.Lock() defer p.startMu.Unlock() p.ctx, p.cancel = context.WithCancel(context.Background()) if p.stopped { return fmt.Errorf("proxy is stopped") } if err := p.startVideoStreamListener(); err != nil { return fmt.Errorf("failed to start video stream listener: %w", err) } if err := p.doStart(); err != nil { return fmt.Errorf("failed to start native process: %w", err) } go p.monitorProcess() return nil } // monitorProcess monitors the native process and restarts it if it crashes func (p *NativeProxy) monitorProcess() { for { if p.stopped { return } select { case <-p.ctx.Done(): p.logger.Trace().Msg("context done, stopping monitor process [before wait]") return default: } p.cmdMu.Lock() err := fmt.Errorf("native process not started") if p.cmd != nil { err = p.cmd.Wait() } p.cmdMu.Unlock() if p.stopped { return } select { case <-p.ctx.Done(): p.logger.Trace().Msg("context done, stopping monitor process [after wait]") return default: } 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.restarts++ logger := p.logger.With().Uint("attempt", p.restarts).Uint("maxAttempts", p.options.MaxRestartAttempts).Logger() if p.restarts >= p.options.MaxRestartAttempts { logger.Fatal().Msgf("max restart attempts reached, exiting: %s", supervisor.FailsafeReasonVideoMaxRestartAttemptsReached) return fmt.Errorf("max restart attempts reached") } if p.stopped { return fmt.Errorf("proxy is stopped") } // Close old client p.clientMu.Lock() if p.client != nil { if err := p.client.Close(); err != nil { logger.Warn().Err(err).Msg("failed to close gRPC client") } p.client = nil // set to nil to avoid closing it again } p.clientMu.Unlock() logger.Info().Msg("gRPC client closed") logger.Info().Msg("attempting to restart native process") if err := p.doStart(); err != nil { logger.Error().Err(err).Msg("failed to start native process") return fmt.Errorf("failed to start native process: %w", err) } logger.Info().Msg("native process restarted successfully") return nil } // Stop stops the native process func (p *NativeProxy) Stop() error { p.startMu.Lock() defer p.startMu.Unlock() p.stopped = true 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 } func zeroValue[V string | bool | float64]() V { var v V return v } func nativeProxyClientExec[K comparable, V string | bool | float64](p *NativeProxy, fn func(*GRPCClient) (V, error)) (V, error) { p.clientMu.RLock() defer p.clientMu.RUnlock() if p.client == nil { return zeroValue[V](), fmt.Errorf("gRPC client not initialized") } return fn(p.client) } func nativeProxyClientExecWithoutArgument(p *NativeProxy, fn func(*GRPCClient) error) error { p.clientMu.RLock() defer p.clientMu.RUnlock() if p.client == nil { return fmt.Errorf("gRPC client not initialized") } return fn(p.client) } // Implement all Native methods by forwarding to gRPC client func (p *NativeProxy) VideoSetSleepMode(enabled bool) error { return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { return client.VideoSetSleepMode(enabled) }) } func (p *NativeProxy) VideoGetSleepMode() (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.VideoGetSleepMode() }) } func (p *NativeProxy) VideoSleepModeSupported() bool { result, _ := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.VideoSleepModeSupported(), nil }) return result } func (p *NativeProxy) VideoSetQualityFactor(factor float64) error { return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { return client.VideoSetQualityFactor(factor) }) } func (p *NativeProxy) VideoGetQualityFactor() (float64, error) { return nativeProxyClientExec[float64](p, func(client *GRPCClient) (float64, error) { return client.VideoGetQualityFactor() }) } func (p *NativeProxy) VideoSetEDID(edid string) error { return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { return client.VideoSetEDID(edid) }) } func (p *NativeProxy) VideoGetEDID() (string, error) { return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { return client.VideoGetEDID() }) } func (p *NativeProxy) VideoLogStatus() (string, error) { return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { return client.VideoLogStatus() }) } func (p *NativeProxy) VideoStop() error { return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { return client.VideoStop() }) } func (p *NativeProxy) VideoStart() error { return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { return client.VideoStart() }) } func (p *NativeProxy) GetLVGLVersion() (string, error) { return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { return client.GetLVGLVersion() }) } func (p *NativeProxy) UIObjHide(objName string) (bool, error) { result, err := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjHide(objName) }) return result, err } func (p *NativeProxy) UIObjShow(objName string) (bool, error) { result, err := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjShow(objName) }) return result, err } func (p *NativeProxy) UISetVar(name string, value string) { _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { client.UISetVar(name, value) return nil }) } func (p *NativeProxy) UIGetVar(name string) string { result, _ := nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) { return client.UIGetVar(name), nil }) return result } func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjAddState(objName, state) }) } func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjClearState(objName, state) }) } func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjAddFlag(objName, flag) }) } func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjClearFlag(objName, flag) }) } func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjFadeIn(objName, duration) }) } func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjFadeOut(objName, duration) }) } func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjSetLabelText(objName, text) }) } func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjSetImageSrc(objName, image) }) } func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.UIObjSetOpacity(objName, opacity) }) } func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) { return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) { return client.DisplaySetRotation(rotation) }) } func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) { _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { client.UpdateLabelIfChanged(objName, newText) return nil }) } func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) { _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { client.UpdateLabelAndChangeVisibility(objName, newText) return nil }) } func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) { _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { client.SwitchToScreenIf(screenName, shouldSwitch) return nil }) } func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) { _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { client.SwitchToScreenIfDifferent(screenName) return nil }) } func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() { _ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error { client.DoNotUseThisIsForCrashTestingOnly() return nil }) }