Commit d701720f by PLN (Algolia)

perf-tray: one Quit, and the rig starts the tray

The tray is up exactly when the rig is up. parvagues-sc Wants it, so any
bring-up brings it; Quit is the single exit and ends the session when the rig
runs (the tray never leaves by itself under a running rig), and just closes the
tray when it does not. Drops the End session / Quit tray only split.
parent 37098d6f
...@@ -28,6 +28,12 @@ Wants=pipewire.service ...@@ -28,6 +28,12 @@ Wants=pipewire.service
# Recording follows the rig, so the rig starts it. Wants=, not Requires=: a recorder that # Recording follows the rig, so the rig starts it. Wants=, not Requires=: a recorder that
# cannot start must never stop the sound. # cannot start must never stop the sound.
Wants=gig-record.service Wants=gig-record.service
# The tray is the rig's face and its one Quit, and it must exist whenever the rig does.
# perf-tray is `on-demand` in rig_units.py, which turned out to mean "only if a converge
# remembers": a tray Quit on 2026-10-11 left it dark for 42 h under a running rig. Not
# PartOf: SuperDirt stops on the exit-3 path of end_session (Ardour still open) and the
# watchdog bounces it, and the tray must survive both (GIG UP lives in it).
Wants=perf-tray.service
# Bound the restart loop (see Restart= below): three tries in five minutes, then stay # Bound the restart loop (see Restart= below): three tries in five minutes, then stay
# down and stay loud. This is what makes recovery safe without flapping. # down and stay loud. This is what makes recovery safe without flapping.
StartLimitIntervalSec=300 StartLimitIntervalSec=300
......
...@@ -295,32 +295,15 @@ class PerfTray: ...@@ -295,32 +295,15 @@ class PerfTray:
active, _ = sc_state() active, _ = sc_state()
return bool(active) return bool(active)
def _ask_quit(self):
"""'end' | 'tray' | 'cancel'. Separate from the logic so a test can answer it."""
box = QMessageBox()
box.setWindowTitle("perf-tray")
box.setIcon(QMessageBox.Question)
box.setText("The rig is still running.")
box.setInformativeText(
"If you only quit the tray, SuperDirt, the black box recorder and the helpers "
"keep running and music mode stays on.")
end = box.addButton("End session", QMessageBox.AcceptRole)
tray = box.addButton("Quit tray only", QMessageBox.DestructiveRole)
cancel = box.addButton("Cancel", QMessageBox.RejectRole)
box.setDefaultButton(cancel)
box.exec_()
clicked = box.clickedButton()
return "end" if clicked is end else "tray" if clicked is tray else "cancel"
def _ask_end(self): def _ask_end(self):
box = QMessageBox() box = QMessageBox()
box.setWindowTitle("End session") box.setWindowTitle("Quit")
box.setIcon(QMessageBox.Question) box.setIcon(QMessageBox.Question)
box.setText("End the session?") box.setText("Quit the rig?")
box.setInformativeText( box.setInformativeText(
"Stops SuperDirt (the black box stops with it), then the helpers, and leaves " "Stops SuperDirt (the black box stops with it), then the helpers, and leaves "
"music mode. It never closes Ardour, and it refuses while a take is rolling.") "music mode. It never closes Ardour, and it refuses while a take is rolling.")
ok = box.addButton("End session", QMessageBox.AcceptRole) ok = box.addButton("Quit", QMessageBox.AcceptRole)
cancel = box.addButton("Cancel", QMessageBox.RejectRole) cancel = box.addButton("Cancel", QMessageBox.RejectRole)
box.setDefaultButton(cancel) box.setDefaultButton(cancel)
box.exec_() box.exec_()
...@@ -334,20 +317,16 @@ class PerfTray: ...@@ -334,20 +317,16 @@ class PerfTray:
"--user", "--collect", "--quiet", f"--unit=parvagues-end-session-{int(time.time())}", "--user", "--collect", "--quiet", f"--unit=parvagues-end-session-{int(time.time())}",
"/usr/bin/python3", str(END_SESSION)]) "/usr/bin/python3", str(END_SESSION)])
def on_end_session(self):
if self._ask_end():
self._run_end_session()
def on_quit_clicked(self): def on_quit_clicked(self):
"""Quit asks only when there is something Quit would leave behind.""" """The one exit. Tray up iff rig up: with the rig down Quit just closes the tray; with
the rig up it ends the session, and the session's last act stops this tray. The tray
never exits on its own while the rig runs (that was the 2026-10-11 gap: a tray Quit
left the rig up, nothing brought the tray back, and it was dark for 42 h). If the
end refuses (a take is rolling, Ardour is open) the tray stays, which is still true."""
if not self._rig_is_up(): if not self._rig_is_up():
self.app.quit() self.app.quit()
return elif self._ask_end():
choice = self._ask_quit()
if choice == "end":
self._run_end_session() self._run_end_session()
elif choice == "tray":
self.app.quit()
def on_tray_activated(self, reason): def on_tray_activated(self, reason):
"""Left/middle click opens the same menu right-click does. """Left/middle click opens the same menu right-click does.
...@@ -594,14 +573,9 @@ class PerfTray: ...@@ -594,14 +573,9 @@ class PerfTray:
self.menu.aboutToShow.connect(self.refresh) self.menu.aboutToShow.connect(self.refresh)
self.menu.addSeparator() self.menu.addSeparator()
# Two different things, and the menu says so. "Quit" used to be the only exit and it # One exit. The tray is up exactly when the rig is up (parvagues-sc Wants it), so
# only closed THIS window: SuperDirt (a headless unit), the black box and the helpers # Quit means "I am done": it ends the rig when it runs, closes the tray when it does not.
# kept running and music mode stayed on, so "I quit everything" was not true quit_act = QAction("&Quit", self.menu)
# (2026-10-11). End session is the honest "I am done".
self.end_act = QAction("■ End session (stop the rig)…", self.menu)
self.end_act.triggered.connect(self.on_end_session)
self.menu.addAction(self.end_act)
quit_act = QAction("&Quit perf-tray (rig keeps running)", self.menu)
quit_act.triggered.connect(self.on_quit_clicked) quit_act.triggered.connect(self.on_quit_clicked)
self.menu.addAction(quit_act) self.menu.addAction(quit_act)
......
"""The tray's Quit must not lie about what it leaves running. """The tray has ONE exit, and the tray is up exactly when the rig is up.
2026-10-11: Quit closed only the tray. SuperDirt (headless), the black box and the helpers 2026-10-11: Quit closed only the tray; the rig kept running, nothing brought the tray back,
kept running and music mode stayed on, so "I quit everything" was false and the owner got and it was dark for 42 h while SuperDirt and the black box ran. The contract now: a single
alarms about a rig they believed was down. "Quit"; with the rig up it ends the session (which stops the tray last), with the rig down it
just closes the tray; the tray never exits by itself under a running rig; and parvagues-sc
Wants the tray so a rig bring-up always brings it.
""" """
import os
import pathlib import pathlib
import re
import pytest import pytest
TRAY_SRC = (pathlib.Path(__file__).resolve().parents[3] / "perf-tray.py").read_text() ROOT = pathlib.Path(__file__).resolve().parents[3]
TRAY_SRC = (ROOT / "perf-tray.py").read_text()
def test_the_menu_offers_end_session_and_a_quit_that_says_what_it_leaves(): def test_the_menu_has_a_single_quit_and_nothing_else_that_ends_things():
assert "End session" in TRAY_SRC assert 'QAction("&Quit", self.menu)' in TRAY_SRC
assert "Quit perf-tray (rig keeps running)" in TRAY_SRC assert "quit_act.triggered.connect(self.on_quit_clicked)" in TRAY_SRC
assert re.search(r"quit_act\.triggered\.connect\(self\.on_quit_clicked\)", TRAY_SRC) for gone in ("Quit tray only", "rig keeps running", "End session", "_ask_quit", "on_end_session"):
assert gone not in TRAY_SRC, gone
assert "triggered.connect(self.app.quit)" not in TRAY_SRC # no bare quit left behind assert "triggered.connect(self.app.quit)" not in TRAY_SRC # no bare quit left behind
...@@ -25,14 +27,21 @@ def test_end_session_runs_in_its_own_unit_not_as_a_child_of_the_tray(): ...@@ -25,14 +27,21 @@ def test_end_session_runs_in_its_own_unit_not_as_a_child_of_the_tray():
assert "systemd-run" in TRAY_SRC and "--collect" in TRAY_SRC and "--unit=parvagues-end-session" in TRAY_SRC assert "systemd-run" in TRAY_SRC and "--collect" in TRAY_SRC and "--unit=parvagues-end-session" in TRAY_SRC
def test_the_rig_starts_the_tray():
sc = (ROOT / "parvagues-sc.service").read_text()
assert any(l.strip() == "Wants=perf-tray.service" for l in sc.splitlines())
assert not any(l.strip().startswith("PartOf=parvagues-sc") for l in
(ROOT / "perf-tray.service").read_text().splitlines()), \
"PartOf would kill the tray on end_session's exit-3 path and on every watchdog bounce"
@pytest.fixture @pytest.fixture
def tray(monkeypatch): def tray(monkeypatch):
pytest.importorskip("PyQt5.QtWidgets") pytest.importorskip("PyQt5.QtWidgets")
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen") monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
import importlib.util import importlib.util
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
spec = importlib.util.spec_from_file_location("perf_tray_under_test", spec = importlib.util.spec_from_file_location("perf_tray_under_test", ROOT / "perf-tray.py")
pathlib.Path(__file__).resolve().parents[3] / "perf-tray.py")
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) spec.loader.exec_module(mod)
app = QApplication.instance() or QApplication([]) app = QApplication.instance() or QApplication([])
...@@ -43,25 +52,17 @@ def tray(monkeypatch): ...@@ -43,25 +52,17 @@ def tray(monkeypatch):
return t return t
def test_quit_with_the_rig_down_just_quits(tray, monkeypatch): def test_quit_with_the_rig_down_just_closes_the_tray_without_asking(tray):
tray._rig_is_up = lambda: False tray._rig_is_up = lambda: False
tray._ask_quit = lambda: pytest.fail("must not ask when nothing would be left running") tray._ask_end = lambda: pytest.fail("must not ask when nothing would be ended")
tray.on_quit_clicked() tray.on_quit_clicked()
assert tray._quit_calls == [1] and tray._end_calls == [] assert tray._quit_calls == [1] and tray._end_calls == []
@pytest.mark.parametrize("answer,quit_n,end_n", [("end", 0, 1), ("tray", 1, 0), ("cancel", 0, 0)]) @pytest.mark.parametrize("answer,end_n", [(True, 1), (False, 0)])
def test_quit_with_the_rig_up_asks_and_obeys(tray, answer, quit_n, end_n): def test_quit_with_the_rig_up_ends_the_session_or_does_nothing_never_exits_the_tray(tray, answer, end_n):
tray._rig_is_up = lambda: True tray._rig_is_up = lambda: True
tray._ask_quit = lambda: answer tray._ask_end = lambda: answer
tray.on_quit_clicked() tray.on_quit_clicked()
assert (len(tray._quit_calls), len(tray._end_calls)) == (quit_n, end_n) assert tray._quit_calls == [] # tray up iff rig up: it must not leave by itself
assert len(tray._end_calls) == end_n
def test_end_session_menu_item_confirms_before_acting(tray):
tray._ask_end = lambda: False
tray.on_end_session()
assert tray._end_calls == []
tray._ask_end = lambda: True
tray.on_end_session()
assert tray._end_calls == [1]
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