Skip to content

fix(kvm): close browser websocket immediately on AMT disconnect#820

Open
nmgaston wants to merge 2 commits intodevice-management-toolkit:mainfrom
nmgaston:fixKVMNotShowingDisconnected
Open

fix(kvm): close browser websocket immediately on AMT disconnect#820
nmgaston wants to merge 2 commits intodevice-management-toolkit:mainfrom
nmgaston:fixKVMNotShowingDisconnected

Conversation

@nmgaston
Copy link
Contributor

@nmgaston nmgaston commented Mar 3, 2026

NOTE: Use with sample-web-ui PR: device-management-toolkit/sample-web-ui#3159

Problem

When an AMT device disconnects (e.g. the device is powered off or the network drops), the KVM/SOL/IDER session would appear frozen in the browser UI. The browser websocket was only closed after ListenToBrowser unblocked from its ReadMessage call, which could take up to 30 seconds (the inactivity timeout) or indefinitely if the browser wasn't sending data.

Fix

In ListenToDevice, the deferred cleanup now immediately sends a websocket CloseMessage to the browser and closes the connection before cancelling the context. This means the browser receives the close frame as soon as the AMT device side drops, allowing the UI to update without waiting for ListenToBrowser to unblock.

Changes

  • internal/usecase/devices/interceptor.go — updated the ListenToDevice defer to send websocket.CloseMessage and call Close() on the browser connection before cancel()
  • internal/usecase/devices/interceptor_private_test.go — added TestListenToDeviceClosesWebSocketOnAMTDisconnect to verify WriteMessage(CloseMessage) and Close() are called on the browser websocket when the AMT side disconnects

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves KVM/SOL/IDER session teardown behavior by proactively closing the browser WebSocket as soon as the AMT/device side disconnects, preventing the UI from appearing “frozen” while ListenToBrowser remains blocked.

Changes:

  • Updated ListenToDevice cleanup to send a WebSocket close frame to the browser and close the connection immediately on exit.
  • Added a private unit test to verify CloseMessage + Close() are invoked when the AMT side drops.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
internal/usecase/devices/interceptor.go Sends CloseMessage + closes browser WebSocket in ListenToDevice defer; minor comment tweak near length casting.
internal/usecase/devices/interceptor_private_test.go Adds a spy-based unit test asserting the browser WebSocket is closed when AMT disconnects.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +220 to +224
_ = deviceConnection.Conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "AMT session ended"),
)
_ = deviceConnection.Conn.Close()
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ListenToDevice, the function writes device data using the locally captured conn := deviceConnection.Conn, but the new deferred cleanup closes deviceConnection.Conn directly. Since deviceConnection.Conn can be updated elsewhere (e.g., reconnect path), this can lead to closing a different WebSocket than the one used for writes (and potentially leaving the original connection open). Consider using a single, consistent reference for both writing and cleanup (e.g., close conn, or access/update Conn under a lock and always use the current value).

Suggested change
_ = deviceConnection.Conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "AMT session ended"),
)
_ = deviceConnection.Conn.Close()
if conn != nil {
_ = conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "AMT session ended"),
)
_ = conn.Close()
}

Copilot uses AI. Check for mistakes.
}

length := uint32(totalLength) //nolint:gosec // Ignore potential integer overflow here as overflow is validated earlier in code
length := uint32(totalLength) // overflow validated above
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

length := uint32(totalLength) will likely be flagged by the enabled gosec linter (integer conversion) even though there's a bounds check above. Since this file already uses //nolint:gosec for similar validated conversions, consider restoring a //nolint:gosec on this line or refactoring the conversion in a way that satisfies gosec so CI doesn't fail.

Suggested change
length := uint32(totalLength) // overflow validated above
length := uint32(totalLength) //nolint:gosec // overflow validated above via explicit bounds check

Copilot uses AI. Check for mistakes.
cancel: cancel,
healthTicker: time.NewTicker(HeartbeatInterval),
}

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test allocates a ticker and a cancellable context but never stops/cancels them. Even though it’s a unit test, it’s better to avoid leaking timers/resources (and it helps with leak-detecting tooling). Consider using t.Cleanup to stop the ticker and call cancel(), or omit healthTicker entirely if it isn’t needed for this test.

Suggested change
t.Cleanup(func() {
cancel()
deviceConnection.healthTicker.Stop()
})

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console UI does not reflect KVM disconnect after AMT closes idle session (~3 minutes inactivity)

2 participants