Skip to content

Commit 6a4e189

Browse files
author
ly
committed
Supervise self-update status cleanup
1 parent d957a5d commit 6a4e189

2 files changed

Lines changed: 134 additions & 1 deletion

File tree

manage.sh

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ TASK_FAILURES=0
3636
DOCTOR_STATUS="healthy"
3737
DOCTOR_WARNING_OPTIONAL_TOOLS=0
3838
DOCTOR_WARNING_UNMATERIALIZED=0
39+
TEMP_ARTIFACTS=()
3940
ORIGINAL_ARGS=("$@")
4041
CMD_ARGS=()
4142
cmd=""
@@ -163,6 +164,12 @@ function cleanup_on_exit() {
163164
remove_lock_artifacts
164165
fi
165166
[ -n "${CONFIG_FILE:-}" ] && [[ "$CONFIG_FILE" != "$BASE_CONFIG" ]] && rm -f "$CONFIG_FILE"
167+
local artifact
168+
if [ "${#TEMP_ARTIFACTS[@]}" -gt 0 ]; then
169+
for artifact in "${TEMP_ARTIFACTS[@]}"; do
170+
rm -f "$artifact" 2>/dev/null || true
171+
done
172+
fi
166173

167174
local running_jobs=$(jobs -p)
168175
if [ -n "$running_jobs" ]; then
@@ -320,6 +327,56 @@ function run_supervised_command_in_dir() {
320327
wait "$pid"
321328
}
322329

330+
function register_temp_artifact() {
331+
TEMP_ARTIFACTS+=("$1")
332+
}
333+
334+
function unregister_temp_artifact() {
335+
local target="$1"
336+
local remaining=()
337+
local artifact
338+
for artifact in "${TEMP_ARTIFACTS[@]}"; do
339+
if [ "$artifact" != "$target" ]; then
340+
remaining+=("$artifact")
341+
fi
342+
done
343+
if [ "${#remaining[@]}" -gt 0 ]; then
344+
TEMP_ARTIFACTS=("${remaining[@]}")
345+
else
346+
TEMP_ARTIFACTS=()
347+
fi
348+
}
349+
350+
function run_supervised_capture_command_in_dir() {
351+
local result_var="$1"
352+
local timeout_seconds="$2"
353+
local workdir="$3"
354+
shift 3
355+
356+
local command_str
357+
local output_file
358+
local pid
359+
local exit_code
360+
local captured=""
361+
command_str="$(shell_join "$@")"
362+
output_file="$(mktemp "${TMPDIR:-/tmp}/agent-manager-capture.XXXXXX")" || return 1
363+
register_temp_artifact "$output_file"
364+
365+
run_supervised_task "$timeout_seconds" "$workdir" "$command_str" > "$output_file" &
366+
pid=$!
367+
wait "$pid"
368+
exit_code=$?
369+
370+
if [ -f "$output_file" ]; then
371+
captured="$(cat "$output_file")"
372+
fi
373+
unregister_temp_artifact "$output_file"
374+
rm -f "$output_file"
375+
376+
printf -v "$result_var" '%s' "$captured"
377+
return "$exit_code"
378+
}
379+
323380
function run_supervised_interactive_command_in_dir() {
324381
local timeout_seconds="$1"
325382
local workdir="$2"
@@ -935,7 +992,7 @@ function run_self_update() {
935992
echo "当前目录不是 Git 仓库,跳过自更新。"
936993
return 0
937994
fi
938-
if ! repo_status=$(git -C "$ROOT_DIR" status --porcelain); then
995+
if ! run_supervised_capture_command_in_dir repo_status 600 "$ROOT_DIR" git -C "$ROOT_DIR" status --porcelain; then
939996
echo "无法检查仓库状态,跳过自更新。"
940997
return 1
941998
fi

tests/test_manage.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,82 @@ def test_self_update_only_runs_repo_pull(self):
15591559
self.assertIn(f"git -C {temp_root.resolve()} status --porcelain", log_lines[0])
15601560
self.assertIn(f"git -C {temp_root.resolve()} pull --ff-only", log_lines[1])
15611561

1562+
def test_self_update_status_termination_cleans_up_descendant_processes(self):
1563+
with tempfile.TemporaryDirectory() as temp_dir:
1564+
temp_root = Path(temp_dir) / "repo"
1565+
shutil.copytree(ROOT, temp_root, ignore=shutil.ignore_patterns('repositories'))
1566+
1567+
child_pid_file = Path(temp_dir) / "self-update-status-child.pid"
1568+
ready_file = Path(temp_dir) / "self-update-status-ready"
1569+
fake_bin = temp_root / "fake-bin"
1570+
fake_bin.mkdir()
1571+
1572+
self._write_stub(
1573+
fake_bin / "git",
1574+
(
1575+
"#!/usr/bin/env bash\n"
1576+
"if [ \"$1\" = \"-C\" ]; then\n"
1577+
" shift 2\n"
1578+
"fi\n"
1579+
"if [ \"$1\" = \"status\" ] && [ \"$2\" = \"--porcelain\" ]; then\n"
1580+
" python3 - <<'PY'\n"
1581+
"import os\n"
1582+
"import signal\n"
1583+
"import subprocess\n"
1584+
"import sys\n"
1585+
"import time\n"
1586+
"from pathlib import Path\n"
1587+
"child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)'])\n"
1588+
"Path(os.environ['CHILD_PID_FILE']).write_text(str(child.pid), encoding='utf-8')\n"
1589+
"Path(os.environ['READY_FILE']).write_text('ready\\n', encoding='utf-8')\n"
1590+
"signal.signal(signal.SIGTERM, lambda *_args: sys.exit(143))\n"
1591+
"while True:\n"
1592+
" time.sleep(1)\n"
1593+
"PY\n"
1594+
"fi\n"
1595+
"echo \"unexpected git args: $*\" >&2\n"
1596+
"exit 2\n"
1597+
),
1598+
)
1599+
1600+
env = self._managed_env()
1601+
env["PATH"] = f"{fake_bin}:{env['PATH']}"
1602+
env["CHILD_PID_FILE"] = str(child_pid_file)
1603+
env["READY_FILE"] = str(ready_file)
1604+
1605+
proc = subprocess.Popen(
1606+
[str(temp_root / "manage.sh"), "self-update"],
1607+
cwd=temp_root,
1608+
env=env,
1609+
stdout=subprocess.PIPE,
1610+
stderr=subprocess.PIPE,
1611+
text=True,
1612+
)
1613+
1614+
child_pid = None
1615+
try:
1616+
child_pid = self._await_probe_child_pid(
1617+
proc,
1618+
child_pid_file,
1619+
ready_file,
1620+
"self-update status 未按预期启动子进程",
1621+
)
1622+
self._assert_descendant_exits_after_parent_termination(
1623+
proc,
1624+
child_pid,
1625+
f"self-update status 退出后子进程仍存活: {child_pid}",
1626+
)
1627+
finally:
1628+
if proc.poll() is None:
1629+
proc.kill()
1630+
proc.wait(timeout=5)
1631+
self._close_process_pipes(proc)
1632+
if child_pid is not None:
1633+
try:
1634+
os.kill(child_pid, signal.SIGKILL)
1635+
except OSError:
1636+
pass
1637+
15621638
def test_update_pipeline_pulls_secrets_with_ff_only(self):
15631639
with tempfile.TemporaryDirectory() as temp_dir:
15641640
temp_root = Path(temp_dir) / "repo"

0 commit comments

Comments
 (0)