remove display stuff
instead, we now provide the information for a higher level also removes a dep so :)
This commit is contained in:
parent
e50ee0a837
commit
608b7255c9
7 changed files with 53 additions and 252 deletions
|
@ -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).
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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,27 +95,8 @@ 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") {
|
|
||||||
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);
|
self.SetState(VMState.Started);
|
||||||
});
|
});
|
||||||
|
|
||||||
// now that QMP has connected, connect to the display
|
|
||||||
self.display?.Connect();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Start() {
|
async Start() {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
if (!this.definition.forceTcp || process.platform !== 'win32') {
|
||||||
// Remove the VNC UDS socket.
|
// Remove the VNC UDS socket.
|
||||||
try {
|
try {
|
||||||
await unlink(this.GetVncPath());
|
await unlink(this.GetVncPath());
|
||||||
} catch (_) {}
|
} 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue