From f735f57c3d31fbe98e0fa64a2896383d7cb9bc70 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 20 Oct 2025 20:40:46 -0500 Subject: [PATCH] Revamp the OTA and reboot processing OTA supplies port-reboot action to handle the rebooting device. Make sure we force-reload the page after redirection. Move reboot logic into hw.go and make it set the willReboot message with parameters if provided. Improve logging consistency. --- hw.go | 32 ++++++++++++++++++++++++++++++ jsonrpc.go | 30 ++-------------------------- native.go | 3 +++ network.go | 4 +++- ota.go | 24 +++++++--------------- ui/src/components/VideoOverlay.tsx | 1 + ui/src/hooks/useHidRpc.ts | 8 +++++++- ui/src/routes/devices.$id.tsx | 8 +++++++- 8 files changed, 62 insertions(+), 48 deletions(-) diff --git a/hw.go b/hw.go index 20d88ebf..7797adc1 100644 --- a/hw.go +++ b/hw.go @@ -3,6 +3,7 @@ package kvm import ( "fmt" "os" + "os/exec" "regexp" "strings" "sync" @@ -36,6 +37,37 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused return content[0x17:0x1C], nil } +func hwReboot(force bool, postRebootAction *PostRebootAction, delay time.Duration) error { + logger.Info().Msgf("Reboot requested, rebooting in %d seconds...", delay) + + writeJSONRPCEvent("willReboot", postRebootAction, currentSession) + time.Sleep(1 * time.Second) // Wait for the JSONRPCEvent to be sent + + nativeInstance.SwitchToScreenIfDifferent("rebooting_screen") + time.Sleep(delay - (1 * time.Second)) // wait requested extra settle time + + args := []string{} + if force { + args = append(args, "-f") + } + + cmd := exec.Command("reboot", args...) + err := cmd.Start() + if err != nil { + logger.Error().Err(err).Msg("failed to reboot") + switchToMainScreen() + return fmt.Errorf("failed to reboot: %w", err) + } + + // If the reboot command is successful, exit the program after 5 seconds + go func() { + time.Sleep(5 * time.Second) + os.Exit(0) + }() + + return nil +} + var deviceID string var deviceIDOnce sync.Once diff --git a/jsonrpc.go b/jsonrpc.go index 2c06f12b..dede5bf0 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -173,34 +173,8 @@ func rpcGetDeviceID() (string, error) { } func rpcReboot(force bool) error { - logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") - - writeJSONRPCEvent("willReboot", nil, currentSession) - - // Wait for the JSONRPCEvent to be sent - time.Sleep(1 * time.Second) - nativeInstance.SwitchToScreenIfDifferent("rebooting_screen") - - args := []string{} - if force { - args = append(args, "-f") - } - - cmd := exec.Command("reboot", args...) - err := cmd.Start() - if err != nil { - logger.Error().Err(err).Msg("failed to reboot") - switchToMainScreen() - return fmt.Errorf("failed to reboot: %w", err) - } - - // If the reboot command is successful, exit the program after 5 seconds - go func() { - time.Sleep(5 * time.Second) - os.Exit(0) - }() - - return nil + logger.Info().Msg("Got reboot request via RPC") + return hwReboot(force, nil, 0) } var streamFactor = 1.0 diff --git a/native.go b/native.go index 5f26c014..4a523bce 100644 --- a/native.go +++ b/native.go @@ -37,14 +37,17 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { nativeLogger.Trace().Str("event", event).Msg("rpc event received") switch event { case "resetConfig": + nativeLogger.Info().Msg("Reset configuration request via native rpc event") err := rpcResetConfig() if err != nil { nativeLogger.Warn().Err(err).Msg("error resetting config") } _ = rpcReboot(true) case "reboot": + nativeLogger.Info().Msg("Reboot request via native rpc event") _ = rpcReboot(true) case "toggleDHCPClient": + nativeLogger.Info().Msg("Toggle DHCP request via native rpc event") _ = rpcToggleDHCPClient() default: nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received") diff --git a/network.go b/network.go index ff071460..83eae429 100644 --- a/network.go +++ b/network.go @@ -193,6 +193,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re oldIPv4Mode := oldConfig.IPv4Mode.String newIPv4Mode := newConfig.IPv4Mode.String + // IPv4 mode change requires reboot if newIPv4Mode != oldIPv4Mode { rebootRequired = true @@ -284,7 +285,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er } if rebootRequired { - if err := rpcReboot(false); err != nil { + l.Info().Msg("Rebooting due to network changes") + if err := hwReboot(true, postRebootAction, 0); err != nil { return nil, err } } diff --git a/ota.go b/ota.go index 7063c7ff..41bfea96 100644 --- a/ota.go +++ b/ota.go @@ -487,25 +487,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err } if rebootNeeded { - scopedLogger.Info().Msg("System Rebooting in 10s") + scopedLogger.Info().Msg("System Rebooting due to OTA update") - // TODO: Future enhancement - send postRebootAction to redirect to release notes - // Example: - // postRebootAction := &PostRebootAction{ - // HealthCheck: "[..]/device/status", - // RedirectUrl: "[..]/settings/general/update?version=X.Y.Z", - // } - // writeJSONRPCEvent("willReboot", postRebootAction, currentSession) + postRebootAction := &PostRebootAction{ + HealthCheck: "/device/status", + RedirectUrl: "/settings/general/update?version=" + remote.SystemVersion, + } - time.Sleep(10 * time.Second) - cmd := exec.Command("reboot") - err := cmd.Start() - if err != nil { - otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err) - scopedLogger.Error().Err(err).Msg("Failed to start reboot") - return fmt.Errorf("failed to start reboot: %w", err) - } else { - os.Exit(0) + if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil { + return fmt.Errorf("error requesting reboot: %w", err) } } diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index d2785eaf..e59c0987 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -476,6 +476,7 @@ export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayPro // Device is available, redirect to the specified URL console.log('Device is available, redirecting to:', postRebootAction.redirectUrl); window.location.href = postRebootAction.redirectUrl; + window.location.reload(); } } catch (err) { // Ignore errors - they're expected while device is rebooting diff --git a/ui/src/hooks/useHidRpc.ts b/ui/src/hooks/useHidRpc.ts index 3c08d6d6..2db8279f 100644 --- a/ui/src/hooks/useHidRpc.ts +++ b/ui/src/hooks/useHidRpc.ts @@ -78,7 +78,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) { try { data = message.marshal(); } catch (e) { - console.error("Failed to send HID RPC message", e); + console.error("Failed to marshal HID RPC message", e); } if (!data) return; @@ -223,13 +223,19 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) { setRpcHidProtocolVersion(null); }; + const errorHandler = (e: Event) => { + console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`) + }; + rpcHidChannel.addEventListener("message", messageHandler); rpcHidChannel.addEventListener("close", closeHandler); + rpcHidChannel.addEventListener("error", errorHandler); rpcHidChannel.addEventListener("open", openHandler); return () => { rpcHidChannel.removeEventListener("message", messageHandler); rpcHidChannel.removeEventListener("close", closeHandler); + rpcHidChannel.removeEventListener("error", errorHandler); rpcHidChannel.removeEventListener("open", openHandler); }; }, [ diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index d01411f4..bf73902e 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -485,13 +485,15 @@ export default function KvmIdRoute() { const rpcDataChannel = pc.createDataChannel("rpc"); rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); - rpcDataChannel.onerror = (e: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); + rpcDataChannel.onerror = (ev: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${ev}`); rpcDataChannel.onopen = () => { setRpcDataChannel(rpcDataChannel); }; const rpcHidChannel = pc.createDataChannel("hidrpc"); rpcHidChannel.binaryType = "arraybuffer"; + rpcHidChannel.onclose = () => console.log("rpcHidChannel has closed"); + rpcHidChannel.onerror = (ev: Event) => console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${ev}`); rpcHidChannel.onopen = () => { setRpcHidChannel(rpcHidChannel); }; @@ -501,6 +503,8 @@ export default function KvmIdRoute() { maxRetransmits: 0, }); rpcHidUnreliableChannel.binaryType = "arraybuffer"; + rpcHidUnreliableChannel.onclose = () => console.log("rpcHidUnreliableChannel has closed"); + rpcHidUnreliableChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableChannel '${rpcHidUnreliableChannel.label}': ${ev}`); rpcHidUnreliableChannel.onopen = () => { setRpcHidUnreliableChannel(rpcHidUnreliableChannel); }; @@ -510,6 +514,8 @@ export default function KvmIdRoute() { maxRetransmits: 0, }); rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer"; + rpcHidUnreliableNonOrderedChannel.onclose = () => console.log("rpcHidUnreliableNonOrderedChannel has closed"); + rpcHidUnreliableNonOrderedChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableNonOrderedChannel '${rpcHidUnreliableNonOrderedChannel.label}': ${ev}`); rpcHidUnreliableNonOrderedChannel.onopen = () => { setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel); };