@livedesk/client 0.1.236 → 0.1.237

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,59 +1,319 @@
1
- const { execFile, spawn } = require("node:child_process");
2
- const fs = require("node:fs");
3
- const path = require("node:path");
4
- const bootstrapPath = String(process.env.LIVEDESK_CLIENT_UPDATE_BOOTSTRAP_PATH || "").trim(); if (bootstrapPath) { try { fs.rmSync(bootstrapPath, { force: true }); } catch {} }
5
- const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();
1
+ const fs = require('node:fs');
2
+ const net = require('node:net');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+
6
+ const UPDATE_HOST_PROTOCOL_VERSION = 1;
7
+ const UPDATE_HOST_MAX_MESSAGE_BYTES = 2 * 1024 * 1024;
8
+ const bootstrapPath = String(process.env.LIVEDESK_CLIENT_UPDATE_BOOTSTRAP_PATH || '').trim();
9
+ if (bootstrapPath) {
10
+ try { fs.rmSync(bootstrapPath, { force: true }); } catch { /* operation-owned temporary file */ }
11
+ }
12
+
13
+ const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
6
14
  const originalCwd = path.resolve(String(process.env.LIVEDESK_UPDATE_ORIGINAL_CWD || process.cwd()));
7
- const statePath = path.resolve(originalCwd, String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json")));
8
- const neutralCwdValue = String(process.env.LIVEDESK_UPDATE_NEUTRAL_CWD || "").trim();
9
- const neutralCwd = neutralCwdValue ? path.resolve(neutralCwdValue) : "";
10
- const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");
11
- const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);
15
+ const statePath = path.resolve(originalCwd, String(
16
+ process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json')
17
+ ));
18
+ const neutralCwdValue = String(process.env.LIVEDESK_UPDATE_NEUTRAL_CWD || '').trim();
19
+ const neutralCwd = neutralCwdValue ? path.resolve(neutralCwdValue) : '';
20
+ const cleanVersion = value => String(value || '').trim().replace(/^v/i, '');
21
+ const targetProductVersion = cleanVersion(
22
+ process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
23
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
24
+ );
12
25
  const updateDeadlineEpochMs = Number(process.env.LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS || 0);
13
- const deadlineExpired = () => !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs;
14
26
  const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
