CH-J Server Managerpengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J Server Manager

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / Server Manager 368 fail
core-20261017T201717Z-057348c4a70f Ditambah: Tarikh tidak direkodkan · Dibina:
virtualMemoryService.js Ditambah: Tarikh tidak direkodkan 15.7 KB · 229 baris
Kod Muat turun fail
1"use strict";
3const fs = require("node:fs");
4const path = require("node:path");
5const { SessionError } = require("../sessions/sessionManager");
7// Virtual memory configuration for chj.system-monitor: Linux swap (files, partitions, zram, zswap) and Windows
8// pagefiles. Reads never change anything; changes go only through fixed operations with validated arguments.
9const SWAP_SCRIPT = fs.readFileSync(path.join(__dirname, "remoteSwap.sh"), "utf8");
10const PAGEFILE_SCRIPT = fs.readFileSync(path.join(__dirname, "windowsPagefile.ps1"), "utf8");
11const MANAGED_PATH = /^\/(swapfile[A-Za-z0-9._-]{0,32}|swap\/[A-Za-z0-9._-]{1,64}|var\/lib\/swap\/[A-Za-z0-9._-]{1,64})$/;
12const ANY_SWAP_PATH = /^\/[A-Za-z0-9._/-]{1,199}$/;
13const LINUX_ACTIONS = new Set(["create", "activate", "deactivate", "priority", "persist", "resize", "delete"]);
14const WINDOWS_ACTIONS = new Set(["automatic", "set", "remove"]);
16function fail(code, message) { return new SessionError(code, message); }
17const text = (value, limit = 300) => String(value ?? "").replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, limit);
18const number = (value) => { const result = Number(value); return value === "" || value === null || value === undefined || !Number.isFinite(result) ? null : result; };
20function splitSections(stdout) {
21 const sections = {};
22 const pattern = /\x1eCHJ_SECTION=([a-z]+)\r?\n/g;
23 let current = null; let start = 0; let match;
24 while ((match = pattern.exec(stdout))) {
25 if (current) sections[current] = stdout.slice(start, match.index);
26 current = match[1]; start = pattern.lastIndex;
27 }
28 if (current) sections[current] = stdout.slice(start);
29 return sections;
31const lines = (value) => String(value || "").split(/\r?\n/).filter(Boolean);
33function parseProcSwaps(textValue) {
34 return lines(textValue).slice(1).map((line) => {
35 const [name = "", type = "", size = "", used = "", priority = ""] = line.trim().split(/\s+/);
36 const pathName = name.replace(/\\040/g, " ");
37 return { path: text(pathName, 200), type: /^\/dev\/zram\d+$/.test(pathName) ? "zram" : type === "file" ? "file" : type === "partition" ? "partition" : text(type, 16), size: Number(size) * 1024, used: Number(used) * 1024, priority: Number(priority) };
38 });
41function parseLinuxInspect(stdout) {
42 const sections = splitSections(stdout);
43 const meta = Object.fromEntries(lines(sections.meta).map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]));
44 const meminfo = Object.fromEntries(lines(sections.meminfo).map((line) => { const [key, value] = line.split(/:\s+/); return [key, Number.parseInt(value, 10) * 1024]; }));
45 const active = parseProcSwaps(sections.swaps);
46 const fstab = lines(sections.fstab).map((line) => {
47 const index = line.indexOf("|");
48 const raw = line.slice(index + 1);
49 const fields = raw.trim().split(/\s+/);
50 const options = fields[3] || "";
51 const priority = options.match(/(?:^|,)pri=(-?\d+)/);
52 return { line: Number(line.slice(0, index)), spec: text(fields[0], 200), type: text(fields[2], 16), options: text(options, 120), priority: priority ? Number(priority[1]) : null, commented: raw.trim().startsWith("#"), managedByChj: /# CH-J chj\.system-monitor/.test(raw) };
53 }).filter((entry) => entry.type === "swap" && !entry.commented);
54 const units = lines(sections.units).map((line) => { const [unit, active, sub] = line.split("|"); return { unit: text(unit, 120), active: text(active, 20), state: text(sub, 20) }; });
55 const files = lines(sections.files).map((line) => {
56 const [pathName, kind, fsType, size, owner, mode, managed, free] = line.split("|");
57 return { path: text(pathName, 200), kind, fsType: fsType === "-" ? null : text(fsType, 24), size: number(size), owner: owner === "0" && kind !== "file" ? null : text(owner, 32), mode: mode === "-" ? null : text(mode, 8), managed: managed === "managed", diskFree: number(free) };
58 });
59 const locations = lines(sections.locations).map((line) => { const [dir, fsType, free] = line.split("|"); return { dir, fsType: text(fsType, 24), free: number(free) }; });
60 const zram = lines(sections.zram).map((line) => { const [device, diskSize, stat] = line.split("|"); const values = String(stat || "").split(",").map(Number); return { device, diskSize: number(diskSize), originalData: values[0] ?? null, compressedData: values[1] ?? null, memoryUsed: values[2] ?? null }; });
61 const areas = new Map();
62 for (const entry of active) areas.set(entry.path, { ...entry, active: true });
63 for (const file of files) if (!areas.has(file.path)) areas.set(file.path, { path: file.path, type: file.kind, size: file.size, used: 0, priority: null, active: false });
64 for (const entry of fstab) if (entry.spec.startsWith("/") && !areas.has(entry.spec)) areas.set(entry.spec, { path: entry.spec, type: entry.spec.startsWith("/dev/") ? "partition" : "file", size: null, used: 0, priority: entry.priority, active: false });
65 const swapAreas = [...areas.values()].map((area) => {
66 const file = files.find((candidate) => candidate.path === area.path);
67 const persistent = fstab.find((entry) => entry.spec === area.path) || null;
68 const unit = units.find((candidate) => candidate.unit === `${area.path.replace(/^\//, "").replace(/-/g, "\\x2d").replace(/\//g, "-")}.swap`) || null;
69 const management = area.type === "zram" ? "read-only" : area.type === "partition" ? "partition" : file?.managed || MANAGED_PATH.test(area.path) ? "managed" : "foreign-file";
70 return {
71 ...area,
72 fsType: file?.fsType || null,
73 mode: file?.mode || null,
74 owner: file?.owner || null,
75 persistent: Boolean(persistent),
76 persistentPriority: persistent?.priority ?? null,
77 persistentByChj: persistent?.managedByChj === true,
78 systemdUnit: unit,
79 management,
80 // Partitions are never deleted or resized; zram is read-only in this phase.
81 allowedActions: management === "read-only" ? [] : management === "partition" ? ["activate", "deactivate", "priority", "persist"]
82 : management === "foreign-file" ? ["activate", "deactivate", "priority", "persist"] : ["activate", "deactivate", "priority", "persist", "resize", "delete"]
83 };
84 });
85 return {
86 platform: "linux",
87 root: meta.root === "1",
88 systemd: meta.systemd === "1",
89 btrfsMkswapfile: meta.btrfsMkswapfile === "1",
90 os: text(meta.os, 120),
91 memory: { total: meminfo.MemTotal ?? null, available: meminfo.MemAvailable ?? null, swapTotal: meminfo.SwapTotal ?? null, swapFree: meminfo.SwapFree ?? null },
92 swapAreas,
93 fstab,
94 units,
95 locations,
96 zram
97 };
100function parseSteps(stdout) {
101 return [...String(stdout).matchAll(/\x1eCHJ_STEP=([a-z-]+)\|([a-z]+)\|([^\n]*)\n/g)].map((match) => ({ step: match[1], status: match[2], message: text(match[3]) }));
104function mapWindowsState(raw) {
105 const mb = (value) => (number(value) === null ? null : Number(value) * 1048576);
106 const configured = (raw.configured || []).map((entry) => ({ name: text(entry.name, 260), initial: mb(entry.initialMb), maximum: mb(entry.maximumMb), systemManagedSize: Number(entry.initialMb) === 0 && Number(entry.maximumMb) === 0 }));
107 const active = (raw.active || []).map((entry) => ({ name: text(entry.name, 260), allocated: mb(entry.allocatedMb), used: mb(entry.currentMb), peak: mb(entry.peakMb) }));
108 const normalize = (value) => String(value || "").toLowerCase().replace(/^\\\?\?\\/, "").split(" ")[0];
109 const registryConfigured = (raw.registryPagingFiles || []).map(normalize).sort();
110 const existing = (raw.existingPageFiles || []).map(normalize).sort();
111 const names = new Set([...configured.map((entry) => entry.name.toLowerCase()), ...active.map((entry) => entry.name.toLowerCase())]);
112 const pagefiles = [...names].map((name) => {
113 const setting = configured.find((entry) => entry.name.toLowerCase() === name) || null;
114 const usage = active.find((entry) => entry.name.toLowerCase() === name) || null;
115 const volume = (raw.volumes || []).find((entry) => name.startsWith(String(entry.drive).toLowerCase()));
116 return { name: setting?.name || usage?.name, drive: (setting?.name || usage?.name || "").slice(0, 2).toUpperCase(), configured: Boolean(setting), active: Boolean(usage), initial: setting?.initial ?? null, maximum: setting?.maximum ?? null, systemManagedSize: setting?.systemManagedSize ?? null, allocated: usage?.allocated ?? null, used: usage?.used ?? null, peak: usage?.peak ?? null, volumeFree: volume ? volume.free : null };
117 });
118 return {
119 platform: "windows",
120 elevated: raw.elevated === true,
121 automatic: typeof raw.automatic === "boolean" ? raw.automatic : null,
122 pagefiles,
123 // Commit limit = physical memory + pagefiles; it is not the pagefile size.
124 commit: { limit: number(raw.commitLimit), committed: number(raw.committed) },
125 volumes: (raw.volumes || []).map((volume) => ({ drive: text(volume.drive, 3), format: text(volume.format, 12), free: number(volume.free), total: number(volume.total) })),
126 systemDrive: text(raw.systemDrive, 3),
127 crashDumpEnabled: Number(raw.crashDumpEnabled) || 0,
128 restartPending: Boolean(raw.pending) || (raw.automatic === false && registryConfigured.length > 0 && JSON.stringify(registryConfigured) !== JSON.stringify(existing)),
129 pendingChanges: (raw.pending?.changes || []).map((change) => ({ at: change.at, description: text(change.description, 300) })),
130 bootTime: raw.bootTime || null,
131 unavailable: (raw.errors || []).map((entry) => text(entry, 200))
132 };
135class VirtualMemoryService {
136 constructor(options) {
137 this.sessionManager = options.sessionManager;
138 this.executor = options.privilegedExecutor;
139 this.logger = options.logger;
140 this.running = new Set();
141 }
143 async _platform(sessionId) {
144 const record = this.sessionManager._requireConnected(sessionId);
145 const platform = await this.sessionManager._platform(record);
146 if (platform.os === "unknown") throw fail("VM_PLATFORM_UNSUPPORTED", `The remote operating system could not be identified: ${platform.reason || "unknown"}.`);
147 return { record, platform };
148 }
150 async read(sessionId, options = {}) {
151 const { record, platform } = await this._platform(sessionId);
152 if (platform.os === "windows") {
153 const raw = await this.sessionManager._runWindowsScript(record, PAGEFILE_SCRIPT, { op: "inspect" }, 90000).catch((error) => { throw this._windowsError(error); });
154 return { ...mapWindowsState(raw), canManage: raw.elevated === true, readOnlyReason: raw.elevated ? null : "Changing the pagefile requires an SSH session with an elevated administrator token.", collectedAt: new Date().toISOString() };
155 }
156 const privilege = await this.executor.probe(sessionId);
157 const privileged = Boolean(options.sudoPassword) || options.privileged === true || ["root", "sudo-nopasswd"].includes(privilege.mode);
158 const run = await this.executor.run(sessionId, { script: SWAP_SCRIPT, args: ["inspect"], privileged, sudoPassword: options.sudoPassword, purpose: "system.swap.inspect", timeoutMs: 60000, errorPrefix: "VM_" });
159 const state = parseLinuxInspect(run.stdout);
160 return {
161 ...state,
162 privilege,
163 canManage: privilege.mode !== "none",
164 readOnlyReason: privilege.mode === "none" ? "The SSH account has no sudo rights; swap is shown read-only." : null,
165 collectedAt: new Date().toISOString()
166 };
167 }
169 async manage(sessionId, payload = {}) {
170 const { platform } = await this._platform(sessionId);
171 if (this.running.has(sessionId)) throw fail("VM_BUSY", "Another virtual memory change is still running for this session.");
172 this.running.add(sessionId);
173 try {
174 return platform.os === "windows" ? await this._manageWindows(sessionId, payload) : await this._manageLinux(sessionId, payload);
175 } finally { this.running.delete(sessionId); }
176 }
178 async _manageLinux(sessionId, payload) {
179 const action = String(payload.action || "");
180 if (!LINUX_ACTIONS.has(action)) throw fail("VM_ACTION_INVALID", "Unsupported swap action.");
181 const target = String(payload.path || "");
182 if (!ANY_SWAP_PATH.test(target) || target.includes("..")) throw fail("VM_SWAP_PATH_INVALID", "Invalid swap path.");
183 if (["create", "resize", "delete"].includes(action) && !MANAGED_PATH.test(target)) throw fail("VM_SWAP_PATH_INVALID", "CH-J creates, resizes and deletes swap files only as /swapfile*, /swap/<name> or /var/lib/swap/<name>.");
184 if (target.startsWith("/dev/zram")) throw fail("VM_SWAP_ZRAM_READ_ONLY", "zram is shown read-only; CH-J does not configure it.");
185 const size = ["create", "resize"].includes(action) ? Number(payload.sizeMiB) : 0;
186 if (["create", "resize"].includes(action) && (!Number.isInteger(size) || size < 64 || size > 65536)) throw fail("VM_SWAP_SIZE_INVALID", "Size must be a whole number of MiB between 64 MiB and 64 GiB.");
187 const priority = payload.priority === undefined || payload.priority === null || payload.priority === "" ? -1 : Number(payload.priority);
188 if (!Number.isInteger(priority) || priority < -1 || priority > 32767) throw fail("VM_SWAP_PRIORITY_INVALID", "Priority must be -1 (default) or 0-32767.");
189 const persist = payload.persist === true ? "1" : payload.persist === false ? "0" : "keep";
190 if (["create", "persist"].includes(action) && persist === "keep") throw fail("VM_ARGUMENTS_INVALID", "Choose whether the swap space is activated after restart.");
191 const run = await this.executor.run(sessionId, {
192 script: SWAP_SCRIPT, args: ["manage", action, target, String(size), String(priority), persist], privileged: true,
193 sudoPassword: payload.sudoPassword, purpose: `system.swap.${action}`, timeoutMs: 30 * 60 * 1000, errorPrefix: "VM_"
194 });
195 this.logger?.warn("Swap configuration changed.", { sessionId, action, path: target });
196 const state = await this.read(sessionId, { sudoPassword: payload.sudoPassword, privileged: true }).catch(() => null);
197 return { action, path: target, result: "ok", steps: parseSteps(run.stdout), active: parseProcSwaps(splitSections(run.stdout).swaps), state };
198 }
200 async _manageWindows(sessionId, payload) {
201 const action = String(payload.action || "");
202 if (!WINDOWS_ACTIONS.has(action)) throw fail("VM_ACTION_INVALID", "Unsupported pagefile action.");
203 const args = { op: "apply", action };
204 if (action !== "automatic") {
205 args.drive = String(payload.drive || "").toUpperCase();
206 if (!/^[A-Z]:$/.test(args.drive)) throw fail("VM_PAGEFILE_DRIVE_INVALID", "Invalid drive letter.");
207 }
208 if (action === "set") {
209 const initial = Number(payload.initialMb); const maximum = Number(payload.maximumMb);
210 const systemManaged = initial === 0 && maximum === 0;
211 if (!Number.isInteger(initial) || !Number.isInteger(maximum) || (!systemManaged && (initial < 16 || maximum < initial || maximum > 4194304))) {
212 throw fail("VM_PAGEFILE_SIZE_INVALID", "Initial and maximum size must be whole MB values of at least 16 with initial <= maximum, or both 0 for a system-managed size.");
213 }
214 Object.assign(args, { initialMb: initial, maximumMb: maximum });
215 }
216 if (action === "remove") Object.assign(args, { confirmLast: payload.confirmLast === true, confirmCrashDump: payload.confirmCrashDump === true });
217 const record = this.sessionManager._requireConnected(sessionId);
218 const raw = await this.sessionManager._runWindowsScript(record, PAGEFILE_SCRIPT, args, 120000).catch((error) => { throw this._windowsError(error); });
219 this.logger?.warn("Windows pagefile configuration changed (restart pending).", { sessionId, action });
220 return { action, result: raw.result, backup: text(raw.backup, 300), restartRequired: true, state: { ...mapWindowsState(raw.state), canManage: true, collectedAt: new Date().toISOString() } };
221 }
223 _windowsError(error) {
224 const code = String(error?.code || "").replace(/^METRICS_/, "VM_");
225 return code && code !== error.code ? new SessionError(code, error.message) : error;
226 }
229module.exports = { MANAGED_PATH, VirtualMemoryService, mapWindowsState, parseLinuxInspect, parseProcSwaps };
SHA-2565fdce16b12aa6ef216a3714169fb6b02b77d79cc613fe280267b23be83017456

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkibcf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3
Fail