From 741305919372b85ad5c74b184d6f0e92d2ba370d Mon Sep 17 00:00:00 2001 From: modeco80 Date: Sun, 14 Jul 2024 19:04:19 -0400 Subject: [PATCH] qemu: Switch to QMP over stdio Simply a more convinent pipe. Additionally, because the pipe will only break when the process exits, this means we can now remove QMP reconnection logic entirely. Can't exactly have problems when the problem code is factored out ;) --- qemu/src/QemuVM.ts | 101 ++++++++++++------------------------------ qemu/src/QmpClient.ts | 4 +- 2 files changed, 30 insertions(+), 75 deletions(-) diff --git a/qemu/src/QemuVM.ts b/qemu/src/QemuVM.ts index ab99930..ab25a5c 100644 --- a/qemu/src/QemuVM.ts +++ b/qemu/src/QemuVM.ts @@ -6,6 +6,7 @@ import { unlink } from 'node:fs/promises'; import * as Shared from '@cvmts/shared'; import { Socket, connect } from 'net'; +import { Readable, Stream, Writable } from 'stream'; export enum VMState { Stopped, @@ -14,8 +15,6 @@ export enum VMState { Stopping } -// TODO: Add bits to this to allow usage (optionally) -// of VNC/QMP port. This will be needed to fix up Windows support. export type QemuVmDefinition = { id: string; command: string; @@ -25,26 +24,24 @@ export type QemuVmDefinition = { /// Temporary path base (for UNIX sockets/etc.) const kVmTmpPathBase = `/tmp`; -/// The max amount of times QMP connection is allowed to fail before -/// the VM is forcefully stopped. -const kMaxFailCount = 5; - -// writer implementation for net.Socket -class SocketWriter implements IQmpClientWriter { - socket; +// writer implementation for process standard I/O +class StdioWriter implements IQmpClientWriter { + stdout; + stdin; client; - constructor(socket: Socket, client: QmpClient) { - this.socket = socket; + constructor(stdout: Readable, stdin: Writable, client: QmpClient) { + this.stdout = stdout; + this.stdin = stdin; this.client = client; - this.socket.on('data', (data) => { + this.stdout.on('data', (data) => { this.client.feed(data); }); } writeSome(buffer: Buffer) { - this.socket.write(buffer); + this.stdin.write(buffer); } } @@ -53,8 +50,6 @@ export class QemuVM extends EventEmitter { // QMP stuff. private qmpInstance: QmpClient = new QmpClient(); - private qmpSocket: Socket | null = null; - private qmpFailCount = 0; private qemuProcess: ExecaChildProcess | null = null; @@ -91,8 +86,7 @@ export class QemuVM extends EventEmitter { self.SetState(VMState.Started); }) - // now that we've connected to VNC, connect to the display - self.qmpFailCount = 0; + // now that QMP has connected, connect to the display self.display?.Connect(); }); } @@ -108,7 +102,7 @@ export class QemuVM extends EventEmitter { if (!this.addedAdditionalArguments) { cmd += ' -no-shutdown'; if (this.definition.snapshot) cmd += ' -snapshot'; - cmd += ` -qmp unix:${this.GetQmpPath()},server,wait -vnc unix:${this.GetVncPath()}`; + cmd += ` -qmp stdio -vnc unix:${this.GetVncPath()}`; this.definition.command = cmd; this.addedAdditionalArguments = true; } @@ -189,11 +183,6 @@ export class QemuVM extends EventEmitter { private SetState(state: VMState) { this.state = state; this.emit('statechange', this.state); - - // reset QMP fail count when the VM is (re)starting or stopped - if (this.state == VMState.Stopped || this.state == VMState.Starting) { - this.qmpFailCount = 0; - } } private GetQmpPath() { @@ -212,7 +201,11 @@ export class QemuVM extends EventEmitter { this.VMLog().Info(`Starting QEMU with command \"${split}\"`); // Start QEMU - this.qemuProcess = execaCommand(split); + this.qemuProcess = execaCommand(split, { + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe' + }); this.qemuProcess.stderr?.on('data', (data) => { self.VMLog().Error('QEMU stderr: {0}', data.toString('utf8')); @@ -220,8 +213,7 @@ export class QemuVM extends EventEmitter { this.qemuProcess.on('spawn', async () => { self.VMLog().Info('QEMU started'); - await Shared.Sleep(500); - await self.ConnectQmp(); + await self.QmpStdioInit(); }); this.qemuProcess.on('exit', async (code) => { @@ -230,15 +222,14 @@ export class QemuVM extends EventEmitter { // Disconnect from the display and QMP connections. await self.DisconnectDisplay(); - // Remove the sockets for VNC and QMP. + self.qmpInstance.reset(); + self.qmpInstance.setWriter(null); + + // Remove the VNC UDS socket. try { await unlink(this.GetVncPath()); } catch (_) {} - try { - await unlink(this.GetQmpPath()); - } catch (_) {} - if (self.state != VMState.Stopping) { if (code == 0) { // Wait a bit and restart QEMU. @@ -264,51 +255,15 @@ export class QemuVM extends EventEmitter { } } - private async ConnectQmp() { + private async QmpStdioInit() { let self = this; - if (this.qmpSocket) { - // This isn't really a problem (since we gate it) - // but I'd like to see if i could eliminate this - this.VMLog().Warning('QemuVM.ConnectQmp(): Already connected to QMP socket!'); - return; - } - - await Shared.Sleep(500); - this.qmpSocket = connect(this.GetQmpPath()); - - this.qmpSocket.on('close', async () => { - self.qmpSocket?.removeAllListeners(); - self.qmpSocket = null; - - // If we aren't stopping (i.e: disconnection wasn't because we disconnected), - // then we should care QMP disconnected - if (self.state != VMState.Stopping) { - if (self.qmpFailCount++ < kMaxFailCount) { - self.VMLog().Error(`Failed to connect to QMP ${self.qmpFailCount} times.`); - await Shared.Sleep(500); - await self.ConnectQmp(); - } else { - self.VMLog().Error(`Reached max retries, giving up.`); - await self.Stop(); - return; - } - } - }); - - this.qmpSocket.on('error', (e: Error) => { - self.VMLog().Error('QMP socket error: {0}', e.message); - }); - - this.qmpSocket.on('connect', () => { - self.VMLog().Info("Connected to QMP socket"); - - // Setup the QMP client. - let writer = new SocketWriter(self.qmpSocket!, self.qmpInstance); - self.qmpInstance.reset(); - self.qmpInstance.setWriter(writer); - }) + self.VMLog().Info("Initializing QMP over stdio"); + // Setup the QMP client. + let writer = new StdioWriter(this.qemuProcess?.stdout!, this.qemuProcess?.stdin!, self.qmpInstance); + self.qmpInstance.reset(); + self.qmpInstance.setWriter(writer); } private async DisconnectDisplay() { diff --git a/qemu/src/QmpClient.ts b/qemu/src/QmpClient.ts index 68d7a5f..3a31cf0 100644 --- a/qemu/src/QmpClient.ts +++ b/qemu/src/QmpClient.ts @@ -31,7 +31,7 @@ export enum QmpEvent { Stop = 'STOP', VncConnected = 'VNC_CONNECTED', VncDisconnected = 'VNC_DISCONNECTED', - VncInitalized = 'VNC_INITALIZED', + VncInitialized = 'VNC_INITIALIZED', Watchdog = 'WATCHDOG' } @@ -79,7 +79,7 @@ export class QmpClient extends EventEmitter { }); } - setWriter(writer: IQmpClientWriter) { + setWriter(writer: IQmpClientWriter|null) { this.writer = writer; }