feat(v5): Etappe W-1 — Laufwerk: Wache, Typ, Auswerfen mit Nachsehen
Ampel / ampel (push) Successful in 1m21s

kern/laufwerk in drei Schichten: codes.ts (Steuercodes HERGELEITET wie
win_ioctl.py, gegen die Doku-Zahlen getestet), win32.ts (der einzige
koffi-Ort im Kern, Muster aus beweise/laufwerk.js), disc.ts (Logik gegen
die LaufwerkApi-Schnittstelle — classify mit den cdrom.py-Schwellen,
Geraete-Info mit ready/empty/unknown + Klartext-Grund, Auswurf als
entriegeln→auswerfen→NACHSEHEN mit injizierbarem Warten). Dazu wache.ts
im 3-Sekunden-Takt (§ 5 Plan A): Uebergangslogik pur, gescheiterte Runde
behaelt den Stand (R2) und meldet laut (R4).

Fenster zeigt Laufwerke live (Modell, Typ, Groesse, Grund) mit
Auswerfen-Knopf; Ereigniszeilen fuer Disc rein/raus und Fehler.

Bündel-Falle gefunden und behoben: Ein woertliches require überlebt
Rollup nicht — win32 wird per dynamischem import() als eigener Chunk
gebaut; der Fehler war dank R4 im Fenster sichtbar statt still.

Gemessen am echten BU40N (30.08.2026, RIPPY_MESSUNG=1):
  Laufwerk G: status=ready typ=bluray groesse=33759690752
  modell=HL-DT-ST BD-RE BU40N 1.03 serial=0025114C0149
Der Auswurf-Beweis (messung.auswurf) folgt am Ende der Sitzung — der
BU40N kann die Schublade nicht selbst einziehen.

52 Tests gruen (Steuercodes, classify, Auswurf-Ablauf, Geraete-Info,
Wache, Smoke weiterhin gruen mit Laufwerks-Kachel im Bild).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 17:28:46 +02:00
co-authored by Claude Fable 5
parent 6ed8035dcc
commit a45069af5e
13 changed files with 1377 additions and 73 deletions
+88
View File
@@ -0,0 +1,88 @@
// DER EINZIGE ORT MIT koffi im Kern (Regel R1, KONZEPT § 4.3) — die rohe
// Win32-Anbindung für optische Laufwerke. Entscheidungen stehen NICHT hier,
// sondern in disc.ts; die Konstanten samt Herleitung in codes.ts. Dadurch
// ist alles außer diesen Aufrufen ohne Windows und ohne Laufwerk prüfbar
// (dasselbe Muster wie win_ioctl.py/windows.py im Docker-Zweig, dort an
// echter Hardware bewiesen — 28.08.2026, BU40N; hier gemessen in
// beweise/laufwerk.js, 30.08.2026).
import koffi from 'koffi'
import { FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, Win32Fehler, type LaufwerkApi } from './codes'
export type { LaufwerkApi }
let echteApi: LaufwerkApi | null = null
/** Die echten kernel32-Aufrufe — einmal geladen, dann wiederverwendet. */
export function win32Api(): LaufwerkApi {
if (echteApi !== null) return echteApi
const kernel32 = koffi.load('kernel32.dll')
const GetLogicalDrives = kernel32.func('uint32_t __stdcall GetLogicalDrives()')
const GetDriveTypeW = kernel32.func('uint32_t __stdcall GetDriveTypeW(str16 lpRootPathName)')
const CreateFileW = kernel32.func(
'void* __stdcall CreateFileW(str16 lpFileName, uint32_t dwDesiredAccess, uint32_t dwShareMode, void *lpSecurityAttributes, uint32_t dwCreationDisposition, uint32_t dwFlagsAndAttributes, void *hTemplateFile)',
)
const CloseHandle = kernel32.func('bool __stdcall CloseHandle(void *hObject)')
const GetLastError = kernel32.func('uint32_t __stdcall GetLastError()')
const DeviceIoControl = kernel32.func(
'bool __stdcall DeviceIoControl(void *hDevice, uint32_t dwIoControlCode, void *lpInBuffer, uint32_t nInBufferSize, _Out_ void *lpOutBuffer, uint32_t nOutBufferSize, _Out_ uint32_t *lpBytesReturned, void *lpOverlapped)',
)
echteApi = {
laufwerksbuchstaben(): string[] {
const maske = GetLogicalDrives() as number
const buchstaben: string[] = []
for (let i = 0; i < 26; i++) {
if (maske & (1 << i)) buchstaben.push(String.fromCharCode(65 + i))
}
return buchstaben
},
laufwerkstyp(buchstabe: string): number {
return GetDriveTypeW(`${buchstabe}:\\`) as number
},
oeffnen(geraetepfad: string, zugriff: number): unknown {
const handle = CreateFileW(
geraetepfad,
zugriff,
FILE_SHARE_READ | FILE_SHARE_WRITE,
null,
OPEN_EXISTING,
0,
null,
)
const adresse = koffi.address(handle as Parameters<typeof koffi.address>[0])
// INVALID_HANDLE_VALUE ist (HANDLE)-1 — als vorzeichenlose 64-Bit-Zahl
// 0xFFFF…FF, nicht -1 (dieselbe Falle wie in windows.py dokumentiert).
if (adresse === 0n || adresse === 0xffff_ffff_ffff_ffffn) {
throw new Win32Fehler('CreateFileW', GetLastError() as number)
}
return handle
},
schliessen(handle: unknown): void {
CloseHandle(handle)
},
steuern(handle: unknown, code: number, eingabe?: Buffer | null, ausgabeBytes = 0): Buffer {
const aus = ausgabeBytes > 0 ? Buffer.alloc(ausgabeBytes) : null
const zurueck = [0]
const erfolg = DeviceIoControl(
handle,
code,
eingabe ?? null,
eingabe?.length ?? 0,
aus,
ausgabeBytes,
zurueck,
null,
)
if (!erfolg) {
throw new Win32Fehler(`DeviceIoControl(0x${code.toString(16)})`, GetLastError() as number)
}
return aus === null ? Buffer.alloc(0) : aus.subarray(0, zurueck[0])
},
}
return echteApi
}