Skip to content

Commit 8e3b8dc

Browse files
authored
Merge pull request #25 from zouyonghe/main
ci harden dmg cleanup with pre-detach sleep and holder kill
2 parents 4487d40 + 64b7358 commit 8e3b8dc

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

.github/workflows/build-desktop-tauri.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ jobs:
272272
ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }}
273273
ASTRBOT_DESKTOP_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }}
274274
ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS: ${{ vars.ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS || '' }}
275+
ASTRBOT_DESKTOP_MACOS_DETACH_PRE_SLEEP_SECONDS: '8'
276+
ASTRBOT_DESKTOP_MACOS_DETACH_ATTEMPTS: '6'
277+
ASTRBOT_DESKTOP_MACOS_DETACH_SLEEP_SECONDS: '3'
278+
ASTRBOT_DESKTOP_MACOS_LSOF_TIMEOUT_SECONDS: '10'
279+
ASTRBOT_DESKTOP_MACOS_ALLOW_GLOBAL_HELPER_KILL: '1'
275280
GITHUB_TOKEN: ${{ github.token }}
276281
GH_TOKEN: ${{ github.token }}
277282
shell: bash

scripts/ci/cleanup-dmg.sh

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ set -uo pipefail
1313

1414
detach_attempts="${ASTRBOT_DESKTOP_MACOS_DETACH_ATTEMPTS:-3}"
1515
detach_sleep_seconds="${ASTRBOT_DESKTOP_MACOS_DETACH_SLEEP_SECONDS:-2}"
16+
detach_pre_sleep_seconds="${ASTRBOT_DESKTOP_MACOS_DETACH_PRE_SLEEP_SECONDS:-8}"
17+
lsof_timeout_seconds="${ASTRBOT_DESKTOP_MACOS_LSOF_TIMEOUT_SECONDS:-10}"
1618

1719
case "${detach_attempts}" in
1820
''|*[!0-9]*) detach_attempts=3 ;;
@@ -32,6 +34,22 @@ elif [ "${detach_sleep_seconds}" -gt 60 ] 2>/dev/null; then
3234
detach_sleep_seconds=60
3335
fi
3436

37+
case "${detach_pre_sleep_seconds}" in
38+
''|*[!0-9]*) detach_pre_sleep_seconds=8 ;;
39+
esac
40+
if [ "${detach_pre_sleep_seconds}" -gt 120 ] 2>/dev/null; then
41+
detach_pre_sleep_seconds=120
42+
fi
43+
44+
case "${lsof_timeout_seconds}" in
45+
''|*[!0-9]*) lsof_timeout_seconds=10 ;;
46+
esac
47+
if [ "${lsof_timeout_seconds}" -lt 1 ] 2>/dev/null; then
48+
lsof_timeout_seconds=1
49+
elif [ "${lsof_timeout_seconds}" -gt 120 ] 2>/dev/null; then
50+
lsof_timeout_seconds=120
51+
fi
52+
3553
rw_dmg_image_prefix="${ASTRBOT_DESKTOP_MACOS_RW_DMG_IMAGE_PREFIX:-/src-tauri/target/}"
3654
rw_dmg_image_suffix_regex="${ASTRBOT_DESKTOP_MACOS_RW_DMG_IMAGE_SUFFIX_REGEX:-/bundle/macos/rw\\..*\\.dmg$}"
3755
rw_dmg_mountpoint_regex="${ASTRBOT_DESKTOP_MACOS_RW_DMG_MOUNT_REGEX:-^/Volumes/(dmg\\.|rw\\.|dmg-|rw-).*}"
@@ -99,6 +117,7 @@ declare -a canonical_path_cache_keys=()
99117
declare -a canonical_path_cache_values=()
100118
canonicalize_tool="none"
101119
canonicalize_warned_failure=0
120+
lsof_timeout_tool=""
102121

