import { EventEmitter } from 'node:events'; enum QmpClientState { Handshaking, Connected } function qmpStringify(obj: any) { return JSON.stringify(obj) + '\r\n'; } // this writer interface is used to poll back to a higher level // I/O layer that we want to write some data. export interface IQmpClientWriter { writeSome(data: Buffer): void; } export type QmpClientCallback = (err: Error | null, res: any | null) => void; type QmpClientCallbackEntry = { id: number; callback: QmpClientCallback | null; }; export enum QmpEvent { BlockIOError = 'BLOCK_IO_ERROR', Reset = 'RESET', Resume = 'RESUME', RtcChange = 'RTC_CHANGE', Shutdown = 'SHUTDOWN', Stop = 'STOP', VncConnected = 'VNC_CONNECTED', VncDisconnected = 'VNC_DISCONNECTED', VncInitalized = 'VNC_INITALIZED', Watchdog = 'WATCHDOG' } class LineStream extends EventEmitter { // The given line seperator for the stream lineSeperator = '\r\n'; buffer = ''; constructor() { super(); } push(data: Buffer) { this.buffer += data.toString('utf-8'); let lines = this.buffer.split(this.lineSeperator); if (lines.length > 1) { this.buffer = lines.pop()!; lines = lines.filter((l) => !!l); //console.log(lines) lines.forEach(l => this.emit('line', l)); } return []; } reset() { this.buffer = ''; } } // A QMP client export class QmpClient extends EventEmitter { private state = QmpClientState.Handshaking; private writer: IQmpClientWriter | null = null; private lastID = 0; private callbacks = new Array(); private lineStream = new LineStream(); constructor() { super(); let self = this; this.lineStream.on('line', (line: string) => { self.handleQmpLine(line); }); } setWriter(writer: IQmpClientWriter) { this.writer = writer; } feed(data: Buffer): void { // Forward to the line stream. It will generate 'line' events // as it is able to split out lines automatically. this.lineStream.push(data); } private handleQmpLine(line: string) { let obj = JSON.parse(line); switch (this.state) { case QmpClientState.Handshaking: if (obj['return'] != undefined) { this.state = QmpClientState.Connected; this.emit('connected'); return; } let capabilities = qmpStringify({ execute: 'qmp_capabilities' }); this.writer?.writeSome(Buffer.from(capabilities, 'utf8')); break; case QmpClientState.Connected: if (obj['return'] != undefined || obj['error'] != undefined) { if (obj['id'] == null) return; let cb = this.callbacks.find((v) => v.id == obj['id']); if (cb == undefined) return; let error: Error | null = obj.error ? new Error(obj.error.desc) : null; if (cb.callback) cb.callback(error, obj.return); this.callbacks.slice(this.callbacks.indexOf(cb)); } else if (obj['event']) { this.emit(obj.event, { timestamp: obj.timestamp, data: obj.data }); } break; } } executeSync(command: string, args: any | undefined, callback: QmpClientCallback | null) { let entry = { callback: callback, id: ++this.lastID }; let qmpOut: any = { execute: command, id: entry.id }; if (args !== undefined) qmpOut['arguments'] = args; this.callbacks.push(entry); this.writer?.writeSome(Buffer.from(qmpStringify(qmpOut), 'utf8')); } async execute(command: string, args: any | undefined = undefined): Promise { return new Promise((res, rej) => { this.executeSync(command, args, (err, result) => { if (err) rej(err); res(result); }); }); } reset() { this.lineStream.reset(); this.state = QmpClientState.Handshaking; } }