Sursă publicată
Descarcă sursa ZIP
CH-J Server Manager
Navighează directoarele și fișierele pentru o anumită versiune a aplicației.
CH-J / Server Manager
377 fișiere
core-20261019T111616Z-160a00bfcddc
Adăugat: · Construit:
1
"use strict";3
const fs = require("node:fs");4
const path = require("node:path");5
const { SessionError } = require("../sessions/sessionManager");7
// Virtual memory configuration for chj.system-monitor: Linux swap (files, partitions, zram, zswap) and Windows8
// pagefiles. Reads never change anything; changes go only through fixed operations with validated arguments.9
const SWAP_SCRIPT = fs.readFileSync(path.join(__dirname, "remoteSwap.sh"), "utf8");10
const PAGEFILE_SCRIPT = fs.readFileSync(path.join(__dirname, "windowsPagefile.ps1"), "utf8");11
const 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})$/;12
const ANY_SWAP_PATH = /^\/[A-Za-z0-9._/-]{1,199}$/;13
const LINUX_ACTIONS = new Set(["create", "activate", "deactivate", "priority", "persist", "resize", "delete"]);14
const WINDOWS_ACTIONS = new Set(["automatic", "set", "remove"]);16
function fail(code, message) { return new SessionError(code, message); }17
const text = (value, limit = 300) => String(value ?? "").replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, limit);18
const number = (value) => { const result = Number(value); return value === "" || value === null || value === undefined || !Number.isFinite(result) ? null : result; };20
function 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;30
}31
const lines = (value) => String(value || "").split(/\r?\n/).filter(Boolean);33
function 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
});39
}41
function 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 effective77
// 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
zram118
};119
}121
function 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]) }));123
}125
function 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
};154
}156
class 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
}252
}254
module.exports = { MANAGED_PATH, VirtualMemoryService, mapWindowsState, parseLinuxInspect, parseProcSwaps };SHA-256
37c305ae1066b991819a2932ccb87f25c0220cd2bda30f7937e97d5e958b4f6aCH-J Proprietary Software License 1.14 · Sursa este furnizată în cadrul CH-J Proprietary Software License 1.14. Disponibilitatea sa nu modifică termenii licenţei sau nu acordă drepturi suplimentare.
Archive SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a