Sumber yang diterbitkan
Muat turun ZIP sumber
CH-J Server Manager
Layari direktori dan fail untuk keluaran aplikasi tertentu.
CH-J / Server Manager
368 fail
core-20261017T201717Z-057348c4a70f
Ditambah: Tarikh tidak direkodkan · Dibina:
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
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
zram97
};98
}100
function 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]) }));102
}104
function 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
};133
}135
class 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
}227
}229
module.exports = { MANAGED_PATH, VirtualMemoryService, mapWindowsState, parseLinuxInspect, parseProcSwaps };SHA-256
5fdce16b12aa6ef216a3714169fb6b02b77d79cc613fe280267b23be83017456CH-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 arkib
cf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3