Compare commits

..

No commits in common. "main" and "no_display" have entirely different histories.

6 changed files with 11 additions and 175 deletions

View file

@ -1,27 +0,0 @@
# Superqemu Release Notes
## `v0.3.1`
This minor release fixes a bug that would result in the process interface's `dispose()` method never actually being called.
This version is published as `0.3.2` because the npm registry is the most utterly useless piece of garbage I've ever used. This sentiment tends to echo for much of the JavaScript ecosystem. Almost like shoehorning a language meant literally for DHTML clocks and other stuff into being a backend language wasn't a great idea or something...
## `v0.3.0`
This release contains *possibly* breaking changes:
Superqemu now uses a interface to launch and interact with the QEMU process.
This is intended to allow for an external user of superqemu to perform resource control on the QEMU process, which previously was pretty much impossible.
The library does not enforce this and for compatibility with previous versions of superqemu the process launcher argument in QemuVM is optional.
## `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,15 +1,13 @@
{
"name": "@computernewb/superqemu",
"version": "0.3.2",
"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",
"type": "module",
"files": [
"./dist",
"LICENSE",
"README.md",
"RELEASE_NOTES.md"
"LICENSE"
],
"scripts": {
"build": "parcel build src/index.ts --target node --target types"
@ -36,9 +34,5 @@
"pino-pretty": "^11.2.1",
"typescript": ">=3.0.0"
},
"packageManager": "yarn@4.4.0",
"repository": {
"type": "git",
"url": "git+https://git.computernewb.com/computernewb/superqemu.git"
}
"packageManager": "yarn@4.1.1"
}

View file

@ -1,52 +0,0 @@
// Default process implementation.
// This uses execa like the previous code, but conforms to our abstration :)
import EventEmitter from 'events';
import { IProcess, IProcessLauncher, ProcessLaunchOptions } from './ProcessInterface';
import { execaCommand } from 'execa';
import { Readable, Writable } from 'stream';
class DefaultProcess extends EventEmitter implements IProcess {
private process;
stdin: Writable | null = null;
stdout: Readable | null = null;
stderr: Readable | null = null;
constructor(command: string, opts?: ProcessLaunchOptions) {
super();
this.process = execaCommand(command, opts);
this.stdin = this.process.stdin;
this.stdout = this.process.stdout;
this.stderr = this.process.stderr;
let self = this;
this.process.on('spawn', () => {
self.emit('spawn');
});
this.process.on('exit', (code) => {
self.emit('exit', code);
});
}
kill(signal?: number | NodeJS.Signals): boolean {
return this.process.kill(signal);
}
dispose(): void {
this.stdin = null;
this.stdout = null;
this.stderr = null;
this.process.removeAllListeners();
this.removeAllListeners();
}
}
export class DefaultProcessLauncher implements IProcessLauncher {
launch(command: string, opts?: ProcessLaunchOptions | undefined): IProcess {
return new DefaultProcess(command, opts);
}
}

View file

@ -1,37 +0,0 @@
import {type Stream, EventEmitter, Readable, Writable} from 'node:stream';
export type StdioOption =
| 'pipe'
| 'overlapped'
| 'ipc'
| 'ignore'
| 'inherit'
| Stream
| number
| undefined;
// subset of options. FIXME: Add more!!!
export interface ProcessLaunchOptions {
stdin?: StdioOption,
stdout?: StdioOption,
stderr?: StdioOption
}
export interface IProcess extends EventEmitter {
stdin: Writable | null;
stdout: Readable | null;
stderr: Readable | null;
// Escape hatch; only use this if you have no choice
//native() : any;
kill(signal?: number | NodeJS.Signals): boolean;
dispose(): void;
}
// Launches a processs.
export interface IProcessLauncher {
launch(command: string, opts?: ProcessLaunchOptions) : IProcess;
}

View file

