CH-J Server ManagerSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Server Manager

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J / Server Manager 377 Dateien
core-20261019T111616Z-160a00bfcddc Hinzugefügt: · Erstellt:
systemMonitorIntegration.test.js Hinzugefügt: Datum nicht erfasst 10,8 KB · 138 Zeilen
Code Datei herunterladen
1"use strict";
3// Destructive end-to-end tests of System Monitor metrics and swap management in a disposable, PRIVILEGED
4// Docker container (swapon/loop devices). Swap files live on loop-mounted ext4/XFS/Btrfs images inside the
5// container. Run explicitly: CHJ_USERS_INTEGRATION=1 CHJ_USERS_IMAGE=chj-users-it-debian node --test test/systemMonitorIntegration.test.js
6const test = require("node:test");
7const assert = require("node:assert/strict");
8const { execFileSync, spawnSync } = require("node:child_process");
9const { SessionManager } = require("../src/main/sessions/sessionManager");
10const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
11const { VirtualMemoryService } = require("../src/main/system/virtualMemoryService");
13const enabled = process.env.CHJ_USERS_INTEGRATION === "1";
14const image = process.env.CHJ_USERS_IMAGE || "chj-users-it-debian";
15const skip = enabled ? false : "Set CHJ_USERS_INTEGRATION=1 to run destructive swap tests in a disposable privileged container.";
16const container = `chj-swap-it-${process.pid}`;
18function sh(command, { allowFailure = false } = {}) {
19 const result = spawnSync("docker", ["exec", container, "sh", "-c", command], { encoding: "utf8" });
20 if (!allowFailure && result.status !== 0) throw new Error(`container command failed (${result.status}): ${command}\n${result.stderr}`);
21 return result.stdout.trim();
24test("System Monitor metrics and swap management on an isolated Linux container", { skip, timeout: 900000 }, async (t) => {
25 execFileSync("docker", ["run", "-d", "--rm", "--privileged", "--name", container, "-p", "127.0.0.1::22", image]);
26 t.after(() => {
27 spawnSync("docker", ["exec", container, "sh", "-c", "swapoff /swap/* /var/lib/swap/* /btrfs/* 2>/dev/null; true"]);
28 spawnSync("docker", ["rm", "-f", container]);
29 });
30 // /proc/swaps excludes the swap header page, so sizes are compared with a one-page tolerance.
31 const port = Number(execFileSync("docker", ["port", container, "22/tcp"], { encoding: "utf8" }).trim().split(":").pop());
32 for (let attempt = 0; attempt < 50 && spawnSync("docker", ["exec", container, "test", "-s", "/run/sshd.pid"]).status !== 0; attempt += 1) await new Promise((resolve) => setTimeout(resolve, 200));
33 // CHJ_SWAP_SECOND_FS=btrfs forces Btrfs for the second file system.
34 // Loop file systems: ext4 at /swap (3 GiB sparse), XFS at /var/lib/swap (when mkfs.xfs exists), Btrfs at /var/lib/swap only if no XFS.
35 sh("mkdir -p /swap /var/lib/swap && truncate -s 3G /img-ext4 && mkfs.ext4 -q /img-ext4 && mount -o loop /img-ext4 /swap");
36 const xfs = process.env.CHJ_SWAP_SECOND_FS !== "btrfs" && sh("command -v mkfs.xfs >/dev/null && truncate -s 3G /img-xfs && mkfs.xfs -q /img-xfs && mount -o loop /img-xfs /var/lib/swap && echo yes || echo no", { allowFailure: true }) === "yes";
37 const btrfs = !xfs && sh("command -v mkfs.btrfs >/dev/null && truncate -s 3G /img-btrfs && mkfs.btrfs -q /img-btrfs && mount -o loop /img-btrfs /var/lib/swap && echo yes || echo no", { allowFailure: true }) === "yes";
38 sh("printf '# keep me\\nproc /proc proc defaults 0 0\\n' > /etc/fstab");
39 const fstabBefore = sh("cat /etc/fstab");
40 t.diagnostic(`${image}: second file system ${xfs ? "XFS" : btrfs ? "Btrfs" : "none"}; kernel ${sh("uname -r")}`);
42 const hostKeys = new Map();
43 const profiles = new Map([
44 ["admin", { id: "admin", label: "it", host: "127.0.0.1", port, username: "admin", authMethod: "password" }],
45 ["plain", { id: "plain", label: "it", host: "127.0.0.1", port, username: "plain", authMethod: "password" }]
46 ]);
47 const passwords = { admin: "AdminPass-1", plain: "PlainPass-1" };
48 const profileService = { get: (id) => profiles.get(id), getHostKey: (_h, p) => hostKeys.get(p) || null, trustHostKey: (_h, p, fp) => hostKeys.set(p, fp), getStoredPassword: (id) => passwords[id], markUsed() {} };
49 const manager = new SessionManager({ profileService, latencyMonitor: { start: () => () => {} } });
50 const executor = new PrivilegedExecutor({ sessionManager: manager });
51 const service = new VirtualMemoryService({ sessionManager: manager, privilegedExecutor: executor });
52 t.after(() => manager.disconnectAll("test"));
53 for (const id of ["admin", "plain"]) {
54 try { await manager.connect({ sessionId: id, profileId: id }); }
55 catch (error) { manager.trustPending({ sessionId: id, profileId: id, fingerprint: error.fingerprint }); await manager.connect({ sessionId: id, profileId: id }); }
56 }
57 const auth = { sudoPassword: "AdminPass-1" };
59 await t.test("Linux metrics keep their fields and add non-overlapping memory detail", async () => {
60 const metrics = await manager.readSystemMetrics("admin");
61 assert.equal(metrics.os, "linux");
62 assert.ok(metrics.cpu.logicalCores > 0); assert.ok(metrics.memory.total > 0); assert.equal(metrics.loadAverageSupported, true);
63 const detail = metrics.memoryDetail;
64 assert.ok(detail.total > 0 && detail.available !== null);
65 const sum = detail.used + detail.free + detail.buffers + detail.cached + detail.slabReclaimable;
66 assert.ok(Math.abs(sum - detail.total) < 1024 * 1024, "slices add up to MemTotal without double counting");
67 });
69 await t.test("read-only inspection works without sudo; a user without sudo cannot change swap", async () => {
70 const state = await service.read("plain");
71 assert.equal(state.platform, "linux"); assert.equal(state.root, false);
72 assert.ok(Array.isArray(state.swapAreas));
73 await assert.rejects(() => service.manage("plain", { action: "create", path: "/swap/x", sizeMiB: 64, persist: false, sudoPassword: "PlainPass-1" }), { code: "SUDO_NOT_PERMITTED" });
74 assert.equal((await service.read("plain")).canManage, false);
75 });
77 await t.test("invalid requests are refused before anything runs on the server", async () => {
78 for (const [payload, code] of [
79 [{ action: "create", path: "/swap/a", sizeMiB: 10, persist: false }, "VM_SWAP_SIZE_INVALID"],
80 [{ action: "create", path: "/swap/a", sizeMiB: 70000, persist: false }, "VM_SWAP_SIZE_INVALID"],
81 [{ action: "create", path: "/swap/a", sizeMiB: "64; reboot", persist: false }, "VM_SWAP_SIZE_INVALID"],
82 [{ action: "create", path: "/etc/passwd", sizeMiB: 64, persist: false }, "VM_SWAP_PATH_INVALID"],
83 [{ action: "delete", path: "/swap/../etc/shadow" }, "VM_SWAP_PATH_INVALID"],
84 [{ action: "create", path: "/swap/a", sizeMiB: 64 }, "VM_ARGUMENTS_INVALID"],
85 [{ action: "priority", path: "/swap/a", priority: 99999 }, "VM_SWAP_PRIORITY_INVALID"],
86 [{ action: "wipe", path: "/swap/a" }, "VM_ACTION_INVALID"]
87 ]) await assert.rejects(() => service.manage("admin", { ...payload, ...auth }), { code }, JSON.stringify(payload));
88 await assert.rejects(() => service.manage("admin", { action: "create", path: "/swapfile", sizeMiB: 64, persist: false, ...auth }), { code: "VM_SWAP_FS_UNSUPPORTED" }, "overlay root file system is refused");
89 await assert.rejects(() => service.manage("admin", { action: "create", path: "/swap/big", sizeMiB: 2048, persist: false, ...auth }), { code: "VM_SWAP_DISK_FULL" });
90 assert.equal(sh("ls /swap | grep -v lost+found || true"), "", "nothing was left behind");
91 });
93 await t.test("create, persist with fstab backup, resize, priority, second area and delete", async () => {
94 let result = await service.manage("admin", { action: "create", path: "/swap/a", sizeMiB: 128, priority: 5, persist: true, ...auth });
95 assert.ok(result.active.some((area) => area.path === "/swap/a" && area.priority === 5 && Math.abs(area.size - 128 * 1024 * 1024) <= 65536));
96 assert.match(sh("cat /etc/fstab"), /^\/swap\/a none swap sw,pri=5 0 0 # CH-J chj\.system-monitor$/m);
97 assert.ok(sh("cat /etc/fstab").startsWith(fstabBefore), "unrelated fstab lines are kept");
98 assert.match(sh("ls /etc/fstab.chj-backup-*"), /fstab\.chj-backup-/);
99 assert.equal(sh("stat -c '%a %U' /swap/a"), "600 root");
100 result = await service.manage("admin", { action: "resize", path: "/swap/a", sizeMiB: 192, priority: 5, ...auth });
101 assert.ok(result.active.some((area) => area.path === "/swap/a" && Math.abs(area.size - 192 * 1024 * 1024) <= 65536), "resized and active again");
102 result = await service.manage("admin", { action: "priority", path: "/swap/a", priority: 7, ...auth });
103 assert.ok(result.active.some((area) => area.path === "/swap/a" && area.priority === 7));
104 assert.match(sh("cat /etc/fstab"), /pri=7/);
105 if (xfs || btrfs) {
106 result = await service.manage("admin", { action: "create", path: "/var/lib/swap/b", sizeMiB: 96, priority: 1, persist: false, ...auth });
107 assert.ok(result.active.some((area) => area.path === "/var/lib/swap/b"), `${xfs ? "XFS" : "Btrfs"} swap file works`);
108 const state = await service.read("admin", auth);
109 assert.equal(state.swapAreas.filter((area) => area.active).length >= 2, true, "several active swap areas are listed");
110 assert.equal(state.swapAreas.find((area) => area.path === "/var/lib/swap/b").fsType, xfs ? "xfs" : "btrfs");
111 assert.equal(state.swapAreas.find((area) => area.path === "/swap/a").persistent, true);
112 await service.manage("admin", { action: "delete", path: "/var/lib/swap/b", ...auth });
113 }
114 result = await service.manage("admin", { action: "deactivate", path: "/swap/a", ...auth });
115 assert.equal(result.active.some((area) => area.path === "/swap/a"), false);
116 assert.match(sh("cat /etc/fstab"), /\/swap\/a/, "deactivate keeps persistence unless requested");
117 result = await service.manage("admin", { action: "activate", path: "/swap/a", priority: 3, ...auth });
118 assert.ok(result.active.some((area) => area.path === "/swap/a" && area.priority === 3));
119 result = await service.manage("admin", { action: "delete", path: "/swap/a", ...auth });
120 assert.equal(sh("test -e /swap/a && echo yes || echo no"), "no");
121 assert.equal(sh("cat /etc/fstab"), fstabBefore, "fstab back to the original content");
122 });
124 await t.test("concurrent requests are serialized per session and the server lock is released", async () => {
125 const first = service.manage("admin", { action: "create", path: "/swap/c", sizeMiB: 64, persist: false, ...auth });
126 await assert.rejects(() => service.manage("admin", { action: "create", path: "/swap/d", sizeMiB: 64, persist: false, ...auth }), { code: "VM_BUSY" });
127 await first;
128 await service.manage("admin", { action: "delete", path: "/swap/c", ...auth });
129 assert.equal(sh("test -d /run/chj-swap.lock && echo locked || echo free"), "free");
130 });
132 await t.test("a lost SSH connection during an operation is reported, not hidden", async () => {
133 const pending = service.manage("admin", { action: "create", path: "/swap/e", sizeMiB: 1536, persist: false, ...auth });
134 setTimeout(() => manager.disconnect("admin", "test-drop"), 100);
135 await assert.rejects(() => pending, { code: "SESSION_NOT_CONNECTED" });
136 sh("swapoff /swap/e 2>/dev/null; rm -f /swap/e /swap/.e.chj-new; rmdir /run/chj-swap.lock 2>/dev/null; true");
137 });
138});
SHA-256eb9bc74fbdb32522a4d7672742d43428eb93c1ac39c37206934946eb2dc80f33

CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

SHA-256 des Archivsa9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Dateien