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 (
"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

View File

@ -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

View File

@ -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")

View File

@ -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
}
}

24
ota.go
View File

@ -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)
}
}

View File

@ -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

View File

@ -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);
};
}, [

View File

@ -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);
};