qemu: more refactoring
qmp client is now much more robust (and doesn't fight over itself as much). this should hopefully completely eliminate the case where display connects but qmp is half connected. i also forgot QemuDisplay actually emits an event on connection, so we can just use that to wait for when the display connects. which allows us to set the started state there instead of praying when the qmp client connects that we are connected to the display roughly at the same time. i also gated some stuff to require vm state in the server. this is a bit rickity, but does seem to work.
This commit is contained in:
@@ -106,12 +106,11 @@ export default class CollabVMServer {
|
|||||||
this.indefiniteTurn = null;
|
this.indefiniteTurn = null;
|
||||||
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
||||||
|
|
||||||
let initSize = vm.GetDisplay().Size() || {
|
// No size initially, since the
|
||||||
|
this.OnDisplayResized({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0
|
height: 0
|
||||||
};
|
});
|
||||||
|
|
||||||
this.OnDisplayResized(initSize);
|
|
||||||
|
|
||||||
this.VM = vm;
|
this.VM = vm;
|
||||||
|
|
||||||
@@ -120,6 +119,7 @@ export default class CollabVMServer {
|
|||||||
if (config.vm.type == 'qemu') {
|
if (config.vm.type == 'qemu') {
|
||||||
(vm as QemuVM).on('statechange', (newState: VMState) => {
|
(vm as QemuVM).on('statechange', (newState: VMState) => {
|
||||||
if (newState == VMState.Started) {
|
if (newState == VMState.Started) {
|
||||||
|
self.logger.Info("VM started");
|
||||||
// well aware this sucks but whatever
|
// well aware this sucks but whatever
|
||||||
self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size));
|
self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size));
|
||||||
self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
|
self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
|
||||||
@@ -214,12 +214,11 @@ export default class CollabVMServer {
|
|||||||
// Set username
|
// Set username
|
||||||
if (client.countryCode !== null && client.noFlag) {
|
if (client.countryCode !== null && client.noFlag) {
|
||||||
// privacy
|
// privacy
|
||||||
for (let cl of this.clients.filter(c => c !== client)) {
|
for (let cl of this.clients.filter((c) => c !== client)) {
|
||||||
cl.sendMsg(cvm.guacEncode('remuser', '1', client.username!));
|
cl.sendMsg(cvm.guacEncode('remuser', '1', client.username!));
|
||||||
}
|
}
|
||||||
this.renameUser(client, res.username, false);
|
this.renameUser(client, res.username, false);
|
||||||
}
|
} else this.renameUser(client, res.username, true);
|
||||||
else this.renameUser(client, res.username, true);
|
|
||||||
// Set rank
|
// Set rank
|
||||||
client.rank = res.rank;
|
client.rank = res.rank;
|
||||||
if (client.rank === Rank.Admin) {
|
if (client.rank === Rank.Admin) {
|
||||||
@@ -241,18 +240,28 @@ export default class CollabVMServer {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'noflag': {
|
case 'noflag': {
|
||||||
if (client.connectedToNode) // too late
|
if (client.connectedToNode)
|
||||||
|
// too late
|
||||||
return;
|
return;
|
||||||
client.noFlag = true;
|
client.noFlag = true;
|
||||||
}
|
}
|
||||||
case 'list':
|
case 'list':
|
||||||
|
if (this.VM.GetState() == VMState.Started) {
|
||||||
client.sendMsg(cvm.guacEncode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
client.sendMsg(cvm.guacEncode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) {
|
if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) {
|
||||||
client.sendMsg(cvm.guacEncode('connect', '0'));
|
client.sendMsg(cvm.guacEncode('connect', '0'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't allow connecting if the VM hasn't started
|
||||||
|
if (this.VM.GetState() != VMState.Started) {
|
||||||
|
client.sendMsg(cvm.guacEncode('connect', '0'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
client.connectedToNode = true;
|
client.connectedToNode = true;
|
||||||
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
|
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
|
||||||
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
||||||
@@ -275,6 +284,12 @@ export default class CollabVMServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// similar rationale to 'connect'
|
||||||
|
if (this.VM.GetState() != VMState.Started) {
|
||||||
|
client.sendMsg(cvm.guacEncode('connect', '0'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (msgArr[2]) {
|
switch (msgArr[2]) {
|
||||||
case '0':
|
case '0':
|
||||||
client.viewMode = 0;
|
client.viewMode = 0;
|
||||||
@@ -443,20 +458,21 @@ export default class CollabVMServer {
|
|||||||
}
|
}
|
||||||
this.sendVoteUpdate();
|
this.sendVoteUpdate();
|
||||||
break;
|
break;
|
||||||
case "cap": {
|
case 'cap': {
|
||||||
if (msgArr.length < 2) return;
|
if (msgArr.length < 2) return;
|
||||||
// Capabilities can only be announced before connecting to the VM
|
// Capabilities can only be announced before connecting to the VM
|
||||||
if (client.connectedToNode) return;
|
if (client.connectedToNode) return;
|
||||||
var caps = [];
|
var caps = [];
|
||||||
for (const cap of msgArr.slice(1)) switch(cap) {
|
for (const cap of msgArr.slice(1))
|
||||||
case "bin": {
|
switch (cap) {
|
||||||
if (caps.indexOf("bin") !== -1) break;
|
case 'bin': {
|
||||||
|
if (caps.indexOf('bin') !== -1) break;
|
||||||
client.Capabilities.bin = true;
|
client.Capabilities.bin = true;
|
||||||
caps.push("bin");
|
caps.push('bin');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.sendMsg(cvm.guacEncode("cap", ...caps));
|
client.sendMsg(cvm.guacEncode('cap', ...caps));
|
||||||
}
|
}
|
||||||
case 'admin':
|
case 'admin':
|
||||||
if (msgArr.length < 2) return;
|
if (msgArr.length < 2) return;
|
||||||
@@ -719,7 +735,8 @@ export default class CollabVMServer {
|
|||||||
if (announce) this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('rename', '1', oldname, client.username!, client.rank.toString())));
|
if (announce) this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('rename', '1', oldname, client.username!, client.rank.toString())));
|
||||||
} else {
|
} else {
|
||||||
this.logger.Info(`Rename ${client.IP.address} to ${client.username}`);
|
this.logger.Info(`Rename ${client.IP.address} to ${client.username}`);
|
||||||
if (announce) this.clients.forEach((c) => {
|
if (announce)
|
||||||
|
this.clients.forEach((c) => {
|
||||||
c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()));
|
c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()));
|
||||||
if (client.countryCode !== null) c.sendMsg(cvm.guacEncode('flag', client.username!, client.countryCode));
|
if (client.countryCode !== null) c.sendMsg(cvm.guacEncode('flag', client.username!, client.countryCode));
|
||||||
});
|
});
|
||||||
@@ -735,7 +752,7 @@ export default class CollabVMServer {
|
|||||||
|
|
||||||
getFlagMsg(): string {
|
getFlagMsg(): string {
|
||||||
var arr = ['flag'];
|
var arr = ['flag'];
|
||||||
for (let c of this.clients.filter(cl => cl.countryCode !== null && cl.username && (!cl.noFlag || cl.rank === Rank.Unregistered))) {
|
for (let c of this.clients.filter((cl) => cl.countryCode !== null && cl.username && (!cl.noFlag || cl.rank === Rank.Unregistered))) {
|
||||||
arr.push(c.username!, c.countryCode!);
|
arr.push(c.username!, c.countryCode!);
|
||||||
}
|
}
|
||||||
return cvm.guacEncode(...arr);
|
return cvm.guacEncode(...arr);
|
||||||
@@ -814,14 +831,14 @@ export default class CollabVMServer {
|
|||||||
|
|
||||||
private async OnDisplayRectangle(rect: Rect) {
|
private async OnDisplayRectangle(rect: Rect) {
|
||||||
let encoded = await this.MakeRectData(rect);
|
let encoded = await this.MakeRectData(rect);
|
||||||
let encodedb64 = encoded.toString("base64");
|
let encodedb64 = encoded.toString('base64');
|
||||||
let bmsg: CollabVMProtocolMessage = {
|
let bmsg: CollabVMProtocolMessage = {
|
||||||
type: CollabVMProtocolMessageType.rect,
|
type: CollabVMProtocolMessageType.rect,
|
||||||
rect: {
|
rect: {
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
data: encoded
|
data: encoded
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
var encodedbin = msgpack.encode(bmsg);
|
var encodedbin = msgpack.encode(bmsg);
|
||||||
this.clients
|
this.clients
|
||||||
@@ -870,7 +887,7 @@ export default class CollabVMServer {
|
|||||||
};
|
};
|
||||||
client.socket.sendBinary(msgpack.encode(msg));
|
client.socket.sendBinary(msgpack.encode(msg));
|
||||||
} else {
|
} else {
|
||||||
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded.toString("base64")));
|
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded.toString('base64')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,8 +896,7 @@ export default class CollabVMServer {
|
|||||||
let displaySize = display.Size();
|
let displaySize = display.Size();
|
||||||
|
|
||||||
// TODO: actually throw an error here
|
// TODO: actually throw an error here
|
||||||
if(displaySize.width == 0 && displaySize.height == 0)
|
if (displaySize.width == 0 && displaySize.height == 0) return Buffer.from('no');
|
||||||
return Buffer.from("no")
|
|
||||||
|
|
||||||
let encoded = await JPEGEncoder.Encode(display.Buffer(), displaySize, rect);
|
let encoded = await JPEGEncoder.Encode(display.Buffer(), displaySize, rect);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { VMState } from '@cvmts/qemu';
|
||||||
import VMDisplay from './VMDisplay.js';
|
import VMDisplay from './VMDisplay.js';
|
||||||
|
|
||||||
export default interface VM {
|
export default interface VM {
|
||||||
@@ -7,5 +8,6 @@ export default interface VM {
|
|||||||
Reset(): Promise<void>;
|
Reset(): Promise<void>;
|
||||||
MonitorCommand(command: string): Promise<any>;
|
MonitorCommand(command: string): Promise<any>;
|
||||||
GetDisplay(): VMDisplay;
|
GetDisplay(): VMDisplay;
|
||||||
|
GetState(): VMState;
|
||||||
SnapshotsSupported(): boolean;
|
SnapshotsSupported(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import VM from '../VM';
|
|||||||
import VMDisplay from '../VMDisplay';
|
import VMDisplay from '../VMDisplay';
|
||||||
import { Clamp, Logger, Rect, Size, Sleep } from '@cvmts/shared';
|
import { Clamp, Logger, Rect, Size, Sleep } from '@cvmts/shared';
|
||||||
import { VncClient } from '@computernewb/nodejs-rfb';
|
import { VncClient } from '@computernewb/nodejs-rfb';
|
||||||
import { BatchRects } from '@cvmts/qemu';
|
import { BatchRects, VMState } from '@cvmts/qemu';
|
||||||
import { execaCommand } from 'execa';
|
import { execaCommand } from 'execa';
|
||||||
|
|
||||||
export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||||
@@ -125,6 +125,11 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GetState(): VMState {
|
||||||
|
// for now!
|
||||||
|
return VMState.Started;
|
||||||
|
}
|
||||||
|
|
||||||
SnapshotsSupported(): boolean {
|
SnapshotsSupported(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ class SocketWriter implements IQmpClientWriter {
|
|||||||
export class QemuVM extends EventEmitter {
|
export class QemuVM extends EventEmitter {
|
||||||
private state = VMState.Stopped;
|
private state = VMState.Stopped;
|
||||||
|
|
||||||
|
// QMP stuff.
|
||||||
private qmpInstance: QmpClient = new QmpClient();
|
private qmpInstance: QmpClient = new QmpClient();
|
||||||
private qmpSocket: Socket | null = null;
|
private qmpSocket: Socket | null = null;
|
||||||
private qmpConnected = false;
|
|
||||||
private qmpFailCount = 0;
|
private qmpFailCount = 0;
|
||||||
|
|
||||||
private qemuProcess: ExecaChildProcess | null = null;
|
private qemuProcess: ExecaChildProcess | null = null;
|
||||||
|
|
||||||
private display: QemuDisplay | null;
|
private display: QemuDisplay | null = null;
|
||||||
private definition: QemuVmDefinition;
|
private definition: QemuVmDefinition;
|
||||||
private addedAdditionalArguments = false;
|
private addedAdditionalArguments = false;
|
||||||
|
|
||||||
@@ -69,8 +69,6 @@ export class QemuVM extends EventEmitter {
|
|||||||
this.definition = def;
|
this.definition = def;
|
||||||
this.logger = new Shared.Logger(`CVMTS.QEMU.QemuVM/${this.definition.id}`);
|
this.logger = new Shared.Logger(`CVMTS.QEMU.QemuVM/${this.definition.id}`);
|
||||||
|
|
||||||
this.display = new QemuDisplay(this.GetVncPath());
|
|
||||||
|
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
// Handle the STOP event sent when using -no-shutdown
|
// Handle the STOP event sent when using -no-shutdown
|
||||||
@@ -86,12 +84,16 @@ export class QemuVM extends EventEmitter {
|
|||||||
self.VMLog().Info('QMP ready');
|
self.VMLog().Info('QMP ready');
|
||||||
|
|
||||||
this.display = new QemuDisplay(this.GetVncPath());
|
this.display = new QemuDisplay(this.GetVncPath());
|
||||||
self.display?.Connect();
|
|
||||||
|
|
||||||
// QMP has been connected so the VM is ready to be considered started
|
self.display?.on('connected', () => {
|
||||||
self.qmpFailCount = 0;
|
// The VM can now be considered started
|
||||||
self.qmpConnected = true;
|
self.VMLog().Info("Display connected");
|
||||||
self.SetState(VMState.Started);
|
self.SetState(VMState.Started);
|
||||||
|
})
|
||||||
|
|
||||||
|
// now that we've connected to VNC, connect to the display
|
||||||
|
self.qmpFailCount = 0;
|
||||||
|
self.display?.Connect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +172,10 @@ export class QemuVM extends EventEmitter {
|
|||||||
return this.display!;
|
return this.display!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GetState() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
/// Private fun bits :)
|
/// Private fun bits :)
|
||||||
|
|
||||||
private VMLog() {
|
private VMLog() {
|
||||||
@@ -223,7 +229,6 @@ export class QemuVM extends EventEmitter {
|
|||||||
|
|
||||||
// Disconnect from the display and QMP connections.
|
// Disconnect from the display and QMP connections.
|
||||||
await self.DisconnectDisplay();
|
await self.DisconnectDisplay();
|
||||||
await self.DisconnectQmp();
|
|
||||||
|
|
||||||
// Remove the sockets for VNC and QMP.
|
// Remove the sockets for VNC and QMP.
|
||||||
try {
|
try {
|
||||||
@@ -262,8 +267,10 @@ export class QemuVM extends EventEmitter {
|
|||||||
private async ConnectQmp() {
|
private async ConnectQmp() {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
if (this.qmpConnected) {
|
if (this.qmpSocket) {
|
||||||
this.VMLog().Error('Already connected to QMP!');
|
// This isn't really a problem (since we gate it)
|
||||||
|
// but I'd like to see if i could eliminate this
|
||||||
|
this.VMLog().Warning('QemuVM.ConnectQmp(): Already connected to QMP socket!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +278,11 @@ export class QemuVM extends EventEmitter {
|
|||||||
this.qmpSocket = connect(this.GetQmpPath());
|
this.qmpSocket = connect(this.GetQmpPath());
|
||||||
|
|
||||||
this.qmpSocket.on('close', async () => {
|
this.qmpSocket.on('close', async () => {
|
||||||
if (self.qmpConnected) {
|
self.qmpSocket?.removeAllListeners();
|
||||||
await self.DisconnectQmp();
|
self.qmpSocket = null;
|
||||||
|
|
||||||
// If we aren't stopping, then we should care QMP disconnected
|
// If we aren't stopping (i.e: disconnection wasn't because we disconnected),
|
||||||
|
// then we should care QMP disconnected
|
||||||
if (self.state != VMState.Stopping) {
|
if (self.state != VMState.Stopping) {
|
||||||
if (self.qmpFailCount++ < kMaxFailCount) {
|
if (self.qmpFailCount++ < kMaxFailCount) {
|
||||||
self.VMLog().Error(`Failed to connect to QMP ${self.qmpFailCount} times.`);
|
self.VMLog().Error(`Failed to connect to QMP ${self.qmpFailCount} times.`);
|
||||||
@@ -286,17 +294,21 @@ export class QemuVM extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.qmpSocket.on('error', (e: Error) => {
|
this.qmpSocket.on('error', (e: Error) => {
|
||||||
self.VMLog().Error('QMP socket error: {0}', e.message);
|
self.VMLog().Error('QMP socket error: {0}', e.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.qmpSocket.on('connect', () => {
|
||||||
|
self.VMLog().Info("Connected to QMP socket");
|
||||||
|
|
||||||
// Setup the QMP client.
|
// Setup the QMP client.
|
||||||
let writer = new SocketWriter(this.qmpSocket, this.qmpInstance);
|
let writer = new SocketWriter(self.qmpSocket!, self.qmpInstance);
|
||||||
this.qmpInstance.reset();
|
self.qmpInstance.reset();
|
||||||
this.qmpInstance.setWriter(writer);
|
self.qmpInstance.setWriter(writer);
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async DisconnectDisplay() {
|
private async DisconnectDisplay() {
|
||||||
@@ -307,13 +319,4 @@ export class QemuVM extends EventEmitter {
|
|||||||
// oh well lol
|
// oh well lol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async DisconnectQmp() {
|
|
||||||
if (!this.qmpConnected) return;
|
|
||||||
this.qmpConnected = false;
|
|
||||||
|
|
||||||
if (this.qmpSocket == null) return;
|
|
||||||
this.qmpSocket?.end();
|
|
||||||
this.qmpSocket = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user