Skip to content

Commit e8801fa

Browse files
Merge pull request #30 from Factory-AI/ainesh/cli-1171-tctl-close-leak
2 parents 0c10b39 + adff9fa commit e8801fa

1 file changed

Lines changed: 127 additions & 15 deletions

File tree

  • plugins/droid-control/bin

plugins/droid-control/bin/tctl

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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%3N) + grace_ms ))
127+
local alive=1
128+
while (( $(date +%s%3N) <= 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%3N) + grace_ms ))
158+
while kill -0 "$pid" >/dev/null 2>&1 && (( $(date +%s%3N) <= 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+
87196
quote_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

Comments
 (0)