Files

199 lines
8.2 KiB
JavaScript
Raw Permalink Normal View History

/* eslint-disable no-undef */
"use strict";
/**
* Windows PowerShell command helpers for remote SSH hosts.
*
* Every script is UTF-16LE Base64-encoded and invoked via
* powershell -NoProfile -NonInteractive -EncodedCommand <blob>
* so cmd.exe / OpenSSH-Server quoting cannot break it.
*/
/**
* Encode a PowerShell script string as UTF-16LE bytes, then Base64.
* Matches what `powershell -EncodedCommand` expects.
*/
function encodePowerShellScript(script) {
if (typeof Buffer !== "undefined" && Buffer.alloc) {
// Node Buffer path: convert UTF-8 → UTF-16LE → Base64.
const utf8 = Buffer.from(String(script), "utf8");
const utf16 = Buffer.alloc(utf8.length * 2);
for (let i = 0; i < utf8.length; i++) {
utf16[i * 2] = utf8[i]; // low byte (ASCII chars: high byte = 0)
utf16[i * 2 + 1] = 0; // high byte
}
return utf16.toString("base64");
}
// Browser / pure JS fallback.
const chars = String(script);
let out = "";
for (let i = 0; i < chars.length; i++) {
const code = chars.charCodeAt(i);
out += String.fromCharCode(code & 0xff, (code >> 8) & 0xff);
}
return btoa(out);
}
/** Build an SSH-exec-friendly PowerShell invocation. */
function wrapPowerShell(encodedBlob) {
return `powershell -NoProfile -NonInteractive -EncodedCommand ${encodedBlob}`;
}
// ---------------------------------------------------------------------------
// Process list
// ---------------------------------------------------------------------------
const PROCESS_LIST_PS = [
// Minimal Windows process list — speed > precision.
// Single Get-Process pass; CPU% is a rough estimate (cumulative/uptime/cores).
// PPID and command line come from Win32_Process (one WMI query, indexed).
'$ErrorActionPreference = "SilentlyContinue";',
'$procs = Get-Process;',
// Build WMI lookup for PPID + command line
'$wmi = @{};',
'Get-CimInstance Win32_Process | ForEach-Object { $wmi[[int]$_.ProcessId] = $_ };',
// Get logical core count from first processor (faster than ComputerSystem WMI)
'$cores = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum;',
'if (-not $cores -or $cores -le 0) { $cores = 1 };',
'$totalMemMB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1MB, 0);',
'$now = Get-Date;',
'$rows = foreach ($p in $procs) {',
' $cpu = 0;',
' if ($p.CPU -ne $null -and $p.StartTime -ne $null) {',
' $up = ($now - $p.StartTime).TotalSeconds;',
' if ($up -gt 0) { $cpu = [math]::Round([double]$p.CPU / $up / $cores * 100, 2) };',
' if ($cpu -lt 0) { $cpu = 0 };',
' if ($cpu -gt 100) { $cpu = 100 };',
' }',
' $w = $wmi[$p.Id];',
' $ppid = if ($w) { [int]$w.ParentProcessId } else { 0 };',
' $cmd = if ($w -and $w.CommandLine) { $w.CommandLine } else { $p.ProcessName };',
' $el = "";',
' if ($p.StartTime) {',
' $e = $now - $p.StartTime;',
' $el = "{0}:{1}:{2}" -f [int]$e.TotalHours, $e.Minutes, $e.Seconds;',
' };',
' $wsKb = [math]::Round($p.WorkingSet64 / 1024, 0);',
' $mem = if ($totalMemMB -gt 0) { [math]::Round($wsKb / 1024 / $totalMemMB * 100, 2) } else { 0 };',
' [PSCustomObject]@{',
' ProcessId = $p.Id;',
' ParentProcessId = $ppid;',
' Name = $p.ProcessName;',
' CpuPercent = $cpu;',
' MemPercent = $mem;',
' WorkingSetKb = $wsKb;',
' Elapsed = $el;',
' CommandLine = $cmd;',
' }',
'}',
'$rows | Sort-Object WorkingSetKb -Descending | Select-Object -First 200 | ConvertTo-Json -Compress',
].join(" ");
const PROCESS_LIST_PS_COMMAND = wrapPowerShell(encodePowerShellScript(PROCESS_LIST_PS));
// ---------------------------------------------------------------------------
// Port list
// ---------------------------------------------------------------------------
// portOps.cjs already defines LISTEN_PORTS_WINDOWS inline; export it here too
// so other ops can re-use the encoding convention.
const PORT_LIST_PS_INNER = [
'$rows = @();',
'$rows += @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ',
"Select-Object LocalAddress,LocalPort,OwningProcess,@{Name='Protocol';Expression={'tcp'}});",
'$rows += @(Get-NetUDPEndpoint -ErrorAction SilentlyContinue | Where-Object { ',
'$a = [string]$_.LocalAddress; ',
'$wildcard = ($a -eq "0.0.0.0" -or $a -eq "::" -or $a -eq "*"); ',
'$loopback = ($a -eq "127.0.0.1" -or $a -eq "::1"); ',
'if ($wildcard -or $loopback) { $true } else { $_.LocalPort -lt 49152 } ',
"} | Select-Object LocalAddress,LocalPort,OwningProcess,@{Name='Protocol';Expression={'udp'}}); ",
"if ($rows.Count -gt 0) { $rows | ConvertTo-Json -Compress } else { '[]' }",
].join("");
const PORT_LIST_PS_COMMAND = wrapPowerShell(encodePowerShellScript(PORT_LIST_PS_INNER));
// ---------------------------------------------------------------------------
// Service list
// ---------------------------------------------------------------------------
const SERVICE_LIST_PS_INNER = [
// Force UTF-8 output so Chinese service names don't get mangled over SSH.
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;',
'$OutputEncoding = [System.Text.Encoding]::UTF8;',
'$services = Get-Service -ErrorAction SilentlyContinue | ForEach-Object {',
' [PSCustomObject]@{',
' Name = $_.Name;',
' DisplayName = $_.DisplayName;',
' Status = $_.Status.ToString();',
' StartType = $_.StartType.ToString();',
' }',
'};',
'$services | ConvertTo-Json -Compress',
].join(" ");
const SERVICE_LIST_PS_COMMAND = wrapPowerShell(encodePowerShellScript(SERVICE_LIST_PS_INNER));
// ---------------------------------------------------------------------------
// Capability probe (mirrors CAPABILITY_SCRIPT_POSIX marker format)
// ---------------------------------------------------------------------------
const CAPABILITY_PROBE_PS_INNER = [
'$ErrorActionPreference = "SilentlyContinue";',
'Write-Output "__NC_OS__=Windows";',
'if (Get-Command tmux -ErrorAction SilentlyContinue) { Write-Output "__NC_TMUX__=1" };',
'if (Get-Command docker -ErrorAction SilentlyContinue) { Write-Output "__NC_DOCKER__=1" };',
'if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { Write-Output "__NC_NVIDIA_SMI__=1" };',
'if (Get-Command npu-smi -ErrorAction SilentlyContinue) { Write-Output "__NC_NPU_SMI__=1" };',
'if (Get-Command ss -ErrorAction SilentlyContinue) { Write-Output "__NC_SS__=1" };',
// Windows always has netstat.exe
'Write-Output "__NC_NETSTAT__=1";',
'if (Get-Command lsof -ErrorAction SilentlyContinue) { Write-Output "__NC_LSOF__=1" };',
// Windows has system services via Get-Service — treat as "systemctl-equivalent"
'Write-Output "__NC_SYSTEMCTL__=1";',
].join(" ");
const CAPABILITY_PROBE_PS_COMMAND = wrapPowerShell(encodePowerShellScript(CAPABILITY_PROBE_PS_INNER));
// ---------------------------------------------------------------------------
// Service action helper
// ---------------------------------------------------------------------------
function buildServiceActionPsCommand(action, serviceName) {
const safe = String(serviceName).replace(/[^a-zA-Z0-9_\\-]/g, "").slice(0, 256);
if (!safe) return null;
let verb;
switch (action) {
case "start": verb = "Start-Service"; break;
case "stop": verb = "Stop-Service"; break;
case "restart": verb = "Restart-Service"; break;
default: return null;
}
const script = `${verb} -Name "${safe}" -ErrorAction Stop; Write-Output "__NC_OK__"`;
return wrapPowerShell(encodePowerShellScript(script));
}
// ---------------------------------------------------------------------------
// Process kill
// ---------------------------------------------------------------------------
function buildStopProcessPsCommand(pid, force) {
const p = Math.trunc(Number(pid));
if (!Number.isFinite(p) || p <= 0) return null;
const cmd = force
? `Stop-Process -Id ${p} -Force -ErrorAction Stop; Write-Output "__NC_OK__"`
: `Stop-Process -Id ${p} -ErrorAction Stop; Write-Output "__NC_OK__"`;
return wrapPowerShell(encodePowerShellScript(cmd));
}
module.exports = {
encodePowerShellScript,
wrapPowerShell,
PROCESS_LIST_PS_COMMAND,
PORT_LIST_PS_COMMAND,
SERVICE_LIST_PS_COMMAND,
CAPABILITY_PROBE_PS_COMMAND,
buildServiceActionPsCommand,
buildStopProcessPsCommand,
};