شائع شدہ سورس
سورس ZIP ڈاؤن لوڈ کریں
CH-J Tools
ایپلیکیشن کی کسی مخصوص ریلیز کی ڈائریکٹریز اور فائلیں دیکھیں۔
CH-J / Server Manager
872 فائلیں
core-20261030T220507Z-61e4558d889f
شامل کیا گیا: · بلڈ کیا گیا:
1
//! Virtual memory for the System Monitor module (port of `system/virtualMemoryService.js`):2
//! Linux swap (files, partitions, zram, zswap) and Windows pagefiles. Reads never change3
//! anything; changes go only through fixed operations with validated arguments.5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::logging::Logger;7
use crate::core::ssh::platform::detect_platform;8
use crate::core::ssh::privileged::{PrivilegedExecutor, RunOptions};9
use crate::core::ssh::session::SessionManager;10
use crate::core::ssh::windows_exec::run_windows_script;11
use crate::core::users::common::{clean_text, js_number_value, js_string, key_values, lines, now_iso, split_sections};12
use once_cell::sync::Lazy;13
use parking_lot::Mutex;14
use regex::Regex;15
use serde_json::{json, Map, Value};16
use std::collections::HashSet;17
use std::sync::Arc;18
use std::time::Duration;20
const SWAP_SCRIPT: &str = include_str!("../../../resources/remote/remoteSwap.sh");21
const PAGEFILE_SCRIPT: &str = include_str!("../../../resources/remote/windowsPagefile.ps1");22
static MANAGED_PATH: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(swapfile[A-Za-z0-9._-]{0,32}|swap/[A-Za-z0-9._-]{1,64}|var/lib/swap/[A-Za-z0-9._-]{1,64})$").unwrap());23
static ANY_SWAP_PATH: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/[A-Za-z0-9._/-]{1,199}$").unwrap());24
static ZRAM: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/dev/zram\d+$").unwrap());25
static PRI: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?:^|,)pri=(-?\d+)").unwrap());26
static STEP: Lazy<Regex> = Lazy::new(|| Regex::new(r"\x1eCHJ_STEP=([a-z-]+)\|([a-z]+)\|([^\n]*)\n").unwrap());27
const LINUX_ACTIONS: [&str; 7] = ["create", "activate", "deactivate", "priority", "persist", "resize", "delete"];28
const WINDOWS_ACTIONS: [&str; 3] = ["automatic", "set", "remove"];30
fn fail(code: &str, message: &str) -> CoreError {31
CoreError::new(code, message)32
}34
/// 0.1.0 `number()`: empty/null/non-finite ← null.35
fn number(text: &str) -> Value {36
if text.is_empty() {37
return Value::Null;38
}39
match js_number_value(&json!(text)) {40
Some(n) if n.is_finite() => json!(n),41
_ => Value::Null,42
}43
}45
fn num_value(value: &Value) -> Value {46
match value {47
Value::Null => Value::Null,48
Value::String(s) if s.is_empty() => Value::Null,49
other => match js_number_value(other) {50
Some(n) if n.is_finite() => json!(n),51
_ => Value::Null,52
},53
}54
}56
fn js_num(text: &str) -> f64 {57
js_number_value(&json!(text)).unwrap_or(f64::NAN)58
}60
fn json_num(n: f64) -> Value {61
if n.is_finite() { json!(n) } else { Value::Null }62
}64
pub fn parse_proc_swaps(text: &str) -> Vec<Value> {65
lines(text)66
.into_iter()67
.skip(1)68
.map(|line| {69
let fields: Vec<&str> = line.split_whitespace().collect();70
let field = |i: usize| fields.get(i).copied().unwrap_or("");71
let path = field(0).replace("\\040", " ");72
let kind = field(1);73
json!({74
"path": clean_text(&path, 200),75
"type": if ZRAM.is_match(&path) { "zram".to_string() } else if kind == "file" { "file".into() } else if kind == "partition" { "partition".into() } else { clean_text(kind, 16) },76
"size": json_num(js_num(field(2)) * 1024.0),77
"used": json_num(js_num(field(3)) * 1024.0),78
"priority": json_num(js_num(field(4)))79
})80
})81
.collect()82
}84
fn unit_name(path: &str) -> String {85
format!("{}.swap", path.trim_start_matches('/').replace('-', "\\x2d").replace('/', "-"))86
}88
pub fn parse_linux_inspect(stdout: &str) -> Value {89
let sections = split_sections(stdout);90
let section = |name: &str| sections.get(name).map(String::as_str).unwrap_or("");91
let meta = key_values(section("meta"));92
let mut meminfo = Map::new();93
for line in lines(section("meminfo")) {94
let mut parts = line.splitn(2, ':');95
let key = parts.next().unwrap_or("").to_string();96
let value = parts.next().unwrap_or("").trim();97
let digits: String = value.chars().take_while(|c| c.is_ascii_digit() || *c == '-').collect();98
meminfo.insert(key, digits.parse::<f64>().map(|n| json!(n * 1024.0)).unwrap_or(Value::Null));99
}100
let active = parse_proc_swaps(section("swaps"));101
let fstab: Vec<Value> = lines(section("fstab"))102
.into_iter()103
.filter_map(|line| {104
// line|canonical identity|raw line (older helpers sent line|raw).105
let index = line.find('|')?;106
let second = line[index + 1..].find('|').map(|i| i + index + 1);107
let with_canonical = second.is_some_and(|s| !line[index + 1..s].chars().any(char::is_whitespace));108
let (raw, resolved) = match (with_canonical, second) {109
(true, Some(s)) => (&line[s + 1..], json!(clean_text(&line[index + 1..s], 200))),110
_ => (&line[index + 1..], Value::Null),111
};112
let fields: Vec<&str> = raw.split_whitespace().collect();113
let options = fields.get(3).copied().unwrap_or("");114
let priority = PRI.captures(options).and_then(|c| c[1].parse::<i64>().ok());115
let entry = json!({116
"line": json_num(js_num(&line[..index])),117
"spec": clean_text(fields.first().copied().unwrap_or(""), 200),118
"resolved": resolved,119
"type": clean_text(fields.get(2).copied().unwrap_or(""), 16),120
"options": clean_text(options, 120),121
"priority": priority,122
"commented": raw.trim().starts_with('#'),123
"managedByChj": raw.contains("# CH-J chj.system-monitor")124
});125
(entry["type"] == "swap" && entry["commented"] != true).then_some(entry)126
})127
.collect();128
let units: Vec<Value> = lines(section("units"))129
.into_iter()130
.map(|line| {131
let p: Vec<&str> = line.split('|').collect();132
json!({ "unit": clean_text(p.first().copied().unwrap_or(""), 120), "active": clean_text(p.get(1).copied().unwrap_or(""), 20), "state": clean_text(p.get(2).copied().unwrap_or(""), 20) })133
})134
.collect();135
let files: Vec<Value> = lines(section("files"))136
.into_iter()137
.map(|line| {138
let p: Vec<&str> = line.split('|').collect();139
let f = |i: usize| p.get(i).copied().unwrap_or("");140
json!({141
"path": clean_text(f(0), 200),142
"kind": f(1),143
"fsType": if f(2) == "-" { Value::Null } else { json!(clean_text(f(2), 24)) },144
"size": number(f(3)),145
"owner": if f(4) == "0" && f(1) != "file" { Value::Null } else { json!(clean_text(f(4), 32)) },146
"mode": if f(5) == "-" { Value::Null } else { json!(clean_text(f(5), 8)) },147
"managed": f(6) == "managed",148
"diskFree": number(f(7))149
})150
})151
.collect();152
let locations: Vec<Value> = lines(section("locations"))153
.into_iter()154
.map(|line| {155
let p: Vec<&str> = line.split('|').collect();156
json!({ "dir": p.first().copied().unwrap_or(""), "fsType": clean_text(p.get(1).copied().unwrap_or(""), 24), "free": number(p.get(2).copied().unwrap_or("")) })157
})158
.collect();159
let zram: Vec<Value> = lines(section("zram"))160
.into_iter()161
.map(|line| {162
let p: Vec<&str> = line.split('|').collect();163
let values: Vec<f64> = p.get(2).copied().unwrap_or("").split(',').map(js_num).collect();164
let at = |i: usize| values.get(i).map(|v| json_num(*v)).unwrap_or(Value::Null);165
json!({ "device": p.first().copied().unwrap_or(""), "diskSize": number(p.get(1).copied().unwrap_or("")), "originalData": at(0), "compressedData": at(1), "memoryUsed": at(2) })166
})167
.collect();168
// Areas keyed by path in insertion order.169
let mut areas: Vec<(String, Value)> = Vec::new();170
for entry in &active {171
let mut area = entry.clone();172
area["active"] = json!(true);173
let path = js_string(&entry["path"]);174
match areas.iter_mut().find(|(p, _)| *p == path) {175
Some(slot) => slot.1 = area,176
None => areas.push((path, area)),177
}178
}179
for file in &files {180
let path = js_string(&file["path"]);181
if !areas.iter().any(|(p, _)| *p == path) {182
areas.push((path.clone(), json!({ "path": path, "type": file["kind"], "size": file["size"], "used": 0, "priority": null, "active": false })));183
}184
}185
let matches = |entry: &Value, path: &str| entry["spec"] == path || entry["resolved"] == path;186
for entry in &fstab {187
let resolved = entry["resolved"].as_str().filter(|r| r.starts_with('/'));188
let identity = resolved.map(str::to_string).unwrap_or_else(|| js_string(&entry["spec"]));189
if identity.starts_with('/') && !areas.iter().any(|(key, _)| matches(entry, key)) {190
areas.push((identity.clone(), json!({ "path": identity, "type": if identity.starts_with("/dev/") { "partition" } else { "file" }, "size": null, "used": 0, "priority": null, "active": false })));191
}192
}193
let mut swap_areas: Vec<Value> = areas194
.into_iter()195
.map(|(path, mut area)| {196
let file = files.iter().find(|f| f["path"] == path.as_str());197
let entries: Vec<&Value> = fstab.iter().filter(|e| matches(e, &path)).collect();198
let persistent = entries.first().copied();199
let unit = units.iter().find(|u| u["unit"] == unit_name(&path).as_str()).cloned();200
let kind = js_string(&area["type"]);201
let management = if kind == "zram" {202
"read-only"203
} else if kind == "partition" {204
"partition"205
} else if file.is_some_and(|f| f["managed"] == true) || MANAGED_PATH.is_match(&path) {206
"managed"207
} else {208
"foreign-file"209
};210
let active = area["active"] == true;211
let priority = area["priority"].as_f64();212
let persistent_priority = persistent.and_then(|p| p["priority"].as_f64());213
let priority_mode = match persistent {214
Some(_) if persistent_priority.is_none() => "auto",215
Some(_) => "custom",216
None if active && priority.is_some_and(|p| p >= 0.0) => "custom",217
None => "auto",218
};219
let pending = active220
&& persistent.is_some()221
&& match persistent_priority {222
None => priority.is_some_and(|p| p >= 0.0),223
Some(pp) => Some(pp) != priority,224
};225
let allowed: Vec<&str> = match management {226
"read-only" => vec![],227
"partition" | "foreign-file" => vec!["activate", "deactivate", "priority", "persist"],228
_ => vec!["activate", "deactivate", "priority", "persist", "resize", "delete"],229
};230
let map = area.as_object_mut().unwrap();231
map.insert("fsType".into(), file.map(|f| f["fsType"].clone()).filter(|v| !v.is_null()).unwrap_or(Value::Null));232
map.insert("mode".into(), file.map(|f| f["mode"].clone()).filter(|v| !v.is_null()).unwrap_or(Value::Null));233
map.insert("owner".into(), file.map(|f| f["owner"].clone()).filter(|v| !v.is_null() && *v != "").unwrap_or(Value::Null));234
map.insert("persistent".into(), json!(persistent.is_some()));235
map.insert("persistentSpec".into(), persistent.map(|p| p["spec"].clone()).unwrap_or(Value::Null));236
map.insert("persistentAmbiguous".into(), json!(entries.len() > 1));237
map.insert("persistentPriority".into(), persistent.map(|p| p["priority"].clone()).unwrap_or(Value::Null));238
map.insert("effectivePriority".into(), if active { area_priority(priority) } else { Value::Null });239
map.insert("priorityMode".into(), json!(priority_mode));240
map.insert("priorityPending".into(), json!(pending));241
map.insert("persistentByChj".into(), json!(persistent.is_some_and(|p| p["managedByChj"] == true)));242
map.insert("systemdUnit".into(), unit.unwrap_or(Value::Null));243
map.insert("management".into(), json!(management));244
map.insert("allowedActions".into(), json!(allowed));245
area246
})247
.collect();248
// Precedence: the kernel fills higher priorities first; equal priorities are used round-robin.249
let mut priorities: Vec<i64> = swap_areas.iter().filter(|a| a["active"] == true).filter_map(|a| a["priority"].as_f64().map(|p| p as i64)).collect::<HashSet<_>>().into_iter().collect();250
priorities.sort_by(|a, b| b.cmp(a));251
let snapshot = swap_areas.clone();252
for area in swap_areas.iter_mut() {253
let active = area["active"] == true;254
let priority = area["priority"].as_f64().map(|p| p as i64);255
area["precedence"] = if active { json!(priority.and_then(|p| priorities.iter().position(|x| *x == p)).map(|i| i as i64 + 1).unwrap_or(0)) } else { Value::Null };256
area["sharedPriority"] = json!(active && snapshot.iter().filter(|o| o["active"] == true && o["priority"].as_f64().map(|p| p as i64) == priority).count() > 1);257
}258
let meta_flag = |key: &str| meta.get(key).map(String::as_str) == Some("1");259
json!({260
"platform": "linux",261
"root": meta_flag("root"),262
"systemd": meta_flag("systemd"),263
"btrfsMkswapfile": meta_flag("btrfsMkswapfile"),264
"os": clean_text(meta.get("os").map(String::as_str).unwrap_or(""), 120),265
"memory": { "total": meminfo.get("MemTotal").cloned().unwrap_or(Value::Null), "available": meminfo.get("MemAvailable").cloned().unwrap_or(Value::Null), "swapTotal": meminfo.get("SwapTotal").cloned().unwrap_or(Value::Null), "swapFree": meminfo.get("SwapFree").cloned().unwrap_or(Value::Null) },266
"swapAreas": swap_areas,267
"fstab": fstab,268
"units": units,269
"locations": locations,270
"zram": zram271
})272
}274
fn area_priority(priority: Option<f64>) -> Value {275
priority.map(Value::from).unwrap_or(Value::Null)276
}278
fn parse_steps(stdout: &str) -> Vec<Value> {279
STEP.captures_iter(stdout).map(|c| json!({ "step": &c[1], "status": &c[2], "message": clean_text(&c[3], 300) })).collect()280
}282
fn normalize_pagefile(value: &Value) -> String {283
let lower = js_string(value).to_lowercase();284
let stripped = lower.strip_prefix("\\??\\").unwrap_or(&lower);285
stripped.split(' ').next().unwrap_or("").to_string()286
}288
pub fn map_windows_state(raw: &Value) -> Value {289
let mb = |value: &Value| match num_value(value) {290
Value::Null => Value::Null,291
v => json!(v.as_f64().unwrap_or(0.0) * 1_048_576.0),292
};293
let list = |key: &str| raw[key].as_array().cloned().unwrap_or_default();294
let configured: Vec<Value> = list("configured")295
.iter()296
.map(|e| json!({ "name": clean_text(&js_string(&e["name"]), 260), "initial": mb(&e["initialMb"]), "maximum": mb(&e["maximumMb"]), "systemManagedSize": js_number_value(&e["initialMb"]) == Some(0.0) && js_number_value(&e["maximumMb"]) == Some(0.0) }))297
.collect();298
let active: Vec<Value> = list("active").iter().map(|e| json!({ "name": clean_text(&js_string(&e["name"]), 260), "allocated": mb(&e["allocatedMb"]), "used": mb(&e["currentMb"]), "peak": mb(&e["peakMb"]) })).collect();299
let mut registry: Vec<String> = list("registryPagingFiles").iter().map(normalize_pagefile).collect();300
registry.sort();301
let mut existing: Vec<String> = list("existingPageFiles").iter().map(normalize_pagefile).collect();302
existing.sort();303
let mut names: Vec<String> = Vec::new();304
for name in configured.iter().chain(active.iter()).map(|e| js_string(&e["name"]).to_lowercase()) {305
if !names.contains(&name) {306
names.push(name);307
}308
}309
let volumes = list("volumes");310
let pagefiles: Vec<Value> = names311
.iter()312
.map(|name| {313
let setting = configured.iter().find(|e| js_string(&e["name"]).to_lowercase() == *name);314
let usage = active.iter().find(|e| js_string(&e["name"]).to_lowercase() == *name);315
let volume = volumes.iter().find(|v| name.starts_with(&js_string(&v["drive"]).to_lowercase()));316
let display = setting.map(|s| js_string(&s["name"])).or_else(|| usage.map(|u| js_string(&u["name"]))).unwrap_or_default();317
json!({318
"name": display,319
"drive": display.chars().take(2).collect::<String>().to_uppercase(),320
"configured": setting.is_some(),321
"active": usage.is_some(),322
"initial": setting.map(|s| s["initial"].clone()).unwrap_or(Value::Null),323
"maximum": setting.map(|s| s["maximum"].clone()).unwrap_or(Value::Null),324
"systemManagedSize": setting.map(|s| s["systemManagedSize"].clone()).unwrap_or(Value::Null),325
"allocated": usage.map(|u| u["allocated"].clone()).unwrap_or(Value::Null),326
"used": usage.map(|u| u["used"].clone()).unwrap_or(Value::Null),327
"peak": usage.map(|u| u["peak"].clone()).unwrap_or(Value::Null),328
"volumeFree": volume.map(|v| v["free"].clone()).unwrap_or(Value::Null)329
})330
})331
.collect();332
let pending_truthy = !matches!(&raw["pending"], Value::Null | Value::Bool(false)) && raw["pending"] != 0 && raw["pending"] != "";333
json!({334
"platform": "windows",335
"elevated": raw["elevated"] == true,336
"automatic": if raw["automatic"].is_boolean() { raw["automatic"].clone() } else { Value::Null },337
"pagefiles": pagefiles,338
// Commit limit = physical memory + pagefiles; it is not the pagefile size.339
"commit": { "limit": num_value(&raw["commitLimit"]), "committed": num_value(&raw["committed"]) },340
"volumes": volumes.iter().map(|v| json!({ "drive": clean_text(&js_string(&v["drive"]), 3), "format": clean_text(&js_string(&v["format"]), 12), "free": num_value(&v["free"]), "total": num_value(&v["total"]) })).collect::<Vec<_>>(),341
"systemDrive": clean_text(&js_string(&raw["systemDrive"]), 3),342
"crashDumpEnabled": js_number_value(&raw["crashDumpEnabled"]).filter(|n| n.is_finite()).unwrap_or(0.0),343
"restartPending": pending_truthy || (raw["automatic"] == false && !registry.is_empty() && registry != existing),344
"pendingChanges": raw["pending"]["changes"].as_array().cloned().unwrap_or_default().iter().map(|c| json!({ "at": c["at"], "description": clean_text(&js_string(&c["description"]), 300) })).collect::<Vec<_>>(),345
"bootTime": if raw["bootTime"].is_null() || raw["bootTime"] == "" { Value::Null } else { raw["bootTime"].clone() },346
"unavailable": list("errors").iter().map(|e| clean_text(&js_string(e), 200)).collect::<Vec<_>>()347
})348
}350
fn windows_error(error: CoreError) -> CoreError {351
match error.code.strip_prefix("METRICS_") {352
Some(rest) => CoreError { code: format!("VM_{rest}"), ..error },353
None => error,354
}355
}357
fn merge(base: Value, extra: Value) -> Value {358
let mut map = base.as_object().cloned().unwrap_or_default();359
for (k, v) in extra.as_object().cloned().unwrap_or_default() {360
map.insert(k, v);361
}362
Value::Object(map)363
}365
pub struct VirtualMemoryService {366
pub sessions: Arc<SessionManager>,367
pub executor: Arc<PrivilegedExecutor>,368
pub logger: Arc<Logger>,369
running: Mutex<HashSet<String>>,370
}372
fn sudo_password(payload: &Value) -> Option<String> {373
payload["sudoPassword"].as_str().filter(|s| !s.is_empty()).map(str::to_string)374
}376
impl VirtualMemoryService {377
pub fn new(sessions: Arc<SessionManager>, executor: Arc<PrivilegedExecutor>, logger: Arc<Logger>) -> Self {378
Self { sessions, executor, logger, running: Mutex::new(HashSet::new()) }379
}381
async fn platform(&self, session_id: &str) -> CoreResult<Value> {382
let record = self.sessions.require_connected(session_id)?;383
let platform = detect_platform(&self.sessions, &record, false).await?;384
if platform["os"] == "unknown" {385
return Err(fail("VM_PLATFORM_UNSUPPORTED", &format!("The remote operating system could not be identified: {}.", platform["reason"].as_str().filter(|s| !s.is_empty()).unwrap_or("unknown"))));386
}387
Ok(platform)388
}390
async fn windows_script(&self, session_id: &str, args: Value, timeout_ms: u64) -> CoreResult<Value> {391
let record = self.sessions.require_connected(session_id)?;392
run_windows_script(&self.sessions, &record, PAGEFILE_SCRIPT, &args, Duration::from_millis(timeout_ms), 4 * 1024 * 1024, "METRICS_").await.map_err(windows_error)393
}395
/// `options`: `{ sudoPassword?, privileged? }`.396
pub async fn read(&self, session_id: &str, options: &Value) -> CoreResult<Value> {397
let platform = self.platform(session_id).await?;398
if platform["os"] == "windows" {399
let raw = self.windows_script(session_id, json!({ "op": "inspect" }), 90_000).await?;400
let elevated = raw["elevated"] == true;401
return Ok(merge(402
map_windows_state(&raw),403
json!({ "canManage": elevated, "readOnlyReason": if elevated { Value::Null } else { json!("Changing the pagefile requires an SSH session with an elevated administrator token.") }, "collectedAt": now_iso() }),404
));405
}406
let privilege = self.executor.probe(session_id, false).await?;407
let privileged = sudo_password(options).is_some() || options["privileged"] == true || ["root", "sudo-nopasswd"].contains(&privilege["mode"].as_str().unwrap_or(""));408
let run = self409
.executor410
.run(411
session_id,412
RunOptions {413
script: SWAP_SCRIPT.into(),414
args: vec!["inspect".into()],415
privileged,416
sudo_password: sudo_password(options),417
purpose: "system.swap.inspect".into(),418
error_prefix: "VM_".into(),419
timeout: Duration::from_secs(60),420
..Default::default()421
},422
)423
.await?;424
let none = privilege["mode"] == "none";425
Ok(merge(426
parse_linux_inspect(&run.stdout),427
json!({ "privilege": privilege, "canManage": !none, "readOnlyReason": if none { json!("The SSH account has no sudo rights; swap is shown read-only.") } else { Value::Null }, "collectedAt": now_iso() }),428
))429
}431
pub async fn manage(&self, session_id: &str, payload: &Value) -> CoreResult<Value> {432
let platform = self.platform(session_id).await?;433
if !self.running.lock().insert(session_id.to_string()) {434
return Err(fail("VM_BUSY", "Another virtual memory change is still running for this session."));435
}436
let result = if platform["os"] == "windows" { self.manage_windows(session_id, payload).await } else { self.manage_linux(session_id, payload).await };437
self.running.lock().remove(session_id);438
result439
}441
async fn manage_linux(&self, session_id: &str, payload: &Value) -> CoreResult<Value> {442
let action = js_string(&payload["action"]);443
if !LINUX_ACTIONS.contains(&action.as_str()) {444
return Err(fail("VM_ACTION_INVALID", "Unsupported swap action."));445
}446
let target = js_string(&payload["path"]);447
if !ANY_SWAP_PATH.is_match(&target) || target.contains("..") {448
return Err(fail("VM_SWAP_PATH_INVALID", "Invalid swap path."));449
}450
let sized = ["create", "resize"].contains(&action.as_str());451
if ["create", "resize", "delete"].contains(&action.as_str()) && !MANAGED_PATH.is_match(&target) {452
return Err(fail("VM_SWAP_PATH_INVALID", "CH-J creates, resizes and deletes swap files only as /swapfile*, /swap/<name> or /var/lib/swap/<name>."));453
}454
if target.starts_with("/dev/zram") {455
return Err(fail("VM_SWAP_ZRAM_READ_ONLY", "zram is shown read-only; CH-J does not configure it."));456
}457
let size = if sized { js_number_value(&payload["sizeMiB"]).unwrap_or(f64::NAN) } else { 0.0 };458
if sized && (size.fract() != 0.0 || !(64.0..=65536.0).contains(&size)) {459
return Err(fail("VM_SWAP_SIZE_INVALID", "Size must be a whole number of MiB between 64 MiB and 64 GiB."));460
}461
// "auto"/-1 = kernel-assigned, 0-32767 = explicit, "keep" = reuse the configured priority (activate/resize/persist).462
let raw = &payload["priority"];463
let priority: Result<i64, ()> = match raw {464
Value::Null => Ok(-1),465
Value::String(s) if s.is_empty() || s == "auto" => Ok(-1),466
Value::String(s) if s == "keep" => Err(()),467
other => match js_number_value(other) {468
Some(n) if n.fract() == 0.0 && (-1.0..=32767.0).contains(&n) => Ok(n as i64),469
_ => return Err(fail("VM_SWAP_PRIORITY_INVALID", "Priority must be automatic or a whole number 0-32767.")),470
},471
};472
if priority.is_err() && ["create", "priority"].contains(&action.as_str()) {473
return Err(fail("VM_SWAP_PRIORITY_INVALID", "Priority must be automatic or a whole number 0-32767."));474
}475
let priority_arg = priority.map(|p| p.to_string()).unwrap_or_else(|_| "keep".into());476
let apply = if payload.get("apply").is_none() || payload["apply"].is_null() { "now".to_string() } else { js_string(&payload["apply"]) };477
if apply != "now" && apply != "later" {478
return Err(fail("VM_ARGUMENTS_INVALID", "Apply must be now or later."));479
}480
let persist = match payload["persist"] {481
Value::Bool(true) => "1",482
Value::Bool(false) => "0",483
_ => "keep",484
};485
if ["create", "persist"].contains(&action.as_str()) && persist == "keep" {486
return Err(fail("VM_ARGUMENTS_INVALID", "Choose whether the swap space is activated after restart."));487
}488
let run = self489
.executor490
.run(491
session_id,492
RunOptions {493
script: SWAP_SCRIPT.into(),494
args: vec!["manage".into(), action.clone(), target.clone(), (size as i64).to_string(), priority_arg, persist.into(), apply],495
privileged: true,496
sudo_password: sudo_password(payload),497
purpose: format!("system.swap.{action}"),498
error_prefix: "VM_".into(),499
timeout: Duration::from_secs(30 * 60),500
..Default::default()501
},502
)503
.await?;504
self.logger.warn("Swap configuration changed.", json!({ "sessionId": session_id, "action": action, "path": target }));505
let state = self.read(session_id, &json!({ "sudoPassword": payload["sudoPassword"], "privileged": true })).await.ok();506
let sections = split_sections(&run.stdout);507
Ok(json!({ "action": action, "path": target, "result": "ok", "steps": parse_steps(&run.stdout), "active": parse_proc_swaps(sections.get("swaps").map(String::as_str).unwrap_or("")), "state": state }))508
}510
async fn manage_windows(&self, session_id: &str, payload: &Value) -> CoreResult<Value> {511
let action = js_string(&payload["action"]);512
if !WINDOWS_ACTIONS.contains(&action.as_str()) {513
return Err(fail("VM_ACTION_INVALID", "Unsupported pagefile action."));514
}515
let mut args = json!({ "op": "apply", "action": action });516
if action != "automatic" {517
let drive = js_string(&payload["drive"]).to_uppercase();518
let bytes = drive.as_bytes();519
if !(bytes.len() == 2 && bytes[0].is_ascii_uppercase() && bytes[1] == b':') {520
return Err(fail("VM_PAGEFILE_DRIVE_INVALID", "Invalid drive letter."));521
}522
args["drive"] = json!(drive);523
}524
if action == "set" {525
let initial = js_number_value(&payload["initialMb"]).unwrap_or(f64::NAN);526
let maximum = js_number_value(&payload["maximumMb"]).unwrap_or(f64::NAN);527
let system_managed = initial == 0.0 && maximum == 0.0;528
if initial.fract() != 0.0 || maximum.fract() != 0.0 || (!system_managed && (initial < 16.0 || maximum < initial || maximum > 4_194_304.0)) {529
return Err(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."));530
}531
args["initialMb"] = json!(initial as i64);532
args["maximumMb"] = json!(maximum as i64);533
}534
if action == "remove" {535
args["confirmLast"] = json!(payload["confirmLast"] == true);536
args["confirmCrashDump"] = json!(payload["confirmCrashDump"] == true);537
}538
let raw = self.windows_script(session_id, args, 120_000).await?;539
self.logger.warn("Windows pagefile configuration changed (restart pending).", json!({ "sessionId": session_id, "action": action }));540
Ok(json!({ "action": action, "result": raw["result"], "backup": clean_text(&js_string(&raw["backup"]), 300), "restartRequired": true, "state": merge(map_windows_state(&raw["state"]), json!({ "canManage": true, "collectedAt": now_iso() })) }))541
}542
}544
#[cfg(test)]545
mod tests {546
use super::*;548
#[test]549
fn parses_linux_and_windows_state() {550
let stdout = "\x1eCHJ_SECTION=meta\nroot=1\nsystemd=0\nos=Debian\n\x1eCHJ_SECTION=meminfo\nMemTotal: 2048 kB\nSwapTotal: 1024 kB\n\x1eCHJ_SECTION=swaps\nFilename Type Size Used Priority\n/swapfile file 1024 0 -2\n/dev/zram0 partition 512 0 100\n\x1eCHJ_SECTION=fstab\n3|/swapfile|/swapfile none swap sw,pri=5 0 0 # CH-J chj.system-monitor\n\x1eCHJ_SECTION=files\n/swapfile|file|ext4|1048576|0|600|managed|1000\n";551
let state = parse_linux_inspect(stdout);552
assert_eq!(state["memory"]["total"], 2048.0 * 1024.0);553
let areas = state["swapAreas"].as_array().unwrap();554
assert_eq!(areas[0]["management"], "managed");555
assert_eq!(areas[0]["priorityMode"], "custom");556
assert_eq!(areas[0]["priorityPending"], true);557
assert_eq!(areas[0]["persistentByChj"], true);558
assert_eq!(areas[1]["type"], "zram");559
assert_eq!(areas[1]["precedence"], 1);560
assert_eq!(areas[0]["precedence"], 2);561
let windows = map_windows_state(&json!({ "elevated": true, "automatic": false, "configured": [{ "name": "C:\\pagefile.sys", "initialMb": 0, "maximumMb": 0 }], "registryPagingFiles": ["c:\\pagefile.sys 0 0"], "existingPageFiles": [] }));562
assert_eq!(windows["pagefiles"][0]["systemManagedSize"], true);563
assert_eq!(windows["restartPending"], true);564
assert_eq!(unit_name("/swap/my-file"), "swap-my\\x2dfile.swap");565
}567
#[tokio::test]568
async fn priority_and_apply_validation_before_remote_execution() {569
use crate::core::{profiles::ProfileService, vault::VaultStore};570
let dir=tempfile::tempdir().unwrap();571
let sessions=SessionManager::new(Arc::new(ProfileService::new(Arc::new(VaultStore::new(dir.path())))),Arc::new(Logger::new(dir.path())),Arc::new(|_,_|{}));572
let service=VirtualMemoryService::new(sessions.clone(),Arc::new(PrivilegedExecutor::new(sessions)),Arc::new(Logger::new(dir.path())));573
for priority in [json!(-3),json!(32768),json!(1.5),json!("abc"),json!("keep")] {574
let error=service.manage_linux("absent",&json!({"action":"priority","path":"/swapfile-chj","priority":priority})).await.unwrap_err();575
assert_eq!(error.code,"VM_SWAP_PRIORITY_INVALID","{priority}");576
}577
assert_eq!(service.manage_linux("absent",&json!({"action":"priority","path":"/swapfile-chj","priority":5,"apply":"soon"})).await.unwrap_err().code,"VM_ARGUMENTS_INVALID");578
for priority in [Value::Null,json!(""),json!("auto"),json!(-1),json!(0),json!(100),json!(32767)] {579
for apply in ["now","later"] {580
assert_eq!(service.manage_linux("absent",&json!({"action":"priority","path":"/swapfile-chj","priority":priority,"apply":apply})).await.unwrap_err().code,"SESSION_NOT_CONNECTED","{priority}/{apply}");581
}582
}583
for action in ["activate","resize","persist"] {584
assert_eq!(service.manage_linux("absent",&json!({"action":action,"path":"/swapfile-chj","priority":"keep","sizeMiB":128,"persist":true})).await.unwrap_err().code,"SESSION_NOT_CONNECTED");585
}586
}588
fn priority_state(swaps:&str,fstab:&str)->Value {589
parse_linux_inspect(&format!("\x1eCHJ_SECTION=meta\nroot=1\n\x1eCHJ_SECTION=swaps\nFilename Type Size Used Priority\n{swaps}\x1eCHJ_SECTION=fstab\n{fstab}\x1eCHJ_SECTION=end\n"))590
}591
fn area(state:&Value,path:&str)->Value {state["swapAreas"].as_array().unwrap().iter().find(|a|a["path"]==path).unwrap().clone()}592
#[test]593
fn automatic_explicit_equal_and_persistent_priority_matrix() {594
let state=priority_state("/swap/a file 1024 0 -2\n/swap/b file 1024 0 -3\n","1|/swap/a|/swap/a none swap sw 0 0 # CH-J chj.system-monitor\n");595
let a=area(&state,"/swap/a");assert_eq!(a["priorityMode"],"auto");assert_eq!(a["priorityPending"],false);assert_eq!(a["precedence"],1);assert_eq!(area(&state,"/swap/b")["precedence"],2);assert!(a["persistentPriority"].is_null());596
let state=priority_state("/swap/a file 1024 0 100\n/swap/b file 1024 0 10\n","1|/swap/a|/swap/a none swap sw,pri=100 0 0\n2|/swap/b|/swap/b none swap sw,pri=40 0 0\n");597
assert_eq!(area(&state,"/swap/a")["priorityPending"],false);assert_eq!(area(&state,"/swap/b")["priorityPending"],true);assert_eq!(area(&state,"/swap/b")["persistentPriority"],40);assert_eq!(area(&state,"/swap/a")["precedence"],1);598
let state=priority_state("/swap/a file 1024 0 5\n/swap/b file 1024 0 5\n","7|/dev/chj-test|UUID=fixture none swap sw,pri=1 0 0\n");599
for path in ["/swap/a","/swap/b"] {let a=area(&state,path);assert_eq!(a["precedence"],1);assert_eq!(a["sharedPriority"],true);assert_eq!(a["persistent"],false);}600
let p=area(&state,"/dev/chj-test");assert_eq!(p["active"],false);assert_eq!(p["persistentSpec"],"UUID=fixture");assert_eq!(p["management"],"partition");601
}603
}SHA-256
4c43d18333252efe4a49866320cd7879855c768e1be044e2c6e5dd92222ef2b2CH-J Proprietary Software License 1.14 · سورس CH-J Proprietary Software License 1.14 کے تحت فراہم کیا جاتا ہے۔ اس کی دستیابی لائسنس کی شرائط نہیں بدلتی اور نہ ہی اضافی حقوق دیتی ہے۔
آرکائیو کا SHA-256
77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df