diff --git a/src/DefaultProcess.ts b/src/DefaultProcess.ts index 806b39d..2c12425 100644 --- a/src/DefaultProcess.ts +++ b/src/DefaultProcess.ts @@ -1,44 +1,52 @@ // Default process implementation. -// This uses execa like the current 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"; +// 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; + private process; + stdin: Writable | null = null; + stdout: Readable | null = null; + stderr: Readable | null = null; - constructor(command: string, opts?: ProcessLaunchOptions) { - super(); + constructor(command: string, opts?: ProcessLaunchOptions) { + super(); - this.process = execaCommand(command, opts); + this.process = execaCommand(command, opts); - this.stdin = this.process.stdin; - this.stdout = this.process.stdout; - this.stderr = this.process.stderr; + 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'); - }); + let self = this; + this.process.on('spawn', () => { + self.emit('spawn'); + }); - this.process.on('exit', (code) => { - self.emit('exit', code); - }); - } + this.process.on('exit', (code) => { + self.emit('exit', code); + }); + } - kill(signal?: number | NodeJS.Signals): boolean { - return this.process.kill(signal); - } + 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); - } -} \ No newline at end of file + launch(command: string, opts?: ProcessLaunchOptions | undefined): IProcess { + return new DefaultProcess(command, opts); + } +} diff --git a/src/ProcessInterface.ts b/src/ProcessInterface.ts index 7d46b59..7bf1b30 100644 --- a/src/ProcessInterface.ts +++ b/src/ProcessInterface.ts @@ -28,6 +28,7 @@ export interface IProcess extends EventEmitter { kill(signal?: number | NodeJS.Signals): boolean; + dispose(): void; } // Launches a processs. diff --git a/src/QemuVM.ts b/src/QemuVM.ts index 5ed9dfd..09138b7 100644 --- a/src/QemuVM.ts +++ b/src/QemuVM.ts @@ -5,6 +5,8 @@ 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, @@ -68,7 +70,8 @@ export class QemuVM extends EventEmitter { // QMP stuff. private qmpInstance: QmpClient = new QmpClient(); - private qemuProcess: ExecaChildProcess | null = null; + private qemuProcess: IProcess | null = null; + private qemuLauncher: IProcessLauncher; private displayInfo: QemuVMDisplayInfo | null = null; private definition: QemuVmDefinition; @@ -76,13 +79,23 @@ export class QemuVM extends EventEmitter { private logger: pino.Logger; - constructor(def: QemuVmDefinition) { + constructor(def: QemuVmDefinition, processLauncher?: IProcessLauncher) { 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.logger.warn('Using default process launcher. If this is not desired review your code to make sure the launcher is passed properly!'); + this.qemuLauncher = new DefaultProcessLauncher(); + } else { + this.qemuLauncher = processLauncher; + } + let self = this; // Handle the STOP event sent when using -no-shutdown @@ -233,7 +246,7 @@ export class QemuVM extends EventEmitter { this.logger.info(`Starting QEMU with command \"${split}\"`); // Start QEMU - this.qemuProcess = execaCommand(split, { + this.qemuProcess = this.qemuLauncher.launch(split, { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' @@ -251,6 +264,10 @@ 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.qmpInstance.reset(); self.qmpInstance.setWriter(null);