@@ -13,6 +13,8 @@ set -uo pipefail
1313
1414detach_attempts=" ${ASTRBOT_DESKTOP_MACOS_DETACH_ATTEMPTS:- 3} "
1515detach_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
1719case " ${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
3335fi
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+
3553rw_dmg_image_prefix=" ${ASTRBOT_DESKTOP_MACOS_RW_DMG_IMAGE_PREFIX:-/ src-tauri/ target/ } "
3654rw_dmg_image_suffix_regex=" ${ASTRBOT_DESKTOP_MACOS_RW_DMG_IMAGE_SUFFIX_REGEX:-/ bundle/ macos/ rw\\ ..* \\ .dmg$} "
3755rw_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=()
99117declare -a canonical_path_cache_values=()
100118canonicalize_tool=" none"
101119canonicalize_warned_failure=0
120+ lsof_timeout_tool=" "
102121
103122select_canonicalize_tool () {
104123 if command -v realpath > /dev/null 2>&1 ; then
@@ -119,13 +138,61 @@ select_canonicalize_tool() {
119138
120139select_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+
122177detach_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+
264415cleanup_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