// MESSUNG: Überlebt ein ausdrücklich ausgeklinktes Kind den Tod seines // Elternprozesses — und stirbt ein normales Kind weiterhin mit? // // Anlass (01.09.2026): Das Update auf 5.1.3 lief nicht. Rippy beendete // sauber, aber der Installer startete nie. Verdacht: Die Prozess-Leine // (§ 3.3, KILL_ON_JOB_CLOSE) erschlägt den Installer, weil // electron-updater ihn per spawn(…, { detached: true }) startet und // `detached` unter Windows KEIN Breakaway setzt. // // Aufbau (drei Läufe, der äußere ruft die inneren): // node beweise/leine-breakaway.js // -> startet zweimal sich selbst als „innen": // innen-ohne : Arbeitsgruppe NUR mit KILL_ON_JOB_CLOSE, Enkel normal // innen-mit : Arbeitsgruppe zusätzlich mit BREAKAWAY_OK, // Enkel mit CREATE_BREAKAWAY_FROM_JOB // Beide beenden sich sofort. Danach schaut der äußere Lauf nach, // welcher Enkel noch lebt. // // Erwartung: ohne Ausklinken stirbt der Enkel, mit Ausklinken lebt er. // // Ausführen: node beweise/leine-breakaway.js const { execFileSync, spawnSync } = require('node:child_process') const path = require('node:path') const koffi = require(path.join(__dirname, '..', 'rippy-windows', 'node_modules', 'koffi')) const JOB_KLASSE = 9 // JobObjectExtendedLimitInformation const KILL_ON_JOB_CLOSE = 0x2000 const BREAKAWAY_OK = 0x0800 const CREATE_BREAKAWAY_FROM_JOB = 0x01000000 const CREATE_NO_WINDOW = 0x08000000 const STRUKTUR_GROESSE = 144 const LIMIT_FLAGS_OFFSET = 16 const STARTUPINFO_GROESSE = 104 const PROCESS_INFORMATION_GROESSE = 24 const kernel32 = koffi.load('kernel32.dll') const CreateJobObjectW = kernel32.func('void* __stdcall CreateJobObjectW(void *a, str16 n)') const SetInformationJobObject = kernel32.func( 'bool __stdcall SetInformationJobObject(void *j, int k, void *i, uint32_t l)', ) const AssignProcessToJobObject = kernel32.func('bool __stdcall AssignProcessToJobObject(void *j, void *p)') const GetCurrentProcess = kernel32.func('void* __stdcall GetCurrentProcess()') const GetLastError = kernel32.func('uint32_t __stdcall GetLastError()') const CreateProcessW = kernel32.func( 'bool __stdcall CreateProcessW(str16 an, void *cl, void *pa, void *ta, bool ih, uint32_t cf, void *env, str16 cd, void *si, void *pi)', ) /** Ein Enkel, der 40 Sekunden lebt — lange genug zum Nachsehen. */ const ENKEL = 'C:\\Windows\\System32\\cmd.exe' const ENKEL_ZEILE = '"C:\\Windows\\System32\\cmd.exe" /c ping -n 40 127.0.0.1' function innen(jobErlaubtAusklinken, kindKlinktSichAus) { const job = CreateJobObjectW(null, null) const info = Buffer.alloc(STRUKTUR_GROESSE) info.writeUInt32LE(KILL_ON_JOB_CLOSE | (jobErlaubtAusklinken ? BREAKAWAY_OK : 0), LIMIT_FLAGS_OFFSET) SetInformationJobObject(job, JOB_KLASSE, info, STRUKTUR_GROESSE) AssignProcessToJobObject(job, GetCurrentProcess()) const si = Buffer.alloc(STARTUPINFO_GROESSE) si.writeUInt32LE(STARTUPINFO_GROESSE, 0) const pi = Buffer.alloc(PROCESS_INFORMATION_GROESSE) const zeile = Buffer.from(ENKEL_ZEILE + '\0', 'utf16le') // CREATE_NO_WINDOW gibt dem Enkel eine EIGENE (unsichtbare) Konsole. // Ohne das teilt er sich die Konsole des Elternprozesses und stirbt mit // IHR, nicht mit der Arbeitsgruppe — der erste Messlauf am 01.09.2026 // lief genau in diese Falle und zeigte fälschlich „Ausklinken wirkt // nicht". Beide Fälle bekommen es, damit der Vergleich fair bleibt. const flags = CREATE_NO_WINDOW | (kindKlinktSichAus ? CREATE_BREAKAWAY_FROM_JOB : 0) const ok = CreateProcessW(ENKEL, zeile, null, null, false, flags, null, null, si, pi) if (!ok) { console.log(`PID=0 FEHLER=${GetLastError()}`) return } // dwProcessId steht in PROCESS_INFORMATION an Offset 16. console.log(`PID=${pi.readUInt32LE(16)} FEHLER=0`) } function lebt(pid) { const ergebnis = spawnSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH'], { encoding: 'utf8' }) return (ergebnis.stdout || '').includes(String(pid)) } function aussen() { const faelle = [ ['1) Leine wie bisher, Kind normal — die Ausgangslage', 'alt', false], // DIE Regressionsfrage: Weicht das blosse ERLAUBEN die Leine auf? // Wenn hier JA stuende, waere Paragraph 3.3 kaputt und makemkvcon // koennte als Waise das Laufwerk festhalten (der rc10-Fund). ['2) Leine erlaubt Ausklinken, Kind normal — MUSS weiter sterben', 'erlaubt-normal', false], ['3) Leine erlaubt Ausklinken, Kind klinkt sich aus — der Installer', 'erlaubt-aus', true], ] for (const [name, modus, erwartetLebend] of faelle) { const ausgabe = execFileSync(process.execPath, [__filename, modus], { encoding: 'utf8' }).trim() const pid = Number(/PID=(\d+)/.exec(ausgabe)?.[1] ?? 0) const fehler = Number(/FEHLER=(\d+)/.exec(ausgabe)?.[1] ?? 0) // Der innere Lauf ist hier schon beendet — die Arbeitsgruppe ist also // zugeschnappt, falls sie zuschnappt. const nochDa = pid > 0 ? lebt(pid) : false const urteil = nochDa === erwartetLebend ? 'WIE ERWARTET' : 'ABWEICHUNG' console.log( `${name}\n Enkel-PID ${pid || '—'}${fehler ? ` (Win32-Fehler ${fehler})` : ''}` + `\n lebt nach dem Tod des Elternprozesses: ${nochDa ? 'JA' : 'NEIN'} -> ${urteil}\n`, ) if (nochDa) spawnSync('taskkill', ['/PID', String(pid), '/F', '/T']) } } const modus = process.argv[2] ?? '' if (modus === 'alt') innen(false, false) else if (modus === 'erlaubt-normal') innen(true, false) else if (modus === 'erlaubt-aus') innen(true, true) else aussen()