15
- const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };
16
- const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
17
- const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };
18
- const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };
19
- const WINDOWS_PROCESS_VERIFY_MAX_CIM_QUERIES = 4; const WINDOWS_PROCESS_VERIFY_MAX_POWERSHELL_SPAWNS = 4; const WINDOWS_PROCESS_VERIFY_TIMEOUT_MS = 4000; const WINDOWS_PROCESS_VERIFY_MAX_TOTAL_CIM_MS = 8000; const WINDOWS_PROCESS_VERIFY_MAX_CPU_MS = 2000; const WINDOWS_PROCESS_VERIFY_MAX_RSS_BYTES = 256 * 1024 * 1024; const WINDOWS_PROCESS_VERIFY_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
20
- const windowsProcessVerification = { powerShellSpawnCount: 0, activePowerShellChildren: 0, maximumConcurrentPowerShellChildren: 0, powerShellResourceEvidenceCount: 0, powerShellResourceEvidenceMissingCount: 0, cimQueryCount: 0, activeCimQueries: 0, cimTotalDurationMs: 0, maximumCimProviderDurationMs: 0, totalPowerShellCpuMs: 0, maximumPowerShellCpuMs: 0, maximumPowerShellRssBytes: 0, activeVerificationWatchdogTimers: 0, stateReadCount: 0, stateWatchEventCount: 0, activeStateWatchers: 0, activeStateFallbackTimers: 0, cleanupCompletedCount: 0, cleanupFailureCount: 0, cleanupBlockedCount: 0, forcedCleanupProofFailureCount: 0, failClosedOwnershipHeld: false, failClosedOwnershipTimerCount: 0, terminalZeroProved: false, blockedExactIdentityCount: 0, blockedExactIdentities: [], directChildKillAttempted: false, directChildExitProved: false };
21
- const assertWindowsVerificationBudget = () => { if (windowsProcessVerification.powerShellSpawnCount > WINDOWS_PROCESS_VERIFY_MAX_POWERSHELL_SPAWNS) throw new Error("Windows update cleanup exceeded its total PowerShell spawn budget."); if (windowsProcessVerification.cimQueryCount > WINDOWS_PROCESS_VERIFY_MAX_CIM_QUERIES) throw new Error("Windows update cleanup exceeded its total CIM query budget."); if (windowsProcessVerification.cimTotalDurationMs > WINDOWS_PROCESS_VERIFY_MAX_TOTAL_CIM_MS) throw new Error("Windows update cleanup exceeded its total CIM duration budget."); if (windowsProcessVerification.powerShellResourceEvidenceMissingCount !== 0 || windowsProcessVerification.powerShellResourceEvidenceCount !== windowsProcessVerification.powerShellSpawnCount) throw new Error("Windows update cleanup has unknown PowerShell resource evidence."); if (windowsProcessVerification.totalPowerShellCpuMs > WINDOWS_PROCESS_VERIFY_MAX_CPU_MS * WINDOWS_PROCESS_VERIFY_MAX_POWERSHELL_SPAWNS) throw new Error("Windows update cleanup exceeded its cumulative PowerShell CPU budget."); if (windowsProcessVerification.maximumPowerShellCpuMs > WINDOWS_PROCESS_VERIFY_MAX_CPU_MS) throw new Error("Windows update cleanup exceeded its per-process PowerShell CPU budget."); if (windowsProcessVerification.maximumPowerShellRssBytes > WINDOWS_PROCESS_VERIFY_MAX_RSS_BYTES) throw new Error("Windows update cleanup exceeded its PowerShell peak RSS budget."); };
22
- const writeWindowsVerificationEvidence = (outcome, child = null) => { const evidencePath = String(process.env.LIVEDESK_CLIENT_UPDATE_PROCESS_EVIDENCE_PATH || "").trim(); if (!evidencePath) return; try { const evidence = { outcome, platform: process.platform, budgets: { maximumCimQueries: WINDOWS_PROCESS_VERIFY_MAX_CIM_QUERIES, maximumPowerShellSpawns: WINDOWS_PROCESS_VERIFY_MAX_POWERSHELL_SPAWNS, maximumCimDurationMs: WINDOWS_PROCESS_VERIFY_MAX_TOTAL_CIM_MS, maximumPowerShellCpuMs: WINDOWS_PROCESS_VERIFY_MAX_CPU_MS, maximumPowerShellRssBytes: WINDOWS_PROCESS_VERIFY_MAX_RSS_BYTES, maximumTotalPowerShellCpuMs: WINDOWS_PROCESS_VERIFY_MAX_CPU_MS * WINDOWS_PROCESS_VERIFY_MAX_POWERSHELL_SPAWNS, scope: "whole-cleanup-operation" }, ...windowsProcessVerification, terminalSnapshotPromise: Boolean(child?.livedeskWindowsSnapshotPromise), terminalDrainPromise: Boolean(child?.livedeskWindowsDrainPromise), terminalTrackingTimer: false }; const temporary = evidencePath + "." + process.pid + ".tmp"; fs.mkdirSync(path.dirname(evidencePath), { recursive: true }); fs.writeFileSync(temporary, JSON.stringify(evidence, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, evidencePath); } catch {} };
23
- const encodePowerShellCommand = value => Buffer.from(String(value || ""), "utf16le").toString("base64");
24
- const spawnCapturedWindowsPowerShell = command => { const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodePowerShellCommand(command)], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }); windowsProcessVerification.powerShellSpawnCount += 1; windowsProcessVerification.activePowerShellChildren += 1; windowsProcessVerification.maximumConcurrentPowerShellChildren = Math.max(windowsProcessVerification.maximumConcurrentPowerShellChildren, windowsProcessVerification.activePowerShellChildren); let stdoutBytes = 0; let stderrBytes = 0; let stdout = ""; let stderr = ""; let outputOverflow = false; const append = (chunk, isError) => { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); const previous = isError ? stderrBytes : stdoutBytes; const remaining = Math.max(0, WINDOWS_PROCESS_VERIFY_MAX_OUTPUT_BYTES - previous); if (buffer.length > remaining) outputOverflow = true; const retained = remaining > 0 ? buffer.subarray(0, remaining).toString("utf8") : ""; if (isError) { stderrBytes += buffer.length; stderr += retained; } else { stdoutBytes += buffer.length; stdout += retained; } }; child.stdout?.on("data", chunk => append(chunk, false)); child.stderr?.on("data", chunk => append(chunk, true)); const completion = new Promise(resolve => { let settled = false; const finish = (code, signal, error) => { if (settled) return; settled = true; windowsProcessVerification.activePowerShellChildren -= 1; resolve({ code, signal, error, stdout, stderr, outputOverflow }); }; child.once("error", error => finish(null, null, error)); child.once("close", (code, signal) => finish(code, signal, null)); }); return { child, completion }; };
25
- const runWindowsPowerShell = async (script, kind = "control") => { if (windowsProcessVerification.powerShellSpawnCount + 1 > WINDOWS_PROCESS_VERIFY_MAX_POWERSHELL_SPAWNS) throw new Error("Windows update cleanup exhausted its measured PowerShell spawn budget."); if (kind === "cim" && windowsProcessVerification.cimQueryCount >= WINDOWS_PROCESS_VERIFY_MAX_CIM_QUERIES) throw new Error("Windows update cleanup exhausted its CIM query budget."); const remainingCimDurationMs = WINDOWS_PROCESS_VERIFY_MAX_TOTAL_CIM_MS - windowsProcessVerification.cimTotalDurationMs; if (kind === "cim" && remainingCimDurationMs <= 0) throw new Error("Windows update cleanup exhausted its cumulative CIM duration budget."); const timeoutMs = kind === "cim" ? Math.max(1, Math.min(WINDOWS_PROCESS_VERIFY_TIMEOUT_MS, Math.ceil(remainingCimDurationMs))) : WINDOWS_PROCESS_VERIFY_TIMEOUT_MS; const startedAtMs = Date.now(); if (kind === "cim") { windowsProcessVerification.cimQueryCount += 1; windowsProcessVerification.activeCimQueries += 1; } const action = Buffer.from(String(script || ""), "utf16le").toString("base64"); const wrapper = `$ErrorActionPreference = "Stop"; $result = [ordered]@{ EvidenceAvailable = $false; ProcessId = [int]$PID; StartOrder = ""; CpuMs = $null; PeakRssBytes = $null; Ok = $false; Output = ""; ActionError = "" }; $self = [Diagnostics.Process]::GetCurrentProcess(); try { $rawStartTicks = [long]$self.StartTime.ToUniversalTime().Ticks; $result.StartOrder = [string]($rawStartTicks - ($rawStartTicks % 10000)); $actionText = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String("${action}")); $actionOutput = & ([ScriptBlock]::Create($actionText)) | Out-String; $result.Output = [string]$actionOutput.TrimEnd(); $result.Ok = $true } catch { $result.ActionError = $_.Exception.Message } finally { try { $self.Refresh(); $result.CpuMs = [double]$self.TotalProcessorTime.TotalMilliseconds; $result.PeakRssBytes = [double]$self.PeakWorkingSet64; $result.EvidenceAvailable = $true } catch { if (-not $result.ActionError) { $result.ActionError = $_.Exception.Message } }; $result | ConvertTo-Json -Compress }`; let owner = null; let watchdog = null; let watchdogExpired = false; let completion = null; try { owner = spawnCapturedWindowsPowerShell(wrapper); windowsProcessVerification.activeVerificationWatchdogTimers += 1; watchdog = setTimeout(() => { watchdogExpired = true; try { owner?.child.kill("SIGKILL"); } catch {} }, timeoutMs); completion = await owner.completion; } finally { if (watchdog) { clearTimeout(watchdog); watchdog = null; windowsProcessVerification.activeVerificationWatchdogTimers -= 1; } if (owner?.child.exitCode === null && owner?.child.signalCode === null) { try { owner.child.kill("SIGKILL"); } catch {} } if (owner) await owner.completion; if (kind === "cim") { windowsProcessVerification.activeCimQueries -= 1; const elapsedCimMs = Math.max(0, Date.now() - startedAtMs); windowsProcessVerification.cimTotalDurationMs += elapsedCimMs; } } let envelope = null; try { envelope = JSON.parse(String(completion?.stdout || "").trim()); } catch {} const cpuMs = Number(envelope?.CpuMs); const peakRssBytes = Number(envelope?.PeakRssBytes); const exactResourceEvidence = envelope?.EvidenceAvailable === true && Number(envelope?.ProcessId) === Number(owner?.child.pid) && /^\d+$/.test(String(envelope?.StartOrder || "")) && Number.isFinite(cpuMs) && cpuMs >= 0 && Number.isFinite(peakRssBytes) && peakRssBytes > 0; if (!exactResourceEvidence) { windowsProcessVerification.powerShellResourceEvidenceMissingCount += 1; throw new Error("Windows PowerShell returned unknown exact-identity CPU/RSS evidence."); } windowsProcessVerification.powerShellResourceEvidenceCount += 1; windowsProcessVerification.totalPowerShellCpuMs += cpuMs; windowsProcessVerification.maximumPowerShellCpuMs = Math.max(windowsProcessVerification.maximumPowerShellCpuMs, cpuMs); windowsProcessVerification.maximumPowerShellRssBytes = Math.max(windowsProcessVerification.maximumPowerShellRssBytes, peakRssBytes); assertWindowsVerificationBudget(); if (watchdogExpired) throw new Error("Windows PowerShell child exceeded its exact timeout."); if (completion?.outputOverflow) throw new Error("Windows PowerShell output exceeded its bounded buffer."); if (completion?.error || Number(completion?.code) !== 0) throw new Error(String(completion?.stderr || completion?.error?.message || "PowerShell failed without a complete resource envelope.").trim()); if (envelope.Ok !== true || envelope.ActionError) throw new Error(String(envelope.ActionError || "PowerShell action failed.").trim()); return String(envelope.Output || "").trim(); };
26
- const queryWindowsProcessTable = async () => { const stdout = await runWindowsPowerShell(`$ErrorActionPreference = "Stop"; $watch = [Diagnostics.Stopwatch]::StartNew(); $records = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object { if ($_.CreationDate) { $rawTicks = [long]$_.CreationDate.ToUniversalTime().Ticks; [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = [string]($rawTicks - ($rawTicks % 10000)) } } }); $watch.Stop(); [pscustomobject]@{ Records = $records; CimDurationMs = [double]$watch.Elapsed.TotalMilliseconds } | ConvertTo-Json -Compress -Depth 4`, "cim"); if (!stdout) throw new Error("Windows process snapshot returned no data"); const parsed = JSON.parse(stdout); const cimDurationMs = Number(parsed?.CimDurationMs); if (!Number.isFinite(cimDurationMs) || cimDurationMs < 0) throw new Error("Windows process snapshot omitted bounded CIM evidence."); windowsProcessVerification.maximumCimProviderDurationMs = Math.max(windowsProcessVerification.maximumCimProviderDurationMs, cimDurationMs); assertWindowsVerificationBudget(); const values = parsed?.Records == null ? [] : (Array.isArray(parsed.Records) ? parsed.Records : [parsed.Records]); return values.map(item => ({ pid: Number(item?.ProcessId || 0), parentPid: Number(item?.ParentProcessId || 0), startOrder: String(item?.CreationUtcTicks || "") })).filter(item => Number.isInteger(item.pid) && item.pid > 1 && /^\d+$/.test(item.startOrder)); };
27
- const queryTrackedWindowsProcessTable = child => { if (child.livedeskWindowsSnapshotPromise) return child.livedeskWindowsSnapshotPromise; const pending = queryWindowsProcessTable(); child.livedeskWindowsSnapshotPromise = pending; const clear = () => { if (child.livedeskWindowsSnapshotPromise === pending) child.livedeskWindowsSnapshotPromise = null; }; pending.then(clear, clear); return pending; };
28
- const sameWindowsIdentity = (left, right) => Number(left?.pid || 0) === Number(right?.pid || 0) && String(left?.startOrder || "") === String(right?.startOrder || "");
29
- const windowsTicksAtMilliseconds = value => BigInt(Math.trunc(Number(value) || Date.now())) * 10000n + 621355968000000000n;
30
- const setWindowsRootLifetimeEnd = (child, ticks) => { const candidate = BigInt(ticks); const previous = child.livedeskWindowsRootLifetimeEndTicks; if (!previous || candidate < BigInt(previous)) child.livedeskWindowsRootLifetimeEndTicks = String(candidate); return BigInt(child.livedeskWindowsRootLifetimeEndTicks); };
31
- const mergeRootAbsentWindowsTree = (child, rootPid, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const exactCurrentByPid = new Map(); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } const lowerBound = windowsTicksAtMilliseconds(Number(child.livedeskWindowsSpawnedAtMs || Date.now()) - 2000); const upperBound = child.livedeskWindowsRootLifetimeEndTicks ? BigInt(child.livedeskWindowsRootLifetimeEndTicks) : null; let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === rootPid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < lowerBound || (upperBound && recordStart > upperBound)) continue; } catch { continue; } const parent = record.parentPid === rootPid ? { depth: 0 } : exactCurrentByPid.get(record.parentPid); if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };
32
- const mergeExactWindowsTree = (child, root, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const currentRoot = currentByPid.get(root.pid) || null; const rootIsExact = sameWindowsIdentity(currentRoot, root); const rootWasReused = Boolean(currentRoot && !rootIsExact); if (rootWasReused) setWindowsRootLifetimeEnd(child, BigInt(currentRoot.startOrder) - 1n); else if (!currentRoot && child.livedeskWindowsRootExitedAtMs) setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); const rootLifetimeClosed = Boolean(child.livedeskWindowsRootLifetimeEndTicks); if (rootIsExact) tracked.set(`${root.pid}:${root.startOrder}`, { ...root, depth: 0 }); const exactCurrentByPid = new Map(); if (rootIsExact) exactCurrentByPid.set(root.pid, { ...root, depth: 0 }); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === root.pid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < BigInt(root.startOrder)) continue; if (rootLifetimeClosed && recordStart > BigInt(child.livedeskWindowsRootLifetimeEndTicks)) continue; } catch { continue; } let parent = exactCurrentByPid.get(record.parentPid) || null; if (!parent && !currentRoot && rootLifetimeClosed && record.parentPid === root.pid) parent = { ...root, depth: 0 }; if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };
33
- const refreshTrackedWindowsTree = async (child, state = readState()) => { const snapshot = await queryTrackedWindowsProcessTable(child); const lowerBound = windowsTicksAtMilliseconds(Number(child.livedeskWindowsSpawnedAtMs || Date.now()) - 2000); const upperBound = child.livedeskWindowsRootLifetimeEndTicks ? BigInt(child.livedeskWindowsRootLifetimeEndTicks) : null; const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const seedPids = new Set([Number(child.pid || 0), Number(state?.supervisorPid || 0)].filter(pid => Number.isInteger(pid) && pid > 1)); for (const seedPid of seedPids) { const seed = currentByPid.get(seedPid); if (!seed) continue; let seedStart; try { seedStart = BigInt(seed.startOrder); } catch { continue; } if (seedStart < lowerBound || (upperBound && seedStart > upperBound)) continue; mergeExactWindowsTree(child, seed, snapshot); } mergeRootAbsentWindowsTree(child, Number(child.pid || 0), snapshot); return snapshot; };
34
- const stopExactWindowsRecords = async records => { if (!Array.isArray(records) || records.length === 0) return false; const encoded = Buffer.from(JSON.stringify(records.map(record => ({ pid: record.pid, startOrder: record.startOrder }))), "utf8").toString("base64"); const script = `$ErrorActionPreference = "Stop"; $targetsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encoded}")); $targets = @(ConvertFrom-Json -InputObject $targetsJson); $failures = [System.Collections.Generic.List[string]]::new(); foreach ($item in $targets) { $targetPid = [int]$item.pid; $expectedStartTicks = [string]$item.startOrder; try { $target = Get-Process -Id $targetPid -ErrorAction SilentlyContinue | Select-Object -First 1; if (-not $target) { continue }; $rawTicks = [long]$target.StartTime.ToUniversalTime().Ticks; $targetStartTicks = [string]($rawTicks - ($rawTicks % 10000)); if ($targetStartTicks -ne $expectedStartTicks) { continue }; Stop-Process -Id $targetPid -Force -ErrorAction Stop; try { [void]$target.WaitForExit(750) } catch {} } catch { $failures.Add("pid=$targetPid $($_.Exception.Message)") } }; $verifiedAbsent = 0; foreach ($item in $targets) { $targetPid = [int]$item.pid; $expectedStartTicks = [string]$item.startOrder; try { $current = Get-Process -Id $targetPid -ErrorAction SilentlyContinue | Select-Object -First 1; if ($current) { $rawTicks = [long]$current.StartTime.ToUniversalTime().Ticks; $currentStartTicks = [string]($rawTicks - ($rawTicks % 10000)); if ($currentStartTicks -eq $expectedStartTicks) { $failures.Add("pid=$targetPid exact identity survived Stop-Process"); continue } }; $verifiedAbsent += 1 } catch { $failures.Add("pid=$targetPid verification failed: $($_.Exception.Message)") } }; if ($failures.Count -gt 0) { throw ($failures -join "; ") }; [pscustomobject]@{ TerminalIdentityProof = $true; VerifiedAbsent = $verifiedAbsent; TargetCount = $targets.Count } | ConvertTo-Json -Compress`; const stdout = await runWindowsPowerShell(script, "control"); const proof = JSON.parse(stdout || "{}"); if (proof?.TerminalIdentityProof !== true || Number(proof?.VerifiedAbsent) !== records.length || Number(proof?.TargetCount) !== records.length) throw new Error("Windows exact stop omitted terminal identity proof."); return true; };
35
- const trackWindowsChild = child => { if (process.platform !== "win32" || Number(child?.pid || 0) <= 1 || child.livedeskWindowsTrackingInitialized) return; child.livedeskWindowsTrackingInitialized = true; child.livedeskWindowsTrackedRecords = new Map(); child.livedeskWindowsSpawnedAtMs = Date.now(); child.livedeskWindowsRootExitedAtMs = null; child.livedeskWindowsTrackingStopped = false; child.once("exit", () => { if (!child.livedeskWindowsRootExitedAtMs) child.livedeskWindowsRootExitedAtMs = Date.now(); setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); }); };
36
- const stopWindowsTracking = child => { child.livedeskWindowsTrackingStopped = true; child.livedeskWindowsSnapshotPromise = null; };
37
- const terminateExactWindowsTrackedTree = child => { if (child.livedeskWindowsDrainPromise) return child.livedeskWindowsDrainPromise; const pending = (async () => { let emptyPasses = 0; let lastError = null; for (let attempt = 1; attempt <= WINDOWS_PROCESS_VERIFY_MAX_CIM_QUERIES; attempt += 1) { try { const snapshot = await refreshTrackedWindowsTree(child, readState()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const remaining = [...child.livedeskWindowsTrackedRecords.values()].filter(target => sameWindowsIdentity(currentByPid.get(target.pid), target)).sort((left, right) => Number(right.depth || 0) - Number(left.depth || 0)); if (Number(process.env.LIVEDESK_TEST_FORCE_CLEANUP_PROOF_FAILURE_ONCE || 0) > 0 && windowsProcessVerification.forcedCleanupProofFailureCount === 0 && remaining.length > 0) { windowsProcessVerification.forcedCleanupProofFailureCount = 1; throw new Error("Simulated exact cleanup proof failure."); } if (remaining.length === 0) { emptyPasses += 1; } else { emptyPasses = await stopExactWindowsRecords(remaining) ? 1 : 0; } if (emptyPasses >= 2) { stopWindowsTracking(child); assertWindowsVerificationBudget(); windowsProcessVerification.terminalZeroProved = true; return; } child.livedeskWindowsTrackingError = null; lastError = null; } catch (error) { if (error?.message === "Simulated exact cleanup proof failure.") throw error; emptyPasses = 0; lastError = error; child.livedeskWindowsTrackingError = error; } if (attempt < WINDOWS_PROCESS_VERIFY_MAX_CIM_QUERIES) await sleep(150 * attempt); } stopWindowsTracking(child); throw lastError || new Error("Windows update cleanup did not reach two exact terminal identity proofs."); })(); child.livedeskWindowsDrainPromise = pending; const clear = () => { if (child.livedeskWindowsDrainPromise === pending) child.livedeskWindowsDrainPromise = null; }; pending.then(clear, clear); return pending; };
38
- const stateLockPath = statePath + ".lock"; const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));
39
- const waitForStateLock = ms => Atomics.wait(lockWaitBuffer, 0, 0, Math.max(1, ms));
40
- const removeAbandonedStateLock = () => { let owner = null; try { owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); } catch {} const ownerPid = Number(owner?.pid || 0); if (Number.isInteger(ownerPid) && ownerPid > 1) { if (isAlive(ownerPid)) return false; } else { try { if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5000) return false; } catch { return true; } } try { fs.rmSync(stateLockPath); return true; } catch { return false; } };
41
- const withStateLock = callback => { fs.mkdirSync(path.dirname(stateLockPath), { recursive: true }); const deadline = Date.now() + 10000; let descriptor = null; let token = ""; while (descriptor === null) { try { descriptor = fs.openSync(stateLockPath, "wx", 0o600); token = process.pid + "-" + Date.now() + "-" + Math.random().toString(16).slice(2); fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token, acquiredAt: new Date().toISOString() }), "utf8"); } catch (error) { if (descriptor !== null) { try { fs.closeSync(descriptor); } catch {} descriptor = null; try { fs.rmSync(stateLockPath, { force: true }); } catch {} } if (error?.code !== "EEXIST") throw error; if (removeAbandonedStateLock()) continue; if (Date.now() >= deadline) throw new Error("Timed out waiting for the LiveDesk update state lock: " + stateLockPath); waitForStateLock(Math.min(25, Math.max(1, deadline - Date.now()))); } } try { return callback(); } finally { try { fs.closeSync(descriptor); } catch {} try { const owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true }); } catch {} } };
42
- const writeFailure = (error, cancelRequested = false) => { try { withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown" || previous.stage === "connected" || previous.stage === "restored") return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: cancelRequested || previous.cancelRequested === true, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); }); } catch {} process.stderr.write("LiveDesk update handoff failed: " + (error?.message || error) + "\n"); process.exitCode = 1; };
43
- const writeCleanupBlocked = error => { try { withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown" || previous.stage === "connected" || previous.stage === "restored") return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cleanupBlocked: true, cleanupTerminalZero: false, cleanupBlockedExactIdentities: windowsProcessVerification.blockedExactIdentities, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".cleanup-blocked.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); }); } catch {} process.stderr.write("LiveDesk update cleanup blocked: " + (error?.message || error) + "\n"); process.exitCode = 1; };
44
- const writeCancellation = error => withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return "superseded"; if (previous.operationId !== operationId) return false; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown") return "ready"; if (previous.stage === "failed") return previous.cancelRequested === true ? "cancelled" : "failed"; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: true, error: String(error?.message || error).slice(0, 4000), cancelledAt: now, failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".cancel.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return "cancelled"; });
45
- const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { await terminateExactWindowsTrackedTree(child); if (windowsProcessVerification.activePowerShellChildren !== 0 || windowsProcessVerification.activeCimQueries !== 0) throw new Error("Windows update cleanup retained an active verification child."); return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };
46
- const writeHandoffStarted = () => withStateLock(() => { if (deadlineExpired()) throw new Error("The absolute LiveDesk Client update deadline expired before handoff."); const previous = readState(); if (previous.operationId !== operationId) throw new Error("LiveDesk update handoff was superseded before exact-package launch."); const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "handoff-started", starterPid: process.pid, updateDeadlineEpochMs, restartVerified: false, error: "", updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff-started.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); });
47
- const stopDirectOwnedChild = async child => { const alreadyExited = child.exitCode !== null || child.signalCode !== null; windowsProcessVerification.directChildKillAttempted = !alreadyExited; if (!alreadyExited) { try { child.kill("SIGKILL"); } catch {} } const exited = child.exitCode !== null || child.signalCode !== null ? true : await new Promise(resolve => { let settled = false; const finish = value => { if (settled) return; settled = true; clearTimeout(deadline); child.removeListener("close", onClose); resolve(value); }; const onClose = () => finish(true); const deadline = setTimeout(() => finish(false), 750); child.once("close", onClose); }); for (const stream of [child.stdin, child.stdout, child.stderr]) { try { stream?.destroy?.(); } catch {} } windowsProcessVerification.directChildExitProved = exited || child.exitCode !== null || child.signalCode !== null; return windowsProcessVerification.directChildExitProved; };
48
- const monitorPreflight = child => { let settled = false; let draining = false; let drainPromise = null; let stageWatcher = null; let stageFallback = null; let timeout = null; let inspectScheduled = false; const releaseObservation = () => { if (stageFallback) { clearInterval(stageFallback); stageFallback = null; windowsProcessVerification.activeStateFallbackTimers -= 1; } if (timeout) { clearTimeout(timeout); timeout = null; } if (stageWatcher) { try { stageWatcher.close(); } catch {} stageWatcher = null; windowsProcessVerification.activeStateWatchers -= 1; } }; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; releaseObservation(); stopWindowsTracking(child); child.unref(); windowsProcessVerification.failClosedOwnershipHeld = false; windowsProcessVerification.failClosedOwnershipTimerCount = 0; writeWindowsVerificationEvidence(error ? "error" : (preserveFailure ? "failed" : "preflight-ready"), child); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const finishCleanupBlocked = async cleanupError => { if (settled) return; settled = true; draining = false; releaseObservation(); child.livedeskWindowsDrainPromise = null; drainPromise = null; windowsProcessVerification.cleanupBlockedCount += 1; windowsProcessVerification.failClosedOwnershipHeld = false; windowsProcessVerification.failClosedOwnershipTimerCount = 0; windowsProcessVerification.terminalZeroProved = false; windowsProcessVerification.blockedExactIdentities = [...(child.livedeskWindowsTrackedRecords?.values?.() || [])].map(record => ({ pid: Number(record.pid || 0), startOrder: String(record.startOrder || ""), depth: Number(record.depth || 0) })).filter(record => record.pid > 1 && /^\d+$/.test(record.startOrder)); windowsProcessVerification.blockedExactIdentityCount = windowsProcessVerification.blockedExactIdentities.length; await stopDirectOwnedChild(child); stopWindowsTracking(child); child.unref(); writeWindowsVerificationEvidence("cleanup-blocked", child); writeCleanupBlocked(cleanupError); }; const drainAndFinish = (error, preserveFailure = false) => { if (settled) return Promise.resolve(); draining = true; if (!drainPromise) drainPromise = terminateTree(child).then(() => { windowsProcessVerification.cleanupCompletedCount += 1; finish(error, preserveFailure); }).catch(async cleanupError => { windowsProcessVerification.cleanupFailureCount += 1; child.livedeskWindowsDrainPromise = null; drainPromise = null; await finishCleanupBlocked(cleanupError); }); return drainPromise; }; const inspect = () => { if (draining || settled) return; windowsProcessVerification.stateReadCount += 1; const state = readState(); if (state.operationId !== operationId) return; const committedOwner = Number(state.starterPid || 0) === process.pid && Number(state.supervisorPid || 0) > 1; if ((state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") && committedOwner) finish(); else if (state.stage === "failed") void drainAndFinish(null, true); }; const scheduleInspect = () => { if (inspectScheduled || draining || settled) return; inspectScheduled = true; setImmediate(() => { inspectScheduled = false; inspect(); }); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); draining = true; while (!settled) { let outcome; try { outcome = writeCancellation(error); } catch (lockError) { await drainAndFinish(lockError); return; } if (outcome === "ready") { draining = false; inspect(); return; } if (outcome === "failed" || outcome === "cancelled" || outcome === "superseded") { await drainAndFinish(null, true); return; } await sleep(25); } }; try { stageWatcher = fs.watch(path.dirname(statePath), { persistent: false }, (_event, filename) => { if (filename && String(filename) !== path.basename(statePath)) return; windowsProcessVerification.stateWatchEventCount += 1; scheduleInspect(); }); windowsProcessVerification.activeStateWatchers += 1; stageWatcher.once("error", error => { if (settled || draining) return; void drainAndFinish(error); }); } catch {} stageFallback = setInterval(inspect, 2000); windowsProcessVerification.activeStateFallbackTimers += 1; stageFallback.unref?.(); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); timeout = setTimeout(() => { void cancelTimedOutHandoff().catch(error => { void drainAndFinish(error); }); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (settled || draining) return; const error = new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ")."); void drainAndFinish(error); }); inspect(); };
49
- const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true }); const unexpectedEntry = fs.readdirSync(neutralCwd)[0]; if (unexpectedEntry) { const shadow = path.join(neutralCwd, unexpectedEntry); throw new Error("LiveDesk update neutral working directory is shadowed by " + shadow + ": " + neutralCwd); } };
50
- if (!operationId || !versionPattern.test(targetProductVersion) || deadlineExpired()) { writeFailure(new Error("Invalid or expired LiveDesk exact-package update handoff."), true); } else { try { prepareNeutralCwd(); writeHandoffStarted();
51
- const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_workspaces = "false"; env.npm_config_include_workspace_root = "false";
52
- const nodeDir = path.dirname(process.execPath);
53
- const npxCli = [env.LIVEDESK_NPX_CLI_PATH, env.npm_execpath ? path.join(path.dirname(env.npm_execpath), "npx-cli.js") : "", env.LIVEDESK_NPX_EXECUTABLE ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), "node_modules", "npm", "bin", "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].find(value => value && fs.existsSync(value));
54
- const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");
55
- const args = ["-y", "--prefer-online", "--prefix", neutralCwd, "--workspaces=false", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];
56
- const quoteCmd = value => "\"" + String(value).replaceAll("%", "%%").replaceAll("\"", "\"\"") + "\"";
57
- const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };
58
- if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); trackWindowsChild(child); child.once("error", writeFailure); child.once("spawn", () => { trackWindowsChild(child); monitorPreflight(child); }); } catch (error) { writeFailure(error); } }
59
- } catch (error) { writeFailure(error, deadlineExpired()); } }
27
+ const stateLockPath = `${statePath}.lock`;
28
+ const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));
29
+
30
+ function readJson(filePath) {
31
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
32
+ }
33
+
34
+ function isAlive(pid) {
35
+ try {
36
+ process.kill(Number(pid), 0);
37
+ return true;
38
+ } catch (error) {
39
+ return error?.code === 'EPERM';
40
+ }
41
+ }
42
+
43
+ function removeAbandonedStateLock() {
44
+ let owner = null;
45
+ try { owner = JSON.parse(fs.readFileSync(stateLockPath, 'utf8')); } catch { /* checked below */ }
46
+ const ownerPid = Number(owner?.pid || 0);
47
+ if (Number.isInteger(ownerPid) && ownerPid > 1) {
48
+ if (isAlive(ownerPid)) return false;
49
+ } else {
50
+ try {
51
+ if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5_000) return false;
52
+ } catch {
53
+ return true;
54
+ }
55
+ }
56
+ try {
57
+ fs.rmSync(stateLockPath);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function withStateLock(callback) {
65
+ fs.mkdirSync(path.dirname(stateLockPath), { recursive: true });
66
+ const deadline = Date.now() + 10_000;
67
+ let descriptor = null;
68
+ let token = '';
69
+ while (descriptor === null) {
70
+ try {
71
+ descriptor = fs.openSync(stateLockPath, 'wx', 0o600);
72
+ token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
73
+ fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token }), 'utf8');
74
+ } catch (error) {
75
+ if (descriptor !== null) {
76
+ try { fs.closeSync(descriptor); } catch { /* best effort */ }
77
+ descriptor = null;
78
+ try { fs.rmSync(stateLockPath, { force: true }); } catch { /* best effort */ }
79
+ }
80
+ if (error?.code !== 'EEXIST') throw error;
81
+ if (removeAbandonedStateLock()) continue;
82
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for the LiveDesk update state lock: ${stateLockPath}`);
83
+ Atomics.wait(lockWaitBuffer, 0, 0, Math.min(25, Math.max(1, deadline - Date.now())));
84
+ }
85
+ }
86
+ try {
87
+ return callback();
88
+ } finally {
89
+ try { fs.closeSync(descriptor); } catch { /* best effort */ }
90
+ try {
91
+ const owner = JSON.parse(fs.readFileSync(stateLockPath, 'utf8'));
92
+ if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true });
93
+ } catch { /* another owner already replaced a stale lock */ }
94
+ }
95
+ }
96
+
97
+ function writeState(mutator, suffix) {
98
+ return withStateLock(() => {
99
+ const previous = readJson(statePath) || {};
100
+ const next = mutator(previous);
101
+ if (!next) return previous;
102
+ const temporary = `${statePath}.${process.pid}.${suffix}.tmp`;
103
+ fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 });
104
+ fs.renameSync(temporary, statePath);
105
+ return next;
106
+ });
107
+ }
108
+
109
+ function writeFailure(error, cancelRequested = false) {
110
+ try {
111
+ writeState(previous => {
112
+ if (previous.operationId && previous.operationId !== operationId) return null;
113
+ if (['preflight-ready', 'waiting-for-shutdown', 'connected', 'restored'].includes(previous.stage)) return null;
114
+ const now = new Date().toISOString();
115
+ return {
116
+ ...previous,
117
+ operationId,
118
+ stage: 'failed',
119
+ targetProductVersion,
120
+ restartVerified: false,
121
+ cancelRequested: cancelRequested || previous.cancelRequested === true,
122
+ error: String(error?.message || error).slice(0, 4000),
123
+ failedAt: now,
124
+ updatedAt: now
125
+ };
126
+ }, 'handoff-failed');
127
+ } catch { /* preserve the original handoff failure */ }
128
+ process.stderr.write(`LiveDesk update handoff failed: ${error?.message || error}\n`);
129
+ process.exitCode = 1;
130
+ }
131
+
132
+ function prepareNeutralCwd() {
133
+ if (!neutralCwd) throw new Error('LiveDesk update neutral working directory is unavailable.');
134
+ fs.mkdirSync(neutralCwd, { recursive: true });
135
+ const unexpectedEntry = fs.readdirSync(neutralCwd)[0];
136
+ if (unexpectedEntry) {
137
+ throw new Error(`LiveDesk update neutral working directory is shadowed by ${path.join(neutralCwd, unexpectedEntry)}.`);
138
+ }
139
+ }
140
+
141
+ function writeHandoffStarted() {
142
+ return writeState(previous => {
143
+ if (Date.now() >= updateDeadlineEpochMs) {
144
+ throw new Error('The absolute LiveDesk Client update deadline expired before handoff.');
145
+ }
146
+ if (previous.operationId !== operationId) {
147
+ throw new Error('LiveDesk update handoff was superseded before exact-package launch.');
148
+ }
149
+ const now = new Date().toISOString();
150
+ return {
151
+ ...previous,
152
+ operationId,
153
+ stage: 'handoff-started',
154
+ starterPid: process.pid,
155
+ updateDeadlineEpochMs,
156
+ restartVerified: false,
157
+ error: '',
158
+ updatedAt: now
159
+ };
160
+ }, 'handoff-started');
161
+ }
162
+
163
+ function readUpdateHostStatus() {
164
+ const statusPath = String(process.env.LIVEDESK_UPDATE_HOST_STATUS_PATH || '').trim();
165
+ return statusPath ? readJson(statusPath) : null;
166
+ }
167
+
168
+ function submitUpdateHostJob(job) {
169
+ return new Promise((resolveRequest, rejectRequest) => {
170
+ const status = readUpdateHostStatus();
171
+ const endpoint = String(process.env.LIVEDESK_UPDATE_HOST_ENDPOINT || status?.endpoint || '').trim();
172
+ const token = String(status?.token || '');
173
+ if (process.env.LIVEDESK_UPDATE_HOST_AVAILABLE !== '1'
174
+ || Number(process.env.LIVEDESK_UPDATE_HOST_PROTOCOL_VERSION || 0) !== UPDATE_HOST_PROTOCOL_VERSION
175
+ || status?.protocolVersion !== UPDATE_HOST_PROTOCOL_VERSION
176
+ || !endpoint
177
+ || !/^[a-f0-9]{64}$/i.test(token)) {
178
+ rejectRequest(new Error('The independent LiveDesk Update Host is unavailable; the existing Client was left running.'));
179
+ return;
180
+ }
181
+ const request = JSON.stringify({
182
+ protocolVersion: UPDATE_HOST_PROTOCOL_VERSION,
183
+ token,
184
+ type: 'run-worker',
185
+ job
186
+ });
187
+ if (Buffer.byteLength(request, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
188
+ rejectRequest(new Error('The LiveDesk Client update request exceeded the Update Host message limit.'));
189
+ return;
190
+ }
191
+ const socket = net.createConnection(endpoint);
192
+ socket.setEncoding('utf8');
193
+ let responseText = '';
194
+ let settled = false;
195
+ const finish = (error, value) => {
196
+ if (settled) return;
197
+ settled = true;
198
+ clearTimeout(timeout);
199
+ socket.destroy();
200
+ if (error) rejectRequest(error);
201
+ else resolveRequest(value);
202
+ };
203
+ const timeout = setTimeout(() => {
204
+ finish(new Error('Timed out registering the Client update with the independent Update Host.'));
205
+ }, 10_000);
206
+ socket.once('connect', () => socket.write(`${request}\n`));
207
+ socket.on('data', chunk => {
208
+ responseText += chunk;
209
+ if (Buffer.byteLength(responseText, 'utf8') > UPDATE_HOST_MAX_MESSAGE_BYTES) {
210
+ finish(new Error('The LiveDesk Update Host response exceeded its message limit.'));
211
+ }
212
+ });
213
+ socket.once('error', error => finish(error));
214
+ socket.once('end', () => {
215
+ try {
216
+ const response = JSON.parse(responseText || '{}');
217
+ if (response?.ok !== true || response?.accepted !== true || Number(response?.job?.workerPid || 0) <= 1) {
218
+ throw new Error(String(response?.error || 'The LiveDesk Update Host did not accept the Client update.'));
219
+ }
220
+ finish(null, response);
221
+ } catch (error) {
222
+ finish(error);
223
+ }
224
+ });
225
+ });
226
+ }
227
+
228
+ function resolveExecutable(candidate) {
229
+ const value = String(candidate || '').trim();
230
+ if (!value) return '';
231
+ if (path.isAbsolute(value)) return fs.existsSync(value) ? value : '';
232
+ const extensions = process.platform === 'win32'
233
+ ? String(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
234
+ : [''];
235
+ for (const directory of String(process.env.PATH || process.env.Path || '').split(path.delimiter)) {
236
+ if (!directory) continue;
237
+ for (const extension of extensions) {
238
+ const filePath = path.join(directory, `${value}${extension}`);
239
+ if (fs.existsSync(filePath)) return path.resolve(filePath);
240
+ }
241
+ }
242
+ return '';
243
+ }
244
+
245
+ function buildWorkerInvocation(env) {
246
+ const nodeDirectory = path.dirname(process.execPath);
247
+ const npxCli = [
248
+ env.LIVEDESK_NPX_CLI_PATH,
249
+ env.npm_execpath ? path.join(path.dirname(env.npm_execpath), 'npx-cli.js') : '',
250
+ env.LIVEDESK_NPX_EXECUTABLE
251
+ ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), 'node_modules', 'npm', 'bin', 'npx-cli.js')
252
+ : '',
253
+ path.join(nodeDirectory, 'node_modules', 'npm', 'bin', 'npx-cli.js')
254
+ ].find(value => value && fs.existsSync(value));
255
+ const args = [
256
+ '-y',
257
+ '--prefer-online',
258
+ '--prefix',
259
+ neutralCwd,
260
+ '--workspaces=false',
261
+ `livedesk@${targetProductVersion}`,
262
+ '--internal-legacy-client-update'
263
+ ];
264
+ if (npxCli) return { command: process.execPath, args: [npxCli, ...args] };
265
+ const npxExecutable = resolveExecutable(env.LIVEDESK_NPX_EXECUTABLE || (process.platform === 'win32' ? 'npx.cmd' : 'npx'));
266
+ return { command: npxExecutable, args };
267
+ }
268
+
269
+ async function main() {
270
+ if (!operationId || !versionPattern.test(targetProductVersion)
271
+ || !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs) {
272
+ throw new Error('Invalid or expired LiveDesk exact-package update handoff.');
273
+ }
274
+ prepareNeutralCwd();
275
+ writeHandoffStarted();
276
+
277
+ const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) };
278
+ const isolated = new Set([
279
+ 'init_cwd', 'npm_config_local_prefix', 'npm_config_workspace', 'npm_config_workspaces',
280
+ 'npm_config_include_workspace_root', 'npm_package_json', 'npm_lifecycle_event', 'npm_lifecycle_script'
281
+ ]);
282
+ for (const key of Object.keys(env)) {
283
+ if (isolated.has(key.toLowerCase())) delete env[key];
284
+ }
285
+ env.INIT_CWD = neutralCwd;
286
+ env.npm_config_local_prefix = neutralCwd;
287
+ env.npm_config_workspaces = 'false';
288
+ env.npm_config_include_workspace_root = 'false';
289
+
290
+ const invocation = buildWorkerInvocation(env);
291
+ if (!path.isAbsolute(invocation.command) || !fs.existsSync(invocation.command)) {
292
+ throw new Error('LiveDesk could not prepare an absolute Client update worker command for the independent Update Host.');
293
+ }
294
+ const result = await submitUpdateHostJob({
295
+ jobId: `client-${operationId.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160)}`,
296
+ kind: 'client-update',
297
+ operationId,
298
+ command: invocation.command,
299
+ args: invocation.args,
300
+ cwd: neutralCwd,
301
+ env,
302
+ resultPath: statePath,
303
+ requestedAt: new Date().toISOString(),
304
+ expiresAt: new Date(updateDeadlineEpochMs).toISOString()
305
+ });
306
+
307
+ writeState(previous => {
308
+ if (previous.operationId !== operationId || previous.stage !== 'handoff-started') return null;
309
+ return {
310
+ ...previous,
311
+ updateHostPid: Number(result?.host?.pid || 0),
312
+ updateHostJobId: String(result?.job?.jobId || ''),
313
+ updateWorkerPid: Number(result?.job?.workerPid || 0),
314
+ updatedAt: new Date().toISOString()
315
+ };
316
+ }, 'host-accepted');
317
+ }
318
+
319
+ main().catch(error => writeFailure(error, Date.now() >= updateDeadlineEpochMs));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.236",
3
+ "version": "0.1.237",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.434",
46
- "@livedesk/fast-osx-arm64": "0.1.434",
47
- "@livedesk/fast-osx-x64": "0.1.434",
48
- "@livedesk/fast-win-x64": "0.1.434"
45
+ "@livedesk/fast-linux-x64": "0.1.435",
46
+ "@livedesk/fast-osx-arm64": "0.1.435",
47
+ "@livedesk/fast-osx-x64": "0.1.435",
48
+ "@livedesk/fast-win-x64": "0.1.435"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"