CH-J Server ManagerZarządzanie serwerem przez SSH
Menu
Opublikowane źródło

CH-J Server Manager

Przeglądaj katalogi i pliki dla konkretnego wydania aplikacji.

Pobierz źródło ZIP
CH-J / Server Manager 377 pliki
core-20261019T111616Z-160a00bfcddc Dodano: · Built:
smokeSystemMonitorUi.js Dodano: Data niezanotowana 16,1 KB · 190 linie
Kod Pobierz plik PDF
1"use strict";
2// End-to-end UI check of chj.system-monitor with the real plugin runtime and preload.
3// Linux (default): disposable privileged Docker container (image CHJ_USERS_IMAGE, default chj-users-it-debian);
4// creates and deletes a swap file on a loop-mounted ext4 through the UI forms (sudo dialog).
5// Windows: CHJ_SMOKE_WINDOWS=1 with CHJ_USERS_WINDOWS_HOST/_USER/_FINGERPRINT and _PASSWORD or _KEY;
6// sets a manual pagefile (pending restart only) and restores automatic management. Never restarts Windows.
7// Isolated Linux VM: CHJ_SMOKE_VM=1 with CHJ_VM_HOST/_FINGERPRINT/_KEY and CHJ_VM_SUDO_PASSWORD or _FILE + _LINE;
8// creates two swap files, sets main/backup priority through the priority dialog, then deletes both.
9// Run: npx electron scripts/smokeSystemMonitorUi.js [screenshot-dir]
10const { app, BrowserWindow, ipcMain, protocol } = require("electron");
11const assert = require("node:assert/strict");
12const fs = require("node:fs");
13const os = require("node:os");
14const path = require("node:path");
15const { execFileSync, spawnSync } = require("node:child_process");
16const { hardenSession } = require("../src/main/bootstrap/createMainWindow");
17const { PluginRegistry } = require("../src/main/plugins/pluginRegistry");
18const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");
19const { SessionManager } = require("../src/main/sessions/sessionManager");
20const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
21const { VirtualMemoryService } = require("../src/main/system/virtualMemoryService");
23const env = process.env;
24const windows = env.CHJ_SMOKE_WINDOWS === "1";
25const linuxVm = env.CHJ_SMOKE_VM === "1";
26const vmSudo = !linuxVm ? "" : env.CHJ_VM_SUDO_PASSWORD
27 || fs.readFileSync(env.CHJ_VM_SUDO_PASSWORD_FILE, "utf8").split("\n")[Number(env.CHJ_VM_SUDO_PASSWORD_LINE || 1) - 1].split("=").slice(1).join("=").trim();
28const vmShell = (command) => execFileSync("ssh", ["-i", env.CHJ_VM_KEY, "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", `UserKnownHostsFile=${path.join(root, "known_hosts")}`, "-o", "StrictHostKeyChecking=yes", `${env.CHJ_VM_USER || "ai"}@${env.CHJ_VM_HOST}`, command], { encoding: "utf8" });
29const container = `chj-sysmon-ui-${process.pid}`;
30const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-sysmon-ui-"));
31const shots = process.argv.find((arg, index) => index > 1 && !arg.startsWith("-") && !arg.endsWith(".js")) || null;
32app.setPath("userData", root);
33protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]);
34const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
35const errors = [];
36let window;
37let restoreWindows = async () => {};
38const js = (source) => window.webContents.executeJavaScript(source);
39async function until(source, timeout = 60000) {
40 const started = Date.now();
41 while (Date.now() - started < timeout) { if (await js(`(() => { try { return (${source}); } catch { return false; } })()`)) return; await delay(100); }
42 throw new Error(`UI condition timed out: ${source}\nstatus: ${await js("document.querySelector('#status').textContent + ' | ' + (document.querySelector('#vmStatus')?.textContent || '')")}`);
44async function shot(name) { if (shots) { fs.mkdirSync(shots, { recursive: true }); fs.writeFileSync(path.join(shots, `${name}.png`), (await window.webContents.capturePage()).toPNG()); } }
45async function confirm() { await until("document.querySelector('#confirmDialog').open"); await js("document.querySelector('#confirmForm').requestSubmit()"); }
46async function sudo() { await until("document.querySelector('#sudoDialog').open"); await js(`document.querySelector('#sudoPassword').value = ${JSON.stringify(linuxVm ? vmSudo : "AdminPass-1")}; document.querySelector('#sudoForm').requestSubmit();`); }
47// The sudo dialog appears only when Core reports that a password is needed (once per operation here).
48async function maybeSudo(doneSource) {
49 for (const started = Date.now(); Date.now() - started < 120000; await delay(100)) {
50 if (await js("document.querySelector('#sudoDialog').open")) { await sudo(); return; }
51 if (await js(`(() => { try { return (${doneSource}); } catch { return false; } })()`)) return;
52 }
55async function run() {
56 let profile; let password = "";
57 if (windows) {
58 profile = { id: "w", label: "Windows", host: env.CHJ_USERS_WINDOWS_HOST, port: 22, username: env.CHJ_USERS_WINDOWS_USER, authMethod: env.CHJ_USERS_WINDOWS_KEY ? "privateKey" : "password", privateKeyPath: env.CHJ_USERS_WINDOWS_KEY };
59 password = env.CHJ_USERS_WINDOWS_PASSWORD || "";
60 } else if (linuxVm) {
61 fs.writeFileSync(path.join(root, "known_hosts"), execFileSync("ssh-keyscan", ["-t", "ed25519", env.CHJ_VM_HOST], { encoding: "utf8" }));
62 const seen = execFileSync("ssh-keygen", ["-lf", path.join(root, "known_hosts")], { encoding: "utf8" });
63 assert.ok(seen.includes(env.CHJ_VM_FINGERPRINT), "VM host key matches the recorded fingerprint");
64 profile = { id: "vm", label: "Linux VM", host: env.CHJ_VM_HOST, port: 22, username: env.CHJ_VM_USER || "ai", authMethod: "privateKey", privateKeyPath: env.CHJ_VM_KEY };
65 } else {
66 execFileSync("docker", ["run", "-d", "--rm", "--privileged", "--name", container, "-p", "127.0.0.1::22", env.CHJ_USERS_IMAGE || "chj-users-it-debian"]);
67 await delay(1500);
68 spawnSync("docker", ["exec", container, "sh", "-c", "mkdir -p /swap && truncate -s 3G /img && mkfs.ext4 -q /img && mount -o loop /img /swap && : > /etc/fstab"]);
69 const port = Number(execFileSync("docker", ["port", container, "22/tcp"], { encoding: "utf8" }).trim().split(":").pop());
70 profile = { id: "l", label: "Linux test", host: "127.0.0.1", port, username: "admin", authMethod: "password" };
71 password = "AdminPass-1";
72 }
73 await app.whenReady(); hardenSession();
74 const hostKeys = new Map();
75 if (windows) hostKeys.set(profile.port, Buffer.from(`${env.CHJ_USERS_WINDOWS_FINGERPRINT}=`, "base64").toString("hex"));
76 if (linuxVm) hostKeys.set(profile.port, Buffer.from(`${env.CHJ_VM_FINGERPRINT.replace(/^SHA256:/, "")}=`, "base64").toString("hex"));
77 const profileService = { get: () => profile, getHostKey: (_h, p) => hostKeys.get(p) || null, trustHostKey: (_h, p, fp) => hostKeys.set(p, fp), getStoredPassword: () => password, markUsed() {} };
78 const sessionManager = new SessionManager({ profileService, latencyMonitor: { start: () => () => {} } });
79 try { await sessionManager.connect({ sessionId: "ui", profileId: profile.id }); }
80 catch (error) { sessionManager.trustPending({ sessionId: "ui", profileId: profile.id, fingerprint: error.fingerprint }); await sessionManager.connect({ sessionId: "ui", profileId: profile.id }); }
81 const virtualMemory = new VirtualMemoryService({ sessionManager, privilegedExecutor: new PrivilegedExecutor({ sessionManager }) });
82 const original = windows ? await virtualMemory.read("ui") : null;
83 // Restores automatic pagefile management even when an assertion fails.
84 if (linuxVm) restoreWindows = async () => { for (const name of ["/swapfile-chjui", "/swapfile-chjui2"]) { try { await virtualMemory.manage("ui", { action: "delete", path: name, sudoPassword: vmSudo }); } catch {} } };
85 if (windows) restoreWindows = async () => { if (windows && original?.automatic) { const now = await virtualMemory.read("ui"); if (now.automatic !== true) await virtualMemory.manage("ui", { action: "automatic" }); } };
86 const registry = new PluginRegistry(root, null, { bundledRoot: path.join(__dirname, "../src/main/firstPartyPlugins") });
87 const runtime = new PluginRuntime({ BrowserWindow, registry, sessionManager, virtualMemory, preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => null, isVaultUnlocked: () => true });
88 runtime.registerIpc(ipcMain);
89 runtime.open("chj.system-monitor");
90 window = runtime.windows.get("chj.system-monitor");
91 window.webContents.on("console-message", (event) => { if (event.level === "error") errors.push(event.message); });
92 window.setSize(1320, 1000);
93 await until("document.querySelector('#status').textContent.includes('Aktualizováno')");
94 await delay(5600);
95 await until("document.querySelector('#cpu').textContent.includes('%')", 20000);
96 if (windows) {
97 assert.equal(await js("document.querySelector('#swapLabel').textContent"), "Pagefile");
98 assert.equal(await js("document.querySelector('#kernelLabel').textContent"), "Sestavení");
99 assert.equal(await js("document.querySelector('#load').textContent"), "nedostupné", "no fake load average");
100 } else {
101 assert.equal(await js("document.querySelector('#diskLabel').textContent"), "Disk /");
102 }
103 await js("document.querySelector('#memorySection').open = true");
104 await delay(400);
105 assert.ok(await js("document.querySelectorAll('#ramStack i').length >= 4"), "RAM breakdown rendered");
106 await shot(windows ? "s01-windows-overview" : "s01-linux-overview");
107 await js("document.querySelector('#memorySection').scrollIntoView()");
108 await shot(windows ? "s02-windows-memory" : "s02-linux-memory");
109 await js("document.querySelector('#manageVmButton').click()");
110 await until("document.querySelector('#vmDialog').open && document.querySelector('#vmContent table')");
111 await shot(windows ? "s03-windows-manage" : "s03-linux-manage");
112 if (windows) {
113 await js("document.querySelector('#pfInitialValue').value = '1024'; document.querySelector('#pfMaximumValue').value = '2048'; Array.from(document.querySelectorAll('#vmContent button')).find(b => b.textContent.includes('Uložit')).click();");
114 await confirm();
115 await until("document.querySelector('#vmStatus').textContent.toLowerCase().includes('čeká na restart')", 120000);
116 assert.match(await js("document.querySelector('#vmContent').textContent"), /Čeká na restart/);
117 await shot("s04-windows-pending");
118 if (original.automatic) {
119 await js("Array.from(document.querySelectorAll('#vmContent button')).find(b => b.textContent.includes('automatickou')).click()");
120 await confirm();
121 await until("document.querySelector('#vmStatus').textContent.includes('Uloženo')", 120000);
122 const restored = await virtualMemory.read("ui");
123 assert.equal(restored.automatic, true); assert.equal(restored.restartPending, false, "reverting clears the pending state");
124 }
125 } else if (linuxVm) {
126 const names = ["/swapfile-chjui", "/swapfile-chjui2"];
127 const row = (name) => `Array.from(document.querySelectorAll('#vmContent tbody tr')).find(r => r.cells[0].textContent === '${name}')`;
128 for (const name of names) {
129 await js(`document.querySelector('#swapDir').value = '/'; document.querySelector('#swapName').value = '${name.slice(1)}'; document.querySelector('#swapSizeValue').value = '128'; document.querySelector('#swapSizeUnit').value = 'MiB'; document.querySelector('#swapPriority').value = ''; document.querySelector('#swapPersist').checked = true;`);
130 await js("Array.from(document.querySelectorAll('#vmContent button')).find(b => b.textContent.includes('Vytvořit')).click()");
131 await confirm(); await maybeSudo("document.querySelector('#vmStatus').textContent.includes('Provedeno')");
132 await until("document.querySelector('#vmStatus').textContent.includes('Provedeno')", 120000);
133 await until(`${row(name)}`);
134 }
135 assert.match(await js(`${row(names[0])}.cells[4].textContent`), /-\d+ \(automatická\)/, "automatic negative priority shown as automatic");
136 // Main = 100, backup = 10 through the dialog.
137 for (const [name, value] of [[names[0], 100], [names[1], 10]]) {
138 await js(`Array.from(${row(name)}.querySelectorAll('button')).find(b => b.textContent.startsWith('Priorita')).click()`);
139 await until("document.querySelector('#priorityDialog').open");
140 assert.equal(await js("document.querySelector('#priorityAuto').checked"), true);
141 assert.equal(await js("document.querySelector('#priorityValue').disabled"), true);
142 assert.match(await js("document.querySelector('#priorityFacts').textContent"), /Uložená priorita: automatická/);
143 await js("document.querySelector('#priorityCustom').click(); document.querySelector('#priorityValue').value = '-5'; document.querySelector('#priorityForm').requestSubmit();");
144 assert.equal(await js("document.querySelector('#priorityError').hidden"), false, "negative custom value is refused");
145 if (name === names[0]) await shot("s05-linux-priority-dialog");
146 await js(`document.querySelector('#priorityValue').value = '${value}'; document.querySelector('#priorityNow').checked = true; document.querySelector('#priorityForm').requestSubmit();`);
147 await maybeSudo("document.querySelector('#vmStatus').textContent.includes('Uloženo do /etc/fstab')");
148 await until("document.querySelector('#vmStatus').textContent.includes('Uloženo do /etc/fstab') && document.querySelector('#vmStatus').textContent.includes('Aktivní priorita je nyní " + value + "')", 120000);
149 }
150 await until(`${row(names[0])}.cells[4].textContent.includes('1. pořadí') && ${row(names[1])}.cells[4].textContent.includes('2. pořadí')`);
151 await shot("s06-linux-priorities");
152 const swaps = vmShell("cat /proc/swaps"), fstab = vmShell("cat /etc/fstab");
153 assert.match(swaps, /\/swapfile-chjui\s+file\s+\d+\s+\d+\s+100/); assert.match(swaps, /\/swapfile-chjui2\s+file\s+\d+\s+\d+\s+10\b/);
154 assert.match(fstab, /^\/swapfile-chjui none swap sw,pri=100 0 0 # CH-J chj\.system-monitor$/m); assert.match(fstab, /^\/swapfile-chjui2 none swap sw,pri=10 0 0 # CH-J chj\.system-monitor$/m);
155 // Reopening shows the custom mode with the saved value.
156 await js(`Array.from(${row(names[0])}.querySelectorAll('button')).find(b => b.textContent.startsWith('Priorita')).click()`);
157 await until("document.querySelector('#priorityDialog').open");
158 assert.equal(await js("document.querySelector('#priorityCustom').checked && document.querySelector('#priorityValue').value"), "100");
159 await js("document.querySelector('#priorityDialog [data-close]').click()");
160 for (const name of names) {
161 await js(`${row(name)}.querySelector('button.danger').click()`);
162 await confirm(); await maybeSudo("!document.querySelector('#vmContent').textContent.includes('" + name + " ') && document.querySelector('#vmStatus').textContent.includes('Provedeno')");
163 await until(`!${row(name)} && document.querySelector('#vmStatus').textContent.includes('Provedeno')`, 120000);
164 }
165 assert.doesNotMatch(vmShell("cat /proc/swaps; cat /etc/fstab"), /swapfile-chjui/);
166 } else {
167 await js("document.querySelector('#swapDir').value = '/swap'; document.querySelector('#swapName').value = 'ui-swap'; document.querySelector('#swapSizeValue').value = '256'; document.querySelector('#swapSizeUnit').value = 'MiB'; document.querySelector('#swapPersist').checked = true;");
168 await js("Array.from(document.querySelectorAll('#vmContent button')).find(b => b.textContent.includes('Vytvořit')).click()");
169 await confirm(); await sudo();
170 await until("document.querySelector('#vmStatus').textContent.includes('Provedeno')", 120000);
171 assert.match(spawnSync("docker", ["exec", container, "cat", "/proc/swaps"], { encoding: "utf8" }).stdout, /\/swap\/ui-swap/);
172 await until("document.querySelector('#vmContent').textContent.includes('/swap/ui-swap')");
173 await shot("s04-linux-created");
174 await js("Array.from(document.querySelectorAll('#vmContent tr')).find(r => r.textContent.includes('/swap/ui-swap')).querySelector('button.danger').click()");
175 await confirm(); await sudo();
176 await until("document.querySelector('#vmStatus').textContent.includes('Provedeno')", 120000);
177 assert.doesNotMatch(spawnSync("docker", ["exec", container, "cat", "/proc/swaps"], { encoding: "utf8" }).stdout, /ui-swap/);
178 assert.equal(spawnSync("docker", ["exec", container, "cat", "/etc/fstab"], { encoding: "utf8" }).stdout, "");
179 }
180 assert.deepEqual(errors, [], "no renderer errors");
181 await sessionManager.disconnectAll();
182 console.log(`chj.system-monitor ${windows ? "Windows" : linuxVm ? "Linux VM (priorities)" : "Linux"} UI smoke passed`);
185function cleanup() { if (!windows && !linuxVm) spawnSync("docker", ["rm", "-f", container]); fs.rmSync(root, { recursive: true, force: true }); }
186run().then(() => { cleanup(); app.exit(0); }, async (error) => {
187 console.error(error);
188 try { await restoreWindows(); } catch (restoreError) { console.error("Pagefile restore failed", restoreError); }
189 cleanup(); app.exit(1);
190});
SHA-256922f0daf02ba86a98f7a3020df8d2152f2cf0a31260d9e5b883ac5e30e303104

CH-J Proprietary Software License 1.14 · Źródło jest dostarczane pod CH-J Proprietary Software License 1.14. Jego dostępność nie zmienia warunków licencji ani nie przyznaje dodatkowych praw.

SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Pliki