remove display stuff

instead, we now provide the information for a higher level

also removes a dep so :)
This commit is contained in:
Lily Tsuru 2024-08-23 07:01:21 -04:00
parent e50ee0a837
commit 608b7255c9
7 changed files with 53 additions and 252 deletions

View file

@ -1,3 +1,7 @@
# superqemu # 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).

View file

@ -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 // Note that this example requires a valid desktop environment to function
// due to `-display gtk`, but you can remove it and run it headless. // 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'; import pino from 'pino';
@ -18,6 +22,9 @@ let vm = new QemuVM(
vm.on('statechange', (newState) => { vm.on('statechange', (newState) => {
logger.info(`state changed to ${newState}`); logger.info(`state changed to ${newState}`);
if(newState == VMState.Started) {
logger.info(vm.GetDisplayInfo(), `VM started: display info prepends this message`);
}
}); });
(async () => { (async () => {

View file

@ -1,6 +1,6 @@
{ {
"name": "@computernewb/superqemu", "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", "description": "A simple and easy to use QEMU supervision runtime for Node.js",
"exports": "./dist/index.js", "exports": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -23,7 +23,6 @@
} }
}, },
"dependencies": { "dependencies": {
"@computernewb/nodejs-rfb": "^0.3.0",
"execa": "^8.0.1", "execa": "^8.0.1",
"pino": "^9.3.1" "pino": "^9.3.1"
}, },

View file

@ -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);
}
}

View file

@ -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>): 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
};
}

View file

@ -1,7 +1,6 @@
import { execaCommand, ExecaChildProcess } from 'execa'; import { execaCommand, ExecaChildProcess } from 'execa';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { QmpClient, IQmpClientWriter, QmpEvent } from './QmpClient.js'; import { QmpClient, IQmpClientWriter, QmpEvent } from './QmpClient.js';
import { QemuDisplay } from './QemuDisplay.js';
import { unlink } from 'node:fs/promises'; import { unlink } from 'node:fs/promises';
import pino from 'pino'; import pino from 'pino';
@ -23,6 +22,16 @@ export type QemuVmDefinition = {
vncPort: number | undefined; 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.) /// Temporary path base (for UNIX sockets/etc.)
const kVmTmpPathBase = `/tmp`; const kVmTmpPathBase = `/tmp`;
@ -59,7 +68,7 @@ export class QemuVM extends EventEmitter {
private qemuProcess: ExecaChildProcess | null = null; private qemuProcess: ExecaChildProcess | null = null;
private display: QemuDisplay | null = null; private displayInfo: QemuVMDisplayInfo | null = null;
private definition: QemuVmDefinition; private definition: QemuVmDefinition;
private addedAdditionalArguments = false; private addedAdditionalArguments = false;
@ -86,26 +95,7 @@ export class QemuVM extends EventEmitter {
this.qmpInstance.on('connected', async () => { this.qmpInstance.on('connected', async () => {
self.logger.info('QMP ready'); self.logger.info('QMP ready');
if (this.definition.forceTcp || process.platform === "win32") { self.SetState(VMState.Started);
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();
}); });
} }
@ -121,16 +111,27 @@ export class QemuVM extends EventEmitter {
cmd += ' -no-shutdown'; cmd += ' -no-shutdown';
if (this.definition.snapshot) cmd += ' -snapshot'; if (this.definition.snapshot) cmd += ' -snapshot';
cmd += ` -qmp stdio`; 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 host = this.definition.vncHost || '127.0.0.1';
let port = this.definition.vncPort || 5900; let port = this.definition.vncPort || 5900;
if (port < 5900) { if (port < 5900) {
throw new Error('VNC port must be greater than or equal to 5900'); throw new Error('VNC port must be greater than or equal to 5900');
} }
cmd += ` -vnc ${host}:${port - 5900}`; cmd += ` -vnc ${host}:${port - 5900}`;
this.displayInfo = {
type: 'vnc-tcp',
host: host,
port: port
};
} else { } else {
cmd += ` -vnc unix:${this.GetVncPath()}`; cmd += ` -vnc unix:${this.GetVncPath()}`;
this.displayInfo = {
type: 'vnc-uds',
path: this.GetVncPath()
};
} }
this.definition.command = cmd; this.definition.command = cmd;
this.addedAdditionalArguments = true; this.addedAdditionalArguments = true;
} }
@ -190,8 +191,13 @@ export class QemuVM extends EventEmitter {
}); });
} }
GetDisplay() { // Gets the required information to connect to the VNC server
return this.display!; // 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() { GetState() {
@ -213,6 +219,7 @@ export class QemuVM extends EventEmitter {
this.emit('statechange', this.state); this.emit('statechange', this.state);
} }
// No longer internal
private GetVncPath() { private GetVncPath() {
return `${kVmTmpPathBase}/superqemu-${this.definition.id}-vnc`; return `${kVmTmpPathBase}/superqemu-${this.definition.id}-vnc`;
} }
@ -243,16 +250,15 @@ export class QemuVM extends EventEmitter {
this.qemuProcess.on('exit', async (code) => { this.qemuProcess.on('exit', async (code) => {
self.logger.info('QEMU process exited'); self.logger.info('QEMU process exited');
// Disconnect from the display and QMP connections.
await self.DisconnectDisplay();
self.qmpInstance.reset(); self.qmpInstance.reset();
self.qmpInstance.setWriter(null); self.qmpInstance.setWriter(null);
// Remove the VNC UDS socket. if (!this.definition.forceTcp || process.platform !== 'win32') {
try { // Remove the VNC UDS socket.
await unlink(this.GetVncPath()); try {
} catch (_) {} await unlink(this.GetVncPath());
} catch (_) {}
}
if (self.state != VMState.Stopping) { if (self.state != VMState.Stopping) {
if (code == 0) { if (code == 0) {
@ -287,14 +293,4 @@ export class QemuVM extends EventEmitter {
self.qmpInstance.reset(); self.qmpInstance.reset();
self.qmpInstance.setWriter(writer); self.qmpInstance.setWriter(writer);
} }
private async DisconnectDisplay() {
try {
this.display?.Disconnect();
this.display = null;
} catch (err) {
// oh well lol
}
}
} }

View file

@ -1,4 +1,2 @@
export * from './QemuDisplay.js';
export * from './QemuUtil.js';
export * from './QemuVM.js'; export * from './QemuVM.js';
export * from './QmpClient.js'; export * from './QmpClient.js';