Commit 46ef4fc5 by PLN (Algolia)

perf-tray: Quit says what it leaves running, and there is an End session

Quit used to close only the tray. Now, with the rig up, it asks: End session,
Quit tray only, or Cancel. End session runs in its own transient unit, because
the script's last step stops perf-tray.service and a child of the tray would be
killed mid-way through switching music mode off.
parent 40202e33
...@@ -40,7 +40,7 @@ import urllib.request ...@@ -40,7 +40,7 @@ import urllib.request
from pathlib import Path from pathlib import Path
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QSystemTrayIcon, QMenu, QAction, QActionGroup, QApplication, QSystemTrayIcon, QMenu, QAction, QActionGroup, QMessageBox,
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QIcon, QPixmap, QPainter, QColor, QFont, QPen, QPainterPath, QCursor, QIcon, QPixmap, QPainter, QColor, QFont, QPen, QPainterPath, QCursor,
...@@ -58,6 +58,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent / "tools")) ...@@ -58,6 +58,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent / "tools"))
import rig_health # noqa: E402 (the one health.json gig-log publishes; this only READS it) import rig_health # noqa: E402 (the one health.json gig-log publishes; this only READS it)
SERVICE = "perf-tray" # systemd --user unit controlling autostart SERVICE = "perf-tray" # systemd --user unit controlling autostart
END_SESSION = Path(__file__).resolve().parent / "tools" / "end_session.py"
REFRESH_MS = 2000 REFRESH_MS = 2000
TRAY_WAIT_S = 120 # how long to wait for the tray host (a GNOME extension) at login TRAY_WAIT_S = 120 # how long to wait for the tray host (a GNOME extension) at login
BRIDGE_URL = "http://127.0.0.1:8773/" BRIDGE_URL = "http://127.0.0.1:8773/"
...@@ -289,6 +290,65 @@ class PerfTray: ...@@ -289,6 +290,65 @@ class PerfTray:
self.timer.start(REFRESH_MS) self.timer.start(REFRESH_MS)
self.refresh() self.refresh()
# ------------------------------------------------------------- ending a session
def _rig_is_up(self):
active, _ = sc_state()
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):
box = QMessageBox()
box.setWindowTitle("End session")
box.setIcon(QMessageBox.Question)
box.setText("End the session?")
box.setInformativeText(
"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.")
ok = box.addButton("End session", QMessageBox.AcceptRole)
cancel = box.addButton("Cancel", QMessageBox.RejectRole)
box.setDefaultButton(cancel)
box.exec_()
return box.clickedButton() is ok
def _run_end_session(self):
"""Its OWN transient unit, not our child: the last thing the script does is stop
perf-tray.service, and a child of this unit would be killed with it, mid-way
through switching music mode off. The script reports the outcome by notification."""
return QProcess.startDetached("systemd-run", [
"--user", "--collect", "--quiet", f"--unit=parvagues-end-session-{int(time.time())}",
"/usr/bin/python3", str(END_SESSION)])
def on_end_session(self):
if self._ask_end():
self._run_end_session()
def on_quit_clicked(self):
"""Quit asks only when there is something Quit would leave behind."""
if not self._rig_is_up():
self.app.quit()
return
choice = self._ask_quit()
if choice == "end":
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.
...@@ -534,8 +594,15 @@ class PerfTray: ...@@ -534,8 +594,15 @@ class PerfTray:
self.menu.aboutToShow.connect(self.refresh) self.menu.aboutToShow.connect(self.refresh)
self.menu.addSeparator() self.menu.addSeparator()
quit_act = QAction("&Quit perf-tray", self.menu) # Two different things, and the menu says so. "Quit" used to be the only exit and it
quit_act.triggered.connect(self.app.quit) # only closed THIS window: SuperDirt (a headless unit), the black box and the helpers
# kept running and music mode stayed on, so "I quit everything" was not true
# (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)
self.menu.addAction(quit_act) self.menu.addAction(quit_act)
# ------------------------------------------------------------ the gearbox # ------------------------------------------------------------ the gearbox
......
"""The tray's Quit must not lie about what it leaves running.
2026-10-11: Quit closed only the tray. SuperDirt (headless), the black box and the helpers
kept running and music mode stayed on, so "I quit everything" was false and the owner got
alarms about a rig they believed was down.
"""
import os
import pathlib
import re
import pytest
TRAY_SRC = (pathlib.Path(__file__).resolve().parents[3] / "perf-tray.py").read_text()
def test_the_menu_offers_end_session_and_a_quit_that_says_what_it_leaves():
assert "End session" in TRAY_SRC
assert "Quit perf-tray (rig keeps running)" in TRAY_SRC
assert re.search(r"quit_act\.triggered\.connect\(self\.on_quit_clicked\)", TRAY_SRC)
assert "triggered.connect(self.app.quit)" not in TRAY_SRC # no bare quit left behind
def test_end_session_runs_in_its_own_unit_not_as_a_child_of_the_tray():
"""The script's last act is stopping perf-tray.service; a child would die with it."""
assert "systemd-run" in TRAY_SRC and "--collect" in TRAY_SRC and "--unit=parvagues-end-session" in TRAY_SRC
@pytest.fixture
def tray(monkeypatch):
pytest.importorskip("PyQt5.QtWidgets")
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
import importlib.util
from PyQt5.QtWidgets import QApplication
spec = importlib.util.spec_from_file_location("perf_tray_under_test",
pathlib.Path(__file__).resolve().parents[3] / "perf-tray.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
app = QApplication.instance() or QApplication([])
t = mod.PerfTray(app)
t._quit_calls, t._end_calls = [], []
app.quit = lambda: t._quit_calls.append(1)
t._run_end_session = lambda: t._end_calls.append(1) or True
return t
def test_quit_with_the_rig_down_just_quits(tray, monkeypatch):
tray._rig_is_up = lambda: False
tray._ask_quit = lambda: pytest.fail("must not ask when nothing would be left running")
tray.on_quit_clicked()
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)])
def test_quit_with_the_rig_up_asks_and_obeys(tray, answer, quit_n, end_n):
tray._rig_is_up = lambda: True
tray._ask_quit = lambda: answer
tray.on_quit_clicked()
assert (len(tray._quit_calls), len(tray._end_calls)) == (quit_n, 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