better display/vm stuff

- moved superqemu's "QemuDisplay" here; the VNC VM and Qemu both share it (and it has been renamed to a less goofy dumb name)

- VNC VM has been heavily refactored to just use the VNC display we have (this means only one source of truth, less bugs, and it's generally just Better to share the code imho). this means that future plans to abstract this further (or implement the client in cvm-rs in general) won't cause any explosions, or require duplicate effort

- vms are now in src/vm/... just better organization

- superqemu doesn't manage a display anymore (or care about it, other than making sure the socket is unlinked on stop). Instead now it provides info for us to setup our own VNC client. This is also why we provide our own shim interface

This currently relies on a alpha version of superqemu.

Before this is merged into cvmts main I will publish a stable tag and point cvmts to that new version
This commit is contained in:
modeco80
2024-08-23 07:25:57 -04:00
parent 7d9fab2485
commit e839f7f5aa
15 changed files with 384 additions and 149 deletions

34
cvmts/src/vm/interface.ts Normal file
View File

@@ -0,0 +1,34 @@
import { VMState } from '@computernewb/superqemu';
import { VMDisplay } from '../display/interface.js';
import { EventEmitter } from 'node:events';
// Abstraction of VM interface
export default interface VM {
// Starts the VM.
Start(): Promise<void>;
// Stops the VM.
Stop(): Promise<void>;
// Reboots the VM.
Reboot(): Promise<void>;
// Resets the VM.
Reset(): Promise<void>;
// Monitor command
MonitorCommand(command: string): Promise<any>;
// Start/connect the display
StartDisplay(): void;
// Gets the current active display
// TODO: this could probaly be replaced with an event or something
GetDisplay(): VMDisplay | null;
GetState(): VMState;
SnapshotsSupported(): boolean;
Events(): EventEmitter;
}

89
cvmts/src/vm/qemu.ts Normal file
View File

@@ -0,0 +1,89 @@
import EventEmitter from 'events';
import VM from './interface.js';
import { QemuVM, QemuVmDefinition, VMState } from '@computernewb/superqemu';
import { VMDisplay } from '../display/interface.js';
import { VncDisplay } from '../display/vnc.js';
import pino from 'pino';
// shim over superqemu because it diverges from the VM interface
export class QemuVMShim implements VM {
private vm;
private display: VncDisplay | null = null;
private logger;
constructor(def: QemuVmDefinition) {
this.vm = new QemuVM(def);
this.logger = pino({ name: `CVMTS.QemuVMShim/${def.id}` });
}
Start(): Promise<void> {
return this.vm.Start();
}
async Stop(): Promise<void> {
await this.vm.Stop();
this.display?.Disconnect();
this.display = null;
}
Reboot(): Promise<void> {
return this.vm.Reboot();
}
Reset(): Promise<void> {
return this.vm.Reset();
}
MonitorCommand(command: string): Promise<any> {
return this.vm.MonitorCommand(command);
}
StartDisplay(): void {
// boot it up
let info = this.vm.GetDisplayInfo();
if (info == null) throw new Error('its dead jim');
switch (info.type) {
case 'vnc-tcp':
this.display = new VncDisplay({
host: info.host || '127.0.0.1',
port: info.port || 5900,
path: null
});
break;
case 'vnc-uds':
this.display = new VncDisplay({
path: info.path
});
break;
}
let self = this;
this.display?.on('connected', () => {
// The VM can now be considered started
self.logger.info('Display connected');
});
// now that QMP has connected, connect to the display
self.display?.Connect();
}
GetDisplay(): VMDisplay | null {
return this.display;
}
GetState(): VMState {
return this.vm.GetState();
}
SnapshotsSupported(): boolean {
return this.vm.SnapshotsSupported();
}
Events(): EventEmitter {
return this.vm;
}
}

111
cvmts/src/vm/vnc/VNCVM.ts Normal file
View File

@@ -0,0 +1,111 @@
import EventEmitter from 'events';
import VNCVMDef from './VNCVMDef';
import VM from '../interface.js';
import { VMDisplay } from '../../display/interface.js';
import { VMState } from '@computernewb/superqemu';
import { execaCommand } from 'execa';
import pino from 'pino';
import { VncDisplay } from '../../display/vnc.js';
function Clamp(input: number, min: number, max: number) {
return Math.min(Math.max(input, min), max);
}
async function Sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export default class VNCVM extends EventEmitter implements VM {
def: VNCVMDef;
logger;
private vnc: VncDisplay | null = null;
private state = VMState.Stopped;
constructor(def: VNCVMDef) {
super();
this.def = def;
// TODO: Now that we're using an actual structured logger can we please
this.logger = pino({ name: `CVMTS.VNCVM/${this.def.vncHost}:${this.def.vncPort}` });
}
async Reset(): Promise<void> {
if (this.def.restoreCmd) await execaCommand(this.def.restoreCmd, { shell: true });
else {
await this.Stop();
await Sleep(1000);
await this.Start();
}
}
private Connect() {
if (this.vnc) {
this.Disconnect();
}
this.vnc = new VncDisplay({
host: this.def.vncHost,
port: this.def.vncPort
});
let self = this;
this.vnc.on('connected', () => {
self.logger.info('Connected');
self.SetState(VMState.Started);
});
}
private Disconnect() {
if (this.vnc) {
this.vnc.Disconnect();
this.vnc.removeAllListeners();
this.vnc = null;
}
}
private SetState(newState: VMState) {
this.state = newState;
this.emit('statechange', newState);
}
StartDisplay(): void {
this.logger.info('Connecting to VNC server');
this.Connect();
}
async Start(): Promise<void> {
if (this.def.startCmd) await execaCommand(this.def.startCmd, { shell: true });
this.SetState(VMState.Started);
}
async Stop(): Promise<void> {
this.logger.info('Disconnecting');
this.Disconnect();
if (this.def.stopCmd) await execaCommand(this.def.stopCmd, { shell: true });
this.SetState(VMState.Stopped);
}
async Reboot(): Promise<void> {
if (this.def.rebootCmd) await execaCommand(this.def.rebootCmd, { shell: true });
}
async MonitorCommand(command: string): Promise<any> {
// TODO: This can maybe run a specified command?
return 'This VM does not support monitor commands.';
}
GetDisplay(): VMDisplay | null {
return this.vnc;
}
GetState(): VMState {
return this.state;
}
SnapshotsSupported(): boolean {
return true;
}
Events(): EventEmitter {
return this;
}
}

View File

@@ -0,0 +1,8 @@
export default interface VNCVMDef {
vncHost: string;
vncPort: number;
startCmd: string | null;
stopCmd: string | null;
rebootCmd: string | null;
restoreCmd: string | null;
}