Compare commits

...

2 commits

Author SHA1 Message Date
e3123252a4 release 0.2.4
Please see the release notes for additional information regarding this superqemu release.
2024-08-23 09:58:08 -04:00
608b7255c9 remove display stuff
instead, we now provide the information for a higher level

also removes a dep so :)
2024-08-23 07:01:21 -04:00
8 changed files with 75 additions and 255 deletions

View file

@ -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).

12
RELEASE_NOTES.md Normal file
View file

@ -0,0 +1,12 @@
# Superqemu Release Notes
## `v0.2.4`
This release contans breaking changes:
- Superqemu no longer depends on nodejs-rfb, or provides its own VNC client support. Instead, it still sets up VNC in QEMU, and it provides the required information to connect, but allows you the control to connect to the VNC server QEMU has setup yourself.
## `v0.2.3`
- TCP support

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
// 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 () => {

View file

@ -1,13 +1,15 @@
{
"name": "@computernewb/superqemu",
"version": "0.2.3",
"version": "0.2.4",
"description": "A simple and easy to use QEMU supervision runtime for Node.js",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"files": [
"./dist",
"LICENSE"
"LICENSE",
"README.md",
"RELEASE_NOTES.md"
],
"scripts": {
"build": "parcel build src/index.ts --target node --target types"
@ -23,7 +25,6 @@
}
},
"dependencies": {
"@computernewb/nodejs-rfb": "^0.3.0",
"execa": "^8.0.1",
"pino": "^9.3.1"
},
@ -35,5 +36,9 @@
"pino-pretty": "^11.2.1",
"typescript": ">=3.0.0"
},
"packageManager": "yarn@4.1.1"
"packageManager": "yarn@4.4.0",
"repository": {
"type": "git",
"url": "git+https://git.computernewb.com/computernewb/superqemu.git"
}
}

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 { 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';
@ -14,6 +13,7 @@ export enum VMState {
Stopping
}
/// VM definition.
export type QemuVmDefinition = {
id: string;
command: string;
@ -23,6 +23,17 @@ export type QemuVmDefinition = {
vncPort: number | undefined;
};
/// Display information.
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 +70,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;
@ -85,27 +96,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 +112,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 +192,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 +220,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 +251,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 +294,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
}
}
}

View file

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