pithos/core/winctrl.js
Local Dev c63946db56 Pithos: stop s3d gently and never in the middle of an upload
On Windows s3d was stopped with TerminateProcess. One killed during an
upload (a Theseus restart, a cut-off flush) had deleted four buffered
files without recording them as uploaded; every flush afterwards failed
on them and the files were gone.

A stop now waits while a flush Pithos started is running, or while s3d
has begun an upload batch in the last two minutes (up to five minutes;
twenty seconds when the host is quitting), then asks s3d to shut down
cleanly: Ctrl+Break sent to its own hidden console on Windows (Windows
disables Ctrl+C in a new process group; Go reads both as an interrupt),
SIGTERM elsewhere. It is ended by force only if still there a minute
later. Checked against s3d v0.2.0: 'shutdown complete', exit code 0.
2026-10-06 19:14:43 +02:00

33 lines
1.8 KiB
JavaScript

// A gentle stop for s3d on Windows. child.kill() there is TerminateProcess:
// s3d dies wherever it is, and one stopped in the middle of an upload had
// lost buffered files whose database rows still said "waiting". s3d shuts
// down cleanly on an interrupt, so this attaches a short PowerShell to s3d's
// own (hidden) console and sends Ctrl+Break there.
//
// Ctrl+Break, not Ctrl+C: Windows disables Ctrl+C for a process started in
// a new process group, and Go reads both as os.Interrupt. A spawned console
// program gets a console of its own (Node passes CREATE_NO_WINDOW), so the
// event reaches s3d and not Pithos. It ends the PowerShell too, which is
// why only "could not attach" (2) and "could not send" (3) count as failure.
import { execFile } from 'node:child_process';
const SCRIPT = (pid) => [
"Add-Type -Name K -Namespace PithosCtrl -MemberDefinition '",
'[DllImport("kernel32.dll")] public static extern bool FreeConsole();',
'[DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);',
'[DllImport("kernel32.dll")] public static extern bool GenerateConsoleCtrlEvent(uint ev, uint group);',
"';",
'[PithosCtrl.K]::FreeConsole() | Out-Null;',
`if (-not [PithosCtrl.K]::AttachConsole(${Number(pid) >>> 0})) { exit 2 };`,
'if ([PithosCtrl.K]::GenerateConsoleCtrlEvent(1, 0)) { exit 0 } else { exit 3 }',
].join(' ');
// Resolves true when Ctrl+Break was sent to the console of `pid`.
export function sendBreak(pid) {
if (process.platform !== 'win32' || !pid) return Promise.resolve(false);
return new Promise((resolve) => {
execFile('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', SCRIPT(pid)],
{ windowsHide: true, timeout: 20_000 }, (err) => resolve(!err || (err.code !== 2 && err.code !== 3 && !err.killed)));
});
}