103122
select_canonicalize_tool() {
104123
if command -v realpath >/dev/null 2>&1; then
@@ -119,13 +138,61 @@ select_canonicalize_tool() {
119138

120139
select_canonicalize_tool
121140

141+
select_lsof_timeout_tool() {
142+
if command -v gtimeout >/dev/null 2>&1; then
143+
lsof_timeout_tool="gtimeout"
144+
return
145+
fi
146+
if command -v timeout >/dev/null 2>&1; then
147+
lsof_timeout_tool="timeout"
148+
return
149+
fi
150+
if command -v python3 >/dev/null 2>&1; then
151+
lsof_timeout_tool="python3"
152+
return
153+
fi
154+
lsof_timeout_tool=""
155+
}
156+
157+
select_lsof_timeout_tool
158+
159+
resolve_disk_identifier() {
160+
local target="$1"
161+
if [[ "${target}" =~ ^/dev/disk[0-9]+$ ]]; then
162+
printf '%s\n' "${target}"
163+
return 0
164+
fi
165+
if ! command -v diskutil >/dev/null 2>&1; then
166+
return 0
167+
fi
168+
local disk_name=""
169+
disk_name="$(
170+
diskutil info "${target}" 2>/dev/null | awk -F': *' '/Part of Whole/ {print $2; exit}'
171+
)"
172+
if [[ "${disk_name}" =~ ^disk[0-9]+$ ]]; then
173+
printf '/dev/%s\n' "${disk_name}"
174+
fi
175+
}
176+
122177
detach_target() {
123178
local target="$1"
124179
local pass=1
180+
if [ "${detach_pre_sleep_seconds}" -gt 0 ]; then
181+
echo "Sleeping ${detach_pre_sleep_seconds}s before detaching ${target}" >&2
182+
sleep "${detach_pre_sleep_seconds}"
183+
fi
125184
while [ "${pass}" -le "${detach_attempts}" ]; do
126185
if hdiutil detach "${target}" >/dev/null 2>&1; then
127186
return 0
128187
fi
188+
if command -v diskutil >/dev/null 2>&1; then
189+
local disk_target=""
190+
disk_target="$(resolve_disk_identifier "${target}")"
191+
if [[ "${disk_target}" =~ ^/dev/disk[0-9]+$ ]]; then
192+
diskutil unmountDisk force "${disk_target}" >/dev/null 2>&1 || true
193+
fi
194+
diskutil unmount force "${target}" >/dev/null 2>&1 || true
195+
fi
129196
hdiutil detach -force "${target}" >/dev/null 2>&1 || true
130197
sleep "${detach_sleep_seconds}"
131198
pass=$((pass + 1))
@@ -184,6 +251,9 @@ log_cleanup_configuration() {
184251
echo " canonicalize_tool=${canonicalize_tool}" >&2
185252
echo " detach_attempts=${detach_attempts}" >&2
186253
echo " detach_sleep_seconds=${detach_sleep_seconds}" >&2
254+
echo " detach_pre_sleep_seconds=${detach_pre_sleep_seconds}" >&2
255+
echo " lsof_timeout_seconds=${lsof_timeout_seconds}" >&2
256+
echo " lsof_timeout_tool=${lsof_timeout_tool:-none}" >&2
187257
echo " rw_dmg_image_prefix=${rw_dmg_image_prefix}" >&2
188258
echo " rw_dmg_image_suffix_regex=${rw_dmg_image_suffix_regex}" >&2
189259
echo " rw_dmg_mountpoint_regex=${rw_dmg_mountpoint_regex}" >&2
@@ -261,6 +331,87 @@ terminate_pid_soft_then_hard() {
261331
fi
262332
}
263333

334+
kill_mount_holders() {
335+
local mount_point="$1"
336+
if [ "${allow_global_helper_cleanup}" != "1" ]; then
337+
echo "Skip mount-holder cleanup for ${mount_point} (set ASTRBOT_DESKTOP_MACOS_ALLOW_GLOBAL_HELPER_KILL=1 to enable)." >&2
338+
return 0
339+
fi
340+
if ! command -v lsof >/dev/null 2>&1; then
341+
return 0
342+
fi
343+
local holder_pids
344+
if [ "${lsof_timeout_tool}" = "gtimeout" ] || [ "${lsof_timeout_tool}" = "timeout" ]; then
345+
local lsof_output=""
346+
local lsof_status=0
347+
lsof_output="$("${lsof_timeout_tool}" "${lsof_timeout_seconds}" lsof -t +D "${mount_point}" 2>/dev/null)" || lsof_status=$?
348+
if [ "${lsof_status}" -eq 124 ]; then
349+
echo "WARN: lsof timed out while scanning ${mount_point}; skip mount-holder cleanup." >&2
350+
return 0
351+
fi
352+
if [ "${lsof_status}" -ne 0 ] && [ -z "${lsof_output}" ]; then
353+
echo "WARN: lsof failed while scanning ${mount_point} (tool=${lsof_timeout_tool}, exit=${lsof_status}); skip mount-holder cleanup." >&2
354+
return 0
355+
fi
356+
holder_pids="$(printf '%s\n' "${lsof_output}" | awk 'NF' | sort -u)"
357+
elif [ "${lsof_timeout_tool}" = "python3" ]; then
358+
local lsof_output=""
359+
local lsof_status=0
360+
lsof_output="$(
361+
python3 - "${mount_point}" "${lsof_timeout_seconds}" <<'PY'
362+
import subprocess
363+
import sys
364+
365+
mount_point = sys.argv[1]
366+
timeout_seconds = float(sys.argv[2])
367+
368+
try:
369+
proc = subprocess.run(
370+
["lsof", "-t", "+D", mount_point],
371+
capture_output=True,
372+
text=True,
373+
timeout=timeout_seconds,
374+
check=False,
375+
)
376+
except subprocess.TimeoutExpired:
377+
sys.exit(124)
378+
379+
if proc.stdout:
380+
sys.stdout.write(proc.stdout)
381+
382+
sys.exit(proc.returncode)
383+
PY
384+
)" || lsof_status=$?
385+
if [ "${lsof_status}" -eq 124 ]; then
386+
echo "WARN: lsof timed out while scanning ${mount_point}; skip mount-holder cleanup." >&2
387+
return 0
388+
fi
389+
if [ "${lsof_status}" -ne 0 ] && [ -z "${lsof_output}" ]; then
390+
echo "WARN: lsof failed while scanning ${mount_point} (tool=python3, exit=${lsof_status}); skip mount-holder cleanup." >&2
391+
return 0
392+
fi
393+
holder_pids="$(printf '%s\n' "${lsof_output}" | awk 'NF' | sort -u)"
394+
else
395+
holder_pids="$(lsof -t +D "${mount_point}" 2>/dev/null | awk 'NF' | sort -u || true)"
396+
fi
397+
if [ -z "${holder_pids}" ]; then
398+
return 0
399+
fi
400+
401+
while IFS= read -r pid; do
402+
[ -z "${pid}" ] && continue
403+
[ "${pid}" = "$$" ] && continue
404+
local proc_name=""
405+
proc_name="$(ps -p "${pid}" -o comm= 2>/dev/null | awk 'NF{print; exit}' || true)"
406+
if [ -n "${proc_name}" ]; then
407+
echo "Killing mount-holder pid=${pid} (${proc_name}) for ${mount_point}" >&2
408+
else
409+
echo "Killing mount-holder pid=${pid} for ${mount_point}" >&2
410+
fi
411+
terminate_pid_soft_then_hard "${pid}"
412+
done <<< "${holder_pids}"
413+
}
414+
264415
cleanup_stale_dmg_state() {
265416
local dmg_mounts
266417
dmg_mounts="$(mount | awk -F ' on | \\(' -v mount_regex="${rw_dmg_mountpoint_regex}" '
@@ -270,6 +421,7 @@ cleanup_stale_dmg_state() {
270421
while IFS= read -r mount_point; do
271422
[ -z "${mount_point}" ] && continue
272423
echo "Detaching stale mount ${mount_point}"
424+
kill_mount_holders "${mount_point}"
273425
detach_target "${mount_point}" || true
274426
done <<< "${dmg_mounts}"
275427
fi

0 commit comments

Comments
 (0)