CH-J Server Managerإدارة الخوادم عبر SSH
القائمة
المصدر المنشور

CH-J Server Manager

تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.

تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J / Server Manager 377 ملفات
core-20261019T111616Z-160a00bfcddc تاريخ الإضافة: · تاريخ البناء:
virtualMemoryService.js تاريخ الإضافة: لم يُسجَّل التاريخ ١٧٫٨ KB · 254 أسطر
الشيفرة تنزيل الملف
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 // line|canonical identity|raw line (older helpers sent line|raw).
48 const index = line.indexOf("|"), second = line.indexOf("|", index + 1);
49 const withCanonical = second > index && !/\s/.test(line.slice(index + 1, second));
50 const raw = withCanonical ? line.slice(second + 1) : line.slice(index + 1);
51 const resolved = withCanonical ? text(line.slice(index + 1, second), 200) : null;
52 const fields = raw.trim().split(/\s+/);
53 const options = fields[3] || "";
54 const priority = options.match(/(?:^|,)pri=(-?\d+)/);
55 return { line: Number(line.slice(0, index)), spec: text(fields[0], 200), resolved, 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) };
56 }).filter((entry) => entry.type === "swap" && !entry.commented);
57 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) }; });
58 const files = lines(sections.files).map((line) => {
59 const [pathName, kind, fsType, size, owner, mode, managed, free] = line.split("|");
60 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) };
61 });
62 const locations = lines(sections.locations).map((line) => { const [dir, fsType, free] = line.split("|"); return { dir, fsType: text(fsType, 24), free: number(free) }; });
63 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 }; });
64 const areas = new Map();
65 for (const entry of active) areas.set(entry.path, { ...entry, active: true });
66 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 });
67 const matches = (entry, areaPath) => entry.spec === areaPath || entry.resolved === areaPath;
68 for (const entry of fstab) {
69 const identity = entry.resolved?.startsWith("/") ? entry.resolved : entry.spec;
70 if (identity.startsWith("/") && ![...areas.keys()].some((key) => matches(entry, key))) areas.set(identity, { path: identity, type: identity.startsWith("/dev/") ? "partition" : "file", size: null, used: 0, priority: null, active: false });
71 }
72 const swapAreas = [...areas.values()].map((area) => {
73 const file = files.find((candidate) => candidate.path === area.path);
74 const entries = fstab.filter((entry) => matches(entry, area.path));
75 const persistent = entries[0] || null;
76 // Effective = what the kernel uses now; configured = pri= in fstab (null = automatic). Negative effective
77 // values are kernel-assigned automatic priorities, never user input.
78 const unit = units.find((candidate) => candidate.unit === `${area.path.replace(/^\//, "").replace(/-/g, "\\x2d").replace(/\//g, "-")}.swap`) || null;
79 const management = area.type === "zram" ? "read-only" : area.type === "partition" ? "partition" : file?.managed || MANAGED_PATH.test(area.path) ? "managed" : "foreign-file";
80 return {
81 ...area,
82 fsType: file?.fsType || null,
83 mode: file?.mode || null,
84 owner: file?.owner || null,
85 persistent: Boolean(persistent),
86 persistentSpec: persistent?.spec ?? null,
87 persistentAmbiguous: entries.length > 1,
88 persistentPriority: persistent?.priority ?? null,
89 effectivePriority: area.active ? area.priority : null,
90 priorityMode: persistent ? (persistent.priority === null ? "auto" : "custom") : area.active && area.priority >= 0 ? "custom" : "auto",
91 priorityPending: Boolean(area.active && persistent && (persistent.priority === null ? area.priority >= 0 : persistent.priority !== area.priority)),
92 persistentByChj: persistent?.managedByChj === true,
93 systemdUnit: unit,
94 management,
95 // Partitions are never deleted or resized; zram is read-only in this phase.
96 allowedActions: management === "read-only" ? [] : management === "partition" ? ["activate", "deactivate", "priority", "persist"]
97 : management === "foreign-file" ? ["activate", "deactivate", "priority", "persist"] : ["activate", "deactivate", "priority", "persist", "resize", "delete"]
98 };
99 });
100 // Precedence: the kernel fills higher priorities first; equal priorities are used round-robin.
101 const activePriorities = [...new Set(swapAreas.filter((area) => area.active).map((area) => area.priority))].sort((a, b) => b - a);
102 for (const area of swapAreas) {
103 area.precedence = area.active ? activePriorities.indexOf(area.priority) + 1 : null;
104 area.sharedPriority = area.active && swapAreas.filter((other) => other.active && other.priority === area.priority).length > 1;
105 }
106 return {
107 platform: "linux",
108 root: meta.root === "1",
109 systemd: meta.systemd === "1",
110 btrfsMkswapfile: meta.btrfsMkswapfile === "1",
111 os: text(meta.os, 120),
112 memory: { total: meminfo.MemTotal ?? null, available: meminfo.MemAvailable ?? null, swapTotal: meminfo.SwapTotal ?? null, swapFree: meminfo.SwapFree ?? null },
113 swapAreas,
114 fstab,
115 units,
116 locations,
117 zram
118 };
121function parseSteps(stdout) {
122 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]) }));
125function mapWindowsState(raw) {
126 const mb = (value) => (number(value) === null ? null : Number(value) * 1048576);
127 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 }));
128 const active = (raw.active || []).map((entry) => ({ name: text(entry.name, 260), allocated: mb(entry.allocatedMb), used: mb(entry.currentMb), peak: mb(entry.peakMb) }));
129 const normalize = (value) => String(value || "").toLowerCase().replace(/^\\\?\?\\/, "").split(" ")[0];
130 const registryConfigured = (raw.registryPagingFiles || []).map(normalize).sort();
131 const existing = (raw.existingPageFiles || []).map(normalize).sort();
132 const names = new Set([...configured.map((entry) => entry.name.toLowerCase()), ...active.map((entry) => entry.name.toLowerCase())]);
133 const pagefiles = [...names].map((name) => {
134 const setting = configured.find((entry) => entry.name.toLowerCase() === name) || null;
135 const usage = active.find((entry) => entry.name.toLowerCase() === name) || null;
136 const volume = (raw.volumes || []).find((entry) => name.startsWith(String(entry.drive).toLowerCase()));
137 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 };
138 });
139 return {
140 platform: "windows",
141 elevated: raw.elevated === true,
142 automatic: typeof raw.automatic === "boolean" ? raw.automatic : null,
143 pagefiles,
144 // Commit limit = physical memory + pagefiles; it is not the pagefile size.
145 commit: { limit: number(raw.commitLimit), committed: number(raw.committed) },
146 volumes: (raw.volumes || []).map((volume) => ({ drive: text(volume.drive, 3), format: text(volume.format, 12), free: number(volume.free), total: number(volume.total) })),
147 systemDrive: text(raw.systemDrive, 3),
148 crashDumpEnabled: Number(raw.crashDumpEnabled) || 0,
149 restartPending: Boolean(raw.pending) || (raw.automatic === false && registryConfigured.length > 0 && JSON.stringify(registryConfigured) !== JSON.stringify(existing)),
150 pendingChanges: (raw.pending?.changes || []).map((change) => ({ at: change.at, description: text(change.description, 300) })),
151 bootTime: raw.bootTime || null,
152 unavailable: (raw.errors || []).map((entry) => text(entry, 200))
153 };
156class VirtualMemoryService {
157 constructor(options) {
158 this.sessionManager = options.sessionManager;
159 this.executor = options.privilegedExecutor;
160 this.logger = options.logger;
161 this.running = new Set();
162 }
164 async _platform(sessionId) {
165 const record = this.sessionManager._requireConnected(sessionId);
166 const platform = await this.sessionManager._platform(record);
167 if (platform.os === "unknown") throw fail("VM_PLATFORM_UNSUPPORTED", `The remote operating system could not be identified: ${platform.reason || "unknown"}.`);
168 return { record, platform };
169 }
171 async read(sessionId, options = {}) {
172 const { record, platform } = await this._platform(sessionId);
173 if (platform.os === "windows") {
174 const raw = await this.sessionManager._runWindowsScript(record, PAGEFILE_SCRIPT, { op: "inspect" }, 90000).catch((error) => { throw this._windowsError(error); });
175 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() };
176 }
177 const privilege = await this.executor.probe(sessionId);
178 const privileged = Boolean(options.sudoPassword) || options.privileged === true || ["root", "sudo-nopasswd"].includes(privilege.mode);
179 const run = await this.executor.run(sessionId, { script: SWAP_SCRIPT, args: ["inspect"], privileged, sudoPassword: options.sudoPassword, purpose: "system.swap.inspect", timeoutMs: 60000, errorPrefix: "VM_" });
180 const state = parseLinuxInspect(run.stdout);
181 return {
182 ...state,
183 privilege,
184 canManage: privilege.mode !== "none",
185 readOnlyReason: privilege.mode === "none" ? "The SSH account has no sudo rights; swap is shown read-only." : null,
186 collectedAt: new Date().toISOString()
187 };
188 }
190 async manage(sessionId, payload = {}) {
191 const { platform } = await this._platform(sessionId);
192 if (this.running.has(sessionId)) throw fail("VM_BUSY", "Another virtual memory change is still running for this session.");
193 this.running.add(sessionId);
194 try {
195 return platform.os === "windows" ? await this._manageWindows(sessionId, payload) : await this._manageLinux(sessionId, payload);
196 } finally { this.running.delete(sessionId); }
197 }
199 async _manageLinux(sessionId, payload) {
200 const action = String(payload.action || "");
201 if (!LINUX_ACTIONS.has(action)) throw fail("VM_ACTION_INVALID", "Unsupported swap action.");
202 const target = String(payload.path || "");
203 if (!ANY_SWAP_PATH.test(target) || target.includes("..")) throw fail("VM_SWAP_PATH_INVALID", "Invalid swap path.");
204 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>.");
205 if (target.startsWith("/dev/zram")) throw fail("VM_SWAP_ZRAM_READ_ONLY", "zram is shown read-only; CH-J does not configure it.");
206 const size = ["create", "resize"].includes(action) ? Number(payload.sizeMiB) : 0;
207 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.");
208 // "auto"/-1 = kernel-assigned, 0-32767 = explicit, "keep" = reuse the configured priority (activate/resize/persist).
209 const raw = payload.priority;
210 const priority = raw === undefined || raw === null || raw === "" || raw === "auto" ? -1 : raw === "keep" ? "keep" : Number(raw);
211 if (priority === "keep" ? ["create", "priority"].includes(action) : !Number.isInteger(priority) || priority < -1 || priority > 32767) throw fail("VM_SWAP_PRIORITY_INVALID", "Priority must be automatic or a whole number 0-32767.");
212 const apply = payload.apply === undefined ? "now" : payload.apply;
213 if (!["now", "later"].includes(apply)) throw fail("VM_ARGUMENTS_INVALID", "Apply must be now or later.");
214 const persist = payload.persist === true ? "1" : payload.persist === false ? "0" : "keep";
215 if (["create", "persist"].includes(action) && persist === "keep") throw fail("VM_ARGUMENTS_INVALID", "Choose whether the swap space is activated after restart.");
216 const run = await this.executor.run(sessionId, {
217 script: SWAP_SCRIPT, args: ["manage", action, target, String(size), String(priority), persist, apply], privileged: true,
218 sudoPassword: payload.sudoPassword, purpose: `system.swap.${action}`, timeoutMs: 30 * 60 * 1000, errorPrefix: "VM_"
219 });
220 this.logger?.warn("Swap configuration changed.", { sessionId, action, path: target });
221 const state = await this.read(sessionId, { sudoPassword: payload.sudoPassword, privileged: true }).catch(() => null);
222 return { action, path: target, result: "ok", steps: parseSteps(run.stdout), active: parseProcSwaps(splitSections(run.stdout).swaps), state };
223 }
225 async _manageWindows(sessionId, payload) {
226 const action = String(payload.action || "");
227 if (!WINDOWS_ACTIONS.has(action)) throw fail("VM_ACTION_INVALID", "Unsupported pagefile action.");
228 const args = { op: "apply", action };
229 if (action !== "automatic") {
230 args.drive = String(payload.drive || "").toUpperCase();
231 if (!/^[A-Z]:$/.test(args.drive)) throw fail("VM_PAGEFILE_DRIVE_INVALID", "Invalid drive letter.");
232 }
233 if (action === "set") {
234 const initial = Number(payload.initialMb); const maximum = Number(payload.maximumMb);
235 const systemManaged = initial === 0 && maximum === 0;
236 if (!Number.isInteger(initial) || !Number.isInteger(maximum) || (!systemManaged && (initial < 16 || maximum < initial || maximum > 4194304))) {
237 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.");
238 }
239 Object.assign(args, { initialMb: initial, maximumMb: maximum });
240 }
241 if (action === "remove") Object.assign(args, { confirmLast: payload.confirmLast === true, confirmCrashDump: payload.confirmCrashDump === true });
242 const record = this.sessionManager._requireConnected(sessionId);
243 const raw = await this.sessionManager._runWindowsScript(record, PAGEFILE_SCRIPT, args, 120000).catch((error) => { throw this._windowsError(error); });
244 this.logger?.warn("Windows pagefile configuration changed (restart pending).", { sessionId, action });
245 return { action, result: raw.result, backup: text(raw.backup, 300), restartRequired: true, state: { ...mapWindowsState(raw.state), canManage: true, collectedAt: new Date().toISOString() } };
246 }
248 _windowsError(error) {
249 const code = String(error?.code || "").replace(/^METRICS_/, "VM_");
250 return code && code !== error.code ? new SessionError(code, error.message) : error;
251 }
254module.exports = { MANAGED_PATH, VirtualMemoryService, mapWindowsState, parseLinuxInspect, parseProcSwaps };
SHA-25637c305ae1066b991819a2932ccb87f25c0220cd2bda30f7937e97d5e958b4f6a

CH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.

SHA-256 للأرشيفa9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
الملفات