You can now take turns and control the VM.
This commit is contained in:
10
package.json
10
package.json
@@ -11,12 +11,18 @@
|
|||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/sharp": "^0.31.1",
|
||||||
"@types/ws": "^8.5.4",
|
"@types/ws": "^8.5.4",
|
||||||
|
"async-mutex": "^0.4.0",
|
||||||
|
"execa": "^6.1.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
|
"jimp": "^0.16.2",
|
||||||
"mnemonist": "^0.39.5",
|
"mnemonist": "^0.39.5",
|
||||||
"rfb2": "^0.2.2",
|
"rfb2": "github:elijahr2411/node-rfb2",
|
||||||
|
"sharp": "^0.31.3",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"ws": "^8.12.0"
|
"ws": "^8.12.0"
|
||||||
}
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/Framebuffer.ts
Normal file
44
src/Framebuffer.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Mutex } from "async-mutex";
|
||||||
|
|
||||||
|
export default class Framebuffer {
|
||||||
|
private fb : Buffer;
|
||||||
|
private writemutex : Mutex;
|
||||||
|
size : {height : number, width : number};
|
||||||
|
constructor() {
|
||||||
|
this.fb = Buffer.alloc(1);
|
||||||
|
this.size = {height: 0, width: 0};
|
||||||
|
this.writemutex = new Mutex();
|
||||||
|
}
|
||||||
|
setSize(w : number, h : number) {
|
||||||
|
var size = h * w * 4;
|
||||||
|
this.size.height = h;
|
||||||
|
this.size.width = w;
|
||||||
|
this.fb = Buffer.alloc(size);
|
||||||
|
}
|
||||||
|
loadDirtyRect(rect : Buffer, x : number, y : number, width : number, height : number) {
|
||||||
|
if (this.fb.length < rect.length)
|
||||||
|
throw new Error("Dirty rect larger than framebuffer (did you forget to set the size?)");
|
||||||
|
this.writemutex.runExclusive(() => {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
var byteswritten = 0;
|
||||||
|
for (var i = 0; i < height; i++) {
|
||||||
|
byteswritten += rect.copy(this.fb, 4 * ((y + i) * this.size.width + x), byteswritten, byteswritten + (width * 4));
|
||||||
|
}
|
||||||
|
res();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getFb() : Promise<Buffer> {
|
||||||
|
return new Promise<Buffer>(async (res, rej) => {
|
||||||
|
var v = await this.writemutex.runExclusive(() => {
|
||||||
|
return new Promise<Buffer>((reso, reje) => {
|
||||||
|
var buff = Buffer.alloc(this.fb.length);
|
||||||
|
this.fb.copy(buff);
|
||||||
|
reso(buff);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res(v);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
125
src/QEMUVM.ts
125
src/QEMUVM.ts
@@ -1,9 +1,9 @@
|
|||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import IConfig from "./IConfig";
|
import IConfig from "./IConfig.js";
|
||||||
import * as rfb from 'rfb2';
|
import * as rfb from 'rfb2';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { spawn, ChildProcess } from "child_process";
|
import { execa, ExecaChildProcess } from "execa";
|
||||||
import QMPClient from "./QMPClient";
|
import QMPClient from "./QMPClient.js";
|
||||||
|
|
||||||
export default class QEMUVM extends EventEmitter {
|
export default class QEMUVM extends EventEmitter {
|
||||||
vnc? : rfb.RfbClient;
|
vnc? : rfb.RfbClient;
|
||||||
@@ -11,9 +11,16 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
qmpSock : string;
|
qmpSock : string;
|
||||||
qmpClient : QMPClient;
|
qmpClient : QMPClient;
|
||||||
qemuCmd : string;
|
qemuCmd : string;
|
||||||
qemuProcess? : ChildProcess;
|
qemuProcess? : ExecaChildProcess;
|
||||||
qmpErrorLevel : number;
|
qmpErrorLevel : number;
|
||||||
vncErrorLevel : number;
|
vncErrorLevel : number;
|
||||||
|
processRestartErrorLevel : number;
|
||||||
|
expectedExit : boolean;
|
||||||
|
|
||||||
|
vncReconnectTimeout? : NodeJS.Timer;
|
||||||
|
qmpReconnectTimeout? : NodeJS.Timer;
|
||||||
|
qemuRestartTimeout? : NodeJS.Timer;
|
||||||
|
|
||||||
constructor(Config : IConfig) {
|
constructor(Config : IConfig) {
|
||||||
super();
|
super();
|
||||||
if (Config.vm.vncPort < 5900) {
|
if (Config.vm.vncPort < 5900) {
|
||||||
@@ -25,12 +32,15 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
this.qemuCmd = `${Config.vm.qemuArgs} -snapshot -no-shutdown -vnc 127.0.0.1:${this.vncPort - 5900} -qmp unix:${this.qmpSock},server`;
|
this.qemuCmd = `${Config.vm.qemuArgs} -snapshot -no-shutdown -vnc 127.0.0.1:${this.vncPort - 5900} -qmp unix:${this.qmpSock},server`;
|
||||||
this.qmpErrorLevel = 0;
|
this.qmpErrorLevel = 0;
|
||||||
this.vncErrorLevel = 0;
|
this.vncErrorLevel = 0;
|
||||||
|
this.processRestartErrorLevel = 0;
|
||||||
|
this.expectedExit = false;
|
||||||
this.qmpClient = new QMPClient(this.qmpSock);
|
this.qmpClient = new QMPClient(this.qmpSock);
|
||||||
this.qmpClient.on('connected', () => this.qmpConnected());
|
this.qmpClient.on('connected', () => this.qmpConnected());
|
||||||
|
this.qmpClient.on('close', () => this.qmpClosed());
|
||||||
}
|
}
|
||||||
|
|
||||||
Start() {
|
Start() : Promise<void> {
|
||||||
return new Promise(async (res, rej) => {
|
return new Promise<void>(async (res, rej) => {
|
||||||
if (fs.existsSync(this.qmpSock))
|
if (fs.existsSync(this.qmpSock))
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(this.qmpSock);
|
fs.unlinkSync(this.qmpSock);
|
||||||
@@ -39,20 +49,34 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
var qemuArr = this.qemuCmd.split(" ");
|
var qemuArr = this.qemuCmd.split(" ");
|
||||||
this.qemuProcess = spawn(qemuArr[0], qemuArr.slice(1));
|
this.qemuProcess = execa(qemuArr[0], qemuArr.slice(1));
|
||||||
process.on("beforeExit", () => {
|
|
||||||
this.qemuProcess?.kill(9);
|
|
||||||
});
|
|
||||||
this.qemuProcess.stderr?.on('data', (d) => console.log(d.toString()));
|
this.qemuProcess.stderr?.on('data', (d) => console.log(d.toString()));
|
||||||
this.qemuProcess.on('spawn', () => {
|
this.qemuProcess.on('spawn', () => {
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
this.qmpClient.connect();
|
await this.qmpClient.connect();
|
||||||
}, 1000)
|
}, 1000)
|
||||||
});
|
});
|
||||||
|
this.qemuProcess.on('exit', () => {
|
||||||
|
if (this.expectedExit) return;
|
||||||
|
clearTimeout(this.qmpReconnectTimeout);
|
||||||
|
clearTimeout(this.vncReconnectTimeout);
|
||||||
|
this.processRestartErrorLevel++;
|
||||||
|
if (this.processRestartErrorLevel > 4) {
|
||||||
|
console.error("[FATAL] QEMU failed to launch 5 times.");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
console.warn("QEMU exited unexpectly. Restarting in 3 seconds...");
|
||||||
|
this.qmpClient.disconnect();
|
||||||
|
this.vnc?.end();
|
||||||
|
this.qemuRestartTimeout = setTimeout(() => this.Start(), 3000);
|
||||||
|
});
|
||||||
|
this.once('vncconnect', () => res());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private qmpConnected() {
|
private qmpConnected() {
|
||||||
|
this.qmpErrorLevel = 0;
|
||||||
|
this.processRestartErrorLevel = 0;
|
||||||
console.log("QMP Connected");
|
console.log("QMP Connected");
|
||||||
setTimeout(() => this.startVNC(), 1000);
|
setTimeout(() => this.startVNC(), 1000);
|
||||||
}
|
}
|
||||||
@@ -62,9 +86,86 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: this.vncPort,
|
port: this.vncPort,
|
||||||
});
|
});
|
||||||
|
this.vnc.on("close", () => this.vncClosed());
|
||||||
|
this.vnc.on("connect", () => this.vncConnected());
|
||||||
|
this.vnc.on("rect", (r) => this.onVNCRect(r));
|
||||||
|
this.vnc.on("resize", (s) => this.onVNCSize(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSize() {
|
||||||
|
if (!this.vnc) return {height:0,width:0};
|
||||||
|
return {height: this.vnc.height, width: this.vnc.width}
|
||||||
}
|
}
|
||||||
|
|
||||||
private qmpClosed() {
|
private qmpClosed() {
|
||||||
|
if (this.expectedExit) return;
|
||||||
|
this.qmpErrorLevel++;
|
||||||
|
if (this.qmpErrorLevel > 4) {
|
||||||
|
console.error("[FATAL] Failed to connect to QMP after 5 attempts");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.warn("Failed to connect to QMP. Retrying in 3 seconds...");
|
||||||
|
this.qmpReconnectTimeout = setTimeout(() => this.qmpClient.connect(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private vncClosed() {
|
||||||
|
if (this.expectedExit) return;
|
||||||
|
this.vncErrorLevel++;
|
||||||
|
if (this.vncErrorLevel > 4) {
|
||||||
|
console.error("[FATAL] Failed to connect to VNC after 5 attempts");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.vnc?.end();
|
||||||
|
} catch {};
|
||||||
|
console.warn("Failed to connect to VNC. Retrying in 3 seconds...");
|
||||||
|
this.vncReconnectTimeout = setTimeout(() => this.startVNC(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private vncConnected() {
|
||||||
|
this.emit('vncconnect');
|
||||||
|
this.vncErrorLevel = 0;
|
||||||
|
}
|
||||||
|
private async onVNCRect(rect : any) {
|
||||||
|
var buff = Buffer.alloc(rect.height * rect.width * 4)
|
||||||
|
var offset = 0;
|
||||||
|
for (var i = 0; i < rect.data.length; i += 4) {
|
||||||
|
buff[offset++] = rect.data[i + 2];
|
||||||
|
buff[offset++] = rect.data[i + 1];
|
||||||
|
buff[offset++] = rect.data[i];
|
||||||
|
buff[offset++] = 255;
|
||||||
|
}
|
||||||
|
this.emit("dirtyrect", buff, rect.x, rect.y, rect.width, rect.height);
|
||||||
|
if (!this.vnc) throw new Error();
|
||||||
|
this.vnc.requestUpdate(true, 0, 0, this.vnc.height, this.vnc.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onVNCSize(size : any) {
|
||||||
|
this.emit("size", {height: size.height, width: size.width});
|
||||||
|
}
|
||||||
|
|
||||||
|
Reboot() : Promise<void> {
|
||||||
|
return this.qmpClient.reboot();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Restore() {
|
||||||
|
await this.Stop();
|
||||||
|
this.expectedExit = false;
|
||||||
|
this.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stop() : Promise<void> {
|
||||||
|
return new Promise<void>(async (res, rej) => {
|
||||||
|
this.expectedExit = true;
|
||||||
|
this.vnc?.end();
|
||||||
|
this.qmpClient.disconnect();
|
||||||
|
var killTimeout = setTimeout(() => {
|
||||||
|
console.log("Force killing QEMU after 10 seconds of waiting for shutdown");
|
||||||
|
this.qemuProcess?.kill(9);
|
||||||
|
}, 10000)
|
||||||
|
await this.qemuProcess;
|
||||||
|
clearTimeout(killTimeout);
|
||||||
|
res();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,52 @@
|
|||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
|
import { Mutex } from "async-mutex";
|
||||||
|
|
||||||
export default class QMPClient extends EventEmitter {
|
export default class QMPClient extends EventEmitter {
|
||||||
socketfile : string;
|
socketfile : string;
|
||||||
socket : Socket;
|
socket : Socket;
|
||||||
connected : boolean;
|
connected : boolean;
|
||||||
sentConnected : boolean;
|
sentConnected : boolean;
|
||||||
|
cmdMutex : Mutex; // So command outputs don't get mixed up
|
||||||
constructor(socketfile : string) {
|
constructor(socketfile : string) {
|
||||||
super();
|
super();
|
||||||
this.socketfile = socketfile;
|
this.socketfile = socketfile;
|
||||||
this.socket = new Socket();
|
this.socket = new Socket();
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.sentConnected = false;
|
this.sentConnected = false;
|
||||||
|
this.cmdMutex = new Mutex();
|
||||||
}
|
}
|
||||||
connect() {
|
connect() : Promise<void> {
|
||||||
if (this.connected) return;
|
return new Promise((res, rej) => {
|
||||||
try {
|
if (this.connected) {res(); return;}
|
||||||
this.socket.connect(this.socketfile);
|
try {
|
||||||
} catch (e) {
|
this.socket.connect(this.socketfile);
|
||||||
this.emit("")
|
} catch (e) {
|
||||||
}
|
this.onClose();
|
||||||
this.connected = true;
|
}
|
||||||
this.socket.on('data', (data) => this.onData(data));
|
this.connected = true;
|
||||||
this.socket.on('close', () => this.onClose());
|
this.socket.on('error', (err) => false); // Disable throwing if QMP errors
|
||||||
|
this.socket.on('data', (data) => this.onData(data));
|
||||||
|
this.socket.on('close', () => this.onClose());
|
||||||
|
this.once('connected', () => res());
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private onData(data : Buffer) {
|
disconnect() {
|
||||||
|
this.connected = false;
|
||||||
|
this.socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onData(data : Buffer) {
|
||||||
var msgraw = data.toString();
|
var msgraw = data.toString();
|
||||||
var msg = JSON.parse(msgraw);
|
var msg = JSON.parse(msgraw);
|
||||||
console.log(msg);
|
|
||||||
if (msg.QMP) {
|
if (msg.QMP) {
|
||||||
if (this.sentConnected) return;
|
if (this.sentConnected) return;
|
||||||
this.socket.write(JSON.stringify({ execute: "qmp_capabilities" }));
|
await this.execute({ execute: "qmp_capabilities" });
|
||||||
this.emit('connected');
|
this.emit('connected');
|
||||||
this.sentConnected = true;
|
this.sentConnected = true;
|
||||||
}
|
}
|
||||||
|
if (msg.return) this.emit("qmpreturn", msg.return);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClose() {
|
private onClose() {
|
||||||
@@ -42,4 +54,36 @@ export default class QMPClient extends EventEmitter {
|
|||||||
this.sentConnected = false;
|
this.sentConnected = false;
|
||||||
this.emit('close');
|
this.emit('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reboot() {
|
||||||
|
if (!this.connected) return;
|
||||||
|
await this.execute({"execute": "system_reset"});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ExitQEMU() {
|
||||||
|
if (!this.connected) return;
|
||||||
|
await this.execute({"execute": "quit"});
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(args : object) {
|
||||||
|
return new Promise(async (res, rej) => {
|
||||||
|
var result:any = 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
runMonitorCmd(command : string) {
|
||||||
|
return new Promise(async (res, rej) => {
|
||||||
|
var result : any = await this.execute({execute: "human-monitor-command", arguments: {"command-line": command}});
|
||||||
|
res(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventEmitter } from "stream";
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
// Class to ratelimit a resource (chatting, logging in, etc)
|
// Class to ratelimit a resource (chatting, logging in, etc)
|
||||||
export default class RateLimiter extends EventEmitter {
|
export default class RateLimiter extends EventEmitter {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as Utilities from './Utilities';
|
import * as Utilities from './Utilities.js';
|
||||||
import * as guacutils from './guacutils';
|
import * as guacutils from './guacutils.js';
|
||||||
import {WebSocket} from 'ws';
|
import {WebSocket} from 'ws';
|
||||||
import IConfig from './IConfig';
|
import IConfig from './IConfig.js';
|
||||||
import RateLimiter from './RateLimiter';
|
import RateLimiter from './RateLimiter.js';
|
||||||
export class User {
|
export class User {
|
||||||
socket : WebSocket;
|
socket : WebSocket;
|
||||||
nopSendInterval : NodeJS.Timer;
|
nopSendInterval : NodeJS.Timer;
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import {WebSocketServer, WebSocket} from 'ws';
|
import {WebSocketServer, WebSocket} from 'ws';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import IConfig from './IConfig';
|
import IConfig from './IConfig.js';
|
||||||
import internal from 'stream';
|
import internal from 'stream';
|
||||||
import * as Utilities from './Utilities';
|
import * as Utilities from './Utilities.js';
|
||||||
import { User, Rank } from './User';
|
import { User, Rank } from './User.js';
|
||||||
import * as guacutils from './guacutils';
|
import * as guacutils from './guacutils.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { CircularBuffer, Queue } from 'mnemonist';
|
// I hate that you have to do it like this
|
||||||
|
import CircularBuffer from 'mnemonist/circular-buffer.js';
|
||||||
|
import Queue from 'mnemonist/queue.js';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
|
import QEMUVM from './QEMUVM.js';
|
||||||
|
import Framebuffer from './Framebuffer.js';
|
||||||
|
import sharp, { Sharp } from 'sharp';
|
||||||
|
|
||||||
export default class WSServer {
|
export default class WSServer {
|
||||||
private Config : IConfig;
|
private Config : IConfig;
|
||||||
@@ -21,7 +26,9 @@ export default class WSServer {
|
|||||||
private TurnInterval? : NodeJS.Timer;
|
private TurnInterval? : NodeJS.Timer;
|
||||||
private TurnIntervalRunning : boolean;
|
private TurnIntervalRunning : boolean;
|
||||||
private ModPerms : number;
|
private ModPerms : number;
|
||||||
constructor(config : IConfig) {
|
private VM : QEMUVM;
|
||||||
|
private framebuffer : Framebuffer;
|
||||||
|
constructor(config : IConfig, vm : QEMUVM) {
|
||||||
this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, 5);
|
this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, 5);
|
||||||
this.TurnQueue = new Queue<User>();
|
this.TurnQueue = new Queue<User>();
|
||||||
this.TurnTime = 0;
|
this.TurnTime = 0;
|
||||||
@@ -33,6 +40,12 @@ export default class WSServer {
|
|||||||
this.socket = new WebSocketServer({noServer: true});
|
this.socket = new WebSocketServer({noServer: true});
|
||||||
this.server.on('upgrade', (req : http.IncomingMessage, socket : internal.Duplex, head : Buffer) => this.httpOnUpgrade(req, socket, head));
|
this.server.on('upgrade', (req : http.IncomingMessage, socket : internal.Duplex, head : Buffer) => this.httpOnUpgrade(req, socket, head));
|
||||||
this.socket.on('connection', (ws : WebSocket, req : http.IncomingMessage) => this.onConnection(ws, req));
|
this.socket.on('connection', (ws : WebSocket, req : http.IncomingMessage) => this.onConnection(ws, req));
|
||||||
|
var initSize = vm.getSize();
|
||||||
|
this.framebuffer = new Framebuffer();
|
||||||
|
this.newsize(initSize);
|
||||||
|
this.VM = vm;
|
||||||
|
this.VM.on("dirtyrect", (j, x, y, w, h) => this.newrect(j, x, y, w, h));
|
||||||
|
this.VM.on("size", (s) => this.newsize(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
@@ -123,13 +136,12 @@ export default class WSServer {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.clients.forEach((c) => c.sendMsg(guacutils.encode("remuser", "1", user.username)));
|
this.clients.forEach((c) => c.sendMsg(guacutils.encode("remuser", "1", user.username)));
|
||||||
}
|
}
|
||||||
fuck = fs.readFileSync("/home/elijah/Pictures/thumb.txt").toString();
|
private async onMessage(client : User, message : string) {
|
||||||
private onMessage(client : User, message : string) {
|
|
||||||
var msgArr = guacutils.decode(message);
|
var msgArr = guacutils.decode(message);
|
||||||
if (msgArr.length < 1) return;
|
if (msgArr.length < 1) return;
|
||||||
switch (msgArr[0]) {
|
switch (msgArr[0]) {
|
||||||
case "list":
|
case "list":
|
||||||
client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, this.fuck))
|
client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, 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) {
|
||||||
@@ -140,8 +152,10 @@ export default class WSServer {
|
|||||||
client.sendMsg(guacutils.encode("connect", "1", "1", "1", "0"));
|
client.sendMsg(guacutils.encode("connect", "1", "1", "1", "0"));
|
||||||
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
||||||
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
||||||
client.sendMsg(guacutils.encode("size", "0", "400", "300"));
|
client.sendMsg(guacutils.encode("size", "0", this.framebuffer.size.width.toString(), this.framebuffer.size.height.toString()));
|
||||||
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.fuck));
|
var jpg = await sharp(await this.framebuffer.getFb(), {raw: {height: this.framebuffer.size.height, width: this.framebuffer.size.width, channels: 4}}).jpeg().toBuffer();
|
||||||
|
var jpg64 = jpg.toString("base64");
|
||||||
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64));
|
||||||
break;
|
break;
|
||||||
case "rename":
|
case "rename":
|
||||||
if (!client.RenameRateLimit.request()) return;
|
if (!client.RenameRateLimit.request()) return;
|
||||||
@@ -231,6 +245,23 @@ export default class WSServer {
|
|||||||
}
|
}
|
||||||
this.sendTurnUpdate();
|
this.sendTurnUpdate();
|
||||||
break;
|
break;
|
||||||
|
case "mouse":
|
||||||
|
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
||||||
|
if (!this.VM.vnc) throw new Error("VNC Client was undefined");
|
||||||
|
var x = parseInt(msgArr[1]);
|
||||||
|
var y = parseInt(msgArr[2]);
|
||||||
|
var mask = parseInt(msgArr[3]);
|
||||||
|
if (x === undefined || y === undefined || mask === undefined) return;
|
||||||
|
this.VM.vnc.pointerEvent(x, y, mask);
|
||||||
|
break;
|
||||||
|
case "key":
|
||||||
|
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
||||||
|
if (!this.VM.vnc) throw new Error("VNC Client was undefined");
|
||||||
|
var keysym = parseInt(msgArr[1]);
|
||||||
|
var down = parseInt(msgArr[2]);
|
||||||
|
if (keysym === undefined || (down !== 0 && down !== 1)) return;
|
||||||
|
this.VM.vnc.keyEvent(keysym, down);
|
||||||
|
break;
|
||||||
case "admin":
|
case "admin":
|
||||||
if (msgArr.length < 2) return;
|
if (msgArr.length < 2) return;
|
||||||
switch (msgArr[1]) {
|
switch (msgArr[1]) {
|
||||||
@@ -312,4 +343,25 @@ export default class WSServer {
|
|||||||
this.nextTurn();
|
this.nextTurn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async newrect(buff : Buffer, x : number, y : number, width : number, height : number) {
|
||||||
|
var jpg = await sharp(buff, {raw: {height: height, width: width, channels: 4}}).jpeg().toBuffer();
|
||||||
|
var jpg64 = jpg.toString("base64");
|
||||||
|
this.clients.filter(c => c.connectedToNode).forEach(c => c.sendMsg(guacutils.encode("png", "0", "0", x.toString(), y.toString(), jpg64)));
|
||||||
|
this.framebuffer.loadDirtyRect(buff, x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private newsize(size : {height:number,width:number}) {
|
||||||
|
this.framebuffer.setSize(size.width, size.height);
|
||||||
|
this.clients.filter(c => c.connectedToNode).forEach(c => c.sendMsg(guacutils.encode("size", "0", size.width.toString(), size.height.toString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnail() : Promise<string> {
|
||||||
|
return new Promise(async (res, rej) => {
|
||||||
|
var jpg = await sharp(await this.framebuffer.getFb(), {raw: {height: this.framebuffer.size.height, width: this.framebuffer.size.width, channels: 4}})
|
||||||
|
.resize(400, 300, {fit: 'fill'})
|
||||||
|
.jpeg().toBuffer();
|
||||||
|
res(jpg.toString("base64"));
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
22
src/index.ts
22
src/index.ts
@@ -1,8 +1,8 @@
|
|||||||
import * as toml from 'toml';
|
import * as toml from 'toml';
|
||||||
import IConfig from './IConfig';
|
import IConfig from './IConfig.js';
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import WSServer from './WSServer';
|
import WSServer from './WSServer.js';
|
||||||
import QEMUVM from './QEMUVM';
|
import QEMUVM from './QEMUVM.js';
|
||||||
|
|
||||||
// Parse the config file
|
// Parse the config file
|
||||||
|
|
||||||
@@ -20,10 +20,14 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire up the VM
|
|
||||||
var VM = new QEMUVM(Config);
|
|
||||||
VM.Start();
|
|
||||||
|
|
||||||
// Start up the websocket server
|
async function start() {
|
||||||
var WS = new WSServer(Config);
|
// Fire up the VM
|
||||||
WS.listen();
|
var VM = new QEMUVM(Config);
|
||||||
|
await VM.Start();
|
||||||
|
|
||||||
|
// Start up the websocket server
|
||||||
|
var WS = new WSServer(Config, VM);
|
||||||
|
WS.listen();
|
||||||
|
}
|
||||||
|
start();
|
||||||
@@ -25,16 +25,16 @@
|
|||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "commonjs", /* Specify what module code is generated. */
|
"module": "ES2015", /* Specify what module code is generated. */
|
||||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
// "moduleSuffixes": [".js", ".d.ts", ".ts", ".mjs", ".cjs", ".json"], /* List of file name suffixes to search when resolving a module. */
|
||||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user