From 61c2fa4a88bfb284be5319d77dd40b080b92d1b3 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Thu, 20 Nov 2025 19:27:19 +0000 Subject: [PATCH 1/5] fix(ota): set updating to false when no updates are available --- internal/ota/ota.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/ota/ota.go b/internal/ota/ota.go index 52cbb6e27..b42ba1e14 100644 --- a/internal/ota/ota.go +++ b/internal/ota/ota.go @@ -228,6 +228,12 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error { s.triggerComponentUpdateState("system", systemUpdate) } + if !appUpdate.pending && !systemUpdate.pending { + scopedLogger.Info().Msg("No updates available") + s.updating = false + return nil + } + scopedLogger.Trace().Bool("pending", appUpdate.pending).Msg("Checking for app update") if appUpdate.pending { From 86be6df1d376651e48a2f4a2807dcbd4fdca9225 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Thu, 20 Nov 2025 20:24:17 +0000 Subject: [PATCH 2/5] fix(network): only trigger time sync when network transitions to online rather than every state change --- cmd/main.go | 19 ++++++++++++++----- network.go | 10 +++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index fcf2cdfee..0e49174f0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -101,15 +101,22 @@ func supervise() error { cmd.Args = os.Args logFile, err := os.CreateTemp("", "jetkvm-stdout.log") - defer func() { - // we don't care about the errors here - _ = logFile.Close() - _ = os.Remove(logFile.Name()) - }() if err != nil { return fmt.Errorf("failed to create log file: %w", err) } + logFileName := logFile.Name() + defer func() { + // Close file if it's still open (safe to call even if already closed) + if logFile != nil { + _ = logFile.Close() + } + // Only remove if file still exists at original location (wasn't renamed) + if _, err := os.Stat(logFileName); err == nil { + _ = os.Remove(logFileName) + } + }() + // Use io.MultiWriter to write to both the original streams and our buffers cmd.Stdout = io.MultiWriter(os.Stdout, logFile) cmd.Stderr = io.MultiWriter(os.Stderr, logFile) @@ -133,6 +140,8 @@ func supervise() error { } if exiterr, ok := cmdErr.(*exec.ExitError); ok { + // createErrorDump will close and rename the file if successful + // Note: os.Exit bypasses defer, but file is already handled by createErrorDump createErrorDump(logFile) os.Exit(exiterr.ExitCode()) } diff --git a/network.go b/network.go index eb14c70d6..b2b224f06 100644 --- a/network.go +++ b/network.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "reflect" + "sync/atomic" "time" "github.com/jetkvm/kvm/internal/confparser" @@ -118,6 +119,10 @@ func setPublicIPReadyState(ipv4Ready, ipv6Ready bool) { publicIPState.SetIPv4AndIPv6(ipv4Ready, ipv6Ready) } +var ( + isOnline = &atomic.Bool{} +) + func networkStateChanged(_ string, state types.InterfaceState) { // do not block the main thread go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed") @@ -126,7 +131,10 @@ func networkStateChanged(_ string, state types.InterfaceState) { writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession) } - if state.Online { + previousOnline := isOnline.Load() + isOnline.Store(state.Online) + + if state.Online && !previousOnline { networkLogger.Info().Msg("network state changed to online, triggering time sync") triggerTimeSyncOnNetworkStateChange() } From 8a76a70d9eb56890561307a104e455e011ec8e07 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Thu, 20 Nov 2025 20:39:23 +0000 Subject: [PATCH 3/5] fix(network): sort IPv6 addresses using wrong references --- cmd/main.go | 12 ------------ pkg/nmlite/interface_state.go | 30 ++++++++++++++++++++++++++++-- pkg/nmlite/utils.go | 14 ++++++++------ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0e49174f0..2fd27dae5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -105,18 +105,6 @@ func supervise() error { return fmt.Errorf("failed to create log file: %w", err) } - logFileName := logFile.Name() - defer func() { - // Close file if it's still open (safe to call even if already closed) - if logFile != nil { - _ = logFile.Close() - } - // Only remove if file still exists at original location (wasn't renamed) - if _, err := os.Stat(logFileName); err == nil { - _ = os.Remove(logFileName) - } - }() - // Use io.MultiWriter to write to both the original streams and our buffers cmd.Stdout = io.MultiWriter(os.Stdout, logFile) cmd.Stderr = io.MultiWriter(os.Stderr, logFile) diff --git a/pkg/nmlite/interface_state.go b/pkg/nmlite/interface_state.go index 087cf0101..ba3e1b0d2 100644 --- a/pkg/nmlite/interface_state.go +++ b/pkg/nmlite/interface_state.go @@ -16,7 +16,10 @@ func (im *InterfaceManager) updateInterfaceState() error { return fmt.Errorf("failed to get interface: %w", err) } - var stateChanged bool + var ( + stateChanged bool + changeReason string + ) attrs := nl.Attrs() @@ -29,6 +32,7 @@ func (im *InterfaceManager) updateInterfaceState() error { if im.state.Up != isUp { im.state.Up = isUp stateChanged = true + changeReason = "oper state changed" } // Check if the interface is online @@ -36,12 +40,14 @@ func (im *InterfaceManager) updateInterfaceState() error { if im.state.Online != isOnline { im.state.Online = isOnline stateChanged = true + changeReason = "online state changed" } // Check if the MAC address has changed if im.state.MACAddress != attrs.HardwareAddr.String() { im.state.MACAddress = attrs.HardwareAddr.String() stateChanged = true + changeReason = "MAC address changed" } // Update IP addresses @@ -49,6 +55,7 @@ func (im *InterfaceManager) updateInterfaceState() error { im.logger.Error().Err(err).Msg("failed to update IP addresses") } else if ipChanged { stateChanged = true + changeReason = "IP addresses changed" } im.state.LastUpdated = time.Now() @@ -56,7 +63,10 @@ func (im *InterfaceManager) updateInterfaceState() error { // Notify callback if state changed if stateChanged && im.onStateChange != nil { - im.logger.Debug().Interface("state", im.state).Msg("notifying state change") + im.logger.Debug(). + Str("changeReason", changeReason). + Interface("state", im.state). + Msg("notifying state change") im.onStateChange(*im.state) } @@ -80,6 +90,7 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, ipv6Gateway string ipv4Ready, ipv6Ready = false, false stateChanged = false + stateChangeReason string ) routes, _ := mgr.ListDefaultRoutes(link.AfInet6) @@ -123,40 +134,55 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, if !sortAndCompareStringSlices(im.state.IPv4Addresses, ipv4Addresses) { im.state.IPv4Addresses = ipv4Addresses stateChanged = true + stateChangeReason = "IPv4 addresses changed" } if !sortAndCompareIPv6AddressSlices(im.state.IPv6Addresses, ipv6Addresses) { im.state.IPv6Addresses = ipv6Addresses stateChanged = true + stateChangeReason = "IPv6 addresses changed" } if im.state.IPv4Address != ipv4Addr { im.state.IPv4Address = ipv4Addr stateChanged = true + stateChangeReason = "IPv4 address changed" } if im.state.IPv6Address != ipv6Addr { im.state.IPv6Address = ipv6Addr stateChanged = true + stateChangeReason = "IPv6 address changed" } if im.state.IPv6LinkLocal != ipv6LinkLocal { im.state.IPv6LinkLocal = ipv6LinkLocal stateChanged = true + stateChangeReason = "IPv6 link local address changed" } if im.state.IPv6Gateway != ipv6Gateway { im.state.IPv6Gateway = ipv6Gateway stateChanged = true + stateChangeReason = "IPv6 gateway changed" } if im.state.IPv4Ready != ipv4Ready { im.state.IPv4Ready = ipv4Ready stateChanged = true + stateChangeReason = "IPv4 ready state changed" } if im.state.IPv6Ready != ipv6Ready { im.state.IPv6Ready = ipv6Ready stateChanged = true + stateChangeReason = "IPv6 ready state changed" + } + + if stateChanged { + im.logger.Trace(). + Str("changeReason", stateChangeReason). + Interface("state", im.state). + Msg("interface state changed") } return stateChanged, nil diff --git a/pkg/nmlite/utils.go b/pkg/nmlite/utils.go index 49ed0078e..11952d0b4 100644 --- a/pkg/nmlite/utils.go +++ b/pkg/nmlite/utils.go @@ -42,17 +42,19 @@ func sortAndCompareStringSlices(a, b []string) bool { return true } +func sortIPv6AddressSlicesStable(a []types.IPv6Address) { + sort.SliceStable(a, func(i, j int) bool { + return a[i].Address.String() < a[j].Address.String() + }) +} + func sortAndCompareIPv6AddressSlices(a, b []types.IPv6Address) bool { if len(a) != len(b) { return false } - sort.SliceStable(a, func(i, j int) bool { - return a[i].Address.String() < b[j].Address.String() - }) - sort.SliceStable(b, func(i, j int) bool { - return b[i].Address.String() < a[j].Address.String() - }) + sortIPv6AddressSlicesStable(a) + sortIPv6AddressSlicesStable(b) for i := range a { if a[i].Address.String() != b[i].Address.String() { From ad4f7c09e10058ae6408699ea083a72956616b07 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Thu, 20 Nov 2025 20:41:37 +0000 Subject: [PATCH 4/5] Revert "fix(network): only trigger time sync when network transitions to online rather than every state change" This reverts commit 86be6df1d376651e48a2f4a2807dcbd4fdca9225. --- cmd/main.go | 7 +++++-- network.go | 10 +--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 2fd27dae5..fcf2cdfee 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -101,6 +101,11 @@ func supervise() error { cmd.Args = os.Args logFile, err := os.CreateTemp("", "jetkvm-stdout.log") + defer func() { + // we don't care about the errors here + _ = logFile.Close() + _ = os.Remove(logFile.Name()) + }() if err != nil { return fmt.Errorf("failed to create log file: %w", err) } @@ -128,8 +133,6 @@ func supervise() error { } if exiterr, ok := cmdErr.(*exec.ExitError); ok { - // createErrorDump will close and rename the file if successful - // Note: os.Exit bypasses defer, but file is already handled by createErrorDump createErrorDump(logFile) os.Exit(exiterr.ExitCode()) } diff --git a/network.go b/network.go index b2b224f06..eb14c70d6 100644 --- a/network.go +++ b/network.go @@ -6,7 +6,6 @@ import ( "net" "net/http" "reflect" - "sync/atomic" "time" "github.com/jetkvm/kvm/internal/confparser" @@ -119,10 +118,6 @@ func setPublicIPReadyState(ipv4Ready, ipv6Ready bool) { publicIPState.SetIPv4AndIPv6(ipv4Ready, ipv6Ready) } -var ( - isOnline = &atomic.Bool{} -) - func networkStateChanged(_ string, state types.InterfaceState) { // do not block the main thread go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed") @@ -131,10 +126,7 @@ func networkStateChanged(_ string, state types.InterfaceState) { writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession) } - previousOnline := isOnline.Load() - isOnline.Store(state.Online) - - if state.Online && !previousOnline { + if state.Online { networkLogger.Info().Msg("network state changed to online, triggering time sync") triggerTimeSyncOnNetworkStateChange() } From 4edf75395646eb3563c534d7536c9fe70fdeab5b Mon Sep 17 00:00:00 2001 From: Siyuan Date: Fri, 21 Nov 2025 12:24:25 +0000 Subject: [PATCH 5/5] chore(nmlite/ifstate): store change reasons as a slice of enums --- pkg/nmlite/interface_state.go | 49 ++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/pkg/nmlite/interface_state.go b/pkg/nmlite/interface_state.go index ba3e1b0d2..efa5f087b 100644 --- a/pkg/nmlite/interface_state.go +++ b/pkg/nmlite/interface_state.go @@ -2,6 +2,7 @@ package nmlite import ( "fmt" + "strings" "time" "github.com/jetkvm/kvm/internal/network/types" @@ -9,6 +10,40 @@ import ( "github.com/vishvananda/netlink" ) +type IfStateChangeReason uint + +const ( + IfStateOperStateChanged IfStateChangeReason = 1 + IfStateOnlineStateChanged IfStateChangeReason = 2 + IfStateMACAddressChanged IfStateChangeReason = 3 + IfStateIPAddressesChanged IfStateChangeReason = 4 +) + +type IfStateChangeReasons []IfStateChangeReason + +func (r IfStateChangeReason) String() string { + switch r { + case IfStateOperStateChanged: + return "oper state changed" + case IfStateOnlineStateChanged: + return "online state changed" + case IfStateMACAddressChanged: + return "MAC address changed" + case IfStateIPAddressesChanged: + return "IP addresses changed" + default: + return fmt.Sprintf("unknown change reason %d", r) + } +} + +func (rs IfStateChangeReasons) String() string { + reasons := []string{} + for _, r := range rs { + reasons = append(reasons, r.String()) + } + return strings.Join(reasons, ", ") +} + // updateInterfaceState updates the current interface state func (im *InterfaceManager) updateInterfaceState() error { nl, err := im.link() @@ -17,8 +52,8 @@ func (im *InterfaceManager) updateInterfaceState() error { } var ( - stateChanged bool - changeReason string + stateChanged bool + changeReasons IfStateChangeReasons ) attrs := nl.Attrs() @@ -32,7 +67,7 @@ func (im *InterfaceManager) updateInterfaceState() error { if im.state.Up != isUp { im.state.Up = isUp stateChanged = true - changeReason = "oper state changed" + changeReasons = append(changeReasons, IfStateOperStateChanged) } // Check if the interface is online @@ -40,14 +75,14 @@ func (im *InterfaceManager) updateInterfaceState() error { if im.state.Online != isOnline { im.state.Online = isOnline stateChanged = true - changeReason = "online state changed" + changeReasons = append(changeReasons, IfStateOnlineStateChanged) } // Check if the MAC address has changed if im.state.MACAddress != attrs.HardwareAddr.String() { im.state.MACAddress = attrs.HardwareAddr.String() stateChanged = true - changeReason = "MAC address changed" + changeReasons = append(changeReasons, IfStateMACAddressChanged) } // Update IP addresses @@ -55,7 +90,7 @@ func (im *InterfaceManager) updateInterfaceState() error { im.logger.Error().Err(err).Msg("failed to update IP addresses") } else if ipChanged { stateChanged = true - changeReason = "IP addresses changed" + changeReasons = append(changeReasons, IfStateIPAddressesChanged) } im.state.LastUpdated = time.Now() @@ -64,7 +99,7 @@ func (im *InterfaceManager) updateInterfaceState() error { // Notify callback if state changed if stateChanged && im.onStateChange != nil { im.logger.Debug(). - Str("changeReason", changeReason). + Stringer("changeReasons", changeReasons). Interface("state", im.state). Msg("notifying state change") im.onStateChange(*im.state)