2023-01-31 22:00:30 -05:00
|
|
|
import EventEmitter from "events";
|
|
|
|
|
import { Socket } from "net";
|
2023-02-02 21:19:55 -05:00
|
|
|
import { Mutex } from "async-mutex";
|
2023-02-24 22:54:28 -05:00
|
|
|
import log from "./log.js";
|
2023-01-31 22:00:30 -05:00
|
|
|
|
|
|
|
|
export default class QMPClient extends EventEmitter {
|
|
|
|
|
socketfile : string;
|
2023-02-11 15:58:20 +01:00
|
|
|
sockettype: string;
|
2023-01-31 22:00:30 -05:00
|
|
|
socket : Socket;
|
|
|
|
|
connected : boolean;
|
|
|
|
|
sentConnected : boolean;
|
2023-02-02 21:19:55 -05:00
|
|
|
cmdMutex : Mutex; // So command outputs don't get mixed up
|
2023-02-11 15:58:20 +01:00
|
|
|
constructor(socketfile : string, sockettype: string) {
|
2023-01-31 22:00:30 -05:00
|
|
|
super();
|
2023-02-11 15:58:20 +01:00
|
|
|
this.sockettype = sockettype;
|
2023-01-31 22:00:30 -05:00
|
|
|
this.socketfile = socketfile;
|
|
|
|
|
this.socket = new Socket();
|
|
|
|
|
this.connected = false;
|
|
|
|
|
this.sentConnected = false;
|
2023-02-02 21:19:55 -05:00
|
|
|
this.cmdMutex = new Mutex();
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
2023-02-02 21:19:55 -05:00
|
|
|
connect() : Promise<void> {
|
|
|
|
|
return new Promise((res, rej) => {
|
|
|
|
|
if (this.connected) {res(); return;}
|
|
|
|
|
try {
|
2023-02-11 15:58:20 +01:00
|
|
|
if(this.sockettype == "tcp:") {
|
|
|
|
|
let _sock = this.socketfile.split(':');
|
|
|
|
|
this.socket.connect(parseInt(_sock[1]), _sock[0]);
|
|
|
|
|
}else{
|
|
|
|
|
this.socket.connect(this.socketfile);
|
|
|
|
|
}
|
2023-02-02 21:19:55 -05:00
|
|
|
} catch (e) {
|
|
|
|
|
this.onClose();
|
|
|
|
|
}
|
|
|
|
|
this.connected = true;
|
2023-02-24 22:54:28 -05:00
|
|
|
this.socket.on('error', () => false); // Disable throwing if QMP errors
|
2023-02-02 21:19:55 -05:00
|
|
|
this.socket.on('data', (data) => this.onData(data));
|
|
|
|
|
this.socket.on('close', () => this.onClose());
|
2023-02-07 12:29:33 -05:00
|
|
|
this.once('connected', () => {res();});
|
2023-02-02 21:19:55 -05:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
|
this.connected = false;
|
|
|
|
|
this.socket.destroy();
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
|
|
|
|
|
2023-02-02 21:19:55 -05:00
|
|
|
private async onData(data : Buffer) {
|
2023-04-06 21:11:13 +02:00
|
|
|
let msgraw = data.toString();
|
|
|
|
|
let msg;
|
2023-02-07 22:38:41 -05:00
|
|
|
|
2023-04-06 21:11:13 +02:00
|
|
|
try {
|
|
|
|
|
msg = JSON.parse(msgraw);
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
|
2023-04-06 21:11:13 +02:00
|
|
|
if (msg.QMP !== undefined) {
|
|
|
|
|
if (this.sentConnected)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await this.execute({ execute: "qmp_capabilities" });
|
2023-04-05 20:34:15 +02:00
|
|
|
|
2023-04-06 21:11:13 +02:00
|
|
|
this.emit('connected');
|
|
|
|
|
this.sentConnected = true;
|
2023-04-05 20:34:15 +02:00
|
|
|
}
|
2023-04-06 21:11:13 +02:00
|
|
|
if (msg.return !== undefined)
|
|
|
|
|
this.emit("qmpreturn", msg.return);
|
|
|
|
|
else if(msg.event !== undefined) {
|
|
|
|
|
switch(msg.event) {
|
|
|
|
|
case "STOP":
|
|
|
|
|
{
|
|
|
|
|
log("INFO", "The VM was shut down, restarting...");
|
|
|
|
|
this.reboot();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "RESET":
|
|
|
|
|
{
|
|
|
|
|
log("INFO", "QEMU reset event occured");
|
|
|
|
|
this.resume();
|
|
|
|
|
break;
|
|
|
|
|
};
|
|
|
|
|
default: {
|
|
|
|
|
this.emit("qmpreturn", '');
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}else
|
|
|
|
|
// for now just return an empty string.
|
|
|
|
|
// This is a giant hack but avoids a deadlock
|
|
|
|
|
this.emit("qmpreturn", '');
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onClose() {
|
|
|
|
|
this.connected = false;
|
|
|
|
|
this.sentConnected = false;
|
2023-02-07 22:38:41 -05:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
if (this.socket.readyState === 'open')
|
|
|
|
|
this.socket.destroy();
|
2023-02-07 22:38:41 -05:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
this.cmdMutex.cancel();
|
|
|
|
|
this.cmdMutex.release();
|
|
|
|
|
this.socket = new Socket();
|
2023-01-31 22:00:30 -05:00
|
|
|
this.emit('close');
|
|
|
|
|
}
|
2023-02-02 21:19:55 -05:00
|
|
|
|
|
|
|
|
async reboot() {
|
2023-02-07 22:38:41 -05:00
|
|
|
if (!this.connected)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-02-02 21:19:55 -05:00
|
|
|
await this.execute({"execute": "system_reset"});
|
|
|
|
|
}
|
2023-02-11 20:52:25 +01:00
|
|
|
|
|
|
|
|
async resume() {
|
|
|
|
|
if (!this.connected)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await this.execute({"execute": "cont"});
|
|
|
|
|
}
|
2023-02-02 21:19:55 -05:00
|
|
|
|
|
|
|
|
async ExitQEMU() {
|
2023-02-07 22:38:41 -05:00
|
|
|
if (!this.connected)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-02-02 21:19:55 -05:00
|
|
|
await this.execute({"execute": "quit"});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
execute(args : object) {
|
|
|
|
|
return new Promise(async (res, rej) => {
|
2023-02-07 12:29:33 -05:00
|
|
|
var result:any;
|
|
|
|
|
try {
|
|
|
|
|
result = await this.cmdMutex.runExclusive(() => {
|
|
|
|
|
// I kinda hate having two promises but IDK how else to do it /shrug
|
|
|
|
|
return new Promise((reso, reje) => {
|
|
|
|
|
this.once('qmpreturn', (e) => {
|
|
|
|
|
reso(e);
|
|
|
|
|
});
|
|
|
|
|
this.socket.write(JSON.stringify(args));
|
2023-02-02 21:19:55 -05:00
|
|
|
});
|
|
|
|
|
});
|
2023-02-07 22:38:41 -05:00
|
|
|
} catch {
|
|
|
|
|
res({});
|
|
|
|
|
}
|
2023-02-02 21:19:55 -05:00
|
|
|
res(result);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runMonitorCmd(command : string) {
|
|
|
|
|
return new Promise(async (res, rej) => {
|
2023-02-07 22:38:41 -05:00
|
|
|
res(await this.execute({execute: "human-monitor-command", arguments: {"command-line": command}}));
|
2023-02-02 21:19:55 -05:00
|
|
|
});
|
|
|
|
|
}
|
2023-04-06 21:11:13 +02:00
|
|
|
}
|