cvm-rs: merge guac and jpeg libs together into one

doesn't really need to be two seperate libraries. also preperation for other funnies

the build script has been replaced with a much saner justfile which uses much saner "yarn workspace" invocations instead of blindly cding all over the place
This commit is contained in:
modeco80
2024-06-22 21:14:05 -04:00
parent 87a377a10f
commit b8ed177885
26 changed files with 246 additions and 414 deletions

8
.gitignore vendored
View File

@@ -10,9 +10,5 @@ cvmts/attic
**/dist/
# Guac-rs
guac-rs/target
guac-rs/index.node
# jpegturbo-rs
jpegturbo-rs/target
jpegturbo-rs/index.node
cvm-rs/target
cvm-rs/index.node

8
Justfile Normal file
View File

@@ -0,0 +1,8 @@
all:
yarn workspace @cvmts/cvm-rs run build
yarn workspace @cvmts/shared run build
yarn workspace @cvmts/qemu run build
yarn workspace @cvmts/cvmts run build
pkg:
yarn

View File

@@ -7,9 +7,9 @@ This is a drop-in replacement for the dying CollabVM 1.2.11. Currently in beta
**TODO**: These instructions are not finished for the refactor branch.
1. Copy config.example.toml to config.toml, and fill out fields
2. Install dependencies: `npm i`
3. Build it: `npm run build`
4. Run it: `npm run serve`
2. Install dependencies: `yarn`
3. Build it: `yarn build`
4. Run it: `yarn serve`
## FAQ
### When I try to access the admin panel, the server crashes!

View File

