Publicerad källa
Ladda ner källa ZIP
CH-J Server Manager
Bläddra i kataloger och filer för en specifik programutgåva.
CH-J / Server Manager
377 filer
core-20261019T111616Z-160a00bfcddc
Tillagd: · Byggd:
1
"use strict";2
const { spawn } = require("node:child_process");3
const net = require("node:net");4
const { setTimeout: delay } = require("node:timers/promises");5
const { DiagnosticError, checkAbort, stats, now } = require("./common");6
const { parsePing } = require("../sessions/latencyMonitor");7
function runProcess(executable, args, { timeoutMs = 5000, signal, onLine, maxBytes = 256 * 1024, spawnImpl = spawn, env } = {}) {8
checkAbort(signal);9
return new Promise((resolve, reject) => {10
const child = spawnImpl(executable, args, { shell: false, windowsHide: true, env: env || { ...process.env, LC_ALL: "C", LANG: "C" }, stdio: ["ignore", "pipe", "pipe"] });11
let stdout = [], stderr = [], bytes = 0, pending = "", failure, settled = false, killTimer;12
const terminate = (error) => { failure ||= error; child.kill(); killTimer ||= setTimeout(() => child.kill("SIGKILL"), 1000); killTimer.unref?.(); };13
const abort = () => terminate(new DiagnosticError("CANCELLED"));14
const timer = setTimeout(() => terminate(new DiagnosticError("TIMEOUT")), timeoutMs);15
signal?.addEventListener("abort", abort, { once: true });16
const finish = (error, code) => { if (settled) return; settled = true; clearTimeout(timer); clearTimeout(killTimer); signal?.removeEventListener("abort", abort); if (pending && onLine) onLine(pending); error || failure ? reject(error || failure) : resolve({ code, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr).toString("utf8") }); };17
child.once("error", (error) => finish(error));18
child.stdout.on("data", (chunk) => { bytes += chunk.length; if (bytes > maxBytes) return terminate(new DiagnosticError("OUTPUT_LIMIT")); stdout.push(chunk); if (onLine) { pending += chunk.toString("utf8"); const lines = pending.split(/\r?\n/); pending = lines.pop(); lines.forEach(onLine); } });19
child.stderr.on("data", (chunk) => { bytes += chunk.length; if (bytes > maxBytes) return terminate(new DiagnosticError("OUTPUT_LIMIT")); stderr.push(chunk); });20
child.once("close", (code) => finish(null, code));21
});22
}23
// Optional ICMP tuning is passed only when set, so OS defaults stay unchanged.24
// Don't-fragment applies to IPv4 only; IPv6 routers never fragment in transit.25
function pingCommand(address, timeoutMs, platform = process.platform, tuning = {}) {26
if (!net.isIP(address)) throw new DiagnosticError("INVALID_IP");27
const v6 = net.isIP(address) === 6, { packetSize = null, ttl = null, dontFragment = false } = tuning;28
const size = (flag) => Number.isInteger(packetSize) ? [flag, String(packetSize)] : [];29
const hops = (flag) => Number.isInteger(ttl) ? [flag, String(ttl)] : [];30
const df = (...flags) => dontFragment && !v6 ? flags : [];31
if (platform === "win32") return { executable: "ping.exe", args: [v6 ? "-6" : "-4", "-n", "1", "-w", String(timeoutMs), ...size("-l"), ...hops("-i"), ...df("-f"), address] };32
if (platform === "darwin") return { executable: v6 ? "/sbin/ping6" : "/sbin/ping", args: ["-n", "-c", "1", ...(v6 ? [] : ["-W", String(timeoutMs)]), ...size("-s"), ...hops(v6 ? "-h" : "-m"), ...df("-D"), address] };33
if (platform === "linux") return { executable: "ping", args: [v6 ? "-6" : "-4", "-n", "-c", "1", "-W", String(Math.ceil(timeoutMs / 1000)), ...size("-s"), ...hops("-t"), ...df("-M", "do"), address] };34
throw new DiagnosticError("PLATFORM_UNSUPPORTED");35
}36
function parsePingReply(output) {37
const rttMs = parsePing(output), upper = String(output).match(/<\s*(\d+(?:[.,]\d+)?)\s*ms/i), ttl = String(output).match(/(?:ttl|hlim|hop limit)[=:\s]+(\d+)/i);38
return { received: rttMs !== null || Boolean(upper), rttMs, upperBoundMs: upper ? Number(upper[1].replace(",", ".")) : null, ttl: ttl ? Number(ttl[1]) : null };39
}40
const MAX_PING_IN_FLIGHT = 32, MAX_CONSECUTIVE_PING_ERRORS = 50;41
const CONFIG_ERROR = /not permitted|permission denied|invalid option|unknown option|invalid argument|out of range|illegal|usage:/i;42
const TRANSIENT_ERROR = /network is unreachable|no route to host|general failure|transmit failed|no buffer space|resource temporarily unavailable|host is down/i;43
// Every probe is an independent single-packet OS ping, so sub-second intervals44
// need no root/flood privileges on any platform. Probes overlap up to a bound;45
// the OS tool measures each RTT itself, so spawn jitter does not skew latency.46
async function ping(address, options, signal, progress, runner = runProcess) {47
const intervalMs = options.intervalMs || 1000, timeoutMs = options.pingTimeoutMs || options.timeoutMs;48
const tuning = { packetSize: options.packetSize, ttl: options.ttl, dontFragment: options.dontFragment };49
const total = options.continuous ? Infinity : options.count * options.repetitions;50
const maxInFlight = Math.max(1, Math.min(MAX_PING_IN_FLIGHT, Math.ceil((timeoutMs + 500) / intervalMs) + 1));51
const packets = [], rtts = [], inFlight = new Set();52
let sent = 0, received = 0, launched = 0, throttled = 0, errorPackets = 0, consecutiveErrors = 0, failure = null;53
const summary = () => ({ sent, received, errorPackets, lossPercent: sent ? 100 * (sent - received) / sent : null, statistics: stats(rtts) });54
const probe = async (sequence) => {55
let packet;56
try {57
const command = pingCommand(address, timeoutMs, process.platform, tuning);58
const result = await runner(command.executable, command.args, { timeoutMs: timeoutMs + 500, signal });59
const raw = result.stdout.toString("utf8"); const reply = parsePingReply(raw);60
const explicitTimeout = /Request timed out|Request timeout|Zeitüberschreitung|Vypršel.*časov[ýy].*limit/i.test(raw);61
// Successful replies keep a short raw line; failures keep more for diagnosis (bounded IPC/history size).62
packet = { sequence, address, ...reply, status: reply.received ? "success" : explicitTimeout ? "timeout" : "no-reply", raw: (raw + result.stderr).slice(0, reply.received ? 300 : 4096) };63
if (!reply.received && (result.code > (process.platform === "darwin" ? 2 : 1) || CONFIG_ERROR.test(result.stderr) || TRANSIENT_ERROR.test(result.stderr))) {64
packet.status = "error"; packet.fatal = CONFIG_ERROR.test(result.stderr);65
}66
} catch (error) {67
if (signal.aborted) return;68
packet = { sequence, address, received: false, rttMs: null, status: error.code === "ENOENT" ? "unavailable" : error.code === "TIMEOUT" ? "timeout" : "error", code: error.code, raw: String(error.message).slice(0, 400) };69
}70
if (signal.aborted || failure) return;71
packet.measuredAt = new Date().toISOString();72
// A missing tool, an option/permission problem, or an error before any probe settled73
// stops the run. Later transient errors (no buffer space, route flaps, ARP/Hello74
// failures) count as lost packets, so a long run is not discarded by one bad probe.75
const fatal = packet.status === "unavailable" || packet.status === "error" && (packet.fatal || sent === 0 || consecutiveErrors + 1 >= MAX_CONSECUTIVE_PING_ERRORS);76
delete packet.fatal;77
if (fatal) { if (packet.status === "error" && sent > 0) packet.code ||= "PING_REPEATED_ERRORS"; failure = packet; packets.push(packet); return; }78
consecutiveErrors = packet.status === "error" ? consecutiveErrors + 1 : 0; if (packet.status === "error") errorPackets++;79
sent++; if (packet.received) { received++; rtts[sequence - 1] = packet.rttMs; }80
packets.push(packet); if (packets.length > 500) packets.shift();81
progress({ kind: "ping-packet", ...packet, ...summary() });82
};83
const started = now(); let nextAt = started, lastLaunchAt = started;84
try {85
while (launched < total && !failure) {86
checkAbort(signal);87
if (inFlight.size >= maxInFlight) { throttled++; await Promise.race(inFlight); continue; }88
const sequence = ++launched, task = probe(sequence).finally(() => inFlight.delete(task)); inFlight.add(task);89
lastLaunchAt = now();90
// Keep a steady cadence without a catch-up burst after the in-flight bound held probes back.91
nextAt = Math.max(nextAt + intervalMs, lastLaunchAt - intervalMs / 2);92
if (launched < total && nextAt > now()) await delay(nextAt - now(), undefined, { signal });93
}94
} finally { await Promise.allSettled(inFlight); }95
checkAbort(signal);96
packets.sort((a, b) => a.sequence - b.sequence);97
const timing = { intervalMs, effectiveIntervalMs: launched > 1 ? (lastLaunchAt - started) / (launched - 1) : null, timeoutMs, maxInFlight, throttled, packetSize: options.packetSize ?? null, ttl: options.ttl ?? null, dontFragment: Boolean(options.dontFragment) };98
if (failure) progress({ kind: "ping-packet", ...failure, ...summary() });99
if (failure) return { status: failure.status, code: failure.code || "PING_TOOL_ERROR", reason: failure.raw, address, packets, ...summary(), timing, note: "ICMP failure does not establish server unavailability or prove a firewall." };100
return { status: received ? "success" : "warning", address, packets, ...summary(), timing, note: "No ICMP reply may mean filtering, ICMP rate limiting or loss; it does not establish server unavailability." };101
}102
function tracerouteCommand(address, options, platform = process.platform) {103
const v6 = net.isIP(address) === 6;104
if (!net.isIP(address)) throw new DiagnosticError("INVALID_IP");105
if (platform === "win32") return { executable: "tracert.exe", args: [v6 ? "-6" : "-4", "-d", "-h", String(options.maxHops), "-w", String(options.timeoutMs), address] };106
if (platform === "darwin") return { executable: v6 ? "/usr/sbin/traceroute6" : "/usr/sbin/traceroute", args: ["-n", "-m", String(options.maxHops), "-w", String(Math.ceil(options.timeoutMs / 1000)), address] };107
if (platform === "linux") return { executable: "traceroute", args: [v6 ? "-6" : "-4", "-n", "-m", String(options.maxHops), "-w", String(options.timeoutMs / 1000), address] };108
throw new DiagnosticError("PLATFORM_UNSUPPORTED");109
}110
function parseHop(line) {111
const match = String(line).match(/^\s*(\d{1,2})\s+(.+)/); if (!match) return null;112
const tokens = match[2].split(/\s+/), addresses = tokens.map((token) => token.replace(/[()[\],]/g, "")).filter((value) => net.isIP(value));113
const probes = [...match[2].matchAll(/(<\s*)?(\d+(?:[.,]\d+)?)\s*ms|\*/gi)].map((m) => m[0] === "*" ? { status: "timeout", rttMs: null } : { status: "success", rttMs: m[1] ? null : Number(m[2].replace(",", ".")), upperBoundMs: m[1] ? Number(m[2]) : null });114
if (!probes.length && !addresses.length) return null;115
return { hop: Number(match[1]), addresses: [...new Set(addresses)], probes, averageMs: stats(probes.map((p) => p.rttMs)).average, lossPercent: probes.length ? 100 * probes.filter((p) => p.status === "timeout").length / probes.length : null, raw: line.slice(0, 1024) };116
}117
async function traceroute(address, options, signal, progress, runner = runProcess) {118
const routes = [];119
for (let iteration = 0; iteration < options.repetitions; iteration++) {120
checkAbort(signal); const hops = [], command = tracerouteCommand(address, options);121
try {122
const result = await runner(command.executable, command.args, { signal, timeoutMs: Math.min(180000, options.maxHops * Math.ceil(options.timeoutMs / 1000) * 1000 * 3 + 2000), onLine(line) { const hop = parseHop(line); if (hop) { hops.push(hop); progress({ kind: "trace-hop", iteration, address, ...hop }); } } });123
routes.push({ iteration, hops, exitCode: result.code, raw: result.stdout.toString("utf8").slice(0, 32000), reason: result.stderr.slice(0, 1000) });124
} catch (error) { if (signal.aborted) throw error; if (hops.length) routes.push({ iteration, hops, incomplete: true, reason: error.message }); return { status: error.code === "ENOENT" ? "unavailable" : "error", code: error.code, reason: error.message, address, routes }; }125
}126
return { status: routes.some((route) => route.hops.some((hop) => hop.addresses.some((ip) => ip === address || net.isIP(ip) === 6 && net.isIP(address) === 6 && new URL(`http://[${ip}]/`).hostname === new URL(`http://[${address}]/`).hostname))) ? "success" : routes.every((route) => route.exitCode !== 0 && !route.hops.length) ? "error" : "warning", address, routes, effectiveProbeTimeoutMs: process.platform === "darwin" ? Math.ceil(options.timeoutMs / 1000) * 1000 : options.timeoutMs, routeChanged: routes.length > 1 ? routes.some((route) => JSON.stringify(route.hops.map((h) => h.addresses)) !== JSON.stringify(routes[0].hops.map((h) => h.addresses))) : null, method: process.platform === "win32" ? "ICMP tracert" : "System UDP traceroute", note: "Intermediate timeouts do not prove a broken route. Loss is the fraction of unanswered displayed probes, not end-to-end packet loss." };127
}128
module.exports = { runProcess, pingCommand, parsePingReply, ping, tracerouteCommand, parseHop, traceroute };SHA-256
d305e248cd633473c5c2ee4b4e937bf2034b1279f88d96e0efd82f2868b2cb04CH-J Proprietary Software License 1.14 · Källan finns under CH-J Proprietary Software License 1.14. Dess tillgänglighet ändrar inte licensvillkoren eller ger ytterligare rättigheter.
Arkiv SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a