Electron 44 + TypeScript, drei Prozesse (haupt / kern als utilityProcess / fenster mit React), Nachrichten-Schema mit Pruef-Funktionen, node:sqlite als einzige Datenbank-Stelle (kern/speicher/db.ts, § 6.7), Prozess-Leine im Haupt (Job Object aus beweise/leine.js — Begruendung fuer den Ort im Dateikopf), MessagePort direkt Fenster<->Kern, Einzelinstanz-Sperre, Kern-Neustart-Wache, strikte CSP im gebauten Fenster. Wächter-Tests nach § 4.3: koffi nur an zwei benannten Orten (R1), kein leerer catch und kein catch-mit-Leerwert (R2/R4), kein HTTP-Server (§ 4.1), node:sqlite nur in db.ts (§ 6.7). Dazu Datenbank- und Schema-Tests: 18/18 gruen, Typpruefung in drei Kontexten. Der W-0-Beweis (npm run smoke, echtes Programm): SMOKE: OK — fenster=geladen kern=pid:13676,node:24.18.1 datenbank=ok leine=gesetzt pong=ok version=5.0.0-w0 Nachgemessen ausserdem: taskkill /F auf den Haupt-Prozess (ohne /T, der Absturz-Fall aus rc10) — der Kern stirbt mit. Bau-Fallen dokumentiert (BAUEN.md): npm 11 blockt Electrons Install-Skript (§ 3.5), plugin-react 6 verlangt Vite 8 (deshalb 5.2), TypeScript bewusst auf 5.9.3 gepinnt statt tsgo 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
3.9 KiB
TypeScript
93 lines
3.9 KiB
TypeScript
// Die Prozess-Leine (Windows-Arbeitsgruppe / Job Object) — KONZEPT § 3.3,
|
|
// gemessen in beweise/leine.js am 30.08.2026. Der Befund dahinter
|
|
// (SAVEPOINT v4.0-rc10): Windows räumt Kindprozesse NICHT auf. Ein hart
|
|
// beendeter Rippy hinterließe ein makemkvcon, das das Laufwerk festhält.
|
|
//
|
|
// Mechanik: Dieser Prozess legt die Arbeitsgruppe an, setzt
|
|
// KILL_ON_JOB_CLOSE und hängt SICH SELBST hinein. Kinder (der Kern, später
|
|
// makemkvcon/HandBrakeCLI im Kern) erben die Mitgliedschaft. Stirbt der
|
|
// Haupt-Prozess — Absturz, taskkill /F, Drüber-Installieren —, schließt
|
|
// Windows sein einziges Handle auf die Gruppe, und ALLE Mitglieder sterben
|
|
// mit (§ 4.1: „hält die Prozess-Leine — ALLES stirbt mit ihm").
|
|
//
|
|
// WARUM die Leine HIER liegt und nicht in kern/werkzeuge/leine.ts (wie die
|
|
// Ordnerliste § 4.2 sie einsortiert): Das Handle muss bei dem Prozess liegen,
|
|
// dessen Tod alles mitreißen soll — und das ist laut § 4.1 der Haupt-Prozess.
|
|
// Hielte der Kern das Handle, wäre ein hart beendetes Haupt wieder die
|
|
// rc10-Waisen-Falle. Damit ist diese Datei neben kern/laufwerk/win32.ts der
|
|
// ZWEITE erlaubte koffi-Ort; der Wächter-Test R1 kennt genau diese zwei.
|
|
import koffi from 'koffi'
|
|
|
|
// Konstanten aus winnt.h — dieselbe Herleitung wie beweise/leine.js:
|
|
const JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_KLASSE = 9 // JobObjectExtendedLimitInformation
|
|
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
|
|
|
|
// JOBOBJECT_EXTENDED_LIMIT_INFORMATION ist 144 Byte auf x64 (Rippy v5 ist
|
|
// x64-only, § 8); LimitFlags liegt im eingebetteten BASIC_LIMIT_INFORMATION
|
|
// bei Offset 16.
|
|
const STRUKTUR_GROESSE = 144
|
|
const LIMIT_FLAGS_OFFSET = 16
|
|
|
|
export type LeineStatus = { ok: true } | { ok: false; fehler: string }
|
|
|
|
let gesetzt: LeineStatus | null = null
|
|
|
|
/**
|
|
* Setzt die Prozess-Leine für DIESEN Prozess. Einmal pro Programmlauf,
|
|
* VOR dem Start des Kerns — Kinder erben die Mitgliedschaft nur, wenn sie
|
|
* nach dem Anlegen gestartet werden.
|
|
*
|
|
* Ein Fehlschlag wird als Wert zurückgegeben, nie verschluckt (R4): Der
|
|
* Aufrufer protokolliert ihn und zeigt ihn im Fenster an.
|
|
*/
|
|
export function leineSetzen(): LeineStatus {
|
|
if (gesetzt !== null) return gesetzt
|
|
try {
|
|
const kernel32 = koffi.load('kernel32.dll')
|
|
const CreateJobObjectW = kernel32.func(
|
|
'void* __stdcall CreateJobObjectW(void *lpJobAttributes, str16 lpName)',
|
|
)
|
|
const SetInformationJobObject = kernel32.func(
|
|
'bool __stdcall SetInformationJobObject(void *hJob, int JobObjectInformationClass, void *lpJobObjectInformation, uint32_t cbJobObjectInformationLength)',
|
|
)
|
|
const AssignProcessToJobObject = kernel32.func(
|
|
'bool __stdcall AssignProcessToJobObject(void *hJob, void *hProcess)',
|
|
)
|
|
const GetCurrentProcess = kernel32.func('void* __stdcall GetCurrentProcess()')
|
|
const GetLastError = kernel32.func('uint32_t __stdcall GetLastError()')
|
|
|
|
const job = CreateJobObjectW(null, null)
|
|
if (koffi.address(job) === 0n) {
|
|
gesetzt = { ok: false, fehler: `CreateJobObjectW: Win32-Fehler ${GetLastError()}` }
|
|
return gesetzt
|
|
}
|
|
|
|
const info = Buffer.alloc(STRUKTUR_GROESSE)
|
|
info.writeUInt32LE(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, LIMIT_FLAGS_OFFSET)
|
|
if (
|
|
!SetInformationJobObject(
|
|
job,
|
|
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_KLASSE,
|
|
info,
|
|
STRUKTUR_GROESSE,
|
|
)
|
|
) {
|
|
gesetzt = { ok: false, fehler: `SetInformationJobObject: Win32-Fehler ${GetLastError()}` }
|
|
return gesetzt
|
|
}
|
|
|
|
if (!AssignProcessToJobObject(job, GetCurrentProcess())) {
|
|
gesetzt = { ok: false, fehler: `AssignProcessToJobObject: Win32-Fehler ${GetLastError()}` }
|
|
return gesetzt
|
|
}
|
|
|
|
// Das Job-Handle wird ABSICHTLICH nie geschlossen — sein Zuschnappen beim
|
|
// Prozess-Tod IST der Mechanismus.
|
|
gesetzt = { ok: true }
|
|
return gesetzt
|
|
} catch (fehler) {
|
|
gesetzt = { ok: false, fehler: `koffi/kernel32 nicht verfügbar: ${String(fehler)}` }
|
|
return gesetzt
|
|
}
|
|
}
|