Skip to content

Commit 2af7337

Browse files
committed
feat: v1.0.2
global: - move instance limit to daemon Web: - fix auth error page css and reset password form not loading admin: - add config file edit/save - make ban and delete safe functions - cpu and mem for sessions - refresh button - fix layout and other bug fixes Daemon: - fix responses - fix cpu and mem stats - add workspace stats endpoint - remove unused config options - move dns servers config to .session - add memory limit Workspaces: - Fix debian wallpaper not always applying
1 parent beb0fda commit 2af7337

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+692
-333
lines changed

apps/daemon/README.md

Lines changed: 0 additions & 17 deletions
This file was deleted.

apps/daemon/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "daemon",
33
"type": "module",
4-
"version": "1.0.1",
4+
"version": "1.0.2",
55
"scripts": {
66
"start": "./stardustd",
77
"build:debug": "bun build src/server.ts --compile --sourcemap --outfile stardustd",

apps/daemon/schema.json

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55
"Config": {
66
"additionalProperties": false,
77
"properties": {
8-
"dnsServers": {
9-
"description": "DNS servers for the session to use.",
10-
"items": {
11-
"type": "string"
12-
},
13-
"type": "array"
14-
},
158
"docker": {
169
"$ref": "#/definitions/DockerConfig"
1710
},
@@ -96,22 +89,28 @@
9689
"SessionConfig": {
9790
"additionalProperties": false,
9891
"properties": {
99-
"bitDepth": {
100-
"default": 24,
101-
"description": "Bit depth for the display",
102-
"type": "number"
103-
},
10492
"blockedProcessNames": {
105-
"description": "List of processes to block on containers. Useful if you want to stop the use of crypto miners or other resource hungry processes.",
93+
"description": "List of processes to block on containers. Useful if you want to stop the use of crypto miners or other resource hungry processes. Will also kill processes on the host. Will be overhauled later",
10694
"items": {
10795
"type": "string"
10896
},
10997
"type": "array"
11098
},
111-
"resolution": {
112-
"default": "1920x1080",
113-
"description": "Resolution for the display",
114-
"type": "string"
99+
"dnsServers": {
100+
"description": "DNS servers for the session to use.",
101+
"items": {
102+
"type": "string"
103+
},
104+
"type": "array"
105+
},
106+
"limit": {
107+
"description": "Max number of sessions allowed on this instance",
108+
"type": "number"
109+
},
110+
"memoryLimit": {
111+
"default": "undefined",
112+
"description": "Maximum memory a container can use, in megabytes",
113+
"type": "number"
115114
},
116115
"showVncPassword": {
117116
"description": "Doesn't unset $VNCPASSWORD in containers",

apps/daemon/src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import scalarCss from "@stardust/theme/scalar-css";
44
import { Elysia } from "elysia";
55
import pkgJson from "~/../package.json";
66
import { authCheck } from "~/auth-middleware";
7+
import generateToken from "~/lib/auth-token";
78
import { getConfig } from "~/lib/config";
9+
import { getCpuUsage } from "~/lib/cpu";
810
import { docker } from "~/lib/docker";
911
import sessionHandler from "~/session";
1012
import workspaceHandler from "~/workspace";
11-
import generateToken from "./lib/auth-token";
1213

1314
const config = getConfig();
1415
export const app = new Elysia()
1516
.get("/", async (c) => {
1617
return {
1718
message:
18-
"✨ Stardust daemon by aetherra. \nSource tree: https://github.com/aetherra/stardust/tree/rewrite/apps/daemon",
19+
"✨ Stardust daemon by aetherra. Source tree: https://github.com/aetherra/stardust/tree/rewrite/apps/daemon",
1920
success: true,
2021
authenticated: (await authCheck(c))?.success !== false,
2122
};
@@ -30,11 +31,12 @@ export const app = new Elysia()
3031
})
3132
.get("/healthcheck", async () => ({
3233
success: true,
33-
cpu: ((os.loadavg()[0] / os.cpus().length) * 100).toFixed(2),
34+
cpu: getCpuUsage().toFixed(2),
3435
mem: (((os.totalmem() - os.freemem()) / os.totalmem()) * 100).toFixed(2),
3536
os: `${os.type()} ${os.release()}`,
3637
version: pkgJson.version,
3738
sessions: (await docker.listContainers()).filter((s) => s.HostConfig.NetworkMode === config.docker.network).length,
39+
limit: config.session.limit,
3840
}))
3941
.use(sessionHandler)
4042
.use(workspaceHandler)

apps/daemon/src/lib/config/types.d.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ export interface Config {
1919
* Do not change unless you know what you're doing.
2020
*/
2121
token?: string;
22-
/**
23-
* DNS servers for the session to use.
24-
*/
25-
dnsServers?: string[];
2622
docker: DockerConfig;
2723
session: SessionConfig;
2824
}
@@ -70,6 +66,10 @@ export interface DockerConfig {
7066
}
7167

7268
export interface SessionConfig {
69+
/**
70+
* Max number of sessions allowed on this instance
71+
*/
72+
limit?: number;
7373
/**
7474
* Default VNC password to use. Defaults to randomly generated.
7575
*/
@@ -79,20 +79,19 @@ export interface SessionConfig {
7979
*/
8080
showVncPassword?: boolean;
8181
/**
82-
* Resolution for the display
83-
* @default 1920x1080
82+
* Maximum memory a container can use, in megabytes
83+
* @default undefined
8484
*/
85-
resolution?: string;
86-
/**
87-
* Bit depth for the display
88-
* @default 24
89-
*/
90-
bitDepth?: number;
85+
memoryLimit?: number;
9186
/**
9287
* Storage limit for containers in GB (in format nG, for example 25G)
9388
* Only works if /var/lib/docker is XFS formatted with pquota enabled
9489
*/
9590
storageLimit?: string;
91+
/**
92+
* DNS servers for the session to use.
93+
*/
94+
dnsServers?: string[];
9695
/**
9796
* List of processes to block on containers.
9897
* Useful if you want to stop the use of crypto miners or other resource hungry processes.

apps/daemon/src/lib/cpu.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import os from "node:os";
2+
3+
let latestCpuPercent = 0;
4+
5+
function cpuAverage() {
6+
const cpus = os.cpus();
7+
let totalIdle = 0,
8+
totalTick = 0;
9+
10+
for (const cpu of cpus) {
11+
for (const type in cpu.times) {
12+
totalTick += cpu.times[type as keyof typeof cpu.times];
13+
}
14+
totalIdle += cpu.times.idle;
15+
}
16+
17+
return {
18+
idle: totalIdle / cpus.length,
19+
total: totalTick / cpus.length,
20+
};
21+
}
22+
23+
async function updateCpuUsage() {
24+
const start = cpuAverage();
25+
await Bun.sleep(100);
26+
const end = cpuAverage();
27+
28+
const idleDiff = end.idle - start.idle;
29+
const totalDiff = end.total - start.total;
30+
latestCpuPercent = 100 - (100 * idleDiff) / totalDiff;
31+
}
32+
33+
export function startCpuMonitoring(intervalMs = 1000) {
34+
setInterval(updateCpuUsage, intervalMs);
35+
updateCpuUsage();
36+
}
37+
38+
export function getCpuUsage() {
39+
return latestCpuPercent;
40+
}

apps/daemon/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ if (!validateConfig(getConfig())) {
1414
import { connect, type Socket } from "node:net";
1515
import generateToken from "~/lib/auth-token";
1616
import { scheduleKillBlockedProcesses } from "~/lib/block-process";
17+
import { startCpuMonitoring } from "~/lib/cpu";
1718
import { docker } from "~/lib/docker";
1819
import checkDockerNetwork from "~/lib/network-check";
1920
import checkSystemService from "~/lib/service-check";
@@ -25,6 +26,7 @@ if (typeof config.service !== "boolean" || config.service === true) {
2526
checkSystemService();
2627
}
2728
scheduleKillBlockedProcesses();
29+
startCpuMonitoring();
2830

2931
// biome-ignore lint: no
3032
const srv = Bun.serve<{ socket: Socket; path: string }, {}>({

apps/daemon/src/session/add-screen.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { docker } from "~/lib/docker";
2-
import { vncBaseFlags } from "./create";
32
// todo
43
export default async function addScreen(id: string) {
54
const container = docker.getContainer(id);
65
console.log(`hi${id}`);
76
await container.restart(id);
87
const exec = await container.exec({
9-
Cmd: ["export", "VNCFLAGS=", "'", vncBaseFlags, " -screen 1 1920x950x24", "'"],
8+
Cmd: ["export", "VNCFLAGS=", "'", " -screen 1 1920x950x24", "'"],
109
});
1110
await exec.start({});
1211
return true;

apps/daemon/src/session/create.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,19 @@ import { randomBytes } from "node:crypto";
22
import { getConfig } from "~/lib/config";
33
import { docker } from "~/lib/docker";
44

5-
const config = getConfig();
6-
export const vncBaseFlags = `-screen 0 ${config.session.resolution || "1920x950"}x${config.session.bitDepth?.toString() || "24"}`;
7-
85
export default async function createSession({
96
workspace,
107
user,
118
password,
129
environment = {},
1310
exposePorts,
14-
memory,
1511
nodeId,
16-
vncFlags,
1712
}: {
1813
workspace: string;
1914
user: string;
2015
password?: string;
2116
environment?: Record<string, string>;
2217
exposePorts?: string[];
23-
memory?: number;
2418
nodeId: string;
2519
vncFlags?: string;
2620
}) {
@@ -35,8 +29,8 @@ export default async function createSession({
3529
HostConfig: {
3630
ShmSize: 1024,
3731
NetworkMode: config.docker.network,
38-
Dns: config.dnsServers,
39-
Memory: memory,
32+
Dns: config.session.dnsServers,
33+
Memory: config.session.memoryLimit ? config.session.memoryLimit * 1024 * 1024 : undefined,
4034
StorageOpt: config.session.storageLimit
4135
? {
4236
size: config.session.storageLimit,
@@ -47,7 +41,6 @@ export default async function createSession({
4741
`STARDUST_USER=${user}`,
4842
`VNCPASSWORD=${pass}`,
4943
`WIPEVNCENV=${config.session.showVncPassword ? "false" : "true"}`,
50-
`VNCFLAGS=${vncBaseFlags + vncFlags}`,
5144
...envArray,
5245
],
5346
ExposedPorts: exposePorts ? Object.fromEntries(exposePorts.map((e) => [e, {}])) : undefined, // world class types by docker

apps/daemon/src/session/index.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export default new Elysia({ prefix: "/sessions" })
1111
.put(
1212
"/create",
1313
async ({ body }) => {
14+
const config = getConfig();
15+
const containers = (await docker.listContainers()).filter(
16+
(s) => s.HostConfig.NetworkMode === config.docker.network,
17+
);
18+
if (config.session.limit && config.session.limit <= containers.length) {
19+
throw new Error("Session limit reached");
20+
}
1421
const session = await createSession(body);
1522
return {
1623
success: true,
@@ -28,7 +35,6 @@ export default new Elysia({ prefix: "/sessions" })
2835
exposePorts: t.Optional(t.Array(t.String())),
2936
memory: t.Optional(t.Number()),
3037
nodeId: t.String(),
31-
vncFlags: t.Optional(t.String()),
3238
}),
3339
},
3440
)
@@ -43,14 +49,31 @@ export default new Elysia({ prefix: "/sessions" })
4349
};
4450
})
4551
.get("/:id", async ({ params: { id } }) => {
46-
const container = await docker.getContainer(id).inspect();
47-
if (!container) {
52+
const info = docker.getContainer(id);
53+
const container = await info.inspect();
54+
const stats = await info.stats({ stream: false });
55+
if (!stats || !container) {
4856
throw new Error(`No such container with id ${id}`);
4957
}
58+
const memUsage = stats.memory_stats.usage;
59+
const memLimit = stats.memory_stats.limit;
60+
const memPercent = memUsage / memLimit;
61+
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
62+
63+
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
64+
let cpuPercent = 0;
65+
if (systemDelta > 0 && cpuDelta > 0) {
66+
const numCpus = stats.cpu_stats.online_cpus || stats.cpu_stats.cpu_usage.percpu_usage.length;
67+
cpuPercent = (cpuDelta / systemDelta) * numCpus * 100;
68+
}
5069
// world class code
5170
const password = container.Config.Env.find((e) => e.startsWith("VNCPASSWORD="))?.split("=")[1];
5271
return {
5372
password,
73+
cpuPercent,
74+
memPercent,
75+
memUsage,
76+
memLimit,
5477
success: true,
5578
...container,
5679
};

0 commit comments

Comments
 (0)