Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 126f67d

Browse files
committed
nanokvm: address review comments
1 parent e3f15bd commit 126f67d

File tree

4 files changed

+59
-52
lines changed

4 files changed

+59
-52
lines changed

packages/jumpstarter-driver-nanokvm/jumpstarter_driver_nanokvm/driver.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,9 @@ async def _get_client(self) -> NanoKVMAPIClient:
8686
def close(self):
8787
"""Clean up resources"""
8888
# Schedule cleanup of client
89-
if self._client is not None:
89+
if self._client_ctx is not None:
9090
try:
91-
import asyncio
92-
loop = asyncio.get_event_loop()
93-
if loop.is_running():
94-
loop.create_task(self._client_ctx.__aexit__(None, None, None))
95-
else:
96-
loop.run_until_complete(self._client_ctx.__aexit__(None, None, None))
91+
anyio.from_thread.run(self._client_ctx.__aexit__(None, None, None))
9792
except Exception as e:
9893
self.logger.debug(f"Error closing client: {e}")
9994

@@ -215,14 +210,9 @@ async def _get_client(self) -> NanoKVMAPIClient:
215210
def close(self):
216211
"""Clean up resources"""
217212
# Schedule cleanup of client
218-
if self._client is not None:
213+
if self._client_ctx is not None:
219214
try:
220-
import asyncio
221-
loop = asyncio.get_event_loop()
222-
if loop.is_running():
223-
loop.create_task(self._client_ctx.__aexit__(None, None, None))
224-
else:
225-
loop.run_until_complete(self._client_ctx.__aexit__(None, None, None))
215+
anyio.from_thread.run(self._client_ctx.__aexit__(None, None, None))
226216
except Exception as e:
227217
self.logger.debug(f"Error closing client: {e}")
228218

@@ -397,13 +387,14 @@ def __post_init__(self):
397387
),
398388
}
399389

390+
super().__post_init__()
391+
400392
# Optionally add serial console access
401393
if self.enable_serial:
402394
# Note: This is a placeholder - actual serial console access via SSH
403395
# would require additional implementation in the nanokvm library
404396
self.logger.warning("Serial console access not yet fully implemented")
405397

406-
super().__post_init__()
407398

408399
@classmethod
409400
def client(cls) -> str:
@@ -415,26 +406,44 @@ async def get_info(self):
415406
# Get info from the video driver's client
416407
video_driver = self.children["video"]
417408

418-
@with_reauth
419-
async def _get_info_impl(driver):
420-
client = await driver._get_client()
421-
info = await client.get_info()
409+
def _format_info(info):
410+
"""Format device info into a dictionary"""
422411
return {
423-
"ips": [{"name": ip.name, "addr": ip.addr, "version": ip.version, "type": ip.type} for ip in info.ips],
412+
"ips": [
413+
{"name": ip.name, "addr": ip.addr, "version": ip.version, "type": ip.type}
414+
for ip in info.ips
415+
],
424416
"mdns": info.mdns,
425417
"image": info.image,
426418
"application": info.application,
427419
"device_key": info.device_key,
428420
}
429421

430-
return await _get_info_impl(video_driver)
422+
try:
423+
client = await video_driver._get_client()
424+
info = await client.get_info()
425+
return _format_info(info)
426+
except Exception as e:
427+
if _is_unauthorized_error(e):
428+
self.logger.warning("Received 401 Unauthorized, re-authenticating...")
429+
await video_driver._reset_client()
430+
# Retry once after re-authentication
431+
client = await video_driver._get_client()
432+
info = await client.get_info()
433+
return _format_info(info)
434+
raise
431435

432436
@export
433437
async def reboot(self):
434438
"""Reboot the NanoKVM device"""
435439
video_driver = self.children["video"]
436-
client = await video_driver._get_client()
437-
await client.reboot_system()
440+
441+
@with_reauth
442+
async def _reboot_impl(driver):
443+
client = await driver._get_client()
444+
await client.reboot_system()
445+
446+
await _reboot_impl(video_driver)
438447
self.logger.info("NanoKVM device rebooted")
439448

440449
@export

packages/jumpstarter-driver-nanokvm/jumpstarter_driver_nanokvm/driver_test.py

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def mock_nanokvm_client():
1818
# Mock authentication
1919
mock_client.authenticate = AsyncMock()
2020
mock_client.logout = AsyncMock()
21+
mock_client.close = AsyncMock()
2122

2223
# Mock info
2324
mock_info = MagicMock()
@@ -43,6 +44,8 @@ async def mock_stream():
4344
# Mock HID functions
4445
mock_client.paste_text = AsyncMock()
4546
mock_client.reset_hid = AsyncMock()
47+
mock_client.mouse_move_abs = AsyncMock()
48+
mock_client.mouse_click = AsyncMock()
4649

