Compare commits

...

2 Commits

Author SHA1 Message Date
Marc Brooks f34d97eedf
Merge f735f57c3d into 2444817455 2025-10-23 04:48:59 +00:00
Marc Brooks f735f57c3d
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.
2025-10-22 23:48:45 -05:00
8 changed files with 62 additions and 48 deletions

32
hw.go
View File

@ -3,6 +3,7 @@ package kvm
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -36,6 +37,37 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused
return content[0x17:0x1C], nil 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 deviceID string
var deviceIDOnce sync.Once var deviceIDOnce sync.Once

View File

@ -173,34 +173,8 @@ func rpcGetDeviceID() (string, error) {
} }
func rpcReboot(force bool) error { func rpcReboot(force bool) error {
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") logger.Info().Msg("Got reboot request via RPC")
return hwReboot(force, nil, 0)
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
} }
var streamFactor = 1.0 var streamFactor = 1.0

View File

@ -37,14 +37,17 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeLogger.Trace().Str("event", event).Msg("rpc event received") nativeLogger.Trace().Str("event", event).Msg("rpc event received")
switch event { switch event {
case "resetConfig": case "resetConfig":
nativeLogger.Info().Msg("Reset configuration request via native rpc event")
err := rpcResetConfig() err := rpcResetConfig()
if err != nil { if err != nil {
nativeLogger.Warn().Err(err).Msg("error resetting config") nativeLogger.Warn().Err(err).Msg("error resetting config")
} }
_ = rpcReboot(true) _ = rpcReboot(true)
case "reboot": case "reboot":
nativeLogger.Info().Msg("Reboot request via native rpc event")
_ = rpcReboot(true) _ = rpcReboot(true)
case "toggleDHCPClient": case "toggleDHCPClient":
nativeLogger.Info().Msg("Toggle DHCP request via native rpc event")
_ = rpcToggleDHCPClient() _ = rpcToggleDHCPClient()
default: default:
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received") nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")

View File

@ -193,6 +193,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
oldIPv4Mode := oldConfig.IPv4Mode.String oldIPv4Mode := oldConfig.IPv4Mode.String
newIPv4Mode := newConfig.IPv4Mode.String newIPv4Mode := newConfig.IPv4Mode.String
// IPv4 mode change requires reboot // IPv4 mode change requires reboot
if newIPv4Mode != oldIPv4Mode { if newIPv4Mode != oldIPv4Mode {
rebootRequired = true rebootRequired = true
@ -284,7 +285,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
} }
if rebootRequired { 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 return nil, err
} }
} }

24
ota.go
View File

@ -487,25 +487,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
} }
if rebootNeeded { 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 postRebootAction := &PostRebootAction{
// Example: HealthCheck: "/device/status",
// postRebootAction := &PostRebootAction{ RedirectUrl: "/settings/general/update?version=" + remote.SystemVersion,
// HealthCheck: "[..]/device/status", }
// RedirectUrl: "[..]/settings/general/update?version=X.Y.Z",
// }
// writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
time.Sleep(10 * time.Second) if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil {
cmd := exec.Command("reboot") return fmt.Errorf("error requesting reboot: %w", err)
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)
} }
} }

View File

@ -476,6 +476,7 @@ export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayPro
// Device is available, redirect to the specified URL // Device is available, redirect to the specified URL
console.log('Device is available, redirecting to:', postRebootAction.redirectUrl); console.log('Device is available, redirecting to:', postRebootAction.redirectUrl);
window.location.href = postRebootAction.redirectUrl; window.location.href = postRebootAction.redirectUrl;
window.location.reload();
} }
} catch (err) { } catch (err) {
// Ignore errors - they're expected while device is rebooting // Ignore errors - they're expected while device is rebooting

View File

@ -78,7 +78,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
try { try {
data = message.marshal(); data = message.marshal();
} catch (e) { } catch (e) {
console.error("Failed to send HID RPC message", e); console.error("Failed to marshal HID RPC message", e);
} }
if (!data) return; if (!data) return;
@ -223,13 +223,19 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
setRpcHidProtocolVersion(null); setRpcHidProtocolVersion(null);
}; };
const errorHandler = (e: Event) => {
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`)
};
rpcHidChannel.addEventListener("message", messageHandler); rpcHidChannel.addEventListener("message", messageHandler);
rpcHidChannel.addEventListener("close", closeHandler); rpcHidChannel.addEventListener("close", closeHandler);
rpcHidChannel.addEventListener("error", errorHandler);
rpcHidChannel.addEventListener("open", openHandler); rpcHidChannel.addEventListener("open", openHandler);
return () => { return () => {
rpcHidChannel.removeEventListener("message", messageHandler); rpcHidChannel.removeEventListener("message", messageHandler);
rpcHidChannel.removeEventListener("close", closeHandler); rpcHidChannel.removeEventListener("close", closeHandler);
rpcHidChannel.removeEventListener("error", errorHandler);
rpcHidChannel.removeEventListener("open", openHandler); rpcHidChannel.removeEventListener("open", openHandler);
}; };
}, [ }, [

View File

@ -485,13 +485,15 @@ export default function KvmIdRoute() {
const rpcDataChannel = pc.createDataChannel("rpc"); const rpcDataChannel = pc.createDataChannel("rpc");
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); 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 = () => { rpcDataChannel.onopen = () => {
setRpcDataChannel(rpcDataChannel); setRpcDataChannel(rpcDataChannel);
}; };
const rpcHidChannel = pc.createDataChannel("hidrpc"); const rpcHidChannel = pc.createDataChannel("hidrpc");
rpcHidChannel.binaryType = "arraybuffer"; rpcHidChannel.binaryType = "arraybuffer";
rpcHidChannel.onclose = () => console.log("rpcHidChannel has closed");
rpcHidChannel.onerror = (ev: Event) => console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${ev}`);
rpcHidChannel.onopen = () => { rpcHidChannel.onopen = () => {
setRpcHidChannel(rpcHidChannel); setRpcHidChannel(rpcHidChannel);
}; };
@ -501,6 +503,8 @@ export default function KvmIdRoute() {
maxRetransmits: 0, maxRetransmits: 0,
}); });
rpcHidUnreliableChannel.binaryType = "arraybuffer"; rpcHidUnreliableChannel.binaryType = "arraybuffer";
rpcHidUnreliableChannel.onclose = () => console.log("rpcHidUnreliableChannel has closed");
rpcHidUnreliableChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableChannel '${rpcHidUnreliableChannel.label}': ${ev}`);
rpcHidUnreliableChannel.onopen = () => { rpcHidUnreliableChannel.onopen = () => {
setRpcHidUnreliableChannel(rpcHidUnreliableChannel); setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
}; };
@ -510,6 +514,8 @@ export default function KvmIdRoute() {
maxRetransmits: 0, maxRetransmits: 0,
}); });
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer"; rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
rpcHidUnreliableNonOrderedChannel.onclose = () => console.log("rpcHidUnreliableNonOrderedChannel has closed");
rpcHidUnreliableNonOrderedChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableNonOrderedChannel '${rpcHidUnreliableNonOrderedChannel.label}': ${ev}`);
rpcHidUnreliableNonOrderedChannel.onopen = () => { rpcHidUnreliableNonOrderedChannel.onopen = () => {
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel); setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
}; };