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
|
||||
|
||||
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
|
||||
// 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 () => {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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 { 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
export * from './QemuDisplay.js';
|
||||
export * from './QemuUtil.js';
|
||||
export * from './QemuVM.js';
|
||||
export * from './QmpClient.js';
|
||||
|
|
Loading…
Reference in a new issue