Giant refactoring (or at least the start)

In short:
- cvmts is now bundled/built via parcel and inside of a npm/yarn workspace with multiple nodejs projects
- cvmts now uses the crusttest QEMU management and RFB library (or a fork, if you so prefer).
- cvmts does NOT use node-canvas anymore, instead we opt for the same route crusttest took and just encode jpegs ourselves from the RFB provoded framebuffer via jpeg-turbo. this means funnily enough sharp is back for more for thumbnails, but actually seems to WORK this time
- IPData is now managed in a very similar way to the original cvm 1.2 implementation where a central manager and reference count exist. tbh it wouldn't be that hard to implement multinode either, but for now, I'm not going to take much time on doing that.

this refactor is still incomplete. please do not treat it as generally available while it's not on the default branch. if you want to use it (and report bugs or send fixes) feel free to, but while it may "just work" in certain situations it may be very broken in others.

(yes, I know windows support is partially totaled by this; it's something that can and will be fixed)
This commit is contained in:
modeco80
2024-04-23 09:57:02 -04:00
parent 28dddfc363
commit cb297e15c4
46 changed files with 5661 additions and 1011 deletions

135
qemu/src/QmpClient.ts Normal file
View File

@@ -0,0 +1,135 @@
// 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<any> {
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();
}
}