Commit fbf3414d by PLN (Algolia)

fix(midiviz): tell systemd the difference between a crash and a decision

PLN reported "too sticky" a SECOND time, and the latch was not the whole
answer. The unit is Restart=always -- correctly, since an unplugged LCXL used
to make this process exit cleanly and Restart=on-failure ignored it, leaving
his window gone for the rest of a session. But Restart=always cannot tell that
exit from a person clicking the X, so a deliberate close came back five
seconds later. I had asserted in an earlier commit that "a clean exit is not a
failure so systemd never restarted it"; that was true of the old unit and
stopped being true when the restart policy changed underneath it.

Two supervisors had to be told, and having convinced only one of them looked
exactly like having convinced neither:

  rig_units.ensure()  -> the $XDG_RUNTIME_DIR latch (already landed)
  systemd             -> this commit

midiviz.py now exits USER_CLOSE_EXIT=78 when the close was deliberate (the X,
Q, Esc -- never a compositor teardown, and only when the exit was otherwise
clean, so a real failure keeps its own code and stays restartable), and the
unit exempts exactly that status with RestartPreventExitStatus=78. Every
failure mode still comes back; a person closing the window does not.

The number is written in two files, so a test asserts they are the same
number, and that it collides with neither a normal exit (0/1) nor the 128+N
signal range. If they ever drift, the symptom is the window reappearing five
seconds after being closed -- which reads as a broken close rather than a
mismatched integer, and would send somebody hunting the wrong layer for the
third time.
parent 8e3ef375
......@@ -370,6 +370,22 @@ def _install_signals() -> None:
# same expression and the test asserts they agree.
UNIT_NAME = "midiviz"
# Exit code for "the human closed this on purpose", paired with
# RestartPreventExitStatus=78 in midiviz.service.
#
# The unit is Restart=always, and rightly so: an unplugged LCXL used to make
# this process exit CLEANLY, which Restart=on-failure ignores, so PLN's window
# stayed gone for the rest of the session. But Restart=always cannot tell that
# exit from a person clicking the X -- so the X closed the window and systemd
# put it back five seconds later, which is PLN reporting "its too sticky" a
# SECOND time after the latch was already written.
#
# A distinct exit status is how systemd is told the difference. Crash, EOF, or
# any unplanned exit still comes back; a deliberate close does not. 78 is
# EX_CONFIG from sysexits.h, chosen only because it is well outside the range
# anything here returns on its own (0/1) and outside the 128+N signal range.
USER_CLOSE_EXIT = 78
def latch_close_path():
rt = os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"
......@@ -1202,6 +1218,11 @@ def main(argv=None) -> int:
rc = app.exec()
if w.reader is not None:
w.reader.close()
# A deliberate close reports itself, so the unit does not undo it. Only
# when the exit was otherwise clean: a real failure keeps its own code and
# stays restartable.
if getattr(w, "_user_closed", False) and rc == 0:
return USER_CLOSE_EXIT
return rc
......
......@@ -260,3 +260,36 @@ def test_controls_are_hittable_and_disjoint_at_every_scale():
assert w._ctrl_left() <= pin.left(), \
"the sparkline is allowed to draw over the controls"
w.close()
def test_user_close_exit_code_matches_the_unit():
"""The code's exit status and the unit's guard are one number, twice.
Restart=always cannot distinguish an aseqdump EOF from PLN clicking the X,
so a deliberate close is signalled by exit status and the unit exempts
exactly that status. If these two ever drift, the X closes the window and
systemd puts it back five seconds later — which is the complaint, reported
twice, and it looks like the close is broken rather than the number.
"""
import importlib.util
import re
import sys as _sys
from pathlib import Path as _Path
root = _Path(__file__).resolve().parents[3]
spec = importlib.util.spec_from_file_location("_mv_exit",
root / "tools/bridge/midiviz.py")
mv = importlib.util.module_from_spec(spec)
_sys.modules["_mv_exit"] = mv
spec.loader.exec_module(mv)
unit = (root / "tools/midiviz.service").read_text()
m = re.search(r"^RestartPreventExitStatus=(\d+)\s*$", unit, re.M)
assert m, ("midiviz.service has no RestartPreventExitStatus, so "
"Restart=always will undo every deliberate close")
assert int(m.group(1)) == mv.USER_CLOSE_EXIT, (
f"the unit exempts exit {m.group(1)} but the code exits "
f"{mv.USER_CLOSE_EXIT} on a user close")
# Must not collide with the codes a normal run or a signal produces.
assert mv.USER_CLOSE_EXIT not in (0, 1), "collides with a normal exit"
assert mv.USER_CLOSE_EXIT < 128, "collides with the 128+N signal range"
......@@ -31,6 +31,16 @@ ExecStart=/usr/bin/python3 %h/Work/Sound/Tidal/tools/bridge/midiviz.py
# it holds no audio ports and nothing waits on it, so there is no real
# crash-loop cost to guard against here.
Restart=always
# ...EXCEPT when the human closed it. Restart=always cannot distinguish "the
# aseqdump pipe hit EOF" from "PLN clicked the X", and treating the second as
# the first is what made the lens feel welded on: the window went away and
# came back five seconds later. midiviz.py exits USER_CLOSE_EXIT=78 for a
# deliberate close only (the X, Q, Esc -- never a compositor teardown), so
# this keeps the belt-and-braces restart for every failure mode while letting
# a person actually close the thing. The latch in $XDG_RUNTIME_DIR stops
# rig_units.ensure() from starting it again in the same breath; this stops
# systemd. Both were needed, and only having one of them looked like neither.
RestartPreventExitStatus=78
RestartSec=5
[Install]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment