Review everything shipped this weekend, and fix what the reviewers caught
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -11,8 +11,8 @@ pragma Singleton
|
||||
//
|
||||
// hyprpaper and mpvpaper both claim the background layer and stacking within a
|
||||
// layer is creation order — a race. So while a video is active hyprpaper's
|
||||
// service is stopped, and stopping the video starts it again; Wallpaper.qml
|
||||
// reapplies the still policy once it returns.
|
||||
// service is stopped, and stopping the video starts it again; this service then
|
||||
// reapplies the still policy through Wallpaper.qml once it returns.
|
||||
//
|
||||
// mpvpaper 1.9 (Terra) is the floor: it carries the libmpv fence-leak
|
||||
// workaround. Known upstream sharp edges — a hotplug segfault and a
|
||||
@@ -125,6 +125,9 @@ Singleton {
|
||||
root.lastError = "";
|
||||
root.path = video;
|
||||
root.manuallyPaused = false;
|
||||
// A new video starts its own patience; the previous file's crashes are
|
||||
// not evidence about this one.
|
||||
root.crashStreak = 0;
|
||||
hyprpaperControl.command = ["systemctl", "--user", "stop", "hyprpaper.service"];
|
||||
hyprpaperControl.running = true;
|
||||
frameProc.command = ["sh", "-c",
|
||||
@@ -141,6 +144,19 @@ Singleton {
|
||||
root.restoreConsumed = true;
|
||||
root.path = "";
|
||||
root.manuallyPaused = false;
|
||||
root.crashStreak = 0;
|
||||
root.teardownPlayers();
|
||||
// The stored path is the restore path, the picker's "current" ring and
|
||||
// what Wallpaper.setSingle reads back. Leaving it set after a deliberate
|
||||
// stop meant the next login started playing the video again.
|
||||
DesktopPreferences.set("videoWallpaperPath", "");
|
||||
stillHandback.restart();
|
||||
}
|
||||
|
||||
// Everything both stop() and the crash bail-out have to do: no player left
|
||||
// running, no timer armed to start another, and hyprpaper handed back the
|
||||
// layer it owns.
|
||||
function teardownPlayers(): void {
|
||||
playerRespawn.stop();
|
||||
spawnDelay.stop();
|
||||
for (const player of root.players) {
|
||||
@@ -151,7 +167,22 @@ Singleton {
|
||||
reaper.running = true;
|
||||
hyprpaperControl.command = ["systemctl", "--user", "start", "hyprpaper.service"];
|
||||
hyprpaperControl.running = true;
|
||||
Qt.callLater(() => Wallpaper.refreshActive());
|
||||
}
|
||||
|
||||
// hyprpaper's service needs a beat to be back on the bus before it can be
|
||||
// asked to draw anything — the same settle Wallpaper.setSingle uses for its
|
||||
// own video→still handoff. When that handoff is the reason we are stopping,
|
||||
// it owns the policy (it holds the new one, which is not yet persisted) and
|
||||
// this stays out of the way.
|
||||
Timer {
|
||||
id: stillHandback
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
if (root.active || Wallpaper.pendingStillPolicy !== null)
|
||||
return;
|
||||
if (!Wallpaper.applyCurrentPolicy(false))
|
||||
Wallpaper.refreshActive();
|
||||
}
|
||||
}
|
||||
|
||||
// Players are created per current output set each (re)spawn, so hotplug
|
||||
@@ -181,15 +212,32 @@ Singleton {
|
||||
reaper.running = true;
|
||||
}
|
||||
|
||||
// pkill returns when the signal is delivered, not when the process is gone,
|
||||
// and mpvpaper takes a moment to tear its GL context down. So the reaper
|
||||
// waits for the corpses rather than the shell guessing at how long that
|
||||
// takes: TERM, then poll pgrep in 50ms steps, escalating to KILL halfway
|
||||
// through and giving up after two seconds so a wedged process cannot hold
|
||||
// the wallpaper hostage.
|
||||
Process {
|
||||
id: reaper
|
||||
command: ["pkill", "-x", "mpvpaper"]
|
||||
command: ["sh", "-c", `
|
||||
pkill -x mpvpaper 2>/dev/null || true
|
||||
step=0
|
||||
while [ "$step" -lt 40 ]; do
|
||||
pgrep -x mpvpaper >/dev/null 2>&1 || exit 0
|
||||
[ "$step" -eq 20 ] && pkill -9 -x mpvpaper 2>/dev/null
|
||||
sleep 0.05
|
||||
step=$((step + 1))
|
||||
done
|
||||
`]
|
||||
onExited: spawnDelay.restart()
|
||||
}
|
||||
|
||||
// The reaper already waited for the old players to die, so this is only the
|
||||
// hand-back to the event loop, not a guess at how long a kill takes.
|
||||
Timer {
|
||||
id: spawnDelay
|
||||
interval: 300
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
if (!root.active)
|
||||
return;
|
||||
@@ -201,10 +249,37 @@ Singleton {
|
||||
spawned.push(player);
|
||||
}
|
||||
root.players = spawned;
|
||||
root.playersStartedAt = Date.now();
|
||||
root.crashCounted = false;
|
||||
pauseSync.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// A crash loop has to end somewhere. mpvpaper dying within three seconds of
|
||||
// being spawned is not the hotplug segfault the supervisor exists for — it
|
||||
// is a file it cannot decode or a VAAPI stack that is not there — and
|
||||
// respawning forever flashes the desktop black once a second for as long as
|
||||
// the session lasts, with no error anywhere to explain it.
|
||||
property real playersStartedAt: 0
|
||||
property int crashStreak: 0
|
||||
// One count per spawned set: on two monitors a single crash fires two
|
||||
// onExited, and counting each would trip the limit half a cycle early.
|
||||
property bool crashCounted: false
|
||||
readonly property int crashStreakLimit: 3
|
||||
readonly property int crashWindowMs: 3000
|
||||
|
||||
function giveUp(): void {
|
||||
root.path = "";
|
||||
root.manuallyPaused = false;
|
||||
root.crashStreak = 0;
|
||||
// Nothing may start it again this session, not even a preference write
|
||||
// landing in tryRestore's lap.
|
||||
root.restoreConsumed = true;
|
||||
root.teardownPlayers();
|
||||
root.lastError = "Video wallpaper kept crashing — check the file and VAAPI decode";
|
||||
stillHandback.restart();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: playerComponent
|
||||
|
||||
@@ -213,12 +288,31 @@ Singleton {
|
||||
property string output: ""
|
||||
property bool retiring: false
|
||||
onExited: {
|
||||
// The object is spent either way: a Process cannot be restarted
|
||||
// and every respawn builds a fresh one per output, so holding
|
||||
// this one leaked a Process per crash cycle.
|
||||
root.players = root.players.filter(candidate => candidate !== player);
|
||||
Qt.callLater(() => player.destroy());
|
||||
|
||||
// A retiring player died because the supervisor killed it —
|
||||
// reacting to that is how the reap loop once ate its young.
|
||||
if (player.retiring || !root.active)
|
||||
return;
|
||||
|
||||
// A dead player while a video is meant to be active is a
|
||||
// crash (mpvpaper has a known hotplug segfault): respawn the
|
||||
// whole set after a beat rather than reasoning per-output.
|
||||
// A retiring player died because the supervisor killed it —
|
||||
// reacting to that is how the reap loop once ate its young.
|
||||
if (!player.retiring && root.active && !playerRespawn.running)
|
||||
if (!root.crashCounted) {
|
||||
root.crashCounted = true;
|
||||
root.crashStreak = Date.now() - root.playersStartedAt < root.crashWindowMs
|
||||
? root.crashStreak + 1
|
||||
: 0;
|
||||
}
|
||||
if (root.crashStreak >= root.crashStreakLimit) {
|
||||
root.giveUp();
|
||||
return;
|
||||
}
|
||||
if (!playerRespawn.running)
|
||||
playerRespawn.restart();
|
||||
}
|
||||
}
|
||||
@@ -289,7 +383,11 @@ Singleton {
|
||||
// The service restores its own video, reactively: at cold start the
|
||||
// preferences file and the mpvpaper probe both land asynchronously, so a
|
||||
// one-shot timer (the still pipeline's approach) raced them and lost.
|
||||
// Once per session — a user's stop() is not to be overridden.
|
||||
// Once per session, and consumed by stop() and giveUp() as well as by the
|
||||
// restore itself: a preference write is all it takes to re-enter tryRestore,
|
||||
// so neither a video the user turned off nor one that crashed out may come
|
||||
// back under them. stop() also clears the stored path, so there is nothing
|
||||
// left to restore at the next login either.
|
||||
property bool restoreConsumed: false
|
||||
// Harness seam, mirroring Wallpaper.startupRestoreEnabled: a test instance
|
||||
// must never start playing the user's real wallpaper.
|
||||
|
||||
Reference in New Issue
Block a user