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:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,6 +1,3 @@
|
||||
[submodule "nodejs-rfb"]
|
||||
path = nodejs-rfb
|
||||
url = https://github.com/computernewb/nodejs-rfb
|
||||
[submodule "collab-vm-1.2-binary-protocol"]
|
||||
path = collab-vm-1.2-binary-protocol
|
||||
url = https://github.com/computernewb/collab-vm-1.2-binary-protocol
|
||||
|
||||
3
Justfile
3
Justfile
@@ -1,8 +1,5 @@
|
||||
all:
|
||||
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/cvmts run build
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
"author": "Elijah R, modeco80",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@computernewb/nodejs-rfb": "^0.3.0",
|
||||
"@computernewb/superqemu": "^0.1.0",
|
||||
"@cvmts/cvm-rs": "*",
|
||||
"@cvmts/qemu": "*",
|
||||
"@maxmind/geoip2-node": "^5.0.0",
|
||||
"execa": "^8.0.1",
|
||||
"mnemonist": "^0.39.5",
|
||||
"msgpackr": "^1.10.2",
|
||||
"pino": "^9.3.1",
|
||||
"sharp": "^0.33.3",
|
||||
"toml": "^3.0.0",
|
||||
"ws": "^8.14.1"
|
||||
@@ -24,6 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.5",
|
||||
"@types/ws": "^8.5.5",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^5.4.4"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import { Rank, User } from './User.js';
|
||||
|
||||
export default class AuthManager {
|
||||
apiEndpoint: string;
|
||||
secretKey: string;
|
||||
|
||||
private logger = new Logger('CVMTS.AuthMan');
|
||||
|
||||
constructor(apiEndpoint: string, secretKey: string) {
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
this.secretKey = secretKey;
|
||||
|
||||
@@ -6,18 +6,20 @@ import * as cvm from '@cvmts/cvm-rs';
|
||||
import CircularBuffer from 'mnemonist/circular-buffer.js';
|
||||
import Queue from 'mnemonist/queue.js';
|
||||
import { createHash } from 'crypto';
|
||||
import { VMState, QemuVM, QemuVmDefinition } from '@cvmts/qemu';
|
||||
import { VMState, QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
|
||||
import { IPDataManager } from './IPData.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import AuthManager from './AuthManager.js';
|
||||
import { Size, Rect, Logger } from '@cvmts/shared';
|
||||
import { JPEGEncoder } from './JPEGEncoder.js';
|
||||
import VM from './VM.js';
|
||||
import { ReaderModel } from '@maxmind/geoip2-node';
|
||||
import * as msgpack from 'msgpackr';
|
||||
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
|
||||
// import.meta properties, which have existed since LTS if not before
|
||||
const __dirname = import.meta.dirname;
|
||||
@@ -36,6 +38,7 @@ type VoteTally = {
|
||||
no: number;
|
||||
};
|
||||
|
||||
|
||||
export default class CollabVMServer {
|
||||
private Config: IConfig;
|
||||
|
||||
@@ -87,7 +90,7 @@ export default class CollabVMServer {
|
||||
// Geoip
|
||||
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) {
|
||||
this.Config = config;
|
||||
@@ -121,7 +124,7 @@ export default class CollabVMServer {
|
||||
if (config.vm.type == 'qemu') {
|
||||
(vm as QemuVM).on('statechange', (newState: VMState) => {
|
||||
if (newState == VMState.Started) {
|
||||
self.logger.Info('VM started');
|
||||
self.logger.info('VM started');
|
||||
// well aware this sucks but whatever
|
||||
self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size));
|
||||
self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
|
||||
@@ -129,7 +132,7 @@ export default class CollabVMServer {
|
||||
|
||||
if (newState == VMState.Stopped) {
|
||||
setTimeout(async () => {
|
||||
self.logger.Info('restarting VM');
|
||||
self.logger.info('restarting VM');
|
||||
await self.VM.Start();
|
||||
}, kRestartTimeout);
|
||||
}
|
||||
@@ -154,7 +157,7 @@ export default class CollabVMServer {
|
||||
try {
|
||||
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode;
|
||||
} 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));
|
||||
@@ -179,7 +182,7 @@ export default class CollabVMServer {
|
||||
|
||||
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 (this.TurnQueue.toArray().indexOf(user) !== -1) {
|
||||
var hadturn = this.TurnQueue.peek() === user;
|
||||
@@ -204,7 +207,7 @@ export default class CollabVMServer {
|
||||
try {
|
||||
let res = await this.auth!.Authenticate(msgArr[1], client);
|
||||
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'));
|
||||
let old = this.clients.find((c) => c.username === res.username);
|
||||
if (old) {
|
||||
@@ -236,7 +239,7 @@ export default class CollabVMServer {
|
||||
}
|
||||
}
|
||||
} 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?
|
||||
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) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -721,10 +724,10 @@ export default class CollabVMServer {
|
||||
|
||||
client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString()));
|
||||
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())));
|
||||
} else {
|
||||
this.logger.Info(`Rename ${client.IP.address} to ${client.username}`);
|
||||
this.logger.info(`Rename ${client.IP.address} to ${client.username}`);
|
||||
if (announce)
|
||||
this.clients.forEach((c) => {
|
||||
c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import { Reader, ReaderModel } from '@maxmind/geoip2-node';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'node:path';
|
||||
@@ -6,18 +5,18 @@ import { Readable } from 'node:stream';
|
||||
import { ReadableStream } from 'node:stream/web';
|
||||
import { finished } from 'node:stream/promises';
|
||||
import { execa } from 'execa';
|
||||
import pino from 'pino';
|
||||
|
||||
export default class GeoIPDownloader {
|
||||
private directory: string;
|
||||
private accountID: string;
|
||||
private licenseKey: string;
|
||||
private logger: Logger
|
||||
private logger = pino({ name: 'CVMTS.GeoIPDownloader' });
|
||||
constructor(filename: string, accountID: string, licenseKey: string) {
|
||||
this.directory = filename;
|
||||
if (!this.directory.endsWith('/')) this.directory += '/';
|
||||
this.accountID = accountID;
|
||||
this.licenseKey = licenseKey;
|
||||
this.logger = new Logger('CVMTS.GeoIPDownloader');
|
||||
}
|
||||
|
||||
private genAuthHeader(): string {
|
||||
@@ -28,17 +27,16 @@ export default class GeoIPDownloader {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(this.directory);
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
var error = e as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOTDIR') {
|
||||
this.logger.Warning('File exists at GeoIP directory path, unlinking...');
|
||||
this.logger.warn('File exists at GeoIP directory path, unlinking...');
|
||||
await fs.unlink(this.directory.substring(0, this.directory.length - 1));
|
||||
} 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);
|
||||
}
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
@@ -49,13 +47,13 @@ export default class GeoIPDownloader {
|
||||
let dbpath = path.join(this.directory, (await this.getLatestVersion()).replace('.tar.gz', ''), 'GeoLite2-Country.mmdb');
|
||||
try {
|
||||
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) {
|
||||
var error = ex as NodeJS.ErrnoException;
|
||||
if (error.code === 'ENOENT') {
|
||||
await this.downloadLatestDatabase();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@@ -65,19 +63,19 @@ export default class GeoIPDownloader {
|
||||
async getLatestVersion(): Promise<string> {
|
||||
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
|
||||
redirect: 'follow',
|
||||
method: "HEAD",
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
"Authorization": this.genAuthHeader()
|
||||
Authorization: this.genAuthHeader()
|
||||
}
|
||||
});
|
||||
let disposition = res.headers.get('Content-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);
|
||||
}
|
||||
let filename = disposition.match(/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);
|
||||
}
|
||||
return filename[1];
|
||||
@@ -85,23 +83,23 @@ export default class GeoIPDownloader {
|
||||
|
||||
async downloadLatestDatabase(): Promise<void> {
|
||||
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 file = await fs.open(dbpath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);
|
||||
let stream = file.createWriteStream();
|
||||
let res = await fetch('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', {
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
"Authorization": this.genAuthHeader()
|
||||
Authorization: this.genAuthHeader()
|
||||
}
|
||||
});
|
||||
await finished(Readable.fromWeb(res.body as ReadableStream<any>).pipe(stream));
|
||||
await file.close();
|
||||
this.logger.Info('Finished downloading latest GeoIP database: {0}', filename);
|
||||
this.logger.Info('Extracting GeoIP database: {0}', filename);
|
||||
this.logger.info('Finished downloading latest GeoIP database: {0}', filename);
|
||||
this.logger.info('Extracting GeoIP database: {0}', filename);
|
||||
// yeah whatever
|
||||
await execa('tar', ['xzf', filename], { cwd: this.directory });
|
||||
this.logger.Info('Unlinking GeoIP tarball');
|
||||
this.logger.info('Unlinking GeoIP tarball');
|
||||
await fs.unlink(dbpath);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import pino from 'pino';
|
||||
|
||||
export class IPData {
|
||||
tempMuteExpireTimeout?: NodeJS.Timeout;
|
||||
@@ -22,7 +22,7 @@ export class IPData {
|
||||
|
||||
export class IPDataManager {
|
||||
static ipDatas = new Map<string, IPData>();
|
||||
static logger = new Logger('CVMTS.IPDataManager');
|
||||
static logger = pino({ name: 'CVMTS.IPDataManager' });
|
||||
|
||||
static GetIPData(address: string) {
|
||||
if (IPDataManager.ipDatas.has(address)) {
|
||||
@@ -64,7 +64,7 @@ export class IPDataManager {
|
||||
setInterval(() => {
|
||||
for (let tuple of IPDataManager.ipDatas) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Size, Rect } from '@cvmts/shared';
|
||||
import { Size, Rect } from './VMDisplay.js';
|
||||
import sharp from 'sharp';
|
||||
import * as cvm from '@cvmts/cvm-rs';
|
||||
|
||||
|
||||
@@ -2,20 +2,19 @@ import EventEmitter from 'events';
|
||||
import NetworkServer from '../NetworkServer.js';
|
||||
import { Server, Socket } from 'net';
|
||||
import IConfig from '../IConfig.js';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import TCPClient from './TCPClient.js';
|
||||
import { IPDataManager } from '../IPData.js';
|
||||
import { User } from '../User.js';
|
||||
import pino from 'pino';
|
||||
|
||||
export default class TCPServer extends EventEmitter implements NetworkServer {
|
||||
listener: Server;
|
||||
Config: IConfig;
|
||||
logger: Logger;
|
||||
logger = pino({name: 'CVMTS.TCPServer'});
|
||||
clients: TCPClient[];
|
||||
|
||||
constructor(config: IConfig) {
|
||||
super();
|
||||
this.logger = new Logger('CVMTS.TCPServer');
|
||||
this.Config = config;
|
||||
this.listener = new Server();
|
||||
this.clients = [];
|
||||
@@ -23,7 +22,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
|
||||
}
|
||||
|
||||
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);
|
||||
this.clients.push(client);
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { IPData } from './IPData.js';
|
||||
import IConfig from './IConfig.js';
|
||||
import RateLimiter from './RateLimiter.js';
|
||||
import { execa, execaCommand, ExecaSyncError } from 'execa';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import NetworkClient from './NetworkClient.js';
|
||||
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
|
||||
import pino from 'pino';
|
||||
|
||||
export class User {
|
||||
socket: NetworkClient;
|
||||
@@ -31,7 +31,7 @@ export class User {
|
||||
TurnRateLimit: 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) {
|
||||
this.IP = ip;
|
||||
@@ -148,7 +148,7 @@ export class User {
|
||||
await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr });
|
||||
this.kick();
|
||||
} 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') {
|
||||
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 });
|
||||
this.kick();
|
||||
} 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) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VMState } from '@cvmts/qemu';
|
||||
import VMDisplay from './VMDisplay.js';
|
||||
import { VMState } from '@computernewb/superqemu';
|
||||
import { VMDisplay } from './VMDisplay.js';
|
||||
|
||||
export default interface VM {
|
||||
Start(): Promise<void>;
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { Size } from '@cvmts/shared';
|
||||
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;
|
||||
Disconnect(): void;
|
||||
Connected(): boolean;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import EventEmitter from 'events';
|
||||
import VNCVMDef from './VNCVMDef';
|
||||
import VM from '../VM';
|
||||
import VMDisplay from '../VMDisplay';
|
||||
import { Clamp, Logger, Rect, Size, Sleep } from '@cvmts/shared';
|
||||
import { Size, Rect, VMDisplay } from '../VMDisplay';
|
||||
import { VncClient } from '@computernewb/nodejs-rfb';
|
||||
import { BatchRects, VMState } from '@cvmts/qemu';
|
||||
import { BatchRects, VMState } from '@computernewb/superqemu';
|
||||
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 {
|
||||
def: VNCVMDef;
|
||||
logger: Logger;
|
||||
logger;
|
||||
private displayVnc = new VncClient({
|
||||
debug: false,
|
||||
fps: 60,
|
||||
@@ -20,7 +28,8 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
constructor(def: VNCVMDef) {
|
||||
super();
|
||||
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.Reconnect();
|
||||
@@ -31,7 +40,7 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
});
|
||||
|
||||
this.displayVnc.on('disconnect', () => {
|
||||
this.logger.Info('Disconnected');
|
||||
this.logger.info('Disconnected');
|
||||
this.Reconnect();
|
||||
});
|
||||
|
||||
@@ -40,7 +49,7 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
});
|
||||
|
||||
this.displayVnc.on('firstFrameUpdate', () => {
|
||||
this.logger.Info('Connected');
|
||||
this.logger.info('Connected');
|
||||
// apparently this library is this good.
|
||||
// at least it's better than the two others which exist.
|
||||
this.displayVnc.changeFps(60);
|
||||
@@ -101,13 +110,13 @@ export default class VNCVM extends EventEmitter implements VM, VMDisplay {
|
||||
}
|
||||
|
||||
async Start(): Promise<void> {
|
||||
this.logger.Info('Connecting');
|
||||
this.logger.info('Connecting');
|
||||
if (this.def.startCmd) await execaCommand(this.def.startCmd, { shell: true });
|
||||
this.Connect();
|
||||
}
|
||||
|
||||
async Stop(): Promise<void> {
|
||||
this.logger.Info('Disconnecting');
|
||||
this.logger.info('Disconnecting');
|
||||
this.Disconnect();
|
||||
if (this.def.stopCmd) await execaCommand(this.def.stopCmd, { shell: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import NetworkClient from '../NetworkClient.js';
|
||||
import EventEmitter from 'events';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
|
||||
export default class WSClient extends EventEmitter implements NetworkClient {
|
||||
socket: WebSocket;
|
||||
|
||||
@@ -8,20 +8,19 @@ import { isIP } from 'net';
|
||||
import { IPDataManager } from '../IPData.js';
|
||||
import WSClient from './WSClient.js';
|
||||
import { User } from '../User.js';
|
||||
import { Logger } from '@cvmts/shared';
|
||||
import pino from 'pino';
|
||||
|
||||
export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
private httpServer: http.Server;
|
||||
private wsServer: WebSocketServer;
|
||||
private clients: WSClient[];
|
||||
private Config: IConfig;
|
||||
private logger: Logger;
|
||||
private logger = pino({ name: 'CVMTS.WSServer' });
|
||||
|
||||
constructor(config: IConfig) {
|
||||
super();
|
||||
this.Config = config;
|
||||
this.clients = [];
|
||||
this.logger = new Logger('CVMTS.WSServer');
|
||||
this.httpServer = http.createServer();
|
||||
this.wsServer = new WebSocketServer({ noServer: true });
|
||||
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 {
|
||||
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
|
||||
if (req.headers['x-forwarded-for'] === undefined) {
|
||||
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;
|
||||
}
|
||||
try {
|
||||
@@ -102,7 +101,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
ip = req.headers['x-forwarded-for']?.toString().replace(/\ /g, '').split(',')[0];
|
||||
} catch {
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
@@ -135,10 +134,10 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
this.emit('connect', user);
|
||||
|
||||
ws.on('error', (e) => {
|
||||
this.logger.Error(`${e} (caused by connection ${ip})`);
|
||||
this.logger.error(`${e} (caused by connection ${ip})`);
|
||||
ws.close();
|
||||
});
|
||||
|
||||
this.logger.Info(`New WebSocket connection from ${user.IP.address}`);
|
||||
this.logger.info(`New WebSocket connection from ${user.IP.address}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import IConfig from './IConfig.js';
|
||||
import * as fs from 'fs';
|
||||
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 WSServer from './WebSocket/WSServer.js';
|
||||
import { User } from './User.js';
|
||||
@@ -13,24 +12,25 @@ import TCPServer from './TCP/TCPServer.js';
|
||||
import VM from './VM.js';
|
||||
import VNCVM from './VNCVM/VNCVM.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
|
||||
|
||||
let Config: IConfig;
|
||||
|
||||
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);
|
||||
}
|
||||
try {
|
||||
var configRaw = fs.readFileSync('config.toml').toString();
|
||||
Config = toml.parse(configRaw);
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -54,15 +54,6 @@ async function start() {
|
||||
let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null;
|
||||
switch (Config.vm.type) {
|
||||
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
|
||||
let def: QemuVmDefinition = {
|
||||
id: Config.collabvm.node,
|
||||
@@ -78,7 +69,7 @@ async function start() {
|
||||
break;
|
||||
}
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
Submodule nodejs-rfb deleted from 55e9e6cd65
@@ -3,8 +3,6 @@
|
||||
"workspaces": [
|
||||
"shared",
|
||||
"cvm-rs",
|
||||
"nodejs-rfb",
|
||||
"qemu",
|
||||
"cvmts",
|
||||
"collab-vm-1.2-binary-protocol"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/// <reference types="../node_modules/@types/node">
|
||||
|
||||
export * from './QemuDisplay.js';
|
||||
export * from './QemuUtil.js';
|
||||
export * from './QemuVM.js';
|
||||
@@ -1 +0,0 @@
|
||||
../tsconfig.json
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
../tsconfig.json
|
||||
402
yarn.lock
402
yarn.lock
@@ -34,18 +34,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@computernewb/nodejs-rfb@npm:*, @computernewb/nodejs-rfb@workspace:nodejs-rfb":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@computernewb/nodejs-rfb@workspace:nodejs-rfb"
|
||||
"@computernewb/nodejs-rfb@npm:^0.3.0":
|
||||
version: 0.3.0
|
||||
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:
|
||||
"@parcel/packager-ts": "npm:2.12.0"
|
||||
"@parcel/transformer-typescript-types": "npm:2.12.0"
|
||||
"@types/node": "npm:^20.12.7"
|
||||
parcel: "npm:^2.12.0"
|
||||
prettier: "npm:^3.2.5"
|
||||
typescript: "npm:>=3.0.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
"@computernewb/nodejs-rfb": "npm:^0.3.0"
|
||||
execa: "npm:^8.0.1"
|
||||
pino: "npm:^9.3.1"
|
||||
checksum: 10c0/7177b46c1093345cc3cbcc09450b8b8b09f09eb74ba5abd283aae39e9d1dbc0780f54187da075e44c78a7b683d47367010a473406c2817c36352edd0ddad2c1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cvmts/collab-vm-1.2-binary-protocol@workspace:collab-vm-1.2-binary-protocol":
|
||||
version: 0.0.0-use.local
|
||||
@@ -70,14 +75,17 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@cvmts/cvmts@workspace:cvmts"
|
||||
dependencies:
|
||||
"@computernewb/nodejs-rfb": "npm:^0.3.0"
|
||||
"@computernewb/superqemu": "npm:^0.1.0"
|
||||
"@cvmts/cvm-rs": "npm:*"
|
||||
"@cvmts/qemu": "npm:*"
|
||||
"@maxmind/geoip2-node": "npm:^5.0.0"
|
||||
"@types/node": "npm:^20.12.5"
|
||||
"@types/ws": "npm:^8.5.5"
|
||||
execa: "npm:^8.0.1"
|
||||
mnemonist: "npm:^0.39.5"
|
||||
msgpackr: "npm:^1.10.2"
|
||||
pino: "npm:^9.3.1"
|
||||
pino-pretty: "npm:^11.2.1"
|
||||
prettier: "npm:^3.2.5"
|
||||
sharp: "npm:^0.33.3"
|
||||
toml: "npm:^3.0.0"
|
||||
@@ -86,26 +94,6 @@ __metadata:
|
||||
languageName: unknown
|
||||
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":
|
||||
version: 1.1.1
|
||||
resolution: "@emnapi/runtime@npm:1.1.1"
|
||||
@@ -1335,57 +1323,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.4.17"
|
||||
@@ -1534,7 +1471,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "@types/node@npm:20.12.7"
|
||||
dependencies:
|
||||
@@ -1577,6 +1514,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.7.5
|
||||
resolution: "abortcontroller-polyfill@npm:1.7.5"
|
||||
@@ -1666,6 +1612,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.2
|
||||
resolution: "balanced-match@npm:1.0.2"
|
||||
@@ -1682,6 +1635,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.3.0
|
||||
resolution: "binary-extensions@npm:2.3.0"
|
||||
@@ -1735,6 +1695,16 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 18.0.2
|
||||
resolution: "cacache@npm:18.0.2"
|
||||
@@ -1898,6 +1868,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 7.2.0
|
||||
resolution: "commander@npm:7.2.0"
|
||||
@@ -2001,6 +1978,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.3.4
|
||||
resolution: "debug@npm:4.3.4"
|
||||
@@ -2190,6 +2174,20 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 8.0.1
|
||||
resolution: "execa@npm:8.0.1"
|
||||
@@ -2245,6 +2243,27 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.0
|
||||
resolution: "fd-slicer@npm:1.1.0"
|
||||
@@ -2406,6 +2425,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.1.0
|
||||
resolution: "htmlnano@npm:2.1.0"
|
||||
@@ -2498,6 +2524,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.3.5
|
||||
resolution: "immutable@npm:4.3.5"
|
||||
@@ -2650,6 +2683,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "js-tokens@npm:4.0.0"
|
||||
@@ -2950,6 +2990,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.1
|
||||
resolution: "minipass-collect@npm:2.0.1"
|
||||
@@ -3265,6 +3312,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.4.0
|
||||
resolution: "once@npm:1.4.0"
|
||||
@@ -3396,6 +3450,68 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.2.0
|
||||
resolution: "postcss-value-parser@npm:4.2.0"
|
||||
@@ -3456,6 +3572,20 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.1
|
||||
resolution: "promise-retry@npm:2.0.1"
|
||||
@@ -3476,6 +3606,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.0.9
|
||||
resolution: "react-error-overlay@npm:6.0.9"
|
||||
@@ -3490,6 +3627,19 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.6.0
|
||||
resolution: "readdirp@npm:3.6.0"
|
||||
@@ -3499,6 +3649,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.13.11
|
||||
resolution: "regenerator-runtime@npm:0.13.11"
|
||||
@@ -3520,13 +3677,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:^5.0.1":
|
||||
"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.1.2
|
||||
resolution: "safer-buffer@npm:2.1.2"
|
||||
@@ -3547,6 +3711,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 7.6.0
|
||||
resolution: "semver@npm:7.6.0"
|
||||
@@ -3687,6 +3858,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.2.0
|
||||
resolution: "source-map-js@npm:1.2.0"
|
||||
@@ -3701,6 +3881,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.3
|
||||
resolution: "sprintf-js@npm:1.1.3"
|
||||
@@ -3753,6 +3940,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.0.1
|
||||
resolution: "strip-ansi@npm:6.0.1"
|
||||
@@ -3778,6 +3974,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 5.5.0
|
||||
resolution: "supports-color@npm:5.5.0"
|
||||
@@ -3834,6 +4037,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.3.0
|
||||
resolution: "timsort@npm:0.3.0"
|
||||
@@ -3888,16 +4100,6 @@ __metadata:
|
||||
languageName: node
|
||||
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>":
|
||||
version: 5.4.5
|
||||
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
|
||||
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":
|
||||
version: 5.26.5
|
||||
resolution: "undici-types@npm:5.26.5"
|
||||
|
||||
Reference in New Issue
Block a user