Files
rippy/beweise/leine-breakaway.js
T
HitonabiandClaude Opus 5 7a4f92036d
Ampel / ampel (push) Successful in 1m33s
test(beweise): dritter Fall — erlaubt das Ausklinken die Leine auf?
Die eigentliche Regressionsfrage fehlte noch: Ein normales Kind muss auch
dann sterben, wenn die Arbeitsgruppe das Ausklinken ERLAUBT. Sonst waere
Paragraph 3.3 aufgeweicht und makemkvcon koennte als Waise das Laufwerk
festhalten (der rc10-Fund).

  1) Leine wie bisher, Kind normal                  -> stirbt  WIE ERWARTET
  2) Leine erlaubt Ausklinken, Kind normal          -> stirbt  WIE ERWARTET
  3) Leine erlaubt Ausklinken, Kind klinkt sich aus -> lebt    WIE ERWARTET

Das blosse Erlauben aendert also nichts; nur wer CREATE_BREAKAWAY_FROM_JOB
verlangt, kommt raus. Genau das tut allein der Update-Installer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:29:23 +02:00

115 lines
5.4 KiB
JavaScript

// 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()