@@ -84,6 +84,115 @@ wait_for_wayland_socket() {
8484 return 1
8585}
8686
87+ collect_tree_pids () {
88+ local root=" $1 "
89+ local pid ppid
90+ local -A children=()
91+ while read -r pid ppid; do
92+ children[" $ppid " ]+=" $pid "
93+ done < <( ps -eo pid=,ppid= 2> /dev/null || true)
94+
95+ local -a result=(" $root " ) queue=(" $root " )
96+ local current
97+ while (( ${# queue[@]} )) ; do
98+ current=" ${queue[0]} "
99+ queue=(" ${queue[@]: 1} " )
100+ for pid in ${children[$current]:- } ; do
101+ result+=(" $pid " )
102+ queue+=(" $pid " )
103+ done
104+ done
105+ printf ' %s\n' " ${result[@]} "
106+ }
107+
108+ # Tear down a true-input compositor and everything under it. Descendant
109+ # enumeration alone misses processes that script(1) moved to a new session;
110+ # a process-group kill alone misses those same processes AND anything
111+ # reparented to init after the leader died. Do both, then escalate.
112+ terminate_true_input_stack () {
113+ local root=" $1 "
114+ local grace_ms=" ${2:- 2000} "
115+ [[ -n " $root " ]] || return 0
116+
117+ local -a targets=()
118+ mapfile -t targets < <( collect_tree_pids " $root " )
119+
120+ local pid
121+ kill -TERM -- " -$root " > /dev/null 2>&1 || true
122+ for pid in " ${targets[@]} " ; do
123+ kill -TERM " $pid " > /dev/null 2>&1 || true
124+ done
125+
126+ local deadline=$(( $(date +% s% 3 N) + grace_ms ))
127+ local alive=1
128+ while (( $(date +% s% 3 N) <= deadline )) ; do
129+ alive=0
130+ kill -0 -- " -$root " > /dev/null 2>&1 && alive=1
131+ if (( ! alive )) ; then
132+ for pid in " ${targets[@]} " ; do
133+ if kill -0 " $pid " > /dev/null 2>&1 ; then
134+ alive=1
135+ break
136+ fi
137+ done
138+ fi
139+ (( alive )) || break
140+ sleep 0.05
141+ done
142+
143+ if (( alive )) ; then
144+ kill -KILL -- " -$root " > /dev/null 2>&1 || true
145+ for pid in " ${targets[@]} " ; do
146+ kill -KILL " $pid " > /dev/null 2>&1 || true
147+ done
148+ fi
149+ }
150+
151+ # SIGINT first so wf-recorder finalizes the container; escalate if it hangs.
152+ terminate_recorder_pid () {
153+ local pid=" $1 "
154+ local grace_ms=" ${2:- 3000} "
155+ [[ -n " $pid " ]] || return 0
156+ kill -INT " $pid " > /dev/null 2>&1 || true
157+ local deadline=$(( $(date +% s% 3 N) + grace_ms ))
158+ while kill -0 " $pid " > /dev/null 2>&1 && (( $(date +% s% 3 N) <= deadline )) ; do
159+ sleep 0.05
160+ done
161+ if kill -0 " $pid " > /dev/null 2>&1 ; then
162+ kill -KILL " $pid " > /dev/null 2>&1 || true
163+ fi
164+ }
165+
166+ pid_is_self_or_ancestor () {
167+ local candidate=" $1 "
168+ local cur=$$ ppid
169+ while [[ -n " $cur " && " $cur " != " 0" && " $cur " != " 1" ]]; do
170+ [[ " $cur " == " $candidate " ]] && return 0
171+ ppid=" $( ps -o ppid= -p " $cur " 2> /dev/null | tr -d ' [:space:]' ) " || return 1
172+ [[ " $ppid " != " $cur " ]] || return 1
173+ cur=" $ppid "
174+ done
175+ return 1
176+ }
177+
178+ # Belt-and-suspenders for sessions whose meta lost the compositor PID (a
179+ # pre-fix launch bug left CAGE_PID empty for every recorded session). Match
180+ # only the session's runner scripts ($dir/run-*.sh) -- matching the bare dir
181+ # path would also hit unrelated processes that merely mention it in argv
182+ # (an inspecting shell, an editor) -- and never kill ourselves or a caller.
183+ terminate_session_strays () {
184+ local session=" $1 "
185+ local dir
186+ dir=" $( session_dir " $session " ) "
187+ [[ -n " $dir " ]] || return 0
188+ local stray
189+ while read -r stray; do
190+ [[ -n " $stray " ]] || continue
191+ pid_is_self_or_ancestor " $stray " && continue
192+ terminate_true_input_stack " $stray " 1000
193+ done < <( pgrep -f -- " $dir /run-" 2> /dev/null || true)
194+ }
195+
87196quote_sh () {
88197 printf ' %q' " $1 "
89198}
@@ -481,7 +590,11 @@ launch_true_input() {
481590 require_cmd cage
482591 require_cmd wtype
483592 require_cmd script
593+ require_cmd setsid
484594 require_cmd " $TERMINAL "
595+ # Fail before the compositor starts, not after: a die inside the
596+ # recording path would strand a live cage.
597+ [[ -z " $record_path " ]] || require_cmd wf-recorder
485598
486599 local dir log_file terminal_cmd runtime_dir socket_path
487600 dir=" $( session_dir " $session " ) "
@@ -519,18 +632,23 @@ launch_true_input() {
519632 WARMED_UP=" 0"
520633 save_session_state " $session "
521634
635+ # setsid: cage leads its own process group, so teardown can group-kill
636+ # the whole stack even after members reparent to init.
522637 XDG_RUNTIME_DIR=" $runtime_dir " \
523638 WLR_BACKENDS=" ${WLR_BACKENDS:- headless} " \
524639 WLR_LIBINPUT_NO_DEVICES=" ${WLR_LIBINPUT_NO_DEVICES:- 1} " \
525- cage -- " ${terminal_cmd[@]} " > /dev/null 2>&1 &
640+ setsid cage -- " ${terminal_cmd[@]} " > /dev/null 2>&1 &
526641 CAGE_PID=" $! "
527- wait_for_wayland_socket " $CAGE_PID " " $socket_path " 5000 \
528- || die " true-input compositor did not create $socket_path "
642+ if ! wait_for_wayland_socket " $CAGE_PID " " $socket_path " 5000; then
643+ terminate_true_input_stack " $CAGE_PID " 1000
644+ die " true-input compositor did not create $socket_path "
645+ fi
646+ # Persist CAGE_PID before anything below calls load_meta: the recording
647+ # path reloads the meta file and would clobber it back to empty.
648+ save_session_state " $session "
529649
530650 if [[ -n " $record_path " ]]; then
531651 start_true_input_recording " $session " " $record_path "
532- else
533- save_session_state " $session "
534652 fi
535653}
536654
@@ -741,8 +859,7 @@ stop_true_input_recording() {
741859 load_meta " $session "
742860 [[ " $BACKEND " == " true-input" ]] || die " tuistory recordings stop when the session exits; use close to finalize the cast"
743861 [[ -n " $RECORDER_PID " ]] || die " no active recorder for session: $session "
744- kill -INT " $RECORDER_PID " > /dev/null 2>&1 || true
745- wait " $RECORDER_PID " 2> /dev/null || true
862+ terminate_recorder_pid " $RECORDER_PID "
746863 RECORDER_PID=" "
747864 save_session_state " $session "
748865}
@@ -1049,14 +1166,9 @@ cmd_close() {
10491166 fi
10501167 (( close_status == 0 )) || die " failed to close tuistory session: $session "
10511168 else
1052- if [[ -n " $RECORDER_PID " ]]; then
1053- kill -INT " $RECORDER_PID " > /dev/null 2>&1 || true
1054- wait " $RECORDER_PID " 2> /dev/null || true
1055- fi
1056- if [[ -n " $CAGE_PID " ]]; then
1057- kill " $CAGE_PID " > /dev/null 2>&1 || true
1058- wait " $CAGE_PID " 2> /dev/null || true
1059- fi
1169+ terminate_recorder_pid " $RECORDER_PID "
1170+ terminate_true_input_stack " $CAGE_PID "
1171+ terminate_session_strays " $session "
10601172 if [[ -n " $RUNTIME_DIR " ]]; then
10611173 rm -rf " $RUNTIME_DIR "
10621174 fi
0 commit comments