cvmts: Use npm versions of superqemu/nodejs-rfb.

We publish them now, so let's use them in cvmts!

Additionally, this removes the 'shared' module entirely, since it has little purpose anymore. The logger is replaced with pino (because superqemu uses pino for logging itself).
This commit is contained in:
modeco80
2024-07-16 08:29:52 -04:00
parent cf9f11819e
commit 432e75d42a
32 changed files with 469 additions and 1131 deletions

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "nodejs-rfb"]
path = nodejs-rfb
url = https://github.com/computernewb/nodejs-rfb
[submodule "collab-vm-1.2-binary-protocol"] [submodule "collab-vm-1.2-binary-protocol"]
path = collab-vm-1.2-binary-protocol path = collab-vm-1.2-binary-protocol
url = https://github.com/computernewb/collab-vm-1.2-binary-protocol url = https://github.com/computernewb/collab-vm-1.2-binary-protocol

View File

@@ -1,8 +1,5 @@
all: all:
yarn workspace @cvmts/cvm-rs run build yarn workspace @cvmts/cvm-rs run build
yarn workspace @computernewb/nodejs-rfb run build
yarn workspace @cvmts/shared run build
yarn workspace @cvmts/qemu run build
yarn workspace @cvmts/collab-vm-1.2-binary-protocol run build yarn workspace @cvmts/collab-vm-1.2-binary-protocol run build
yarn workspace @cvmts/cvmts run build yarn workspace @cvmts/cvmts run build

View File

@@ -11,12 +11,14 @@
"author": "Elijah R, modeco80", "author": "Elijah R, modeco80",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@computernewb/nodejs-rfb": "^0.3.0",
"@computernewb/superqemu": "^0.1.0",
"@cvmts/cvm-rs": "*", "@cvmts/cvm-rs": "*",
"@cvmts/qemu": "*",
"@maxmind/geoip2-node": "^5.0.0", "@maxmind/geoip2-node": "^5.0.0",
"execa": "^8.0.1", "execa": "^8.0.1",
"mnemonist": "^0.39.5", "mnemonist": "^0.39.5",
"msgpackr": "^1.10.2", "msgpackr": "^1.10.2",
"pino": "^9.3.1",
"sharp": "^0.33.3", "sharp": "^0.33.3",
"toml": "^3.0.0", "toml": "^3.0.0",
"ws": "^8.14.1" "ws": "^8.14.1"
@@ -24,6 +26,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.5", "@types/node": "^20.12.5",
"@types/ws": "^8.5.5", "@types/ws": "^8.5.5",
"pino-pretty": "^11.2.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"typescript": "^5.4.4" "typescript": "^5.4.4"
} }

View File

@@ -1,12 +1,9 @@
import { Logger } from '@cvmts/shared';
import { Rank, User } from './User.js'; import { Rank, User } from './User.js';
export default class AuthManager { export default class AuthManager {
apiEndpoint: string; apiEndpoint: string;
secretKey: string; secretKey: string;
private logger = new Logger('CVMTS.AuthMan');
constructor(apiEndpoint: string, secretKey: string) { constructor(apiEndpoint: string, secretKey: string) {
this.apiEndpoint = apiEndpoint; this.apiEndpoint = apiEndpoint;
this.secretKey = secretKey; this.secretKey = secretKey;

View File

@@ -6,18 +6,20 @@ import * as cvm from '@cvmts/cvm-rs';
import CircularBuffer from 'mnemonist/circular-buffer.js'; import CircularBuffer from 'mnemonist/circular-buffer.js';
import Queue from 'mnemonist/queue.js'; import Queue from 'mnemonist/queue.js';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { VMState, QemuVM, QemuVmDefinition } from '@cvmts/qemu'; import { VMState, QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
import { IPDataManager } from './IPData.js'; import { IPDataManager } from './IPData.js';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import AuthManager from './AuthManager.js'; import AuthManager from './AuthManager.js';
import { Size, Rect, Logger } from '@cvmts/shared';
import { JPEGEncoder } from './JPEGEncoder.js'; import { JPEGEncoder } from './JPEGEncoder.js';
import VM from './VM.js'; import VM from './VM.js';
import { ReaderModel } from '@maxmind/geoip2-node'; import { ReaderModel } from '@maxmind/geoip2-node';
import * as msgpack from 'msgpackr'; import * as msgpack from 'msgpackr';
import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol'; import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol';
import { Size, Rect } from './VMDisplay.js';
import pino from 'pino';
// Instead of strange hacks we can just use nodejs provided // Instead of strange hacks we can just use nodejs provided
// import.meta properties, which have existed since LTS if not before // import.meta properties, which have existed since LTS if not before
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
@@ -36,6 +38,7 @@ type VoteTally = {
no: number; no: number;
}; };
export default class CollabVMServer { export default class CollabVMServer {
private Config: IConfig; private Config: IConfig;
@@ -87,7 +90,7 @@ export default class CollabVMServer {
// Geoip // Geoip
private geoipReader: ReaderModel | null; private geoipReader: ReaderModel | null;
private logger = new Logger('CVMTS.Server'); private logger = pino({ name: 'CVMTS.Server' });
constructor(config: IConfig, vm: VM, auth: AuthManager | null, geoipReader: ReaderModel | null) { constructor(config: IConfig, vm: VM, auth: AuthManager | null, geoipReader: ReaderModel | null) {
this.Config = config; this.Config = config;
@@ -121,7 +124,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'); 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));
@@ -129,7 +132,7 @@ export default class CollabVMServer {
if (newState == VMState.Stopped) { if (newState == VMState.Stopped) {
setTimeout(async () => { setTimeout(async () => {
self.logger.Info('restarting VM'); self.logger.info('restarting VM');
await self.VM.Start(); await self.VM.Start();
}, kRestartTimeout); }, kRestartTimeout);
} }
@@ -154,7 +157,7 @@ export default class CollabVMServer {
try { try {
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode; user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode;
} catch (error) { } catch (error) {
this.logger.Warning(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`); this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`);
} }
} }
user.socket.on('msg', (msg: string) => this.onMessage(user, msg)); user.socket.on('msg', (msg: string) => this.onMessage(user, msg));
@@ -179,7 +182,7 @@ export default class CollabVMServer {
this.clients.splice(clientIndex, 1); this.clients.splice(clientIndex, 1);
this.logger.Info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`); this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
if (!user.username) return; if (!user.username) return;
if (this.TurnQueue.toArray().indexOf(user) !== -1) { if (this.TurnQueue.toArray().indexOf(user) !== -1) {
var hadturn = this.TurnQueue.peek() === user; var hadturn = this.TurnQueue.peek() === user;
@@ -204,7 +207,7 @@ export default class CollabVMServer {
try { try {
let res = await this.auth!.Authenticate(msgArr[1], client); let res = await this.auth!.Authenticate(msgArr[1], client);
if (res.clientSuccess) { if (res.clientSuccess) {
this.logger.Info(`${client.IP.address} logged in as ${res.username}`); this.logger.info(`${client.IP.address} logged in as ${res.username}`);
client.sendMsg(cvm.guacEncode('login', '1')); client.sendMsg(cvm.guacEncode('login', '1'));
let old = this.clients.find((c) => c.username === res.username); let old = this.clients.find((c) => c.username === res.username);
if (old) { if (old) {
@@ -236,7 +239,7 @@ export default class CollabVMServer {
} }
} }
} catch (err) { } catch (err) {
this.logger.Error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`); this.logger.error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`);
// for now? // for now?
client.sendMsg(cvm.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible')); client.sendMsg(cvm.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible'));
} }
@@ -679,7 +682,7 @@ export default class CollabVMServer {
} }
} catch (err) { } catch (err) {
// No // No
this.logger.Error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`); this.logger.error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`);
user?.kick(); user?.kick();
} }
} }
@@ -721,10 +724,10 @@ export default class CollabVMServer {
client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString())); client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString()));
if (hadName) { if (hadName) {
this.logger.Info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`); this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
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) if (announce)
this.clients.forEach((c) => { 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()));

View File

@@ -1,4 +1,3 @@
import { Logger } from '@cvmts/shared';
import { Reader, ReaderModel } from '@maxmind/geoip2-node'; import { Reader, ReaderModel } from '@maxmind/geoip2-node';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
@@ -6,102 +5,101 @@ import { Readable } from 'node:stream';
import { ReadableStream } from 'node:stream/web'; import { ReadableStream } from 'node:stream/web';
import { finished } from 'node:stream/promises'; import { finished } from 'node:stream/promises';
import { execa } from 'execa'; import { execa } from 'execa';
import pino from 'pino';
export default class GeoIPDownloader { export default class GeoIPDownloader {
private directory: string; private directory: string;
private accountID: string; private accountID: string;
private licenseKey: string; private licenseKey: string;
private logger: Logger private logger = pino({ name: 'CVMTS.GeoIPDownloader' });
constructor(filename: string, accountID: string, licenseKey: string) { constructor(filename: string, accountID: string, licenseKey: string) {
this.directory = filename; this.directory = filename;
if (!this.directory.endsWith('/')) this.directory += '/'; if (!this.directory.endsWith('/')) this.directory += '/';
this.accountID = accountID; this.accountID = accountID;
this.licenseKey = licenseKey; this.licenseKey = licenseKey;
this.logger = new Logger('CVMTS.GeoIPDownloader'); }
}
private genAuthHeader(): string { private genAuthHeader(): string {
return `Basic ${Buffer.from(`${this.accountID}:${this.licenseKey}`).toString('base64')}`; return `Basic ${Buffer.from(`${this.accountID}:${this.licenseKey}`).toString('base64')}`;
} }
private async ensureDirectoryExists(): Promise<void> { private async ensureDirectoryExists(): Promise<void> {
let stat; let stat;
try { try {
stat = await fs.stat(this.directory); stat = await fs.stat(this.directory);
} } catch (e) {
catch (e) { var error = e as NodeJS.ErrnoException;
var error = e as NodeJS.ErrnoException; if (error.code === 'ENOTDIR') {
if (error.code === 'ENOTDIR') { this.logger.warn('File exists at GeoIP directory path, unlinking...');
this.logger.Warning('File exists at GeoIP directory path, unlinking...'); await fs.unlink(this.directory.substring(0, this.directory.length - 1));
await fs.unlink(this.directory.substring(0, this.directory.length - 1)); } else if (error.code !== 'ENOENT') {
} else if (error.code !== 'ENOENT') { this.logger.error('Failed to access GeoIP directory: {0}', error.message);
this.logger.Error('Failed to access GeoIP directory: {0}', error.message); process.exit(1);
process.exit(1); }
} this.logger.info('Creating GeoIP directory: {0}', this.directory);
this.logger.Info('Creating GeoIP directory: {0}', this.directory); await fs.mkdir(this.directory, { recursive: true });
await fs.mkdir(this.directory, { recursive: true }); return;
return; }
} }
}
async getGeoIPReader(): Promise<ReaderModel> { async getGeoIPReader(): Promise<ReaderModel> {
await this.ensureDirectoryExists(); await this.ensureDirectoryExists();
let dbpath = path.join(this.directory, (await this.getLatestVersion()).replace('.tar.gz', ''), 'GeoLite2-Country.mmdb'); let dbpath = path.join(this.directory, (await this.getLatestVersion()).replace('.tar.gz', ''), 'GeoLite2-Country.mmdb');
try { try {
await fs.access(dbpath, fs.constants.F_OK | fs.constants.R_OK); await fs.access(dbpath, fs.constants.F_OK | fs.constants.R_OK);
this.logger.Info('Loading cached GeoIP database: {0}', dbpath); this.logger.info('Loading cached GeoIP database: {0}', dbpath);
} catch (ex) { } catch (ex) {
var error = ex as NodeJS.ErrnoException; var error = ex as NodeJS.ErrnoException;
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
await this.downloadLatestDatabase(); await this.downloadLatestDatabase();
} else { } else {
this.logger.Error('Failed to access GeoIP database: {0}', error.message); this.logger.error('Failed to access GeoIP database: {0}', error.message);
process.exit(1); process.exit(1);
} }
} }
return await Reader.open(dbpath); return await Reader.open(dbpath);
} }
async getLatestVersion(): Promise<string> { async getLatestVersion(): Promise<string> {
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', { let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
redirect: 'follow', redirect: 'follow',
method: "HEAD", method: 'HEAD',
headers: { headers: {
"Authorization": this.genAuthHeader() Authorization: this.genAuthHeader()
} }
}); });
let disposition = res.headers.get('Content-Disposition'); let disposition = res.headers.get('Content-Disposition');
if (!disposition) { if (!disposition) {
this.logger.Error('Failed to get latest version of GeoIP database: No Content-Disposition header'); this.logger.error('Failed to get latest version of GeoIP database: No Content-Disposition header');
process.exit(1); process.exit(1);
} }
let filename = disposition.match(/filename=(.*)$/); let filename = disposition.match(/filename=(.*)$/);
if (!filename) { if (!filename) {
this.logger.Error('Failed to get latest version of GeoIP database: Could not parse version from Content-Disposition header'); this.logger.error('Failed to get latest version of GeoIP database: Could not parse version from Content-Disposition header');
process.exit(1); process.exit(1);
} }
return filename[1]; return filename[1];
} }
async downloadLatestDatabase(): Promise<void> { async downloadLatestDatabase(): Promise<void> {
let filename = await this.getLatestVersion(); let filename = await this.getLatestVersion();
this.logger.Info('Downloading latest GeoIP database: {0}', filename); this.logger.info('Downloading latest GeoIP database: {0}', filename);
let dbpath = path.join(this.directory, filename); let dbpath = path.join(this.directory, filename);
let file = await fs.open(dbpath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY); let file = await fs.open(dbpath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
let stream = file.createWriteStream(); let stream = file.createWriteStream();
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', { let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
redirect: 'follow', redirect: 'follow',
headers: { headers: {
"Authorization": this.genAuthHeader() Authorization: this.genAuthHeader()
} }
}); });
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream)); await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
await file.close(); await file.close();
this.logger.Info('Finished downloading latest GeoIP database: {0}', filename); this.logger.info('Finished downloading latest GeoIP database: {0}', filename);
this.logger.Info('Extracting GeoIP database: {0}', filename); this.logger.info('Extracting GeoIP database: {0}', filename);
// yeah whatever // yeah whatever
await execa('tar', ['xzf', filename], {cwd: this.directory}); await execa('tar', ['xzf', filename], { cwd: this.directory });
this.logger.Info('Unlinking GeoIP tarball'); this.logger.info('Unlinking GeoIP tarball');
await fs.unlink(dbpath); await fs.unlink(dbpath);
} }
} }