4750
# Mock reboot
4851
mock_client.reboot_system = AsyncMock()
@@ -52,14 +55,19 @@ async def mock_stream():
5255
mock_images.files = ["/data/alpine-standard-3.23.2-x86_64.iso", "/data/cs10-js.iso"]
5356
mock_client.get_images = AsyncMock(return_value=mock_images)
5457

55-
mock_client_class.return_value = mock_client
58+
# Mock context manager behavior
59+
mock_context = AsyncMock()
60+
mock_context.__aenter__ = AsyncMock(return_value=mock_client)
61+
mock_context.__aexit__ = AsyncMock(return_value=None)
62+
mock_client_class.return_value = mock_context
63+
5664
yield mock_client
5765

5866

5967
@pytest.fixture
6068
def mock_aiohttp_session():
6169
"""Create a mock aiohttp ClientSession"""
62-
with patch("jumpstarter_driver_nanokvm.driver.ClientSession") as mock_session_class:
70+
with patch("aiohttp.ClientSession") as mock_session_class:
6371
mock_session = AsyncMock()
6472
mock_session.close = AsyncMock()
6573
mock_session_class.return_value = mock_session
@@ -173,42 +181,28 @@ def test_nanokvm_client_creation():
173181

174182
def test_nanokvm_mouse_move_abs(mock_nanokvm_client, mock_aiohttp_session):
175183
"""Test mouse absolute movement"""
176-
with patch("jumpstarter_driver_nanokvm.driver.ClientSession") as mock_session_class:
177-
mock_ws = AsyncMock()
178-
mock_ws.send_json = AsyncMock()
179-
mock_session = AsyncMock()
180-
mock_session.ws_connect = AsyncMock(return_value=mock_ws)
181-
mock_session.close = AsyncMock()
182-
mock_session_class.return_value = mock_session
183-
184-
hid = NanoKVMHID(host="test.local", username="admin", password="admin")
184+
hid = NanoKVMHID(host="test.local", username="admin", password="admin")
185185

186-
with serve(hid) as client:
187-
# Move mouse to absolute position
188-
client.mouse_move_abs(32768, 32768)
186+
with serve(hid) as client:
187+
# Move mouse to absolute position (normalized 0.0-1.0 coordinates)
188+
client.mouse_move_abs(0.5, 0.5)
189189

190-
# Verify WebSocket message was sent
191-
mock_ws.send_json.assert_called()
190+
# Verify the mock was called
191+
mock_nanokvm_client.mouse_move_abs.assert_called_once_with(0.5, 0.5)
192192

193193

194194
def test_nanokvm_mouse_click(mock_nanokvm_client, mock_aiohttp_session):
195195
"""Test mouse click"""
196-
with patch("jumpstarter_driver_nanokvm.driver.ClientSession") as mock_session_class:
197-
mock_ws = AsyncMock()
198-
mock_ws.send_json = AsyncMock()
199-
mock_session = AsyncMock()
200-
mock_session.ws_connect = AsyncMock(return_value=mock_ws)
201-
mock_session.close = AsyncMock()
202-
mock_session_class.return_value = mock_session
196+
from nanokvm.models import MouseButton
203197

204-
hid = NanoKVMHID(host="test.local", username="admin", password="admin")
198+
hid = NanoKVMHID(host="test.local", username="admin", password="admin")
205199

206-
with serve(hid) as client:
207-
# Click left button
208-
client.mouse_click("left")
200+
with serve(hid) as client:
201+
# Click left button
202+
client.mouse_click("left")
209203

210-
# Verify WebSocket messages were sent (down and up)
211-
assert mock_ws.send_json.call_count >= 2
204+
# Verify the mock was called
205+
mock_nanokvm_client.mouse_click.assert_called_once_with(MouseButton.LEFT, None, None)
212206

213207

214208
def test_nanokvm_get_images(mock_nanokvm_client, mock_aiohttp_session):

packages/jumpstarter-driver-nanokvm/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ dependencies = [
2121
"click",
2222
]
2323

24+
[project.entry-points."jumpstarter.drivers"]
25+
NanoKVM = "jumpstarter_driver_nanokvm.driver:NanoKVM"
26+
2427
[tool.hatch.version]
2528
source = "vcs"
2629
raw-options = { 'root' = '../../'}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jumpstarter-driver-http-power = { workspace = true }
1919
jumpstarter-driver-gpiod = { workspace = true }
2020
jumpstarter-driver-ridesx = { workspace = true }
2121
jumpstarter-driver-network = { workspace = true }
22+
jumpstarter-driver-nanokvm = { workspace = true }
2223
jumpstarter-driver-opendal = { workspace = true }
2324
jumpstarter-driver-power = { workspace = true }
2425
jumpstarter-driver-probe-rs = { workspace = true }

0 commit comments

Comments
 (0)