From 608b7255c96ff4a3187eb037ed290ab937a896ef Mon Sep 17 00:00:00 2001 From: modeco80 Date: Fri, 23 Aug 2024 07:01:21 -0400 Subject: [PATCH] remove display stuff instead, we now provide the information for a higher level also removes a dep so :) --- README.md | 6 +- examples/simple.js | 11 +++- package.json | 3 +- src/QemuDisplay.ts | 148 --------------------------------------------- src/QemuUtil.ts | 55 ----------------- src/QemuVM.ts | 80 ++++++++++++------------ src/index.ts | 2 - 7 files changed, 53 insertions(+), 252 deletions(-) delete mode 100644 src/QemuDisplay.ts delete mode 100644 src/QemuUtil.ts diff --git a/README.md b/README.md index 21a1235..f2429c3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # superqemu -A QEMU supervision library for Node.js +A lean QEMU supervision library for Node.js. + +# Usage + +See the /examples directory in the repo for now. The library is pretty simple though (on purpose). diff --git a/examples/simple.js b/examples/simple.js index 20eb9f1..2526101 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -1,8 +1,12 @@ -// A simple example of how to use superqemu. +// A simple/contrived? example of how to use superqemu. +// // Note that this example requires a valid desktop environment to function // due to `-display gtk`, but you can remove it and run it headless. +// +// Also note that while superqemu automatically sets up QEMU to use VNC, +// it does not provide its own VNC client implementation. -import { QemuVM } from "../dist/index.js"; +import { QemuVM, VMState } from "../dist/index.js"; import pino from 'pino'; @@ -18,6 +22,9 @@ let vm = new QemuVM( vm.on('statechange', (newState) => { logger.info(`state changed to ${newState}`); + if(newState == VMState.Started) { + logger.info(vm.GetDisplayInfo(), `VM started: display info prepends this message`); + } }); (async () => { diff --git a/package.json b/package.json index 1e7c452..556b501 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@computernewb/superqemu", - "version": "0.2.3", + "version": "0.2.4-alpha0", "description": "A simple and easy to use QEMU supervision runtime for Node.js", "exports": "./dist/index.js", "types": "./dist/index.d.ts", @@ -23,7 +23,6 @@ } }, "dependencies": { - "@computernewb/nodejs-rfb": "^0.3.0", "execa": "^8.0.1", "pino": "^9.3.1" }, diff --git a/src/QemuDisplay.ts b/src/QemuDisplay.ts deleted file mode 100644 index ebfab52..0000000 --- a/src/QemuDisplay.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { VncClient } from '@computernewb/nodejs-rfb'; -import { EventEmitter } from 'node:events'; -import { Size, Rect, Clamp, BatchRects } from './QemuUtil.js'; - -const kQemuFps = 60; - -export type VncRect = { - x: number; - y: number; - width: number; - height: number; -}; - -// events: -// -// 'resize' -> (w, h) -> done when resize occurs -// 'rect' -> (x, y, ImageData) -> framebuffer -// 'frame' -> () -> done at end of frame - -export class QemuDisplay extends EventEmitter { - private displayVnc = new VncClient({ - debug: false, - fps: kQemuFps, - - encodings: [ - VncClient.consts.encodings.raw, - - //VncClient.consts.encodings.pseudoQemuAudio, - VncClient.consts.encodings.pseudoDesktopSize - // For now? - //VncClient.consts.encodings.pseudoCursor - ] - }); - - private vncShouldReconnect: boolean = false; - private vncConnectOpts: any; - - constructor(vncConnectOpts: any) { - super(); - - this.vncConnectOpts = vncConnectOpts; - - this.displayVnc.on('connectTimeout', () => { - this.Reconnect(); - }); - - this.displayVnc.on('authError', () => { - this.Reconnect(); - }); - - this.displayVnc.on('disconnect', () => { - this.Reconnect(); - }); - - this.displayVnc.on('closed', () => { - this.Reconnect(); - }); - - this.displayVnc.on('firstFrameUpdate', () => { - // apparently this library is this good. - // at least it's better than the two others which exist. - this.displayVnc.changeFps(kQemuFps); - this.emit('connected'); - - this.emit('resize', { width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight }); - //this.emit('rect', { x: 0, y: 0, width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight }); - this.emit('frame'); - }); - - this.displayVnc.on('desktopSizeChanged', (size: Size) => { - this.emit('resize', size); - }); - - let rects: Rect[] = []; - - this.displayVnc.on('rectUpdateProcessed', (rect: Rect) => { - rects.push(rect); - }); - - this.displayVnc.on('frameUpdated', (fb: Buffer) => { - // use the cvmts batcher - let batched = BatchRects(this.Size(), rects); - this.emit('rect', batched); - - // unbatched (watch the performace go now) - //for(let rect of rects) - // this.emit('rect', rect); - - rects = []; - - this.emit('frame'); - }); - } - - private Reconnect() { - if (this.displayVnc.connected) return; - - if (!this.vncShouldReconnect) return; - - // TODO: this should also give up after a max tries count - // if we fail after max tries, emit a event - - this.displayVnc.connect(this.vncConnectOpts); - } - - Connect() { - this.vncShouldReconnect = true; - this.Reconnect(); - } - - Disconnect() { - this.vncShouldReconnect = false; - this.displayVnc.disconnect(); - - // bye bye! - this.displayVnc.removeAllListeners(); - this.removeAllListeners(); - } - - Connected() { - return this.displayVnc.connected; - } - - Buffer(): Buffer { - return this.displayVnc.fb; - } - - Size(): Size { - if (!this.displayVnc.connected) - return { - width: 0, - height: 0 - }; - - return { - width: this.displayVnc.clientWidth, - height: this.displayVnc.clientHeight - }; - } - - MouseEvent(x: number, y: number, buttons: number) { - if (this.displayVnc.connected) this.displayVnc.sendPointerEvent(Clamp(x, 0, this.displayVnc.clientWidth), Clamp(y, 0, this.displayVnc.clientHeight), buttons); - } - - KeyboardEvent(keysym: number, pressed: boolean) { - if (this.displayVnc.connected) this.displayVnc.sendKeyEvent(keysym, pressed); - } -} diff --git a/src/QemuUtil.ts b/src/QemuUtil.ts deleted file mode 100644 index a90ea5e..0000000 --- a/src/QemuUtil.ts +++ /dev/null @@ -1,55 +0,0 @@ -export type Size = { - width: number; - height: number; -}; - -export type Rect = { - x: number; - y: number; - width: number; - height: number; -}; - -export function Clamp(input: number, min: number, max: number) { - return Math.min(Math.max(input, min), max); -} - -export function BatchRects(size: Size, rects: Array): Rect { - var mergedX = size.width; - var mergedY = size.height; - var mergedHeight = 0; - var mergedWidth = 0; - - // can't batch these - if (rects.length == 0) { - return { - x: 0, - y: 0, - width: size.width, - height: size.height - }; - } - - if (rects.length == 1) { - if (rects[0].width == size.width && rects[0].height == size.height) { - return rects[0]; - } - } - - rects.forEach((r) => { - if (r.x < mergedX) mergedX = r.x; - if (r.y < mergedY) mergedY = r.y; - }); - - rects.forEach((r) => { - if (r.height + r.y - mergedY > mergedHeight) mergedHeight = r.height + r.y - mergedY; - if (r.width + r.x - mergedX > mergedWidth) mergedWidth = r.width + r.x - mergedX; - }); - - return { - x: mergedX, - y: mergedY, - width: mergedWidth, - height: mergedHeight - }; -} diff --git a/src/QemuVM.ts b/src/QemuVM.ts index 2ba357f..734f597 100644 --- a/src/QemuVM.ts +++ b/src/QemuVM.ts @@ -1,7 +1,6 @@ import { execaCommand, ExecaChildProcess } from 'execa'; import { EventEmitter } from 'events'; import { QmpClient, IQmpClientWriter, QmpEvent } from './QmpClient.js'; -import { QemuDisplay } from './QemuDisplay.js'; import { unlink } from 'node:fs/promises'; import pino from 'pino'; @@ -23,6 +22,16 @@ export type QemuVmDefinition = { vncPort: number | undefined; }; +export interface QemuVMDisplayInfo { + type: 'vnc-uds' | 'vnc-tcp'; + // 'vnc-uds' + path?: string; + + // 'vnc-tcp' + host?: string; + port?: number; +} + /// Temporary path base (for UNIX sockets/etc.) const kVmTmpPathBase = `/tmp`; @@ -59,7 +68,7 @@ export class QemuVM extends EventEmitter { private qemuProcess: ExecaChildProcess | null = null; - private display: QemuDisplay | null = null; + private displayInfo: QemuVMDisplayInfo | null = null; private definition: QemuVmDefinition; private addedAdditionalArguments = false; @@ -86,26 +95,7 @@ export class QemuVM extends EventEmitter { this.qmpInstance.on('connected', async () => { self.logger.info('QMP ready'); - if (this.definition.forceTcp || process.platform === "win32") { - this.display = new QemuDisplay({ - host: this.definition.vncHost || '127.0.0.1', - port: this.definition.vncPort || 5900, - path: null - }) - } else { - this.display = new QemuDisplay({ - path: this.GetVncPath() - }); - } - - self.display?.on('connected', () => { - // The VM can now be considered started - self.logger.info('Display connected'); - self.SetState(VMState.Started); - }); - - // now that QMP has connected, connect to the display - self.display?.Connect(); + self.SetState(VMState.Started); }); } @@ -121,16 +111,27 @@ export class QemuVM extends EventEmitter { cmd += ' -no-shutdown'; if (this.definition.snapshot) cmd += ' -snapshot'; cmd += ` -qmp stdio`; - if (this.definition.forceTcp || process.platform === "win32") { + if (this.definition.forceTcp || process.platform === 'win32') { let host = this.definition.vncHost || '127.0.0.1'; let port = this.definition.vncPort || 5900; if (port < 5900) { throw new Error('VNC port must be greater than or equal to 5900'); } cmd += ` -vnc ${host}:${port - 5900}`; + this.displayInfo = { + type: 'vnc-tcp', + host: host, + port: port + }; } else { cmd += ` -vnc unix:${this.GetVncPath()}`; + this.displayInfo = { + type: 'vnc-uds', + path: this.GetVncPath() + }; } + + this.definition.command = cmd; this.addedAdditionalArguments = true; } @@ -190,8 +191,13 @@ export class QemuVM extends EventEmitter { }); } - GetDisplay() { - return this.display!; + // Gets the required information to connect to the VNC server + // that Superqemu enables by adding arguments to the QEMU launch + // command. Superqemu no longer directly has a VNC client. + // + // This is only null if the VM has never been started. + GetDisplayInfo() { + return this.displayInfo; } GetState() { @@ -213,6 +219,7 @@ export class QemuVM extends EventEmitter { this.emit('statechange', this.state); } + // No longer internal private GetVncPath() { return `${kVmTmpPathBase}/superqemu-${this.definition.id}-vnc`; } @@ -243,16 +250,15 @@ export class QemuVM extends EventEmitter { this.qemuProcess.on('exit', async (code) => { self.logger.info('QEMU process exited'); - // Disconnect from the display and QMP connections. - await self.DisconnectDisplay(); - self.qmpInstance.reset(); self.qmpInstance.setWriter(null); - // Remove the VNC UDS socket. - try { - await unlink(this.GetVncPath()); - } catch (_) {} + if (!this.definition.forceTcp || process.platform !== 'win32') { + // Remove the VNC UDS socket. + try { + await unlink(this.GetVncPath()); + } catch (_) {} + } if (self.state != VMState.Stopping) { if (code == 0) { @@ -287,14 +293,4 @@ export class QemuVM extends EventEmitter { self.qmpInstance.reset(); self.qmpInstance.setWriter(writer); } - - private async DisconnectDisplay() { - try { - this.display?.Disconnect(); - this.display = null; - } catch (err) { - // oh well lol - } - } - } diff --git a/src/index.ts b/src/index.ts index fa28604..df7aa2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,2 @@ -export * from './QemuDisplay.js'; -export * from './QemuUtil.js'; export * from './QemuVM.js'; export * from './QmpClient.js';