Julkaistu lähde
Lataa lähde ZIP
CH-J Server Manager
Selaile hakemistoja ja tiedostoja tietyn sovelluksen julkaisua varten.
CH-J / Server Manager
377 tiedostot
core-20261019T111616Z-160a00bfcddc
Lisätty: · Rakennettu:
1
"use strict";3
// Destructive end-to-end tests of System Monitor metrics and swap management in a disposable, PRIVILEGED4
// Docker container (swapon/loop devices). Swap files live on loop-mounted ext4/XFS/Btrfs images inside the5
// container. Run explicitly: CHJ_USERS_INTEGRATION=1 CHJ_USERS_IMAGE=chj-users-it-debian node --test test/systemMonitorIntegration.test.js6
const test = require("node:test");7
const assert = require("node:assert/strict");8
const { execFileSync, spawnSync } = require("node:child_process");9
const { SessionManager } = require("../src/main/sessions/sessionManager");10
const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");11
const { VirtualMemoryService } = require("../src/main/system/virtualMemoryService");13
const enabled = process.env.CHJ_USERS_INTEGRATION === "1";14
const image = process.env.CHJ_USERS_IMAGE || "chj-users-it-debian";15
const skip = enabled ? false : "Set CHJ_USERS_INTEGRATION=1 to run destructive swap tests in a disposable privileged container.";16
const container = `chj-swap-it-${process.pid}`;18
function 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();22
}24
test("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-256
eb9bc74fbdb32522a4d7672742d43428eb93c1ac39c37206934946eb2dc80f33CH-J Proprietary Software License 1.14 · Lähde ilmoitetaan CH-J Proprietary Software License 1.14-koodilla. Sen saatavuus ei muuta lisenssiehtoja eikä myönnä lisäoikeuksia.
Arkisto SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a