View File

@@ -1,4 +1,4 @@
import { Logger } from '@cvmts/shared'; import pino from 'pino';
export class IPData { export class IPData {
tempMuteExpireTimeout?: NodeJS.Timeout; tempMuteExpireTimeout?: NodeJS.Timeout;
@@ -22,7 +22,7 @@ export class IPData {
export class IPDataManager { export class IPDataManager {
static ipDatas = new Map<string, IPData>(); static ipDatas = new Map<string, IPData>();
static logger = new Logger('CVMTS.IPDataManager'); static logger = pino({ name: 'CVMTS.IPDataManager' });
static GetIPData(address: string) { static GetIPData(address: string) {
if (IPDataManager.ipDatas.has(address)) { if (IPDataManager.ipDatas.has(address)) {
@@ -64,7 +64,7 @@ export class IPDataManager {
setInterval(() => { setInterval(() => {
for (let tuple of IPDataManager.ipDatas) { for (let tuple of IPDataManager.ipDatas) {
if (tuple[1].refCount == 0) { if (tuple[1].refCount == 0) {
IPDataManager.logger.Info('Deleted IPData for IP {0}', tuple[0]); IPDataManager.logger.info(`Deleted IPData for IP ${tuple[0]}`);
IPDataManager.ipDatas.delete(tuple[0]); IPDataManager.ipDatas.delete(tuple[0]);
} }
} }

View File

@@ -1,4 +1,4 @@
import { Size, Rect } from '@cvmts/shared'; import { Size, Rect } from './VMDisplay.js';
import sharp from 'sharp'; import sharp from 'sharp';
import * as cvm from '@cvmts/cvm-rs'; import * as cvm from '@cvmts/cvm-rs';

View File

@@ -2,20 +2,19 @@ import EventEmitter from 'events';
import NetworkServer from '../NetworkServer.js'; import NetworkServer from '../NetworkServer.js';
import { Server, Socket } from 'net'; import { Server, Socket } from 'net';
import IConfig from '../IConfig.js'; import IConfig from '../IConfig.js';
import { Logger } from '@cvmts/shared';
import TCPClient from './TCPClient.js'; import TCPClient from './TCPClient.js';
import { IPDataManager } from '../IPData.js'; import { IPDataManager } from '../IPData.js';
import { User } from '../User.js'; import { User } from '../User.js';
import pino from 'pino';
export default class TCPServer extends EventEmitter implements NetworkServer { export default class TCPServer extends EventEmitter implements NetworkServer {
listener: Server; listener: Server;
Config: IConfig; Config: IConfig;
logger: Logger; logger = pino({name: 'CVMTS.TCPServer'});
clients: TCPClient[]; clients: TCPClient[];
constructor(config: IConfig) { constructor(config: IConfig) {
super(); super();
this.logger = new Logger('CVMTS.TCPServer');
this.Config = config; this.Config = config;
this.listener = new Server(); this.listener = new Server();
this.clients = []; this.clients = [];
@@ -23,7 +22,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
} }
private onConnection(socket: Socket) { private onConnection(socket: Socket) {
this.logger.Info(`New TCP connection from ${socket.remoteAddress}`); this.logger.info(`New TCP connection from ${socket.remoteAddress}`);
var client = new TCPClient(socket); var client = new TCPClient(socket);
this.clients.push(client); this.clients.push(client);
this.emit('connect', new User(client, IPDataManager.GetIPData(client.getIP()), this.Config)); this.emit('connect', new User(client, IPDataManager.GetIPData(client.getIP()), this.Config));
@@ -31,7 +30,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
start(): void { start(): void {
this.listener.listen(this.Config.tcp.port, this.Config.tcp.host, () => { this.listener.listen(this.Config.tcp.port, this.Config.tcp.host, () => {
this.logger.Info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`); this.logger.info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`);
}); });
} }
stop(): void { stop(): void {

View File

@@ -4,9 +4,9 @@ import { IPData } from './IPData.js';
import IConfig from './IConfig.js'; import IConfig from './IConfig.js';
import RateLimiter from './RateLimiter.js'; import RateLimiter from './RateLimiter.js';
import { execa, execaCommand, ExecaSyncError } from 'execa'; import { execa, execaCommand, ExecaSyncError } from 'execa';
import { Logger } from '@cvmts/shared';
import NetworkClient from './NetworkClient.js'; import NetworkClient from './NetworkClient.js';
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol'; import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
import pino from 'pino';
export class User { export class User {
socket: NetworkClient; socket: NetworkClient;
@@ -31,7 +31,7 @@ export class User {
TurnRateLimit: RateLimiter; TurnRateLimit: RateLimiter;
VoteRateLimit: RateLimiter; VoteRateLimit: RateLimiter;
private logger = new Logger('CVMTS.User'); private logger = pino({ name: 'CVMTS.User' });
constructor(socket: NetworkClient, ip: IPData, config: IConfig, username?: string, node?: string) { constructor(socket: NetworkClient, ip: IPData, config: IConfig, username?: string, node?: string) {
this.IP = ip; this.IP = ip;
@@ -148,7 +148,7 @@ export class User {
await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr }); await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr });
this.kick(); this.kick();
} else { } else {
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`); this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`);
} }
} else if (typeof this.Config.collabvm.bancmd == 'string') { } else if (typeof this.Config.collabvm.bancmd == 'string') {
let cmd: string = this.banCmdArgs(this.Config.collabvm.bancmd); let cmd: string = this.banCmdArgs(this.Config.collabvm.bancmd);
@@ -156,11 +156,11 @@ export class User {
await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr }); await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr });
this.kick(); this.kick();
} else { } else {
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`); this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): Empty command`);
} }
} }
} catch (e) { } catch (e) {
this.logger.Error(`Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`); this.logger.error(`Failed to ban ${this.IP.address} (${this.username}): ${(e as ExecaSyncError).shortMessage}`);
} }
} }

View File

@@ -1,5 +1,5 @@
import { VMState } from '@cvmts/qemu'; import { VMState } from '@computernewb/superqemu';
import VMDisplay from './VMDisplay.js'; import { VMDisplay } from './VMDisplay.js';
export default interface VM { export default interface VM {
Start(): Promise<void>; Start(): Promise<void>;

View File

@@ -1,7 +1,20 @@
import { Size } from '@cvmts/shared';
import EventEmitter from 'node:events'; import EventEmitter from 'node:events';
export default interface VMDisplay extends EventEmitter { // not great but whatever
// nodejs-rfb COULD probably export them though.
export type Size = {
width: number;
height: number;
};
export type Rect = {
x: number;
y: number;
width: number;
height: number;
};
export interface VMDisplay extends EventEmitter {
Connect(): void; Connect(): void;
Disconnect(): void; Disconnect(): void;
Connected(): boolean; Connected(): boolean;

View File

@@ -1,15 +1,23 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import VNCVMDef from './VNCVMDef'; import VNCVMDef from './VNCVMDef';
import VM from '../VM'; import VM from '../VM';
import VMDisplay from '../VMDisplay'; import { Size, Rect, VMDisplay } from '../VMDisplay';
import { Clamp, Logger, Rect, Size, Sleep } from '@cvmts/shared';
import { VncClient } from '@computernewb/nodejs-rfb'; import { VncClient } from '@computernewb/nodejs-rfb';
import { BatchRects, VMState } from '@cvmts/qemu'; import { BatchRects, VMState } from '@computernewb/superqemu';
import { execaCommand } from 'execa'; import { execaCommand } from 'execa';
import pino from 'pino';
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, VMDisplay { export default class VNCVM extends EventEmitter implements VM, VMDisplay {
def: VNCVMDef; def: VNCVMDef;
logger: Logger; logger;
private displayVnc = new VncClient({ private displayVnc = new VncClient({
debug: false, debug: false,
fps: 60, fps: 60,
@@ -20,7 +28,8 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
constructor(def: VNCVMDef) { constructor(def: VNCVMDef) {
super(); super();
this.def = def; this.def = def;
this.logger = new Logger(`CVMTS.VNCVM/${this.def.vncHost}:${this.def.vncPort}`); // 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}` });
this.displayVnc.on('connectTimeout', () => { this.displayVnc.on('connectTimeout', () => {
this.Reconnect(); this.Reconnect();
@@ -31,7 +40,7 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
}); });
this.displayVnc.on('disconnect', () => { this.displayVnc.on('disconnect', () => {
this.logger.Info('Disconnected'); this.logger.info('Disconnected');
this.Reconnect(); this.Reconnect();
}); });
@@ -40,7 +49,7 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
}); });
this.displayVnc.on('firstFrameUpdate', () => { this.displayVnc.on('firstFrameUpdate', () => {
this.logger.Info('Connected'); this.logger.info('Connected');
// apparently this library is this good. // apparently this library is this good.
// at least it's better than the two others which exist. // at least it's better than the two others which exist.
this.displayVnc.changeFps(60); this.displayVnc.changeFps(60);
@@ -101,13 +110,13 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
} }
async Start(): Promise<void> { async Start(): Promise<void> {
this.logger.Info('Connecting'); this.logger.info('Connecting');
if (this.def.startCmd) await execaCommand(this.def.startCmd, { shell: true }); if (this.def.startCmd) await execaCommand(this.def.startCmd, { shell: true });
this.Connect(); this.Connect();
} }
async Stop(): Promise<void> { async Stop(): Promise<void> {
this.logger.Info('Disconnecting'); this.logger.info('Disconnecting');
this.Disconnect(); this.Disconnect();
if (this.def.stopCmd) await execaCommand(this.def.stopCmd, { shell: true }); if (this.def.stopCmd) await execaCommand(this.def.stopCmd, { shell: true });
} }

View File

@@ -1,7 +1,6 @@
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import NetworkClient from '../NetworkClient.js'; import NetworkClient from '../NetworkClient.js';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { Logger } from '@cvmts/shared';
export default class WSClient extends EventEmitter implements NetworkClient { export default class WSClient extends EventEmitter implements NetworkClient {
socket: WebSocket; socket: WebSocket;

View File

@@ -8,20 +8,19 @@ import { isIP } from 'net';
import { IPDataManager } from '../IPData.js'; import { IPDataManager } from '../IPData.js';
import WSClient from './WSClient.js'; import WSClient from './WSClient.js';
import { User } from '../User.js'; import { User } from '../User.js';
import { Logger } from '@cvmts/shared'; import pino from 'pino';
export default class WSServer extends EventEmitter implements NetworkServer { export default class WSServer extends EventEmitter implements NetworkServer {
private httpServer: http.Server; private httpServer: http.Server;
private wsServer: WebSocketServer; private wsServer: WebSocketServer;
private clients: WSClient[]; private clients: WSClient[];
private Config: IConfig; private Config: IConfig;
private logger: Logger; private logger = pino({ name: 'CVMTS.WSServer' });
constructor(config: IConfig) { constructor(config: IConfig) {
super(); super();
this.Config = config; this.Config = config;
this.clients = []; this.clients = [];
this.logger = new Logger('CVMTS.WSServer');
this.httpServer = http.createServer(); this.httpServer = http.createServer();
this.wsServer = new WebSocketServer({ noServer: true }); this.wsServer = new WebSocketServer({ noServer: true });
this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head)); this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head));
@@ -34,7 +33,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
start(): void { start(): void {
this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => { this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => {
this.logger.Info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`); this.logger.info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`);
}); });
} }
@@ -94,7 +93,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
// Make sure x-forwarded-for is set // Make sure x-forwarded-for is set
if (req.headers['x-forwarded-for'] === undefined) { if (req.headers['x-forwarded-for'] === undefined) {
killConnection(); killConnection();
this.logger.Error('X-Forwarded-For header not set. This is most likely a misconfiguration of your reverse proxy.'); this.logger.error('X-Forwarded-For header not set. This is most likely a misconfiguration of your reverse proxy.');
return; return;
} }
try { try {
@@ -102,7 +101,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
ip = req.headers['x-forwarded-for']?.toString().replace(/\ /g, '').split(',')[0]; ip = req.headers['x-forwarded-for']?.toString().replace(/\ /g, '').split(',')[0];
} catch { } catch {
// If we can't get the IP, kill the connection // If we can't get the IP, kill the connection
this.logger.Error('Invalid X-Forwarded-For header. This is most likely a misconfiguration of your reverse proxy.'); this.logger.error('Invalid X-Forwarded-For header. This is most likely a misconfiguration of your reverse proxy.');
killConnection(); killConnection();
return; return;
} }
@@ -135,10 +134,10 @@ export default class WSServer extends EventEmitter implements NetworkServer {
this.emit('connect', user); this.emit('connect', user);
ws.on('error', (e) => { ws.on('error', (e) => {
this.logger.Error(`${e} (caused by connection ${ip})`); this.logger.error(`${e} (caused by connection ${ip})`);
ws.close(); ws.close();
}); });
this.logger.Info(`New WebSocket connection from ${user.IP.address}`); this.logger.info(`New WebSocket connection from ${user.IP.address}`);
} }
} }

View File

@@ -3,9 +3,8 @@ import IConfig from './IConfig.js';
import * as fs from 'fs'; import * as fs from 'fs';
import CollabVMServer from './CollabVMServer.js'; import CollabVMServer from './CollabVMServer.js';
import { QemuVM, QemuVmDefinition } from '@cvmts/qemu'; import { QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
import * as Shared from '@cvmts/shared';
import AuthManager from './AuthManager.js'; import AuthManager from './AuthManager.js';
import WSServer from './WebSocket/WSServer.js'; import WSServer from './WebSocket/WSServer.js';
import { User } from './User.js'; import { User } from './User.js';
@@ -13,24 +12,25 @@ import TCPServer from './TCP/TCPServer.js';
import VM from './VM.js'; import VM from './VM.js';
import VNCVM from './VNCVM/VNCVM.js'; import VNCVM from './VNCVM/VNCVM.js';
import GeoIPDownloader from './GeoIPDownloader.js'; import GeoIPDownloader from './GeoIPDownloader.js';
import pino from 'pino';
let logger = new Shared.Logger('CVMTS.Init'); let logger = pino();
logger.Info('CollabVM Server starting up'); logger.info('CollabVM Server starting up');
// Parse the config file // Parse the config file
let Config: IConfig; let Config: IConfig;
if (!fs.existsSync('config.toml')) { if (!fs.existsSync('config.toml')) {
logger.Error('Fatal error: Config.toml not found. Please copy config.example.toml and fill out fields'); logger.error('Fatal error: Config.toml not found. Please copy config.example.toml and fill out fields');
process.exit(1); process.exit(1);
} }
try { try {
var configRaw = fs.readFileSync('config.toml').toString(); var configRaw = fs.readFileSync('config.toml').toString();
Config = toml.parse(configRaw); Config = toml.parse(configRaw);
} catch (e) { } catch (e) {
logger.Error('Fatal error: Failed to read or parse the config file: {0}', (e as Error).message); logger.error('Fatal error: Failed to read or parse the config file: {0}', (e as Error).message);
process.exit(1); process.exit(1);
} }
@@ -54,15 +54,6 @@ async function start() {
let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null; let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null;
switch (Config.vm.type) { switch (Config.vm.type) {
case 'qemu': { case 'qemu': {
// Print a warning if qmpSockDir is set
// and the host OS is Windows, as this
// configuration will very likely not work.
if (process.platform === 'win32' && Config.qemu.qmpSockDir !== null) {
logger.Warning("You appear to have the option 'qmpSockDir' enabled in the config.");
logger.Warning('This is not supported on Windows, and you will likely run into issues.');
logger.Warning('To remove this warning, use the qmpHost and qmpPort options instead.');
}
// Fire up the VM // Fire up the VM
let def: QemuVmDefinition = { let def: QemuVmDefinition = {
id: Config.collabvm.node, id: Config.collabvm.node,
@@ -78,7 +69,7 @@ async function start() {
break; break;
} }
default: { default: {
logger.Error('Invalid VM type in config: {0}', Config.vm.type); logger.error(`Invalid VM type in config: ${Config.vm.type}`);
process.exit(1); process.exit(1);
return; return;
} }

Submodule nodejs-rfb deleted from 55e9e6cd65

View File

@@ -3,8 +3,6 @@
"workspaces": [ "workspaces": [
"shared", "shared",
"cvm-rs", "cvm-rs",
"nodejs-rfb",
"qemu",
"cvmts", "cvmts",
"collab-vm-1.2-binary-protocol" "collab-vm-1.2-binary-protocol"
], ],

View File

@@ -1,29 +0,0 @@
{
"name": "@cvmts/qemu",
"version": "1.0.0",
"description": "A simple and easy to use QEMU supervision runtime",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"scripts": {
"build": "parcel build src/index.ts --target node --target types"
},
"author": "",
"license": "MIT",
"targets": {
"types": {},
"node": {
"context": "node",
"isLibrary": true,
"outputFormat": "esmodule"
}
},
"dependencies": {
"@computernewb/nodejs-rfb": "*",
"@cvmts/shared": "*",
"execa": "^8.0.1"
},
"devDependencies": {
"parcel": "^2.12.0"
}
}

View File

@@ -1,151 +0,0 @@
import { VncClient } from '@computernewb/nodejs-rfb';
import { EventEmitter } from 'node:events';
import { BatchRects } from './QemuUtil.js';
import { Size, Rect, Clamp } from '@cvmts/shared';
const kQemuFps = 60;
export type VncRect = {
x: number;
y: number;
width: number;
height: number;
};
// events:
//
// 'resize' -> (w, h) -> done when resize occurs
// 'rect' -> (x, y, ImageData) -> framebuffer
// 'frame' -> () -> done at end of frame
export class QemuDisplay extends EventEmitter {
private displayVnc = new VncClient({
debug: false,
fps: kQemuFps,
encodings: [
VncClient.consts.encodings.raw,
//VncClient.consts.encodings.pseudoQemuAudio,
VncClient.consts.encodings.pseudoDesktopSize
// For now?
//VncClient.consts.encodings.pseudoCursor
]
});
private vncShouldReconnect: boolean = false;
private vncSocketPath: string;
constructor(socketPath: string) {
super();
this.vncSocketPath = socketPath;
this.displayVnc.on('connectTimeout', () => {
this.Reconnect();
});
this.displayVnc.on('authError', () => {
this.Reconnect();
});
this.displayVnc.on('disconnect', () => {
this.Reconnect();
});
this.displayVnc.on('closed', () => {
this.Reconnect();
});
this.displayVnc.on('firstFrameUpdate', () => {
// apparently this library is this good.
// at least it's better than the two others which exist.
this.displayVnc.changeFps(kQemuFps);
this.emit('connected');
this.emit('resize', { width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight });
//this.emit('rect', { x: 0, y: 0, width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight });
this.emit('frame');
});
this.displayVnc.on('desktopSizeChanged', (size: Size) => {
this.emit('resize', size);
});
let rects: Rect[] = [];
this.displayVnc.on('rectUpdateProcessed', (rect: Rect) => {
rects.push(rect);
});
this.displayVnc.on('frameUpdated', (fb: Buffer) => {
// use the cvmts batcher
let batched = BatchRects(this.Size(), rects);
this.emit('rect', batched);
// unbatched (watch the performace go now)
//for(let rect of rects)
// this.emit('rect', rect);
rects = [];
this.emit('frame');
});
}
private Reconnect() {
if (this.displayVnc.connected) return;
if (!this.vncShouldReconnect) return;
// TODO: this should also give up after a max tries count
// if we fail after max tries, emit a event
this.displayVnc.connect({
path: this.vncSocketPath
});
}
Connect() {
this.vncShouldReconnect = true;
this.Reconnect();
}
Disconnect() {
this.vncShouldReconnect = false;
this.displayVnc.disconnect();
// bye bye!
this.displayVnc.removeAllListeners();
this.removeAllListeners();
}
Connected() {
return this.displayVnc.connected;
}
Buffer(): Buffer {
return this.displayVnc.fb;
}
Size(): Size {
if (!this.displayVnc.connected)
return {
width: 0,
height: 0
};
return {
width: this.displayVnc.clientWidth,
height: this.displayVnc.clientHeight
};
}
MouseEvent(x: number, y: number, buttons: number) {
if (this.displayVnc.connected) this.displayVnc.sendPointerEvent(Clamp(x, 0, this.displayVnc.clientWidth), Clamp(y, 0, this.displayVnc.clientHeight), buttons);
}
KeyboardEvent(keysym: number, pressed: boolean) {
if (this.displayVnc.connected) this.displayVnc.sendKeyEvent(keysym, pressed);
}
}

View File

@@ -1,41 +0,0 @@
import { Size, Rect } from '@cvmts/shared';
export function BatchRects(size: Size, rects: Array<Rect>): Rect {
var mergedX = size.width;
var mergedY = size.height;
var mergedHeight = 0;
var mergedWidth = 0;
// can't batch these
if (rects.length == 0) {
return {
x: 0,
y: 0,
width: size.width,
height: size.height
};
}
if (rects.length == 1) {
if (rects[0].width == size.width && rects[0].height == size.height) {
return rects[0];
}
}
rects.forEach((r) => {
if (r.x < mergedX) mergedX = r.x;
if (r.y < mergedY) mergedY = r.y;
});
rects.forEach((r) => {
if (r.height + r.y - mergedY > mergedHeight) mergedHeight = r.height + r.y - mergedY;
if (r.width + r.x - mergedX > mergedWidth) mergedWidth = r.width + r.x - mergedX;
});
return {
x: mergedX,
y: mergedY,
width: mergedWidth,
height: mergedHeight
};
}

View File

@@ -1,271 +0,0 @@
import { execaCommand, ExecaChildProcess } from 'execa';
import { EventEmitter } from 'events';
import { QmpClient, IQmpClientWriter, QmpEvent } from './QmpClient.js';
import { QemuDisplay } from './QemuDisplay.js';
import { unlink } from 'node:fs/promises';
import * as Shared from '@cvmts/shared';
import { Readable, Writable } from 'stream';
export enum VMState {
Stopped,
Starting,
Started,
Stopping
}
export type QemuVmDefinition = {
id: string;
command: string;
snapshot: boolean;
};
/// Temporary path base (for UNIX sockets/etc.)
const kVmTmpPathBase = `/tmp`;
// writer implementation for process standard I/O
class StdioWriter implements IQmpClientWriter {
stdout;
stdin;
client;
constructor(stdout: Readable, stdin: Writable, client: QmpClient) {
this.stdout = stdout;
this.stdin = stdin;
this.client = client;
this.stdout.on('data', (data) => {
this.client.feed(data);
});
}
writeSome(buffer: Buffer) {
if(!this.stdin.closed)
this.stdin.write(buffer);
}
}
export class QemuVM extends EventEmitter {
private state = VMState.Stopped;
// QMP stuff.
private qmpInstance: QmpClient = new QmpClient();
private qemuProcess: ExecaChildProcess | null = null;
private display: QemuDisplay | null = null;
private definition: QemuVmDefinition;
private addedAdditionalArguments = false;
private logger: Shared.Logger;
constructor(def: QemuVmDefinition) {
super();
this.definition = def;
this.logger = new Shared.Logger(`CVMTS.QEMU.QemuVM/${this.definition.id}`);
let self = this;
// Handle the STOP event sent when using -no-shutdown
this.qmpInstance.on(QmpEvent.Stop, async () => {
await self.qmpInstance.execute('system_reset');
});
this.qmpInstance.on(QmpEvent.Reset, async () => {
await self.qmpInstance.execute('cont');
});
this.qmpInstance.on('connected', async () => {
self.VMLog().Info('QMP ready');
this.display = new QemuDisplay(this.GetVncPath());
self.display?.on('connected', () => {
// The VM can now be considered started
self.VMLog().Info('Display connected');
self.SetState(VMState.Started);
});
// now that QMP has connected, connect to the display
self.display?.Connect();
});
}
async Start() {
// Don't start while either trying to start or starting.
//if (this.state == VMState.Started || this.state == VMState.Starting) return;
if (this.qemuProcess) return;
let cmd = this.definition.command;
// Build additional command line statements to enable qmp/vnc over unix sockets
if (!this.addedAdditionalArguments) {
cmd += ' -no-shutdown';
if (this.definition.snapshot) cmd += ' -snapshot';
cmd += ` -qmp stdio -vnc unix:${this.GetVncPath()}`;
this.definition.command = cmd;
this.addedAdditionalArguments = true;
}
await this.StartQemu(cmd);
}
SnapshotsSupported(): boolean {
return this.definition.snapshot;
}
async Reboot(): Promise<void> {
await this.MonitorCommand('system_reset');
}
async Stop() {
this.AssertState(VMState.Started, 'cannot use QemuVM#Stop on a non-started VM');
// Indicate we're stopping, so we don't erroneously start trying to restart everything we're going to tear down.
this.SetState(VMState.Stopping);
// Stop the QEMU process, which will bring down everything else.
await this.StopQemu();
}
async Reset() {
this.AssertState(VMState.Started, 'cannot use QemuVM#Reset on a non-started VM');
await this.StopQemu();
}
async QmpCommand(command: string, args: any | null): Promise<any> {
return await this.qmpInstance?.execute(command, args);
}
async MonitorCommand(command: string) {
this.AssertState(VMState.Started, 'cannot use QemuVM#MonitorCommand on a non-started VM');
let result = await this.QmpCommand('human-monitor-command', {
'command-line': command
});
if (result == null) result = '';
return result;
}
async ChangeRemovableMedia(deviceName: string, imagePath: string): Promise<void> {
this.AssertState(VMState.Started, 'cannot use QemuVM#ChangeRemovableMedia on a non-started VM');
// N.B: if this throws, the code which called this should handle the error accordingly
await this.QmpCommand('blockdev-change-medium', {
device: deviceName, // techinically deprecated, but I don't feel like figuring out QOM path just for a simple function
filename: imagePath
});
}
async EjectRemovableMedia(deviceName: string) {
this.AssertState(VMState.Started, 'cannot use QemuVM#EjectRemovableMedia on a non-started VM');
await this.QmpCommand('eject', {
device: deviceName
});
}
GetDisplay() {
return this.display!;
}
GetState() {
return this.state;
}
/// Private fun bits :)
private VMLog() {
return this.logger;
}
private AssertState(stateShouldBe: VMState, message: string) {
if (this.state !== stateShouldBe) throw new Error(message);
}
private SetState(state: VMState) {
this.state = state;
this.emit('statechange', this.state);
}
private GetVncPath() {
return `${kVmTmpPathBase}/cvmts-${this.definition.id}-vnc`;
}
private async StartQemu(split: string) {
let self = this;
this.SetState(VMState.Starting);
this.VMLog().Info(`Starting QEMU with command \"${split}\"`);
// Start QEMU
this.qemuProcess = execaCommand(split, {
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe'
});
this.qemuProcess.stderr?.on('data', (data) => {
self.VMLog().Error('QEMU stderr: {0}', data.toString('utf8'));
});
this.qemuProcess.on('spawn', async () => {
self.VMLog().Info('QEMU started');
await self.QmpStdioInit();
});
this.qemuProcess.on('exit', async (code) => {
self.VMLog().Info('QEMU process exited');
// Disconnect from the display and QMP connections.
await self.DisconnectDisplay();
self.qmpInstance.reset();
self.qmpInstance.setWriter(null);
// Remove the VNC UDS socket.
try {
await unlink(this.GetVncPath());
} catch (_) {}
if (self.state != VMState.Stopping) {
if (code == 0) {
await self.StartQemu(split);
} else {
self.VMLog().Error('QEMU exited with a non-zero exit code. This usually means an error in the command line. Stopping VM.');
// Note that we've already tore down everything upon entry to this event handler; therefore
// we can simply set the state and move on.
this.SetState(VMState.Stopped);
}
} else {
// Indicate we have stopped.
this.SetState(VMState.Stopped);
}
});
}
private async StopQemu() {
if (this.qemuProcess) {
this.qemuProcess?.kill('SIGTERM');
this.qemuProcess = null;
}
}
private async QmpStdioInit() {
let self = this;
self.VMLog().Info('Initializing QMP over stdio');
// Setup the QMP client.
let writer = new StdioWriter(this.qemuProcess?.stdout!, this.qemuProcess?.stdin!, self.qmpInstance);
self.qmpInstance.reset();
self.qmpInstance.setWriter(writer);
}
private async DisconnectDisplay() {
try {
this.display?.Disconnect();
this.display = null;
} catch (err) {
// oh well lol
}
}
}

View File

@@ -1,170 +0,0 @@
import { EventEmitter } from 'node:events';
enum QmpClientState {
Handshaking,
Connected
}
function qmpStringify(obj: any) {
return JSON.stringify(obj) + '\r\n';
}
// this writer interface is used to poll back to a higher level
// I/O layer that we want to write some data.
export interface IQmpClientWriter {
writeSome(data: Buffer): void;
}
export type QmpClientCallback = (err: Error | null, res: any | null) => void;
type QmpClientCallbackEntry = {
id: number;
callback: QmpClientCallback | null;
};
export enum QmpEvent {
BlockIOError = 'BLOCK_IO_ERROR',
Reset = 'RESET',
Resume = 'RESUME',
RtcChange = 'RTC_CHANGE',
Shutdown = 'SHUTDOWN',
Stop = 'STOP',
VncConnected = 'VNC_CONNECTED',
VncDisconnected = 'VNC_DISCONNECTED',
VncInitialized = 'VNC_INITIALIZED',
Watchdog = 'WATCHDOG'
}
class LineStream extends EventEmitter {
// The given line seperator for the stream
lineSeperator = '\r\n';
buffer = '';
constructor() {
super();
}
push(data: Buffer) {
this.buffer += data.toString('utf-8');
let lines = this.buffer.split(this.lineSeperator);
if (lines.length > 1) {
this.buffer = lines.pop()!;
lines = lines.filter((l) => !!l);
lines.forEach(l => this.emit('line', l));
}
}
reset() {
this.buffer = '';
}
}
// A QMP client
export class QmpClient extends EventEmitter {
private state = QmpClientState.Handshaking;
private writer: IQmpClientWriter | null = null;
private lastID = 0;
private callbacks = new Array<QmpClientCallbackEntry>();
private lineStream = new LineStream();
constructor() {
super();
let self = this;
this.lineStream.on('line', (line: string) => {
self.handleQmpLine(line);
});
}
setWriter(writer: IQmpClientWriter|null) {
this.writer = writer;
}
feed(data: Buffer): void {
// Forward to the line stream. It will generate 'line' events
// as it is able to split out lines automatically.
this.lineStream.push(data);
}
private handleQmpLine(line: string) {
let obj = JSON.parse(line);
switch (this.state) {
case QmpClientState.Handshaking:
if (obj['return'] != undefined) {
// Once we get a return from our handshake execution,
// we have exited handshake state.
this.state = QmpClientState.Connected;
this.emit('connected');
return;
} else if(obj['QMP'] != undefined) {
// Send a `qmp_capabilities` command, to exit handshake state.
// We do not support any of the supported extended QMP capabilities currently,
// and probably never will (due to their relative uselessness.)
let capabilities = qmpStringify({
execute: 'qmp_capabilities'
});
this.writer?.writeSome(Buffer.from(capabilities, 'utf8'));
}
break;
case QmpClientState.Connected:
if (obj['return'] != undefined || obj['error'] != undefined) {
if (obj['id'] == null) return;
let cb = this.callbacks.find((v) => v.id == obj['id']);
if (cb == undefined) return;
let error: Error | null = obj.error ? new Error(obj.error.desc) : null;
if (cb.callback) cb.callback(error, obj.return || null);
this.callbacks.slice(this.callbacks.indexOf(cb));
} else if (obj['event']) {
this.emit(obj.event, {
timestamp: obj.timestamp,
data: obj.data
});
}
break;
}
}
// Executes a QMP command, using a user-provided callback for completion notification
executeCallback(command: string, args: any | undefined, callback: QmpClientCallback | null) {
let entry = {
callback: callback,
id: ++this.lastID
};
let qmpOut: any = {
execute: command,
id: entry.id
};
if (args !== undefined) qmpOut['arguments'] = args;
this.callbacks.push(entry);
this.writer?.writeSome(Buffer.from(qmpStringify(qmpOut), 'utf8'));
}
// Executes a QMP command asynchronously.
async execute(command: string, args: any | undefined = undefined): Promise<any> {
return new Promise((res, rej) => {
this.executeCallback(command, args, (err, result) => {
if (err) rej(err);
res(result);
});
});
}
reset() {
// Reset the line stream so it doesn't go awry
this.lineStream.reset();
this.state = QmpClientState.Handshaking;
}
}

View File

@@ -1,5 +0,0 @@
/// <reference types="../node_modules/@types/node">
export * from './QemuDisplay.js';
export * from './QemuUtil.js';
export * from './QemuVM.js';

View File

@@ -1 +0,0 @@
../tsconfig.json

View File

@@ -1,28 +0,0 @@
{
"name": "@cvmts/shared",
"version": "1.0.0",
"description": "cvmts shared util bits",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"targets": {
"types": {},
"shared": {
"context": "browser",
"isLibrary": true,
"outputFormat": "esmodule"
}
},
"devDependencies": {
"@protobuf-ts/plugin": "^2.9.4",
"parcel": "^2.12.0"
},
"scripts": {
"build": "parcel build src/index.ts --target shared --target types"
},
"author": "",
"license": "ISC"
}

View File

@@ -1,50 +0,0 @@
import { Format } from "./format";
import { StringLike } from "./StringLike";
export enum LogLevel {
VERBOSE = 0,
INFO,
WARNING,
ERROR
};
let gLogLevel = LogLevel.INFO;
export function SetLogLevel(level: LogLevel) {
gLogLevel = level;
}
export class Logger {
private _component: string;
constructor(component: string) {
this._component = component;
}
// TODO: use js argments stuff.
Verbose(pattern: string, ...args: Array<StringLike>) {
if(gLogLevel <= LogLevel.VERBOSE)
console.log(`[${this._component}] [VERBOSE] ${Format(pattern, ...args)}`);
}
Info(pattern: string, ...args: Array<StringLike>) {
if(gLogLevel <= LogLevel.INFO)
console.log(`[${this._component}] [INFO] ${Format(pattern, ...args)}`);
}
Warning(pattern: string, ...args: Array<StringLike>) {
if(gLogLevel <= LogLevel.WARNING)
console.warn(`[${this._component}] [WARNING] ${Format(pattern, ...args)}`);
}
Error(pattern: string, ...args: Array<StringLike>) {
if(gLogLevel <= LogLevel.ERROR)
console.error(`[${this._component}] [ERROR] ${Format(pattern, ...args)}`);
}
}

View File

@@ -1,8 +0,0 @@
// TODO: `Object` has a toString(), but we should probably gate that off
/// Interface for things that can be turned into strings
export interface ToStringable {
toString(): string;
}
/// A type for strings, or things that can (in a valid manner) be turned into strings
export type StringLike = string | ToStringable;

View File

@@ -1,77 +0,0 @@
import { StringLike } from './StringLike';
function isalpha(char: number) {
return RegExp(/^\p{L}/, 'u').test(String.fromCharCode(char));
}
/// A simple function for formatting strings in a more expressive manner.
/// While JavaScript *does* have string interpolation, it's not a total replacement
/// for just formatting strings, and a method like this is better for data independent formatting.
///
/// ## Example usage
///
/// ```typescript
/// let hello = Format("Hello, {0}!", "World");
/// ```
export function Format(pattern: string, ...args: Array<StringLike>) {
let argumentsAsStrings: Array<string> = [...args].map((el) => {
// This catches cases where the thing already is a string
if (typeof el == 'string') return el as string;
return el.toString();
});
let pat = pattern;
// Handle pattern ("{0} {1} {2} {3} {4} {5}") syntax if found
for (let i = 0; i < pat.length; ++i) {
if (pat[i] == '{') {
let replacementStart = i;
let foundSpecifierEnd = false;
// Make sure the specifier is not cut off (the last character of the string)
if (i + 3 > pat.length) {
throw new Error(`Error in format pattern "${pat}": Cutoff/invalid format specifier`);
}
// Try and find the specifier end ('}').
// Whitespace and a '{' are considered errors.
for (let j = i + 1; j < pat.length; ++j) {
switch (pat[j]) {
case '}':
foundSpecifierEnd = true;
i = j;
break;
case '{':
throw new Error(`Error in format pattern "${pat}": Cannot start a format specifier in an existing replacement`);
case ' ':
throw new Error(`Error in format pattern "${pat}": Whitespace inside format specifier`);
case '-':
throw new Error(`Error in format pattern "${pat}": Malformed format specifier`);
default:
if (isalpha(pat.charCodeAt(j))) throw new Error(`Error in format pattern "${pat}": Malformed format specifier`);
break;
}
if (foundSpecifierEnd) break;
}
if (!foundSpecifierEnd) throw new Error(`Error in format pattern "${pat}": No terminating "}" character found`);
// Get the beginning and trailer
let beginning = pat.substring(0, replacementStart);
let trailer = pat.substring(replacementStart + 3);
let argumentIndex = parseInt(pat.substring(replacementStart + 1, i));
if (Number.isNaN(argumentIndex) || argumentIndex > argumentsAsStrings.length) throw new Error(`Error in format pattern "${pat}": Argument index out of bounds`);
// This is seriously the only decent way to do this in javascript
// thanks brendan eich (replace this thanking with more choice words in your head)
pat = beginning + argumentsAsStrings[argumentIndex] + trailer;
}
}
return pat;
}

View File

@@ -1,24 +0,0 @@
// public modules
export * from './StringLike.js';
export * from './Logger.js';
export * from './format.js';
export function Clamp(input: number, min: number, max: number) {
return Math.min(Math.max(input, min), max);
}
export async function Sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export type Size = {
width: number;
height: number;
};
export type Rect = {
x: number;
y: number;
width: number;
height: number;
};

View File

@@ -1 +0,0 @@
../tsconfig.json

402
yarn.lock
View File

@@ -34,18 +34,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@computernewb/nodejs-rfb@npm:*, @computernewb/nodejs-rfb@workspace:nodejs-rfb": "@computernewb/nodejs-rfb@npm:^0.3.0":
version: 0.0.0-use.local version: 0.3.0
resolution: "@computernewb/nodejs-rfb@workspace:nodejs-rfb" resolution: "@computernewb/nodejs-rfb@npm:0.3.0"
checksum: 10c0/915a76118011a4a64e28fcc91fafcffe659e5e86db2505554578f1e5dff4883dbaf5fcc013ac377719fe9c798e730b178aa5030027da0f5a9583355ec2ac8af2
languageName: node
linkType: hard
"@computernewb/superqemu@npm:^0.1.0":
version: 0.1.0
resolution: "@computernewb/superqemu@npm:0.1.0"
dependencies: dependencies:
"@parcel/packager-ts": "npm:2.12.0" "@computernewb/nodejs-rfb": "npm:^0.3.0"
"@parcel/transformer-typescript-types": "npm:2.12.0" execa: "npm:^8.0.1"
"@types/node": "npm:^20.12.7" pino: "npm:^9.3.1"
parcel: "npm:^2.12.0" checksum: 10c0/7177b46c1093345cc3cbcc09450b8b8b09f09eb74ba5abd283aae39e9d1dbc0780f54187da075e44c78a7b683d47367010a473406c2817c36352edd0ddad2c1a
prettier: "npm:^3.2.5" languageName: node
typescript: "npm:>=3.0.0" linkType: hard
languageName: unknown
linkType: soft
"@cvmts/collab-vm-1.2-binary-protocol@workspace:collab-vm-1.2-binary-protocol": "@cvmts/collab-vm-1.2-binary-protocol@workspace:collab-vm-1.2-binary-protocol":
version: 0.0.0-use.local version: 0.0.0-use.local
@@ -70,14 +75,17 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@cvmts/cvmts@workspace:cvmts" resolution: "@cvmts/cvmts@workspace:cvmts"
dependencies: dependencies:
"@computernewb/nodejs-rfb": "npm:^0.3.0"
"@computernewb/superqemu": "npm:^0.1.0"
"@cvmts/cvm-rs": "npm:*" "@cvmts/cvm-rs": "npm:*"
"@cvmts/qemu": "npm:*"
"@maxmind/geoip2-node": "npm:^5.0.0" "@maxmind/geoip2-node": "npm:^5.0.0"
"@types/node": "npm:^20.12.5" "@types/node": "npm:^20.12.5"
"@types/ws": "npm:^8.5.5" "@types/ws": "npm:^8.5.5"
execa: "npm:^8.0.1" execa: "npm:^8.0.1"
mnemonist: "npm:^0.39.5" mnemonist: "npm:^0.39.5"
msgpackr: "npm:^1.10.2" msgpackr: "npm:^1.10.2"
pino: "npm:^9.3.1"
pino-pretty: "npm:^11.2.1"
prettier: "npm:^3.2.5" prettier: "npm:^3.2.5"
sharp: "npm:^0.33.3" sharp: "npm:^0.33.3"
toml: "npm:^3.0.0" toml: "npm:^3.0.0"
@@ -86,26 +94,6 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@cvmts/qemu@npm:*, @cvmts/qemu@workspace:qemu":
version: 0.0.0-use.local
resolution: "@cvmts/qemu@workspace:qemu"
dependencies:
"@computernewb/nodejs-rfb": "npm:*"
"@cvmts/shared": "npm:*"
execa: "npm:^8.0.1"
parcel: "npm:^2.12.0"
languageName: unknown
linkType: soft
"@cvmts/shared@npm:*, @cvmts/shared@workspace:shared":
version: 0.0.0-use.local
resolution: "@cvmts/shared@workspace:shared"
dependencies:
"@protobuf-ts/plugin": "npm:^2.9.4"
parcel: "npm:^2.12.0"
languageName: unknown
linkType: soft
"@emnapi/runtime@npm:^1.1.0": "@emnapi/runtime@npm:^1.1.0":
version: 1.1.1 version: 1.1.1
resolution: "@emnapi/runtime@npm:1.1.1" resolution: "@emnapi/runtime@npm:1.1.1"
@@ -1335,57 +1323,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@protobuf-ts/plugin-framework@npm:^2.9.4":
version: 2.9.4
resolution: "@protobuf-ts/plugin-framework@npm:2.9.4"
dependencies:
"@protobuf-ts/runtime": "npm:^2.9.4"
typescript: "npm:^3.9"
checksum: 10c0/2923852ab1d2d46090245a858fd362fffccd4f556963b01d153d4c5568dfa33d34101dacca0b1f38f23516e5a4d1f765e14be1d885dc1159d5ec37d25000f065
languageName: node
linkType: hard
"@protobuf-ts/plugin@npm:^2.9.4":
version: 2.9.4
resolution: "@protobuf-ts/plugin@npm:2.9.4"
dependencies:
"@protobuf-ts/plugin-framework": "npm:^2.9.4"
"@protobuf-ts/protoc": "npm:^2.9.4"
"@protobuf-ts/runtime": "npm:^2.9.4"
"@protobuf-ts/runtime-rpc": "npm:^2.9.4"
typescript: "npm:^3.9"
bin:
protoc-gen-dump: bin/protoc-gen-dump
protoc-gen-ts: bin/protoc-gen-ts
checksum: 10c0/dbf1506e656d4d8ca91ace656cf3e238aed93d6539747c72c140fb0be29af61ccafae4e8c9f1e6f8369ac20508263d718ccb411dcf2d15276672c8ad7ba8194c
languageName: node
linkType: hard
"@protobuf-ts/protoc@npm:^2.9.4":
version: 2.9.4
resolution: "@protobuf-ts/protoc@npm:2.9.4"
bin:
protoc: protoc.js
checksum: 10c0/4ce4380cdab5560d13dd3b8d3538e6aee508a10b6b43dbd649d2ffe0a774129d59bd0e270ce7f643a95b9703e19088a5c725f68939913f2187fdeb1d6b42d4b5
languageName: node
linkType: hard
"@protobuf-ts/runtime-rpc@npm:^2.9.4":
version: 2.9.4
resolution: "@protobuf-ts/runtime-rpc@npm:2.9.4"
dependencies:
"@protobuf-ts/runtime": "npm:^2.9.4"
checksum: 10c0/91fa7037b669dc92073d393dbe6bb109307d7b884506f6e5a310c6bde43b3920154b1176c826e9739c81ecd108090516b826e94354d58e454df2eef7f50f3a12
languageName: node
linkType: hard
"@protobuf-ts/runtime@npm:^2.9.4":
version: 2.9.4
resolution: "@protobuf-ts/runtime@npm:2.9.4"
checksum: 10c0/78a10c0e2ee33fe98b3e30d15f8a52fe1a9505de3a8c056339bc01a0a076d4108a4efe93b578dc034c91c1b8c85996643b3f4d45f95c7e2bd5c151455b4fd23f
languageName: node
linkType: hard
"@swc/core-darwin-arm64@npm:1.4.17": "@swc/core-darwin-arm64@npm:1.4.17":
version: 1.4.17 version: 1.4.17
resolution: "@swc/core-darwin-arm64@npm:1.4.17" resolution: "@swc/core-darwin-arm64@npm:1.4.17"
@@ -1534,7 +1471,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:*, @types/node@npm:^20.12.5, @types/node@npm:^20.12.7": "@types/node@npm:*, @types/node@npm:^20.12.5":
version: 20.12.7 version: 20.12.7
resolution: "@types/node@npm:20.12.7" resolution: "@types/node@npm:20.12.7"
dependencies: dependencies:
@@ -1577,6 +1514,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"abort-controller@npm:^3.0.0":
version: 3.0.0
resolution: "abort-controller@npm:3.0.0"
dependencies:
event-target-shim: "npm:^5.0.0"
checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5
languageName: node
linkType: hard
"abortcontroller-polyfill@npm:^1.1.9": "abortcontroller-polyfill@npm:^1.1.9":
version: 1.7.5 version: 1.7.5
resolution: "abortcontroller-polyfill@npm:1.7.5" resolution: "abortcontroller-polyfill@npm:1.7.5"
@@ -1666,6 +1612,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"atomic-sleep@npm:^1.0.0":
version: 1.0.0
resolution: "atomic-sleep@npm:1.0.0"
checksum: 10c0/e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a
languageName: node
linkType: hard
"balanced-match@npm:^1.0.0": "balanced-match@npm:^1.0.0":
version: 1.0.2 version: 1.0.2
resolution: "balanced-match@npm:1.0.2" resolution: "balanced-match@npm:1.0.2"
@@ -1682,6 +1635,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"base64-js@npm:^1.3.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
languageName: node
linkType: hard
"binary-extensions@npm:^2.0.0": "binary-extensions@npm:^2.0.0":
version: 2.3.0 version: 2.3.0
resolution: "binary-extensions@npm:2.3.0" resolution: "binary-extensions@npm:2.3.0"
@@ -1735,6 +1695,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"buffer@npm:^6.0.3":
version: 6.0.3
resolution: "buffer@npm:6.0.3"
dependencies:
base64-js: "npm:^1.3.1"
ieee754: "npm:^1.2.1"
checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0
languageName: node
linkType: hard
"cacache@npm:^18.0.0": "cacache@npm:^18.0.0":
version: 18.0.2 version: 18.0.2
resolution: "cacache@npm:18.0.2" resolution: "cacache@npm:18.0.2"
@@ -1898,6 +1868,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"colorette@npm:^2.0.7":
version: 2.0.20
resolution: "colorette@npm:2.0.20"
checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40
languageName: node
linkType: hard
"commander@npm:^7.0.0, commander@npm:^7.2.0": "commander@npm:^7.0.0, commander@npm:^7.2.0":
version: 7.2.0 version: 7.2.0
resolution: "commander@npm:7.2.0" resolution: "commander@npm:7.2.0"
@@ -2001,6 +1978,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dateformat@npm:^4.6.3":
version: 4.6.3
resolution: "dateformat@npm:4.6.3"
checksum: 10c0/e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.3.4": "debug@npm:4, debug@npm:^4.3.4":
version: 4.3.4 version: 4.3.4
resolution: "debug@npm:4.3.4" resolution: "debug@npm:4.3.4"
@@ -2190,6 +2174,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"event-target-shim@npm:^5.0.0":
version: 5.0.1
resolution: "event-target-shim@npm:5.0.1"
checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b
languageName: node
linkType: hard
"events@npm:^3.3.0":
version: 3.3.0
resolution: "events@npm:3.3.0"
checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6
languageName: node
linkType: hard
"execa@npm:^8.0.1": "execa@npm:^8.0.1":
version: 8.0.1 version: 8.0.1
resolution: "execa@npm:8.0.1" resolution: "execa@npm:8.0.1"
@@ -2245,6 +2243,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-copy@npm:^3.0.2":
version: 3.0.2
resolution: "fast-copy@npm:3.0.2"
checksum: 10c0/02e8b9fd03c8c024d2987760ce126456a0e17470850b51e11a1c3254eed6832e4733ded2d93316c82bc0b36aeb991ad1ff48d1ba95effe7add7c3ab8d8eb554a
languageName: node
linkType: hard
"fast-redact@npm:^3.1.1":
version: 3.5.0
resolution: "fast-redact@npm:3.5.0"
checksum: 10c0/7e2ce4aad6e7535e0775bf12bd3e4f2e53d8051d8b630e0fa9e67f68cb0b0e6070d2f7a94b1d0522ef07e32f7c7cda5755e2b677a6538f1e9070ca053c42343a
languageName: node
linkType: hard
"fast-safe-stringify@npm:^2.1.1":
version: 2.1.1
resolution: "fast-safe-stringify@npm:2.1.1"
checksum: 10c0/d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d
languageName: node
linkType: hard
"fd-slicer@npm:~1.1.0": "fd-slicer@npm:~1.1.0":
version: 1.1.0 version: 1.1.0
resolution: "fd-slicer@npm:1.1.0" resolution: "fd-slicer@npm:1.1.0"
@@ -2406,6 +2425,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"help-me@npm:^5.0.0":
version: 5.0.0
resolution: "help-me@npm:5.0.0"
checksum: 10c0/054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb
languageName: node
linkType: hard
"htmlnano@npm:^2.0.0": "htmlnano@npm:^2.0.0":
version: 2.1.0 version: 2.1.0
resolution: "htmlnano@npm:2.1.0" resolution: "htmlnano@npm:2.1.0"
@@ -2498,6 +2524,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ieee754@npm:^1.2.1":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
languageName: node
linkType: hard
"immutable@npm:^4.0.0": "immutable@npm:^4.0.0":
version: 4.3.5 version: 4.3.5
resolution: "immutable@npm:4.3.5" resolution: "immutable@npm:4.3.5"
@@ -2650,6 +2683,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"joycon@npm:^3.1.1":
version: 3.1.1
resolution: "joycon@npm:3.1.1"
checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae
languageName: node
linkType: hard
"js-tokens@npm:^4.0.0": "js-tokens@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "js-tokens@npm:4.0.0" resolution: "js-tokens@npm:4.0.0"
@@ -2950,6 +2990,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minimist@npm:^1.2.6":
version: 1.2.8
resolution: "minimist@npm:1.2.8"
checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6
languageName: node
linkType: hard
"minipass-collect@npm:^2.0.1": "minipass-collect@npm:^2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "minipass-collect@npm:2.0.1" resolution: "minipass-collect@npm:2.0.1"
@@ -3265,6 +3312,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"on-exit-leak-free@npm:^2.1.0":
version: 2.1.2
resolution: "on-exit-leak-free@npm:2.1.2"
checksum: 10c0/faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570
languageName: node
linkType: hard
"once@npm:^1.3.1, once@npm:^1.4.0": "once@npm:^1.3.1, once@npm:^1.4.0":
version: 1.4.0 version: 1.4.0
resolution: "once@npm:1.4.0" resolution: "once@npm:1.4.0"
@@ -3396,6 +3450,68 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:^1.2.0":
version: 1.2.0
resolution: "pino-abstract-transport@npm:1.2.0"
dependencies:
readable-stream: "npm:^4.0.0"
split2: "npm:^4.0.0"
checksum: 10c0/b4ab59529b7a91f488440147fc58ee0827a6c1c5ca3627292339354b1381072c1a6bfa9b46d03ad27872589e8477ecf74da12cf286e1e6b665ac64a3b806bf07
languageName: node
linkType: hard
"pino-pretty@npm:^11.2.1":
version: 11.2.1
resolution: "pino-pretty@npm:11.2.1"
dependencies:
colorette: "npm:^2.0.7"
dateformat: "npm:^4.6.3"
fast-copy: "npm:^3.0.2"
fast-safe-stringify: "npm:^2.1.1"
help-me: "npm:^5.0.0"
joycon: "npm:^3.1.1"
minimist: "npm:^1.2.6"
on-exit-leak-free: "npm:^2.1.0"
pino-abstract-transport: "npm:^1.0.0"
pump: "npm:^3.0.0"
readable-stream: "npm:^4.0.0"
secure-json-parse: "npm:^2.4.0"
sonic-boom: "npm:^4.0.1"
strip-json-comments: "npm:^3.1.1"
bin:
pino-pretty: bin.js
checksum: 10c0/6c7f15b5bf8a007c8b7157eae445675b13cd95097ffa512d5ebd661f9e7abd328fa27592b25708756a09f098f87cb03ca81837518cd725c16e3f801129b941d4
languageName: node
linkType: hard
"pino-std-serializers@npm:^7.0.0":
version: 7.0.0
resolution: "pino-std-serializers@npm:7.0.0"
checksum: 10c0/73e694d542e8de94445a03a98396cf383306de41fd75ecc07085d57ed7a57896198508a0dec6eefad8d701044af21eb27253ccc352586a03cf0d4a0bd25b4133
languageName: node
linkType: hard
"pino@npm:^9.3.1":
version: 9.3.1
resolution: "pino@npm:9.3.1"
dependencies:
atomic-sleep: "npm:^1.0.0"
fast-redact: "npm:^3.1.1"
on-exit-leak-free: "npm:^2.1.0"
pino-abstract-transport: "npm:^1.2.0"
pino-std-serializers: "npm:^7.0.0"
process-warning: "npm:^3.0.0"
quick-format-unescaped: "npm:^4.0.3"
real-require: "npm:^0.2.0"
safe-stable-stringify: "npm:^2.3.1"
sonic-boom: "npm:^4.0.1"
thread-stream: "npm:^3.0.0"
bin:
pino: bin.js
checksum: 10c0/ab1e81b3e5a91852136d80a592939883eeb81442e5d3a2c070bdbdeb47c5aaa297ead246530b10eb6d5ff59445f4645d1333d342f255d9f002f73aea843e74ee
languageName: node
linkType: hard
"postcss-value-parser@npm:^4.2.0": "postcss-value-parser@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "postcss-value-parser@npm:4.2.0" resolution: "postcss-value-parser@npm:4.2.0"
@@ -3456,6 +3572,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"process-warning@npm:^3.0.0":
version: 3.0.0
resolution: "process-warning@npm:3.0.0"
checksum: 10c0/60f3c8ddee586f0706c1e6cb5aa9c86df05774b9330d792d7c8851cf0031afd759d665404d07037e0b4901b55c44a423f07bdc465c63de07d8d23196bb403622
languageName: node
linkType: hard
"process@npm:^0.11.10":
version: 0.11.10
resolution: "process@npm:0.11.10"
checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3
languageName: node
linkType: hard
"promise-retry@npm:^2.0.1": "promise-retry@npm:^2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "promise-retry@npm:2.0.1" resolution: "promise-retry@npm:2.0.1"
@@ -3476,6 +3606,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"quick-format-unescaped@npm:^4.0.3":
version: 4.0.4
resolution: "quick-format-unescaped@npm:4.0.4"
checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4
languageName: node
linkType: hard
"react-error-overlay@npm:6.0.9": "react-error-overlay@npm:6.0.9":
version: 6.0.9 version: 6.0.9
resolution: "react-error-overlay@npm:6.0.9" resolution: "react-error-overlay@npm:6.0.9"
@@ -3490,6 +3627,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"readable-stream@npm:^4.0.0":
version: 4.5.2
resolution: "readable-stream@npm:4.5.2"
dependencies:
abort-controller: "npm:^3.0.0"
buffer: "npm:^6.0.3"
events: "npm:^3.3.0"
process: "npm:^0.11.10"
string_decoder: "npm:^1.3.0"
checksum: 10c0/a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d
languageName: node
linkType: hard
"readdirp@npm:~3.6.0": "readdirp@npm:~3.6.0":
version: 3.6.0 version: 3.6.0
resolution: "readdirp@npm:3.6.0" resolution: "readdirp@npm:3.6.0"
@@ -3499,6 +3649,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"real-require@npm:^0.2.0":
version: 0.2.0
resolution: "real-require@npm:0.2.0"
checksum: 10c0/23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0
languageName: node
linkType: hard
"regenerator-runtime@npm:^0.13.7": "regenerator-runtime@npm:^0.13.7":
version: 0.13.11 version: 0.13.11
resolution: "regenerator-runtime@npm:0.13.11" resolution: "regenerator-runtime@npm:0.13.11"
@@ -3520,13 +3677,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"safe-buffer@npm:^5.0.1": "safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
version: 5.2.1 version: 5.2.1
resolution: "safe-buffer@npm:5.2.1" resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
languageName: node languageName: node
linkType: hard linkType: hard
"safe-stable-stringify@npm:^2.3.1":
version: 2.4.3
resolution: "safe-stable-stringify@npm:2.4.3"
checksum: 10c0/81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768
languageName: node
linkType: hard
"safer-buffer@npm:>= 2.1.2 < 3.0.0": "safer-buffer@npm:>= 2.1.2 < 3.0.0":
version: 2.1.2 version: 2.1.2
resolution: "safer-buffer@npm:2.1.2" resolution: "safer-buffer@npm:2.1.2"
@@ -3547,6 +3711,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"secure-json-parse@npm:^2.4.0":
version: 2.7.0
resolution: "secure-json-parse@npm:2.7.0"
checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4
languageName: node
linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.6.0": "semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.6.0":
version: 7.6.0 version: 7.6.0
resolution: "semver@npm:7.6.0" resolution: "semver@npm:7.6.0"
@@ -3687,6 +3858,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sonic-boom@npm:^4.0.1":
version: 4.0.1
resolution: "sonic-boom@npm:4.0.1"
dependencies:
atomic-sleep: "npm:^1.0.0"
checksum: 10c0/7b467f2bc8af7ff60bf210382f21c59728cc4b769af9b62c31dd88723f5cc472752d2320736cc366acc7c765ddd5bec3072c033b0faf249923f576a7453ba9d3
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0": "source-map-js@npm:>=0.6.2 <2.0.0":
version: 1.2.0 version: 1.2.0
resolution: "source-map-js@npm:1.2.0" resolution: "source-map-js@npm:1.2.0"
@@ -3701,6 +3881,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"split2@npm:^4.0.0":
version: 4.2.0
resolution: "split2@npm:4.2.0"
checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534
languageName: node
linkType: hard
"sprintf-js@npm:^1.1.3": "sprintf-js@npm:^1.1.3":
version: 1.1.3 version: 1.1.3
resolution: "sprintf-js@npm:1.1.3" resolution: "sprintf-js@npm:1.1.3"
@@ -3753,6 +3940,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string_decoder@npm:^1.3.0":
version: 1.3.0
resolution: "string_decoder@npm:1.3.0"
dependencies:
safe-buffer: "npm:~5.2.0"
checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d
languageName: node
linkType: hard
"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
version: 6.0.1 version: 6.0.1
resolution: "strip-ansi@npm:6.0.1" resolution: "strip-ansi@npm:6.0.1"
@@ -3778,6 +3974,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd
languageName: node
linkType: hard
"supports-color@npm:^5.3.0": "supports-color@npm:^5.3.0":
version: 5.5.0 version: 5.5.0
resolution: "supports-color@npm:5.5.0" resolution: "supports-color@npm:5.5.0"
@@ -3834,6 +4037,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"thread-stream@npm:^3.0.0":
version: 3.1.0
resolution: "thread-stream@npm:3.1.0"
dependencies:
real-require: "npm:^0.2.0"
checksum: 10c0/c36118379940b77a6ef3e6f4d5dd31e97b8210c3f7b9a54eb8fe6358ab173f6d0acfaf69b9c3db024b948c0c5fd2a7df93e2e49151af02076b35ada3205ec9a6
languageName: node
linkType: hard
"timsort@npm:^0.3.0": "timsort@npm:^0.3.0":
version: 0.3.0 version: 0.3.0
resolution: "timsort@npm:0.3.0" resolution: "timsort@npm:0.3.0"
@@ -3888,16 +4100,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@npm:^3.9":
version: 3.9.10
resolution: "typescript@npm:3.9.10"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/863cc06070fa18a0f9c6a83265fb4922a8b51bf6f2c6760fb0b73865305ce617ea4bc6477381f9f4b7c3a8cb4a455b054f5469e6e41307733fe6a2bd9aae82f8
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A>=3.0.0#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.4.4#optional!builtin<compat/typescript>": "typescript@patch:typescript@npm%3A>=3.0.0#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.4.4#optional!builtin<compat/typescript>":
version: 5.4.5 version: 5.4.5
resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin<compat/typescript>::version=5.4.5&hash=5adc0c" resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin<compat/typescript>::version=5.4.5&hash=5adc0c"
@@ -3908,16 +4110,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@patch:typescript@npm%3A^3.9#optional!builtin<compat/typescript>":
version: 3.9.10
resolution: "typescript@patch:typescript@npm%3A3.9.10#optional!builtin<compat/typescript>::version=3.9.10&hash=3bd3d3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/9041fb3886e7d6a560f985227b8c941d17a750f2edccb5f9b3a15a2480574654d9be803ad4a14aabcc2f2553c4d272a25fd698a7c42692f03f66b009fb46883c
languageName: node
linkType: hard
"undici-types@npm:~5.26.4": "undici-types@npm:~5.26.4":
version: 5.26.5 version: 5.26.5
resolution: "undici-types@npm:5.26.5" resolution: "undici-types@npm:5.26.5"