// This was originally based off the contents of the node-qemu-qmp package, // but I've modified it possibly to the point where it could be treated as my own creation. import split from 'split'; import { Socket } from 'net'; export type QmpCallback = (err: Error | null, res: any | null) => void; type QmpCommandEntry = { callback: QmpCallback | null; id: number; }; // TODO: Instead of the client "Is-A"ing a Socket, this should instead contain/store a Socket, // (preferrably) passed by the user, to use for QMP communications. // The client shouldn't have to know or care about the protocol, and it effectively hackily uses the fact // Socket extends EventEmitter. export default class QmpClient extends Socket { public qmpHandshakeData: any; private commandEntries: QmpCommandEntry[] = []; private lastID = 0; private ExecuteSync(command: string, args: any | null, callback: QmpCallback | null) { let cmd: QmpCommandEntry = { callback: callback, id: ++this.lastID }; let qmpOut: any = { execute: command, id: cmd.id }; if (args) qmpOut['arguments'] = args; // Add stuff this.commandEntries.push(cmd); this.write(JSON.stringify(qmpOut)); } // TODO: Make this function a bit more ergonomic? async Execute(command: string, args: any | null = null): Promise { return new Promise((res, rej) => { this.ExecuteSync(command, args, (err, result) => { if (err) rej(err); res(result); }); }); } private Handshake(callback: () => void) { this.write( JSON.stringify({ execute: 'qmp_capabilities' }) ); this.once('data', (data) => { // Once QEMU replies to us, the handshake is done. // We do not negotiate anything special. callback(); }); } // this can probably be made async private ConnectImpl() { let self = this; this.once('connect', () => { this.removeAllListeners('error'); }); this.once('error', (err) => { // just rethrow lol //throw err; console.log('you have pants: rules,', err); }); this.once('data', (data) => { // Handshake QMP with the server. self.qmpHandshakeData = JSON.parse(data.toString('utf8')).QMP; self.Handshake(() => { // Now ready to parse QMP responses/events. self.pipe(split(JSON.parse)) .on('data', (json: any) => { if (json == null) return self.end(); if (json.return || json.error) { // Our handshake has a spurious return because we never assign it an ID, // and it is gathered by this pipe for some reason I'm not quite sure about. // So, just for safety's sake, don't process any return objects which don't have an ID attached to them. if (json.id == null) return; let callbackEntry = this.commandEntries.find((entry) => entry.id === json.id); let error: Error | null = json.error ? new Error(json.error.desc) : null; // we somehow didn't find a callback entry for this response. // I don't know how. Techinically not an error..., but I guess you're not getting a reponse to whatever causes this to happen if (callbackEntry == null) return; if (callbackEntry?.callback) callbackEntry.callback(error, json.return); // Remove the completed callback entry. this.commandEntries.slice(this.commandEntries.indexOf(callbackEntry)); } else if (json.event) { this.emit('event', json); } }) .on('error', () => { // Give up. return self.end(); }); this.emit('qmp-ready'); }); }); this.once('close', () => { this.end(); this.removeAllListeners('data'); // wow. good job bud. cool memory leak }); } Connect(host: string, port: number) { super.connect(port, host); this.ConnectImpl(); } ConnectUNIX(path: string) { super.connect(path); this.ConnectImpl(); } }