|
20 | 20 | the key codes, coordinates, button codes and scroll signs intended — |
21 | 21 | including the axis flip ``mouse.scroll()`` applies between the kernel's |
22 | 22 | ``REL_WHEEL`` frame and libei's, and the negative value that flip makes; |
23 | | - * tearing down a *live* context is finally testable — it is safe, which is |
24 | | - what lets ``_teardown`` release a completed session instead of leaking a |
25 | | - context and an fd per process, and this file is the sentinel for that. |
| 23 | + * live cleanup is measured in a bounded child after releasing owned device |
| 24 | + refs; successful process exit and both native boundary markers are required. |
| 25 | + Unrelated child failures cannot count as evidence for safe teardown. |
26 | 26 |
|
27 | 27 | Two things it measures but cannot settle, and says so rather than claiming |
28 | 28 | either way: ``eis_device_pause()`` puts nothing on the wire for a sender |
@@ -386,47 +386,44 @@ def _check_resume_recovers(backend, server) -> str: |
386 | 386 |
|
387 | 387 |
|
388 | 388 | def _live_teardown_sentinel(path: str) -> str: |
389 | | - """Is ``ei_unref`` safe once the handshake has actually completed? |
390 | | -
|
391 | | - ``libei_verify.py`` established that it segfaults on a context whose |
392 | | - backend opened but never handshook. Whether a *live* context is safe was |
393 | | - untestable without a peer; it is testable now, and it is safe — which is |
394 | | - what lets ``_teardown`` release a completed session rather than leaking |
395 | | - its context. This is therefore a regression sentinel for that decision, |
396 | | - and it runs in a subprocess because the answer is a signal, not an |
397 | | - exception. |
398 | | - """ |
399 | | - program = ( |
400 | | - "import sys; sys.path.insert(0, '/opt/verify');" |
401 | | - "from eis_server import RecordingEisServer;" |
402 | | - "from je_auto_control.linux_wayland import libei;" |
403 | | - f"srv = RecordingEisServer({path + '-teardown'!r}); srv.start();" |
404 | | - "b = libei.LibeiBackend();" |
405 | | - f"b.connect(timeout=5.0, socket_path={(path + '-teardown').encode()!r});" |
406 | | - "assert b.is_connected;" |
407 | | - "sym = libei._load_symbols();" |
408 | | - "sym.ei_unref(b._ei);" |
409 | | - "print('survived')" |
| 389 | + """Measure live cleanup after owned device refs are released; unrelated failures never pass.""" |
| 390 | + program = f""" |
| 391 | +import sys |
| 392 | +sys.path.insert(0, '/opt/verify') |
| 393 | +from eis_server import RecordingEisServer |
| 394 | +from je_auto_control.linux_wayland import libei |
| 395 | +server = RecordingEisServer({path+'-teardown'!r}) |
| 396 | +server.start() |
| 397 | +backend = libei.LibeiBackend() |
| 398 | +backend.connect(timeout=5.0, socket_path={(path+'-teardown').encode()!r}) |
| 399 | +if not backend.is_connected: |
| 400 | + raise RuntimeError('live cleanup never reached a connected device') |
| 401 | +symbols = libei._load_symbols() |
| 402 | +if symbols is None: |
| 403 | + raise RuntimeError('libei symbols did not resolve for live cleanup') |
| 404 | +for device in sorted(backend._refs): |
| 405 | + symbols.ei_device_unref(device) |
| 406 | +print('ei_unref live: begin', flush=True) |
| 407 | +symbols.ei_unref(backend._ei) |
| 408 | +print('ei_unref live: end', flush=True) |
| 409 | +""" |
| 410 | + finished = subprocess.run( # nosec B603 # nosemgrep # reason: fixed interpreter/program, isolate native teardown |
| 411 | + [sys.executable, '-X', 'faulthandler', '-c', program], capture_output=True, |
| 412 | + timeout=60, text=True, encoding='utf-8', errors='replace', check=False, |
410 | 413 | ) |
411 | | - # This interpreter, running a program built from literals above; no shell. |
412 | | - finished = subprocess.run([sys.executable, "-c", program], # nosec B603 # nosemgrep |
413 | | - capture_output=True, timeout=60) |
414 | | - if finished.returncode == 0: |
415 | | - return ("safe, which is what _teardown now relies on to release a " |
416 | | - "completed session instead of leaking its context") |
| 414 | + print(finished.stdout, end='') |
| 415 | + print(finished.stderr, end='') |
| 416 | + began = 'ei_unref live: begin' in finished.stdout.splitlines() |
| 417 | + ended = 'ei_unref live: end' in finished.stdout.splitlines() |
417 | 418 | if finished.returncode == -11: |
418 | | - print() |
419 | | - print(" *** REVISIT *** ei_unref now SEGFAULTS on a live") |
420 | | - print(" context too. LibeiBackend._teardown releases completed") |
421 | | - print(" sessions on the measurement that it is safe, so that") |
422 | | - print(" must go back to abandoning them — this is a crash in a") |
423 | | - print(" library that drives the user's desktop. See Progress.md.") |
424 | | - print() |
425 | | - raise AssertionError( |
426 | | - "ei_unref segfaults on a live context (rc=-11); _teardown's " |
427 | | - "release path is no longer safe on this libei") |
428 | | - detail = finished.stderr.decode("utf-8", "replace").strip().splitlines() |
429 | | - return f"inconclusive (rc={finished.returncode}): {detail[-1] if detail else ''}" |
| 419 | + if not began: |
| 420 | + raise AssertionError('native fault before live ei_unref boundary; inspect native trace') |
| 421 | + raise AssertionError('live ei_unref probe exited with SIGSEGV; inspect its recorded boundary and native trace') |
| 422 | + if finished.returncode != 0: |
| 423 | + raise AssertionError(f'unexpected live ei_unref probe exit {finished.returncode}') |
| 424 | + if not began or not ended: |
| 425 | + raise AssertionError('live ei_unref did not reach completed native cleanup') |
| 426 | + return 'safe in this probe: owned device refs released, live ei_unref completed (rc=0)' |
430 | 427 |
|
431 | 428 |
|
432 | 429 | #: An offset region layout: the shape a compositor must advertise for a |
|
0 commit comments