@@ -59,6 +59,16 @@ dependencies = [
"cc",
]
[[package]]
name = "cvm-rs"
version = "0.1.0"
dependencies = [
"neon",
"once_cell",
"tokio",
"turbojpeg-sys",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -82,16 +92,6 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "jpegturbo-rs"
version = "0.1.0"
dependencies = [
"neon",
"once_cell",
"tokio",
"turbojpeg-sys",
]
[[package]]
name = "libc"
version = "0.2.155"

View File

@@ -1,6 +1,6 @@
[package]
name = "jpegturbo-rs"
description = "Rust powered JPEGTurbo sex"
name = "cvm-rs"
description = "Rust utility library for cvmts. Runs all the high performance code"
version = "0.1.0"
edition = "2021"
exclude = ["index.node"]
@@ -10,6 +10,8 @@ crate-type = ["cdylib"]
[dependencies]
neon = "1"
# Required for JPEG
once_cell = "1.19.0"
tokio = { version = "1.38.0", features = [ "rt", "rt-multi-thread" ] }
turbojpeg-sys = "1.0.0"

84
cvm-rs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,84 @@
//
// Guacamole Codec
export function guacDecode(input: string): string[];
export function guacEncode(...items: string[]): string;
interface JpegInputArgs {
width: number,
height: number,
stride: number, // The width of your input framebuffer OR your image width (if encoding a full image)
buffer: Buffer
// TODO: Allow different formats, or export a boxed ffi object which can store a format
// (i.e: new JpegEncoder(FORMAT_xxx)).
}
/// Performs JPEG encoding.
export function jpegEncode(input: JpegInputArgs) : Promise<Buffer>;
// TODO: Version that can downscale?
/* remoting API?
js side api:
class RemotingClient extends EventEmitter {
constructor(uri: string)
Connect(): Promise<void> - connects to server.
Disconnect(): void - disconnects from a server.
get FullScreen(): Buffer - gets the full screen JPEG at a specific moment. This should only be called once
during some user-specific setup (for example: when a new user connects)
get Thumbnail(): Buffer - gets JPEG thumbnail.
KeyEvent(key: number, pressed: boolean) - sends a key event to the server.
MouseEvent(x: number, y: number, buttons: MouseButtonMask) - sends a mouse event (the button mask is semi-standardized for remoting,
the mask can be converted if not applicable for a given protocol)
// explicit property setter APIs, maybe expose the semi-internal remotingSetProperty API if required?
set JpegQuality(q: number) - sets JPEG quality
// events:
on('open', cb: () => void) - on open
//on('firstupdate', cb: (rect: RectWithJpeg) => void) - the first update of a resize is given here
// doesn't really matter
on('resize', cb: (size: Size) => void) - when the server resizes we do too.
on('update', cb: (rects: Array<RectWithJpeg>) => void) - gives screen frame update as jpeg rects
(pre-batched using existing batcher or a new invention or something)
on('close', cb: () => void) - on close
on('cursor', cb: (b: CursorBitmap) => void) - cursor bitmap changed (always rgba8888)
}
binding side API:
remotingNew("vnc://abc.def:1234") - creates a new remoting client which will use the given protocol in the URI
xxx for callbacks (they will get migrated to eventemitter or something on the JS side so it's more "idiomatic", depending on performance.
In all honesty however, remoting will take care of all the performance sensitive tasks, so it probably won't matter at all)
remotingConnect(client) -> promise<void> (throws rejection) - disconnects
remotingDisconnect(client) - disconnects
remotingGetBuffer(client) -> Buffer - gets the buffer used for the screen
remotingSetProperty(client, propertyId, propertyValue) - sets property (e.g: jpeg quality)
e.g: server uri could be set after client creation
with remotingSetProperty(boxedClient, remoting.propertyServerUri, "vnc://another-server.org::2920")
remotingGetThumbnail(client) - gets thumbnail, this is updated by remoting at about 5 fps
remotingKeyEvent(client, key, pressed) - key event
remotingMouseEvent(client, x, y, buttons) - mouse event
on the rust side a boxed client will contain an inner boxed `dyn RemotingProtocolClient` which will contain protocol specific dispatch,
upon parsing a remoting URI we will create a given client (e.g, for `vnc://` we'd make the VNC one)
*/

View File

@@ -2,5 +2,5 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
export let {jpegEncode} = require('./index.node');
export let {guacDecode, guacEncode, jpegEncode} = require('./index.node');

View File

@@ -1,5 +1,5 @@
{
"name": "@cvmts/guac-rs",
"name": "@cvmts/cvm-rs",
"version": "0.1.0",
"packageManager": "yarn@4.1.1",
"type": "module",

View File

@@ -1,6 +1,5 @@
mod guac;
use neon::prelude::*;
use crate::guac;
fn guac_decode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> {
let input = cx.argument::<JsString>(0)?.value(cx);
@@ -40,17 +39,10 @@ fn guac_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsString>
Ok(cx.string(guac::encode_instruction(&elements)))
}
fn guac_decode(mut cx: FunctionContext) -> JsResult<JsArray> {
pub fn guac_decode(mut cx: FunctionContext) -> JsResult<JsArray> {
guac_decode_impl(&mut cx)
}
fn guac_encode(mut cx: FunctionContext) -> JsResult<JsString> {
pub fn guac_encode(mut cx: FunctionContext) -> JsResult<JsString> {
guac_encode_impl(&mut cx)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("guacDecode", guac_decode)?;
cx.export_function("guacEncode", guac_encode)?;
Ok(())
}

View File

@@ -8,8 +8,10 @@ use tokio::runtime::Runtime;
use std::cell::RefCell;
mod jpeg_compressor;
use crate::jpeg_compressor::*;
/// Gives a static Tokio runtime. We should replace this with
/// rayon or something, but for now tokio works.
fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> {
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
@@ -19,7 +21,7 @@ fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> {
}
thread_local! {
static COMPRESSOR: RefCell<jpeg_compressor::JpegCompressor> = RefCell::new(jpeg_compressor::JpegCompressor::new());
static COMPRESSOR: RefCell<JpegCompressor> = RefCell::new(JpegCompressor::new());
}
fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise> {
@@ -52,7 +54,7 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
let clone = Arc::clone(&copy);
let locked = clone.lock().unwrap();
let image: jpeg_compressor::Image = jpeg_compressor::Image {
let image: Image = Image {
buffer: locked.as_slice(),
width: width as u32,
height: height as u32,
@@ -80,12 +82,6 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
Ok(promise)
}
fn jpeg_encode(mut cx: FunctionContext) -> JsResult<JsPromise> {
pub fn jpeg_encode(mut cx: FunctionContext) -> JsResult<JsPromise> {
jpeg_encode_impl(&mut cx)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("jpegEncode", jpeg_encode)?;
Ok(())
}

19
cvm-rs/src/lib.rs Normal file
View File

@@ -0,0 +1,19 @@
mod guac;
mod guac_js;
mod jpeg_compressor;
mod jpeg_js;
use neon::prelude::*;
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// Mostly transitionary, later on API should change
cx.export_function("jpegEncode", jpeg_js::jpeg_encode)?;
cx.export_function("guacDecode", guac_js::guac_decode)?;
cx.export_function("guacEncode", guac_js::guac_encode)?;
Ok(())
}

View File

@@ -11,8 +11,7 @@
"author": "Elijah R, modeco80",
"license": "GPL-3.0",
"dependencies": {
"@cvmts/guac-rs": "*",
"@cvmts/jpegturbo-rs": "*",
"@cvmts/cvm-rs": "*",
"@cvmts/qemu": "*",
"execa": "^8.0.1",
"mnemonist": "^0.39.5",

View File

@@ -1,7 +1,7 @@
import IConfig from './IConfig.js';
import * as Utilities from './Utilities.js';
import { User, Rank } from './User.js';
import * as guac from '@cvmts/guac-rs';
import * as cvm from '@cvmts/cvm-rs';
// I hate that you have to do it like this
import CircularBuffer from 'mnemonist/circular-buffer.js';
import Queue from 'mnemonist/queue.js';
@@ -142,7 +142,7 @@ export default class CollabVMServer {
user.socket.on('msg', (msg: string) => this.onMessage(user, msg));
user.socket.on('disconnect', () => this.connectionClosed(user));
if (this.Config.auth.enabled) {
user.sendMsg(guac.guacEncode('auth', this.Config.auth.apiEndpoint));
user.sendMsg(cvm.guacEncode('auth', this.Config.auth.apiEndpoint));
}
user.sendMsg(this.getAdduserMsg());
}
@@ -171,25 +171,25 @@ export default class CollabVMServer {
if (hadturn) this.nextTurn();
}
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('remuser', '1', user.username!)));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('remuser', '1', user.username!)));
}
private async onMessage(client: User, message: string) {
try {
var msgArr = guac.guacDecode(message);
var msgArr = cvm.guacDecode(message);
if (msgArr.length < 1) return;
switch (msgArr[0]) {
case 'login':
if (msgArr.length !== 2 || !this.Config.auth.enabled) return;
if (!client.connectedToNode) {
client.sendMsg(guac.guacEncode('login', '0', 'You must connect to the VM before logging in.'));
client.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.'));
return;
}
try {
let res = await this.auth!.Authenticate(msgArr[1], client);
if (res.clientSuccess) {
this.logger.Info(`${client.IP.address} logged in as ${res.username}`);
client.sendMsg(guac.guacEncode('login', '1'));
client.sendMsg(cvm.guacEncode('login', '1'));
let old = this.clients.find((c) => c.username === res.username);
if (old) {
// kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that
@@ -202,13 +202,13 @@ export default class CollabVMServer {
// Set rank
client.rank = res.rank;
if (client.rank === Rank.Admin) {
client.sendMsg(guac.guacEncode('admin', '0', '1'));
client.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else if (client.rank === Rank.Moderator) {
client.sendMsg(guac.guacEncode('admin', '0', '3', this.ModPerms.toString()));
client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString()));
}
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('adduser', '1', client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString())));
} else {
client.sendMsg(guac.guacEncode('login', '0', res.error!));
client.sendMsg(cvm.guacEncode('login', '0', res.error!));
if (res.error === 'You are banned') {
client.kick();
}
@@ -216,28 +216,28 @@ export default class CollabVMServer {
} catch (err) {
this.logger.Error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`);
// for now?
client.sendMsg(guac.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'));
}
break;
case 'list':
client.sendMsg(guac.guacEncode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
client.sendMsg(cvm.guacEncode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
break;
case 'connect':
if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) {
client.sendMsg(guac.guacEncode('connect', '0'));
client.sendMsg(cvm.guacEncode('connect', '0'));
return;
}
client.connectedToNode = true;
client.sendMsg(guac.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
if (this.Config.collabvm.motd) client.sendMsg(guac.guacEncode('chat', '', this.Config.collabvm.motd));
if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd));
if (this.screenHidden) {
client.sendMsg(guac.guacEncode('size', '0', '1024', '768'));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768'));
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
} else {
await this.SendFullScreenWithSize(client);
}
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
if (this.voteInProgress) this.sendVoteUpdate(client);
this.sendTurnUpdate(client);
break;
@@ -245,7 +245,7 @@ export default class CollabVMServer {
if (client.connectedToNode) return;
if (client.username || msgArr.length !== 3 || msgArr[1] !== this.Config.collabvm.node) {
// The use of connect here is intentional.
client.sendMsg(guac.guacEncode('connect', '0'));
client.sendMsg(cvm.guacEncode('connect', '0'));
return;
}
@@ -257,22 +257,22 @@ export default class CollabVMServer {
client.viewMode = 1;
break;
default:
client.sendMsg(guac.guacEncode('connect', '0'));
client.sendMsg(cvm.guacEncode('connect', '0'));
return;
}
client.sendMsg(guac.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
if (this.Config.collabvm.motd) client.sendMsg(guac.guacEncode('chat', '', this.Config.collabvm.motd));
if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd));
if (client.viewMode == 1) {
if (this.screenHidden) {
client.sendMsg(guac.guacEncode('size', '0', '1024', '768'));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768'));
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
} else {
await this.SendFullScreenWithSize(client);
}
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
}
if (this.voteInProgress) this.sendVoteUpdate(client);
@@ -282,12 +282,12 @@ export default class CollabVMServer {
if (!client.RenameRateLimit.request()) return;
if (client.connectedToNode && client.IP.muted) return;
if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) {
client.sendMsg(guac.guacEncode('chat', '', 'Go to your account settings to change your username.'));
client.sendMsg(cvm.guacEncode('chat', '', 'Go to your account settings to change your username.'));
return;
}
if (this.Config.auth.enabled && msgArr[1] !== undefined) {
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp
if (client.username) client.sendMsg(guac.guacEncode('chat', '', 'You need to log in to do that.'));
if (client.username) client.sendMsg(cvm.guacEncode('chat', '', 'You need to log in to do that.'));
if (client.rank !== Rank.Unregistered) return;
this.renameUser(client, undefined);
return;
@@ -299,7 +299,7 @@ export default class CollabVMServer {
if (client.IP.muted) return;
if (msgArr.length !== 2) return;
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.chat) {
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
var msg = Utilities.HTMLSanitize(msgArr[1]);
@@ -307,14 +307,14 @@ export default class CollabVMServer {
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, msg)));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msg)));
this.ChatHistory.push({ user: client.username, msg: msg });
client.onMsgSent();
break;
case 'turn':
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) {
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
if (!client.TurnRateLimit.request()) return;
@@ -384,33 +384,33 @@ export default class CollabVMServer {
case '1':
if (!this.voteInProgress) {
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.callForReset) {
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
if (this.voteCooldown !== 0) {
client.sendMsg(guac.guacEncode('vote', '3', this.voteCooldown.toString()));
client.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString()));
return;
}
this.startVote();
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', `${client.username} has started a vote to reset the VM.`)));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has started a vote to reset the VM.`)));
}
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) {
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
return;
} else if (client.IP.vote !== true) {
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', `${client.username} has voted yes.`)));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted yes.`)));
}
client.IP.vote = true;
break;
case '0':
if (!this.voteInProgress) return;
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) {
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
if (client.IP.vote !== false) {
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', `${client.username} has voted no.`)));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted no.`)));
}
client.IP.vote = false;
break;
@@ -423,7 +423,7 @@ export default class CollabVMServer {
case '2':
// Login
if (this.Config.auth.enabled) {
client.sendMsg(guac.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.'));
client.sendMsg(cvm.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.'));
return;
}
if (!client.LoginRateLimit.request() || !client.username) return;
@@ -434,37 +434,37 @@ export default class CollabVMServer {
sha256.destroy();
if (pwdHash === this.Config.collabvm.adminpass) {
client.rank = Rank.Admin;
client.sendMsg(guac.guacEncode('admin', '0', '1'));
client.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
client.rank = Rank.Moderator;
client.sendMsg(guac.guacEncode('admin', '0', '3', this.ModPerms.toString()));
client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString()));
} else if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
client.rank = Rank.Turn;
client.sendMsg(guac.guacEncode('chat', '', 'You may now take turns.'));
client.sendMsg(cvm.guacEncode('chat', '', 'You may now take turns.'));
} else {
client.sendMsg(guac.guacEncode('admin', '0', '0'));
client.sendMsg(cvm.guacEncode('admin', '0', '0'));
return;
}
if (this.screenHidden) {
await this.SendFullScreenWithSize(client);
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
}
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('adduser', '1', client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString())));
break;
case '5':
// QEMU Monitor
if (client.rank !== Rank.Admin) return;
/* Surely there could be rudimentary processing to convert some qemu monitor syntax to [XYZ hypervisor] if possible
if (!(this.VM instanceof QEMUVM)) {
client.sendMsg(guac.guacEncode("admin", "2", "This is not a QEMU VM and therefore QEMU monitor commands cannot be run."));
client.sendMsg(cvm.guacEncode("admin", "2", "This is not a QEMU VM and therefore QEMU monitor commands cannot be run."));
return;
}
*/
if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return;
var output = await this.VM.MonitorCommand(msgArr[3]);
client.sendMsg(guac.guacEncode('admin', '2', String(output)));
client.sendMsg(cvm.guacEncode('admin', '2', String(output)));
break;
case '8':
// Restore
@@ -541,7 +541,7 @@ export default class CollabVMServer {
// Rename user
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) {
client.sendMsg(guac.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.'));
client.sendMsg(cvm.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.'));
}
if (msgArr.length !== 4) return;
var user = this.clients.find((c) => c.username === msgArr[2]);
@@ -554,7 +554,7 @@ export default class CollabVMServer {
if (msgArr.length !== 3) return;
var user = this.clients.find((c) => c.username === msgArr[2]);
if (!user) return;
client.sendMsg(guac.guacEncode('admin', '19', msgArr[2], user.IP.address));
client.sendMsg(cvm.guacEncode('admin', '19', msgArr[2], user.IP.address));
break;
case '20':
// Steal turn
@@ -567,14 +567,14 @@ export default class CollabVMServer {
if (msgArr.length !== 3) return;
switch (client.rank) {
case Rank.Admin:
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, msgArr[2])));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2])));
this.ChatHistory.push({ user: client.username!, msg: msgArr[2] });
break;
case Rank.Moderator:
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, msgArr[2])));
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2])));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2]))));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2]))));
break;
}
break;
@@ -609,9 +609,9 @@ export default class CollabVMServer {
this.clients
.filter((c) => c.rank == Rank.Unregistered)
.forEach((client) => {
client.sendMsg(guac.guacEncode('size', '0', '1024', '768'));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768'));
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
});
break;
case '1':
@@ -626,16 +626,16 @@ export default class CollabVMServer {
});
this.clients.forEach(async (client) => {
client.sendMsg(guac.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', encoded));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
client.sendMsg(cvm.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded));
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
});
break;
}
break;
case '25':
if (client.rank !== Rank.Admin || msgArr.length !== 3) return;
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', msgArr[2])));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2])));
break;
}
break;
@@ -665,7 +665,7 @@ export default class CollabVMServer {
} else {
newName = newName.trim();
if (hadName && newName === oldname) {
client.sendMsg(guac.guacEncode('rename', '0', '0', client.username!, client.rank.toString()));
client.sendMsg(cvm.guacEncode('rename', '0', '0', client.username!, client.rank.toString()));
return;
}
if (this.getUsernameList().indexOf(newName) !== -1) {
@@ -682,13 +682,13 @@ export default class CollabVMServer {
} else client.username = newName;
}
client.sendMsg(guac.guacEncode('rename', '0', status, client.username!, client.rank.toString()));
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.clients.forEach((c) => c.sendMsg(guac.guacEncode('rename', '1', oldname, client.username!, client.rank.toString())));
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.clients.forEach((c) => c.sendMsg(guac.guacEncode('adduser', '1', client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString())));
}
}
@@ -696,13 +696,13 @@ export default class CollabVMServer {
var arr: string[] = ['adduser', this.clients.filter((c) => c.username).length.toString()];
this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!, c.rank.toString()));
return guac.guacEncode(...arr);
return cvm.guacEncode(...arr);
}
getChatHistoryMsg(): string {
var arr: string[] = ['chat'];
this.ChatHistory.forEach((c) => arr.push(c.user, c.msg));
return guac.guacEncode(...arr);
return cvm.guacEncode(...arr);
}
private sendTurnUpdate(client?: User) {
@@ -715,7 +715,7 @@ export default class CollabVMServer {
this.TurnQueue.forEach((c) => arr.push(c.username));
var currentTurningUser = this.TurnQueue.peek();
if (client) {
client.sendMsg(guac.guacEncode(...arr));
client.sendMsg(cvm.guacEncode(...arr));
return;
}
this.clients
@@ -725,12 +725,12 @@ export default class CollabVMServer {
var time;
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
else time = 9999999999;
c.sendMsg(guac.guacEncode(...arr, time.toString()));
c.sendMsg(cvm.guacEncode(...arr, time.toString()));
} else {
c.sendMsg(guac.guacEncode(...arr));
c.sendMsg(cvm.guacEncode(...arr));
}
});
if (currentTurningUser) currentTurningUser.sendMsg(guac.guacEncode(...arr));
if (currentTurningUser) currentTurningUser.sendMsg(cvm.guacEncode(...arr));
}
private nextTurn() {
clearInterval(this.TurnInterval);
@@ -777,8 +777,8 @@ export default class CollabVMServer {
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.sendMsg(guac.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64));
c.sendMsg(guac.guacEncode('sync', Date.now().toString()));
c.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64));
c.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
});
}
@@ -787,7 +787,7 @@ export default class CollabVMServer {
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.sendMsg(guac.guacEncode('size', '0', size.width.toString(), size.height.toString()));
c.sendMsg(cvm.guacEncode('size', '0', size.width.toString(), size.height.toString()));
});
}
@@ -802,8 +802,8 @@ export default class CollabVMServer {
height: displaySize.height
});
client.sendMsg(guac.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', encoded));
client.sendMsg(cvm.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded));
}
private async MakeRectData(rect: Rect) {
@@ -828,7 +828,7 @@ export default class CollabVMServer {
startVote() {
if (this.voteInProgress) return;
this.voteInProgress = true;
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('vote', '0')));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '0')));
this.voteTime = this.Config.collabvm.voteTime;
this.voteInterval = setInterval(() => {
this.voteTime--;
@@ -843,12 +843,12 @@ export default class CollabVMServer {
this.voteInProgress = false;
clearInterval(this.voteInterval);
var count = this.getVoteCounts();
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('vote', '2')));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2')));
if (result === true || (result === undefined && count.yes >= count.no)) {
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', 'The vote to reset the VM has won.')));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has won.')));
this.VM.Reset();
} else {
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', 'The vote to reset the VM has lost.')));
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has lost.')));
}
this.clients.forEach((c) => {
c.IP.vote = null;
@@ -863,7 +863,7 @@ export default class CollabVMServer {
sendVoteUpdate(client?: User) {
if (!this.voteInProgress) return;
var count = this.getVoteCounts();
var msg = guac.guacEncode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString());
var msg = cvm.guacEncode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString());
if (client) client.sendMsg(msg);
else this.clients.forEach((c) => c.sendMsg(msg));
}

View File

@@ -1,6 +1,6 @@
import { Size, Rect } from '@cvmts/shared';
import sharp from 'sharp';
import * as jpeg from '@cvmts/jpegturbo-rs';
import * as cvm from '@cvmts/cvm-rs';
// A good balance. TODO: Configurable?
let gJpegQuality = 35;
@@ -28,7 +28,7 @@ export class JPEGEncoder {
static async Encode(canvas: Buffer, displaySize: Size, rect: Rect): Promise<Buffer> {
let offset = (rect.y * displaySize.width + rect.x) * 4;
return jpeg.jpegEncode({
return cvm.jpegEncode({
width: rect.width,
height: rect.height,
stride: displaySize.width,
@@ -42,7 +42,7 @@ export class JPEGEncoder {
.raw()
.toBuffer({ resolveWithObject: true });
return jpeg.jpegEncode({
return cvm.jpegEncode({
width: kThumbnailSize.width,
height: kThumbnailSize.height,
stride: kThumbnailSize.width,

View File

@@ -1,5 +1,5 @@
import * as Utilities from './Utilities.js';
import * as guac from '@cvmts/guac-rs';
import * as cvm from '@cvmts/cvm-rs';
import { IPData } from './IPData.js';
import IConfig from './IConfig.js';
import RateLimiter from './RateLimiter.js';
@@ -95,7 +95,7 @@ export class User {
}
closeConnection() {
this.socket.send(guac.guacEncode('disconnect'));
this.socket.send(cvm.guacEncode('disconnect'));
this.socket.close();
}
@@ -115,7 +115,7 @@ export class User {
mute(permanent: boolean) {
this.IP.muted = true;
this.sendMsg(guac.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
this.sendMsg(cvm.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
if (!permanent) {
clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
@@ -124,7 +124,7 @@ export class User {
unmute() {
clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.muted = false;
this.sendMsg(guac.guacEncode('chat', '', 'You are no longer muted.'));
this.sendMsg(cvm.guacEncode('chat', '', 'You are no longer muted.'));
}
private banCmdArgs(arg: string): string {

View File

@@ -49,7 +49,14 @@ export default class WSClient extends EventEmitter implements NetworkClient {
}
close(): void {
if(this.isOpen()) {
// While this seems counterintutive, do note that the WebSocket protocol
// *sends* a data frame whilist closing a connection. Therefore, if the other end
// has forcibly hung up (closed) their connection, the best way to handle that
// is to just let the inner TCP socket propegate that, which `ws` will do for us.
// Otherwise, we'll try to send data to a closed client then SIGPIPE.
this.socket.close();
}
}
}

209
guac-rs/Cargo.lock generated
View File

@@ -1,209 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "guac-rs"
version = "0.1.0"
dependencies = [
"neon",
]
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libloading"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "neon"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d75440242411c87dc39847b0e33e961ec1f10326a9d8ecf9c1ea64a3b3c13dc"
dependencies = [
"getrandom",
"libloading",
"neon-macros",
"once_cell",
"semver",
"send_wrapper",
"smallvec",
]
[[package]]
name = "neon-macros"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b"
dependencies = [
"quote",
"syn",
"syn-mid",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn-mid"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

View File

@@ -1,12 +0,0 @@
[package]
name = "guac-rs"
description = "Rust guacamole decoding :)"
version = "0.1.0"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
[dependencies]
neon = "1"

3
guac-rs/index.d.ts vendored
View File

@@ -1,3 +0,0 @@
export function guacDecode(input: string): string[];
export function guacEncode(...items: string[]): string;

View File

@@ -1,6 +0,0 @@
// *sigh*
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
export let {guacDecode, guacEncode} = require('./index.node');

View File

@@ -1,15 +0,0 @@
interface JpegInputArgs {
width: number,
height: number,
stride: number, // The width of your input framebuffer OR your image width (if encoding a full image)
buffer: Buffer
// TODO: Allow different formats, or export a boxed ffi object which can store a format
// (i.e: new JpegEncoder(FORMAT_xxx)).
}
/// Performs JPEG encoding.
export function jpegEncode(input: JpegInputArgs) : Promise<Buffer>;
// TODO: Version that can downscale?

View File

@@ -1,16 +0,0 @@
{
"name": "@cvmts/jpegturbo-rs",
"version": "0.1.0",
"packageManager": "yarn@4.1.1",
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --release --message-format=json-render-diagnostics",
"install": "yarn build",
"test": "cargo test"
},
"devDependencies": {
"cargo-cp-artifact": "^0.1"
}
}

View File

@@ -2,8 +2,7 @@
"name": "cvmts-repo",
"workspaces": [
"shared",
"guac-rs",
"jpegturbo-rs",
"cvm-rs",
"nodejs-rfb",
"qemu",
"cvmts"
@@ -19,7 +18,7 @@
},
"packageManager": "yarn@4.1.1",
"scripts": {
"build": "yarn && cd nodejs-rfb && yarn && yarn build && cd ../shared && yarn build && cd ../qemu && yarn build && cd ../cvmts && yarn build",
"build": "just",
"serve": "node cvmts/dist/index.js"
}
}

View File

@@ -47,12 +47,19 @@ __metadata:
languageName: unknown
linkType: soft
"@cvmts/cvm-rs@npm:*, @cvmts/cvm-rs@workspace:cvm-rs":
version: 0.0.0-use.local
resolution: "@cvmts/cvm-rs@workspace:cvm-rs"
dependencies:
cargo-cp-artifact: "npm:^0.1"
languageName: unknown
linkType: soft
"@cvmts/cvmts@workspace:cvmts":
version: 0.0.0-use.local
resolution: "@cvmts/cvmts@workspace:cvmts"
dependencies:
"@cvmts/guac-rs": "npm:*"
"@cvmts/jpegturbo-rs": "npm:*"
"@cvmts/cvm-rs": "npm:*"
"@cvmts/qemu": "npm:*"
"@types/node": "npm:^20.12.5"
"@types/ws": "npm:^8.5.5"
@@ -66,22 +73,6 @@ __metadata:
languageName: unknown
linkType: soft
"@cvmts/guac-rs@npm:*, @cvmts/guac-rs@workspace:guac-rs":
version: 0.0.0-use.local
resolution: "@cvmts/guac-rs@workspace:guac-rs"
dependencies:
cargo-cp-artifact: "npm:^0.1"
languageName: unknown
linkType: soft
"@cvmts/jpegturbo-rs@npm:*, @cvmts/jpegturbo-rs@workspace:jpegturbo-rs":
version: 0.0.0-use.local
resolution: "@cvmts/jpegturbo-rs@workspace:jpegturbo-rs"
dependencies:
cargo-cp-artifact: "npm:^0.1"
languageName: unknown
linkType: soft
"@cvmts/qemu@npm:*, @cvmts/qemu@workspace:qemu":
version: 0.0.0-use.local
resolution: "@cvmts/qemu@workspace:qemu"