mirror of https://github.com/jetkvm/kvm.git
feat: enhance network change handling and reboot logic
This commit is contained in:
parent
895cd5cc39
commit
1cf7dd4be9
61
network.go
61
network.go
|
|
@ -3,6 +3,7 @@ package kvm
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/confparser"
|
||||
"github.com/jetkvm/kvm/internal/mdns"
|
||||
|
|
@ -170,6 +171,54 @@ func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
|
|||
return nm.SetHostname(hostname, domain)
|
||||
}
|
||||
|
||||
func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (bool, *string) {
|
||||
var rebootRequired bool
|
||||
var suggestedIp *string
|
||||
|
||||
oldDhcpClient := oldConfig.DHCPClient.String
|
||||
|
||||
// DHCP client change always requires reboot
|
||||
if newConfig.DHCPClient.String != oldDhcpClient {
|
||||
rebootRequired = true
|
||||
networkLogger.Info().Str("old", oldDhcpClient).Str("new", newConfig.DHCPClient.String).Msg("DHCP client changed, reboot required")
|
||||
}
|
||||
|
||||
// IPv4 mode change requires reboot when using udhcpc
|
||||
if newConfig.IPv4Mode.String != oldConfig.IPv4Mode.String && oldDhcpClient == "udhcpc" {
|
||||
rebootRequired = true
|
||||
networkLogger.Info().Str("old", oldConfig.IPv4Mode.String).Str("new", newConfig.IPv4Mode.String).Msg("IPv4 mode changed with udhcpc, reboot required")
|
||||
}
|
||||
|
||||
// IPv4 static config changes require reboot
|
||||
if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil {
|
||||
if newConfig.IPv4Static.Address.String != oldConfig.IPv4Static.Address.String {
|
||||
rebootRequired = true
|
||||
suggestedIp = &newConfig.IPv4Static.Address.String
|
||||
networkLogger.Info().Str("old", oldConfig.IPv4Static.Address.String).Str("new", newConfig.IPv4Static.Address.String).Msg("IPv4 address changed, reboot required")
|
||||
}
|
||||
if newConfig.IPv4Static.Netmask.String != oldConfig.IPv4Static.Netmask.String {
|
||||
rebootRequired = true
|
||||
networkLogger.Info().Str("old", oldConfig.IPv4Static.Netmask.String).Str("new", newConfig.IPv4Static.Netmask.String).Msg("IPv4 netmask changed, reboot required")
|
||||
}
|
||||
if newConfig.IPv4Static.Gateway.String != oldConfig.IPv4Static.Gateway.String {
|
||||
rebootRequired = true
|
||||
networkLogger.Info().Str("old", oldConfig.IPv4Static.Gateway.String).Str("new", newConfig.IPv4Static.Gateway.String).Msg("IPv4 gateway changed, reboot required")
|
||||
}
|
||||
if !reflect.DeepEqual(newConfig.IPv4Static.DNS, oldConfig.IPv4Static.DNS) {
|
||||
rebootRequired = true
|
||||
networkLogger.Info().Strs("old", oldConfig.IPv4Static.DNS).Strs("new", newConfig.IPv4Static.DNS).Msg("IPv4 DNS changed, reboot required")
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6 mode change requires reboot when using udhcpc
|
||||
if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" {
|
||||
rebootRequired = true
|
||||
networkLogger.Info().Str("old", oldConfig.IPv6Mode.String).Str("new", newConfig.IPv6Mode.String).Msg("IPv6 mode changed with udhcpc, reboot required")
|
||||
}
|
||||
|
||||
return rebootRequired, suggestedIp
|
||||
}
|
||||
|
||||
func rpcGetNetworkState() *types.RpcInterfaceState {
|
||||
state, _ := networkManager.GetInterfaceState(NetIfName)
|
||||
return state.ToRpcInterfaceState()
|
||||
|
|
@ -189,9 +238,13 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
|||
|
||||
l.Debug().Msg("setting new config")
|
||||
|
||||
var rebootRequired bool
|
||||
if netConfig.DHCPClient.String != config.NetworkConfig.DHCPClient.String {
|
||||
rebootRequired = true
|
||||
// Check if reboot is needed
|
||||
rebootRequired, suggestedIp := shouldRebootForNetworkChange(config.NetworkConfig, netConfig)
|
||||
|
||||
// If reboot required, send willReboot event before applying network config
|
||||
if rebootRequired {
|
||||
l.Info().Msg("Sending willReboot event before applying network config")
|
||||
writeJSONRPCEvent("willReboot", suggestedIp, currentSession)
|
||||
}
|
||||
|
||||
_ = setHostname(networkManager, netConfig.Hostname.String, netConfig.Domain.String)
|
||||
|
|
@ -238,5 +291,5 @@ func rpcToggleDHCPClient() error {
|
|||
return err
|
||||
}
|
||||
|
||||
return rpcReboot(false)
|
||||
return rpcReboot(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
ConnectionFailedOverlay,
|
||||
LoadingConnectionOverlay,
|
||||
PeerConnectionDisconnectedOverlay,
|
||||
RebootingOverlay,
|
||||
} from "@/components/VideoOverlay";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||
|
|
@ -122,7 +123,7 @@ export default function KvmIdRoute() {
|
|||
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
||||
|
||||
const params = useParams() as { id: string };
|
||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||
const { sidebarView, setSidebarView, disableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
|
||||
const [queryParams, setQueryParams] = useSearchParams();
|
||||
|
||||
const {
|
||||
|
|
@ -241,7 +242,7 @@ export default function KvmIdRoute() {
|
|||
{
|
||||
heartbeat: true,
|
||||
retryOnError: true,
|
||||
reconnectAttempts: 15,
|
||||
reconnectAttempts: 2000,
|
||||
reconnectInterval: 1000,
|
||||
onReconnectStop: () => {
|
||||
console.debug("Reconnect stopped");
|
||||
|
|
@ -250,8 +251,7 @@ export default function KvmIdRoute() {
|
|||
|
||||
shouldReconnect(event) {
|
||||
console.debug("[Websocket] shouldReconnect", event);
|
||||
// TODO: Why true?
|
||||
return true;
|
||||
return isLegacySignalingEnabled.current;
|
||||
},
|
||||
|
||||
onClose(event) {
|
||||
|
|
@ -265,6 +265,16 @@ export default function KvmIdRoute() {
|
|||
},
|
||||
onOpen() {
|
||||
console.debug("[Websocket] onOpen");
|
||||
// We want to clear the reboot state when the websocket connection is opened
|
||||
// Currently the flow is:
|
||||
// 1. User clicks reboot
|
||||
// 2. Device sends event 'willReboot'
|
||||
// 3. We set the reboot state
|
||||
// 4. Reboot modal is shown
|
||||
// 5. WS tries to reconnect
|
||||
// 6. WS reconnects
|
||||
// 7. This function is called and now we clear the reboot state
|
||||
setRebootState({ isRebooting: false, suggestedIp: null });
|
||||
},
|
||||
|
||||
onMessage: message => {
|
||||
|
|
@ -340,10 +350,7 @@ export default function KvmIdRoute() {
|
|||
peerConnection.addIceCandidate(candidate);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Don't even retry once we declare failure
|
||||
!connectionFailed && isLegacySignalingEnabled.current === false,
|
||||
}
|
||||
);
|
||||
|
||||
const sendWebRTCSignal = useCallback(
|
||||
|
|
@ -594,6 +601,8 @@ export default function KvmIdRoute() {
|
|||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||
bytesReceived: bytesReceivedDelta,
|
||||
bytesSent: bytesSentDelta,
|
||||
}).catch(() => {
|
||||
// we don't care about errors here, but we don't want unhandled promise rejections
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
|
|
@ -666,6 +675,12 @@ export default function KvmIdRoute() {
|
|||
window.location.href = currentUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.method === "willReboot") {
|
||||
const suggestedIp = resp.params as unknown as string | null;
|
||||
setRebootState({ isRebooting: true, suggestedIp });
|
||||
navigateTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||
|
|
@ -765,6 +780,14 @@ export default function KvmIdRoute() {
|
|||
}, [appVersion, getLocalVersion]);
|
||||
|
||||
const ConnectionStatusElement = useMemo(() => {
|
||||
const isOtherSession = location.pathname.includes("other-session");
|
||||
if (isOtherSession) return null;
|
||||
|
||||
// Rebooting takes priority over connection status
|
||||
if (rebootState?.isRebooting) {
|
||||
return <RebootingOverlay show={true} suggestedIp={rebootState.suggestedIp} />;
|
||||
}
|
||||
|
||||
const hasConnectionFailed =
|
||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
||||
|
||||
|
|
@ -774,9 +797,6 @@ export default function KvmIdRoute() {
|
|||
|
||||
const isDisconnected = peerConnectionState === "disconnected";
|
||||
|
||||
const isOtherSession = location.pathname.includes("other-session");
|
||||
|
||||
if (isOtherSession) return null;
|
||||
if (peerConnectionState === "connected") return null;
|
||||
if (isDisconnected) {
|
||||
return <PeerConnectionDisconnectedOverlay show={true} />;
|
||||
|
|
@ -792,14 +812,7 @@ export default function KvmIdRoute() {
|
|||
}
|
||||
|
||||
return null;
|
||||
}, [
|
||||
connectionFailed,
|
||||
loadingMessage,
|
||||
location.pathname,
|
||||
peerConnection,
|
||||
peerConnectionState,
|
||||
setupPeerConnection,
|
||||
]);
|
||||
}, [location.pathname, rebootState?.isRebooting, rebootState?.suggestedIp, connectionFailed, peerConnectionState, peerConnection, setupPeerConnection, loadingMessage]);
|
||||
|
||||
return (
|
||||
<FeatureFlagProvider appVersion={appVersion}>
|
||||
|
|
@ -841,7 +854,7 @@ export default function KvmIdRoute() {
|
|||
/>
|
||||
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
<WebRTCVideo />
|
||||
<WebRTCVideo hasConnectionIssues={!!ConnectionStatusElement} />
|
||||
<div
|
||||
style={{ animationDuration: "500ms" }}
|
||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue