feat(qemu): per-session extra drives + start_pi payload passthrough
A profile's extra_drive is the same file for everyone; an overlay may need a disk built for THIS session (what the project declared). New seam set_pi_extra_drive_resolver(fn) receives the client id, the board and the start_pi payload and returns raw images, mounted read-only after the profile's own. The WS route forwards msg_data and the bridge gained startPayload so a client can declare it. Generic: no package manager, no OS knowledge in the OSS tree.
This commit is contained in:
parent
72e48073bd
commit
78f0181435
|
|
@ -76,7 +76,9 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
|
|||
if not await board_allowed(websocket, board):
|
||||
await qemu_callback('error', {'message': PRO_BOARD_MESSAGE})
|
||||
else:
|
||||
qemu_manager.start_instance(client_id, board, qemu_callback)
|
||||
# msg_data carries whatever the client declared for this
|
||||
# session (an overlay may materialise extra drives from it).
|
||||
qemu_manager.start_instance(client_id, board, qemu_callback, msg_data)
|
||||
|
||||
elif msg_type == 'stop_pi':
|
||||
qemu_manager.stop_instance(client_id)
|
||||
|
|
|
|||
|
|
@ -155,6 +155,34 @@ _PI_PROFILE_KEYS = frozenset(
|
|||
)
|
||||
|
||||
|
||||
# ── Per-instance extra drives (overlay seam) ─────────────────────────────
|
||||
#
|
||||
# A profile's `extra_drive` is static (the same tar for every session). An
|
||||
# overlay may need a drive built PER SESSION — e.g. the packages a given
|
||||
# project declared. It registers a resolver here; the boot path calls it
|
||||
# with the client id and the start payload and appends whatever raw images
|
||||
# it returns, read-only, after the profile's own.
|
||||
#
|
||||
# Signature: def(client_id: str, board_type: str, payload: dict) -> list[str]
|
||||
_EXTRA_DRIVE_RESOLVER: Callable[[str, str, dict], list] | None = None
|
||||
|
||||
|
||||
def set_pi_extra_drive_resolver(fn) -> None:
|
||||
"""Install (or clear) the overlay's per-instance extra-drive resolver."""
|
||||
global _EXTRA_DRIVE_RESOLVER
|
||||
_EXTRA_DRIVE_RESOLVER = fn
|
||||
|
||||
|
||||
def _resolve_extra_drives(client_id: str, board_type: str, payload: dict) -> list:
|
||||
if _EXTRA_DRIVE_RESOLVER is None:
|
||||
return []
|
||||
try:
|
||||
return list(_EXTRA_DRIVE_RESOLVER(client_id, board_type, payload) or [])
|
||||
except Exception:
|
||||
logger.exception('extra-drive resolver failed (continuing without it)')
|
||||
return []
|
||||
|
||||
|
||||
def register_pi_board_profile(board_type: str, cfg: dict) -> None:
|
||||
"""Register (or override) a QEMU-Linux board profile at runtime.
|
||||
|
||||
|
|
@ -257,6 +285,9 @@ class PiInstance:
|
|||
# Canvas-fed named values served to the guest via the SENS
|
||||
# protocol op (overlay boards' built-in sensors/buttons).
|
||||
self.sensor_state: dict[str, float] = {}
|
||||
# Raw `start_pi` payload — carries whatever the client declared for
|
||||
# this session (e.g. the packages an overlay must materialise).
|
||||
self.start_payload: dict = {}
|
||||
|
||||
async def emit(self, event_type: str, data: dict) -> None:
|
||||
try:
|
||||
|
|
@ -277,7 +308,8 @@ class QemuManager:
|
|||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def start_instance(self, client_id: str, board_type: str,
|
||||
callback: EventCallback) -> None:
|
||||
callback: EventCallback,
|
||||
payload: dict | None = None) -> None:
|
||||
if client_id in self._instances:
|
||||
logger.warning('start_instance: %s already running', client_id)
|
||||
return
|
||||
|
|
@ -288,6 +320,7 @@ class QemuManager:
|
|||
)
|
||||
board_type = DEFAULT_PI_BOARD
|
||||
inst = PiInstance(client_id, callback, board_type=board_type)
|
||||
inst.start_payload = dict(payload or {})
|
||||
self._instances[client_id] = inst
|
||||
asyncio.create_task(self._boot(inst))
|
||||
|
||||
|
|
@ -487,12 +520,15 @@ class QemuManager:
|
|||
# Optional read-only auxiliary disk (overlay-registered board
|
||||
# profiles use it to ship guest-side shim libraries). Shows up as
|
||||
# the second virtio-blk — /dev/vdb on the pci transport.
|
||||
extra_drive = cfg.get('extra_drive')
|
||||
if extra_drive and os.path.exists(extra_drive):
|
||||
extra_drives = [cfg.get('extra_drive')] + _resolve_extra_drives(
|
||||
inst.client_id, inst.board_type, inst.start_payload,
|
||||
)
|
||||
for idx, drive in enumerate(d for d in extra_drives if d and os.path.exists(d)):
|
||||
drive_id = f'aux{idx}'
|
||||
cmd += [
|
||||
'-drive', f'if=none,file={extra_drive},format=raw,readonly=on,id=aux',
|
||||
'-device', ('virtio-blk-pci,drive=aux' if cfg['bus'] == 'pci'
|
||||
else 'virtio-blk-device,drive=aux'),
|
||||
'-drive', f'if=none,file={drive},format=raw,readonly=on,id={drive_id}',
|
||||
'-device', (f'virtio-blk-pci,drive={drive_id}' if cfg['bus'] == 'pci'
|
||||
else f'virtio-blk-device,drive={drive_id}'),
|
||||
]
|
||||
|
||||
logger.info('Launching QEMU for %s: %s',
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ export class RaspberryPi3Bridge {
|
|||
private socket: WebSocket | null = null;
|
||||
private _connected = false;
|
||||
private _booted = false;
|
||||
/** Extra fields merged into the `start_pi` payload (an overlay uses it
|
||||
* to declare what this session needs materialised, e.g. packages). */
|
||||
startPayload: Record<string, unknown> = {};
|
||||
/** quietBoot (ProBoardDef): while true, guest serial output is withheld
|
||||
* from onSerialData (boot detection still runs) and a neutral Velxio
|
||||
* progress line + dots show instead. The run path calls setQuiet(false)
|
||||
|
|
@ -118,7 +121,10 @@ export class RaspberryPi3Bridge {
|
|||
this._connected = true;
|
||||
this.onConnected?.();
|
||||
// Tell the backend which Pi family member to boot.
|
||||
this._send({ type: 'start_pi', data: { board: this.boardKind } });
|
||||
this._send({
|
||||
type: 'start_pi',
|
||||
data: { board: this.boardKind, ...this.startPayload },
|
||||
});
|
||||
if (this.quietBootDefault) {
|
||||
this._quiet = true;
|
||||
const label = this.quietBootLabel || this.boardKind;
|
||||
|
|
|
|||
Loading…
Reference in New Issue