@ -5,8 +5,6 @@ import { unlink } from 'node:fs/promises';
import pino from 'pino';
import { Readable, Writable } from 'stream';
import { IProcess, IProcessLauncher } from './ProcessInterface.js';
import { DefaultProcessLauncher } from './DefaultProcess.js';
export enum VMState {
Stopped,
@ -15,7 +13,6 @@ export enum VMState {
Stopping
}
/// VM definition.
export type QemuVmDefinition = {
id: string;
command: string;
@ -25,7 +22,6 @@ export type QemuVmDefinition = {
vncPort: number | undefined;
};
/// Display information.
export interface QemuVMDisplayInfo {
type: 'vnc-uds' | 'vnc-tcp';
// 'vnc-uds'
@ -70,8 +66,7 @@ export class QemuVM extends EventEmitter {
// QMP stuff.
private qmpInstance: QmpClient = new QmpClient();
private qemuProcess: IProcess | null = null;
private qemuLauncher: IProcessLauncher;
private qemuProcess: ExecaChildProcess | null = null;
private displayInfo: QemuVMDisplayInfo | null = null;
private definition: QemuVmDefinition;
@ -79,22 +74,13 @@ export class QemuVM extends EventEmitter {
private logger: pino.Logger;
constructor(def: QemuVmDefinition, processLauncher?: IProcessLauncher) {
constructor(def: QemuVmDefinition) {
super();
this.definition = def;
this.logger = pino({
name: `SuperQEMU.QemuVM/${this.definition.id}`
});
// Fall back to the default process launcher. This is
// done so we can have our cake (compatibility) and eat it too
// (do this fun process abstraction stuff for whatever really)
if (!processLauncher) {
this.qemuLauncher = new DefaultProcessLauncher();
} else {
this.qemuLauncher = processLauncher;
}
let self = this;
// Handle the STOP event sent when using -no-shutdown
@ -108,6 +94,7 @@ export class QemuVM extends EventEmitter {
this.qmpInstance.on('connected', async () => {
self.logger.info('QMP ready');
self.SetState(VMState.Started);
});
}
@ -144,6 +131,7 @@ export class QemuVM extends EventEmitter {
};
}
this.definition.command = cmd;
this.addedAdditionalArguments = true;
}
@ -159,42 +147,19 @@ export class QemuVM extends EventEmitter {
await this.MonitorCommand('system_reset');
}
async Stop(): Promise<void> {
async Stop() {
this.AssertState(VMState.Started, 'cannot use QemuVM#Stop on a non-started VM');
// I'm not sure this is better, but I'm also not sure it should be an assertion
//if(this.state !== VMState.Started)
// return;
// Indicate we're stopping, so we don't erroneously start trying to restart everything we're going to tear down.
this.SetState(VMState.Stopping);
// Stop the QEMU process, which will bring down everything else.
await this.StopQemu();
// Wait for the VM to reach the stopped state.
return new Promise((res, rej) => {
this.once('statechange', (state) => {
if (state == VMState.Stopped) res();
});
});
}
async Reset(): Promise<void> {
async Reset() {
this.AssertState(VMState.Started, 'cannot use QemuVM#Reset on a non-started VM');
await this.StopQemu();
let self = this;
// Wait for the VM to regain the started state
return new Promise((res, rej) => {
let cb = (state: VMState) => {
if (state == VMState.Started) {
this.removeListener('statechange', cb);
res();
}
};
this.on('statechange', cb);
});
}
async QmpCommand(command: string, args: any | null): Promise<any> {
@ -267,7 +232,7 @@ export class QemuVM extends EventEmitter {
this.logger.info(`Starting QEMU with command \"${split}\"`);
// Start QEMU
this.qemuProcess = this.qemuLauncher.launch(split, {
this.qemuProcess = execaCommand(split, {
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe'
@ -285,11 +250,6 @@ export class QemuVM extends EventEmitter {
this.qemuProcess.on('exit', async (code) => {
self.logger.info('QEMU process exited');
// Dispose events. StartQemu() will assign them again to the new process.
// A bit mucky but /shrug.
self.qemuProcess?.dispose();
self.qemuProcess = null;
self.qmpInstance.reset();
self.qmpInstance.setWriter(null);
@ -308,12 +268,10 @@ export class QemuVM extends EventEmitter {
// Note that we've already tore down everything upon entry to this event handler; therefore
// we can simply set the state and move on.
this.SetState(VMState.Stopped);
self.qemuProcess = null;
}
} else {
// Indicate we have stopped.
this.SetState(VMState.Stopped);
self.qemuProcess = null;
}
});
}
@ -321,6 +279,7 @@ export class QemuVM extends EventEmitter {
private async StopQemu() {
if (this.qemuProcess) {
this.qemuProcess?.kill('SIGTERM');
this.qemuProcess = null;
}
}

View file

@ -1,3 +1,2 @@
export * from './QemuVM.js';
export * from './QmpClient.js';
export * from './ProcessInterface.js';