Repository navigation
release: cuems-engine 0.1.0rc6-3 - #31
Merged
Merged
Conversation
… (869fa89uh) loop_videoCue sent /loop 1 for every video cue, play-once ones included. The videocomposer runs its display latency (33 ms) ahead of MTC, so with wraparound on it reached end of file before the engine hid the layer and showed the first frame again for a frame or two at the end of the cue. Send /loop 1 only when cue.loop != 1, and on the last pass of a counted loop send /loop 0 after the new offset, so the end holds the last frame.
…z) (red) Every GO carries run_seq; STOP, load and unload bump it before they forward anything; a node's follow message is relayed to every node only while the show runs and only with the current run_seq. The four tests that pinned the GO value now expect it to be a dict in every case.
…869fc8ytz) The controller keeps a run counter, started from the clock. STOP, load and unload bump it before forwarding; every GO ships it to the nodes. A node's 'follow' status message (announce or cancel) is forwarded to every node only while running and only with the current value; the check and the forward share one lock with the bump, so a relayed follow is always queued ahead of the STOP that ends its run. The controller keeps no follow state. The GO value is now always a dict. Nodes read nothing from it but the anchor (_resolve_go_anchor), so the caller's own value is no longer passed on. Plan: cuems-RELATIONS Plans/2026-10-05-engine-cross-node-auto-follow.md §3.3-3.4.
The receive hook (bus order, never the command lock), the receiving node's follow table (announce, pre-arm, wait for the instant, dispatch through a commit gate in go(), cancel by entry state, the 2 s rule), and the owning node's announce/cancel (at the reveal or the last loop; one cancel per announcement; the restart cancel from go()). Includes the tests for the seven findings of the code review of the implementation.
…fc8ytz) An Auto follow (post_go='go_at_end') fired its target only on the nodes where the source is local, so a target on another node never started, silently, on every engine line. The node that owns an AudioCue or VideoCue source now announces the follow instant when the cue reveals (or at the start of its last loop; never for an endless loop), through the controller. Every other node pre-arms what it would start, waits for the instant on MTC and dispatches it there through go_from. ActionCue, FadeCue, CueList and DmxCue sources are local on every node and keep firing their own follow. If the follow does not happen the owner cancels: 'stop' (stop, pause or fade-out action), 'restart' (sent from go()'s commit section when the source is started again), 'error'. A STOP, a load or the yield send nothing; every node handles those itself. A cancel that reaches a target already on stage stops it only within 2 s and only for stop/restart. - cues/FollowRelay.py: both halves of the node side; one entry per source, its own lock, never calls into CueHandler while holding it. - NodeCommunications: a receive hook in bus order, before any command thread; send_operation_nowait. - NodeEngine: run_seq recorded and pending follows dropped at receipt; follow messages applied there, never under the command lock; STOP and load marked applied after they run. - CueHandler: go() gets a commit_gate (refusals, gate, restart cancel, assignments); go_from passes arm_epoch/require_stop_epoch/commit_gate through; stop_dispatched_follow and disarm_if_unchanged (guarded disarm under the cue's arm claim); the reveal stamp; go_threaded wraps the pass so every exit decides its cancel once. - loop_cue: on_last_loop callback for audio and video. Verified on test2 (suite 1109 passed; 40 live runs, 0 LATE, 0 fallback; old-node smoke test; PR #22 drills and a Medina sala1 replay unchanged). Plan: cuems-RELATIONS Plans/2026-10-05-engine-cross-node-auto-follow.md.
…pair its wiring at GO edge by edge; honest arm-wait log lines (869fbyjzx) (red) - the player's [OK] RUNNING! stdout line sets Player.ready; the mixer wiring waits for it (one budget with the port wait, after the no-JACK fail-fast; a dead player ends the wait; a player that never reports it is wired on port presence with a WARNING); - player_connection_diff reports missing and stray edges (None when no mixer input resolves); repair_player_connections fixes them one by one and never disconnects an expected edge; the GO-time check uses them; - the routing-equivalence tests of player_connections_correct now drive get_connections (one read per outport), every case kept; - go() and the dispatch site say whether a cue was still being armed (waited for) or not armed at all (a real fallback).
…jzx) The engine wired a player to the mixer as soon as its JACK port was visible, polling every 0.5 s. The port exists before the player has finished starting: RtAudio auto-connects its outports to system:playback only when the stream starts, and the constructor goes on after that. - Wiring before the auto-connect found nothing to disconnect; the auto-connect then landed and stayed, and the cue sounded through the mixer AND straight to the outputs (869fcvz85, ~3.5 % of arms). - A GO waiting on the arm could message a half-built player. - The 0.5 s poll made a pre-arm that started at a hand-off miss a GO 0.5 s later by ~360 ms (869fbyjzx). The player prints [OK] RUNNING! once it is fully started (main.cpp), and the stdout reader now sets Player.ready on it. connect_player_to_outputs waits for that event when given one (after the no-JACK fail-fast, in counted 50 ms slices, sharing today's 14.5 s budget with the port wait), then wires; the existing disconnect step now always finds and removes the auto-connects. A dead player ends the wait; a player that never reports ready is wired on port presence with a WARNING. new_audio_output passes the player's event; the test fixtures' mocked players set it.
…9fbyjzx, 869fcvz85) The GO-time check (player_connections_correct) confirmed only that the expected player->mixer edges exist, so a stray direct edge to system:playback passed, and the cue sounded doubled and outside the mixer's volume and mute (869fcvz85). Its repair was a full rewire (connect_player_to_outputs disconnects every edge first). A GO can reach it while the cue is audible: go() refuses a duplicate only inside one chain pass, the 'play' action has no playing check, and run_audioCue has no early exit. So the rewire also cut the healthy channels. - _expected_edges: the one copy of the routing (L/R pairs, mono fan-out, missing mixer inputs dropped), used by the arm-time wiring and the check, so they cannot drift. - player_connection_diff -> (missing, stray), one get_connections per outport; None when the player's port is gone or no mixer input resolves (a silent cue, never "correct"). - player_connections_correct stays, as a wrapper on the diff. - repair_player_connections: disconnects each stray (re-checked first, since a failed disconnect resets the shared JACK client), connects each missing edge, never touches an expected edge; it reports a failed connect and the strays that survive. - run_audioCue calls _verify_audio_wiring_at_go, which uses the diff and the repair and logs a SILENT error for no mixer input or a failed connect, and an error for a surviving stray. The WARNING keeps its prefix "graph not wired correctly at GO".
… (869fbyjzx)
A GO that found its cue still being armed (a pre-arm, an audio player
still starting) logged "this should not happen, pre-arm may have failed.
Re-arming as fallback." It had not failed: the arm was in flight and the
GO waited for it.
- go() and the dispatch site now ask describe_arm_in_flight first:
- an arm in flight: INFO "still being armed (...) — waiting for the arm
in progress";
- nothing in flight: the WARNING, which keeps "not loaded at go() time"
/ "not loaded at dispatch" and "Re-arming as fallback", so the
harnesses keep counting real fallbacks only;
- a continuation at go() (which never arms there): INFO "dispatching;
its own thread arms it or waits".
- arm() takes an optional wait_report and fills it when it actually waits
for another thread's arm, so "waited Y s" reports what happened, not
the guess made before the call.
- The control flow is unchanged: same arm() calls, same epoch checks.
- Test stand-ins of arm() accept the new optional argument.
…ge by edge at GO (869fbyjzx)
They imported helpers from test_dispatch_reorder, which only resolves when tests/ is on sys.path; the test2 suite run does not put it there.
…byjzx) An audio arm is ~0.47 s cold, so a GO that reaches the cue sooner after its arm began still fires late (second GO at 0.3 s: ~0.2 s late; 0.5 s and later: on time). Only arming before the hand-off removes it (plan B / Part B, not built: no real show hits it today). Recorded in the code and CLAUDE.md.
From a fresh-eyes review of the branch: - new_audio_output: a player without a ready event now logs a WARNING (the wiring would fall back to port presence and the doubled-audio race would come back unnoticed); tests that the player's Event reaches the wiring. - The ready wait and the port wait share the ceiling exactly: the slices the ready wait used are rounded UP into port attempts (they were floored, so the total could exceed the ceiling by up to one retry step); parametrized test. - Player.call_subprocess clears ready first, so a second run of the same Player cannot pass the gate on a stale Event. - repair_player_connections: a stray whose disconnect failed but is no longer connected (removed by a concurrent re-arm wiring) is not reported as left.
fix(audio): wire an audio player only once it reports RUNNING; check and repair its wiring at GO edge by edge (869fbyjzx, 869fcvz85)
One /engine/status/cue_name/<uuid> per cue after the load status, replayed to late joiners, emptied on unload; a second load without unload replaces the map (duplicated projects share cue uuids). Pure unit tests, safe on a live rig.
…fedahu)
The UI named the engine's next cue from whatever project its own page had
loaded, so a tab viewing X while the engine held Y showed a raw uuid. The
controller already pushes cue status and enabled flags per cue; the name is
the missing member of that family.
- cue_names built at load from the script (same walk as _collect_cue_ids;
item.get("name"), None -> "")
- one /engine/status/cue_name/<uuid> per cue, sent AFTER set_status("load"):
duplicated projects share cue uuids, so clients treat a change of load as
"new table" and refill it from this burst
- replayed in _on_ws_client_connect after cue_enabled, before mixer
- emptied on unload; nothing is sent then (clients clear on load "")
- "Cue names initialised for N cues" next to the cue status line
Node engine untouched. Verified on test2 (rc6-2+nodeadopt1 base): wire
capture order, late join, X->Y without unload, unload; full suite 1057 passed.
…rs (869evtdf7) (red) An orphan the node cannot kill (EPERM, owned by another user) made kill_orphaned_audio_processes raise in the middle of _load_project_inner: the old project was torn down, the new one never armed, and the only trace was "Error executing command callback for load: [Errno 1] Operation not permitted". The mixer sweep has the same flaw at mixer start. Red tests for plan Plans/2026-10-07-engine-orphan-kill-eperm-aborts-load.md (cuems-RELATIONS), rev 4: - both sweeps: EPERM / any other kill error logged and the sweep goes on; ESRCH silent; pgrep rc 1 silent, rc >= 2 and "cannot run" logged; a bad pgrep line skipped; the owner named in the EPERM warning (G1, G2); - pgrep asked for own-user processes only, audioplayer argv[0] anchored, with the pattern table (G4); - a configured binary name the cleanup does not know is warned at start (G4b); - the load reaches ready_project/ready_script with the sweep running for real and os.kill raising EPERM; - the command callback failure is logged with its traceback (G6); - cleanup_zombie_jack_clients survives a JACK error (G7). test_player_handler_mixer_orphan.py: pgrep argv now carries -u <euid>.
Both sweeps now share module-level helpers that never raise: _orphan_pids runs pgrep (rc 1 = nothing to do, silent; rc >= 2 or pgrep that cannot run = WARNING with rc and stderr; a bad line is skipped with a WARNING), _kill_orphan kills one PID (ESRCH silent; EPERM = WARNING naming the PID, its owner and the silent-cue risk; anything else = WARNING), and _pid_owner reads the owner from /proc/<pid>/status. Before, an EPERM on an orphan owned by another user escaped kill_orphaned_audio_processes in the middle of _load_project_inner (old project torn down, new one never armed), and escaped kill_orphaned_mixer_processes at mixer start (node without a mixer, all audio silent). Module-level, not methods: the existing tests call the sweeps unbound on a SimpleNamespace stand-in.
… (869evtdf7) Both sweeps ask pgrep for the engine's own user only (-u <euid>, the scope of the node-engine unit's own pkill -u cuems): another user's process cannot hold a client name on our per-user JACK server, and trying to kill it is what produced the EPERM. The audioplayer pattern is anchored on argv[0] (^([^ ]*/)?cuems-audioplayer( |$)), so a tail -f of a cuems-audioplayer log, a grep or a sh -c containing the word are no longer SIGKILLed. The mixer pattern (jack-volume -c) is unchanged. The cleanups now rely on the binary's name, so set_players warns at start when a configured audioplayer/audiomixer/dmxplayer path has a basename they do not know (check_orphan_binary_names): such orphans would escape every cleanup.
A node load that died on an EPERM in the orphan sweep left only "Error executing command callback for load: [Errno 1] Operation not permitted": no step, no line. The error now carries its traceback (exc_info=True; cuemsutils' Logger forwards it to logging).
…df7) cleanup_zombie_jack_clients runs right after the audioplayer orphan sweep in _load_project_inner, and its conn_man.get_ports call was unguarded: a JACK error aborted the load at the same point, after the old project was torn down. Now it logs a WARNING and returns 0; audio cues still fail loudly at their own arm, video and DMX load.
… name (869evtdf7)
fix(orphan-kill): an orphan the node cannot kill no longer aborts the load (869evtdf7)
fix(video): only enable videocomposer wraparound for cues that repeat (869fa89uh)
…it tests (869fbyjzx) Seven tests patched time.sleep process-wide and counted every call. Under pytest-xdist, threads left running by other tests in the same worker sleep through that patch too: on PR #25's CI run the shared-budget check saw 6083 s of foreign sleeps against a 1 s ceiling. _this_thread_sleeps() records only the calls made by the test's own thread (foreign calls are still swallowed), with a guard test. Test-only change.
…-noise test(audio): count only the test thread's sleeps in the AudioMixer wait tests (869fbyjzx)
fix(follow): an Auto follow into a cue on another node starts it (869fc8ytz)
fix(status): broadcast cue names after load on the :9190 channel (869fedahu)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges rc_1 into debian/bookworm and releases 0.1.0rc6-3. All engine PRs are merged (none open).
debian/andpyproject.tomlare unchanged by the merge. The dependency stays at cuems-utils >= 0.1.0rc13, as in rc6-2; utils rc16 is merged but not released. The deb builds on casas. Safety tag on the old debian/bookworm:pre-release-0.1.0rc6-3-merge. After the merge: signed tagv0.1.0rc6-3, deb built on casas from the tag, then rc1_packages.