Compare commits
No commits in common. "main" and "no_display" have entirely different histories.
main
...
no_display
6 changed files with 11 additions and 175 deletions
|
@ -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
|
|
12
package.json
12
package.json
|
@ -1,15 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "@computernewb/superqemu",
|
"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",
|
"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",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./dist",
|
"./dist",
|
||||||
"LICENSE",
|
"LICENSE"
|
||||||
"README.md",
|
|
||||||
"RELEASE_NOTES.md"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "parcel build src/index.ts --target node --target types"
|
"build": "parcel build src/index.ts --target node --target types"
|
||||||
|
@ -36,9 +34,5 @@
|
||||||
"pino-pretty": "^11.2.1",
|
"pino-pretty": "^11.2.1",
|
||||||
"typescript": ">=3.0.0"
|
"typescript": ">=3.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.4.0",
|
"packageManager": "yarn@4.1.1"
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://git.computernewb.com/computernewb/superqemu.git"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -5,8 +5,6 @@ import { unlink } from 'node:fs/promises';
|
||||||
|
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { Readable, Writable } from 'stream';
|
import { Readable, Writable } from 'stream';
|
||||||
import { IProcess, IProcessLauncher } from './ProcessInterface.js';
|
|
||||||
import { DefaultProcessLauncher } from './DefaultProcess.js';
|
|
||||||
|
|
||||||
export enum VMState {
|
export enum VMState {
|
||||||
Stopped,
|
Stopped,
|
||||||
|
@ -15,7 +13,6 @@ export enum VMState {
|
||||||
Stopping
|
Stopping
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VM definition.
|
|
||||||
export type QemuVmDefinition = {
|
export type QemuVmDefinition = {
|
||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
|
@ -25,7 +22,6 @@ export type QemuVmDefinition = {
|
||||||
vncPort: number | undefined;
|
vncPort: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Display information.
|
|
||||||
export interface QemuVMDisplayInfo {
|
export interface QemuVMDisplayInfo {
|
||||||
type: 'vnc-uds' | 'vnc-tcp';
|
type: 'vnc-uds' | 'vnc-tcp';
|
||||||
// 'vnc-uds'
|
// 'vnc-uds'
|
||||||
|
@ -70,8 +66,7 @@ export class QemuVM extends EventEmitter {
|
||||||
// QMP stuff.
|
// QMP stuff.
|
||||||
private qmpInstance: QmpClient = new QmpClient();
|
private qmpInstance: QmpClient = new QmpClient();
|
||||||
|
|
||||||
private qemuProcess: IProcess | null = null;
|
private qemuProcess: ExecaChildProcess | null = null;
|
||||||
private qemuLauncher: IProcessLauncher;
|
|
||||||
|
|
||||||
private displayInfo: QemuVMDisplayInfo | null = null;
|
private displayInfo: QemuVMDisplayInfo | null = null;
|
||||||
private definition: QemuVmDefinition;
|
private definition: QemuVmDefinition;
|
||||||
|
@ -79,22 +74,13 @@ export class QemuVM extends EventEmitter {
|
||||||
|
|
||||||
private logger: pino.Logger;
|
private logger: pino.Logger;
|
||||||
|
|
||||||
constructor(def: QemuVmDefinition, processLauncher?: IProcessLauncher) {
|
constructor(def: QemuVmDefinition) {
|
||||||
super();
|
super();
|
||||||
this.definition = def;
|
this.definition = def;
|
||||||
this.logger = pino({
|
this.logger = pino({
|
||||||
name: `SuperQEMU.QemuVM/${this.definition.id}`
|
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;
|
let self = this;
|
||||||
|
|
||||||
// Handle the STOP event sent when using -no-shutdown
|
// Handle the STOP event sent when using -no-shutdown
|
||||||
|
@ -108,6 +94,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');
|
||||||
|
|
||||||
self.SetState(VMState.Started);
|
self.SetState(VMState.Started);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -144,6 +131,7 @@ export class QemuVM extends EventEmitter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.definition.command = cmd;
|
this.definition.command = cmd;
|
||||||
this.addedAdditionalArguments = true;
|
this.addedAdditionalArguments = true;
|
||||||
}
|
}
|
||||||
|
@ -159,42 +147,19 @@ export class QemuVM extends EventEmitter {
|
||||||
await this.MonitorCommand('system_reset');
|
await this.MonitorCommand('system_reset');
|
||||||
}
|
}
|
||||||
|
|
||||||
async Stop(): Promise<void> {
|
async Stop() {
|
||||||
this.AssertState(VMState.Started, 'cannot use QemuVM#Stop on a non-started VM');
|
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.
|
// Indicate we're stopping, so we don't erroneously start trying to restart everything we're going to tear down.
|
||||||
this.SetState(VMState.Stopping);
|
this.SetState(VMState.Stopping);
|
||||||
|
|
||||||
// Stop the QEMU process, which will bring down everything else.
|
// Stop the QEMU process, which will bring down everything else.
|
||||||
await this.StopQemu();
|
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');
|
this.AssertState(VMState.Started, 'cannot use QemuVM#Reset on a non-started VM');
|
||||||
await this.StopQemu();
|
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> {
|
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}\"`);
|
this.logger.info(`Starting QEMU with command \"${split}\"`);
|
||||||
|
|
||||||
// Start QEMU
|
// Start QEMU
|
||||||
this.qemuProcess = this.qemuLauncher.launch(split, {
|
this.qemuProcess = execaCommand(split, {
|
||||||
stdin: 'pipe',
|
stdin: 'pipe',
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'pipe'
|
stderr: 'pipe'
|
||||||
|
@ -285,11 +250,6 @@ 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');
|
||||||
|
|
||||||
// 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.reset();
|
||||||
self.qmpInstance.setWriter(null);
|
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
|
// Note that we've already tore down everything upon entry to this event handler; therefore
|
||||||
// we can simply set the state and move on.
|
// we can simply set the state and move on.
|
||||||
this.SetState(VMState.Stopped);
|
this.SetState(VMState.Stopped);
|
||||||
self.qemuProcess = null;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Indicate we have stopped.
|
// Indicate we have stopped.
|
||||||
this.SetState(VMState.Stopped);
|
this.SetState(VMState.Stopped);
|
||||||
self.qemuProcess = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -321,6 +279,7 @@ export class QemuVM extends EventEmitter {
|
||||||
private async StopQemu() {
|
private async StopQemu() {
|
||||||
if (this.qemuProcess) {
|
if (this.qemuProcess) {
|
||||||
this.qemuProcess?.kill('SIGTERM');
|
this.qemuProcess?.kill('SIGTERM');
|
||||||
|
this.qemuProcess = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './QemuVM.js';
|
export * from './QemuVM.js';
|
||||||
export * from './QmpClient.js';
|
export * from './QmpClient.js';
|
||||||
export * from './ProcessInterface.js';
|
|
Loading…
Reference in a new issue