mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
fix(macos): treat a vanished DMG mount as detached and force-detach by device node (#156702)
This commit is contained in:
+22
-5
@@ -124,10 +124,16 @@ DMG_RW_PATH="$DMG_TEMP/image-rw.dmg"
|
||||
DMG_OUTPUT_TEMP=""
|
||||
DMG_FINAL_PATH=""
|
||||
MOUNTED=0
|
||||
DMG_DEVICE=""
|
||||
|
||||
dmg_attached() {
|
||||
# mount(8) prints resolved paths; the run-private mount suffix still matches.
|
||||
mount | grep -F -e "$MOUNT_POINT" -e "${DMG_DEVICE:-$MOUNT_POINT} on " >/dev/null
|
||||
}
|
||||
|
||||
cleanup_dmg() {
|
||||
if [[ "$MOUNTED" == "1" ]]; then
|
||||
if hdiutil detach "$MOUNT_POINT" -force 2>/dev/null; then
|
||||
if hdiutil detach "${DMG_DEVICE:-$MOUNT_POINT}" -force 2>/dev/null || ! dmg_attached; then
|
||||
MOUNTED=0
|
||||
else
|
||||
echo "WARN: Preserving DMG temp root because mount is still attached: $DMG_TEMP" >&2
|
||||
@@ -145,17 +151,25 @@ detach_dmg() {
|
||||
local delay
|
||||
# Mount writers have exited and the shell never enters MOUNT_POINT.
|
||||
# Flush writes, then give Finder/Spotlight up to 54s to release the volume.
|
||||
# A volume that vanished on its own (diskarbitration ejected it) is detached.
|
||||
sync
|
||||
for delay in 2 3 4 5 6 7 8 9 10; do
|
||||
sleep "$delay"
|
||||
if hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null; then
|
||||
if hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null || ! dmg_attached; then
|
||||
MOUNTED=0
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "WARN: DMG mount still busy; forcing detach: $MOUNT_POINT" >&2
|
||||
hdiutil detach "$MOUNT_POINT" -force || return 1
|
||||
MOUNTED=0
|
||||
for delay in 2 4 8; do
|
||||
hdiutil detach "${DMG_DEVICE:-$MOUNT_POINT}" -force || hdiutil detach "$MOUNT_POINT" -force || true
|
||||
if ! dmg_attached; then
|
||||
MOUNTED=0
|
||||
return
|
||||
fi
|
||||
sleep "$delay"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
mkdir -p "$DMG_SOURCE" "$MOUNT_POINT"
|
||||
@@ -171,8 +185,11 @@ hdiutil create \
|
||||
-format UDRW \
|
||||
"$DMG_RW_PATH"
|
||||
|
||||
hdiutil attach "$DMG_RW_PATH" -mountpoint "$MOUNT_POINT" -nobrowse
|
||||
DMG_ATTACH_OUTPUT="$(hdiutil attach "$DMG_RW_PATH" -mountpoint "$MOUNT_POINT" -nobrowse)"
|
||||
MOUNTED=1
|
||||
echo "$DMG_ATTACH_OUTPUT"
|
||||
# Detach by device node survives the mount path disappearing under hdiutil.
|
||||
DMG_DEVICE="$(awk -v mount="$MOUNT_POINT" 'index($0, mount) && $1 ~ /^\/dev\/disk/ { print $1; exit }' <<<"$DMG_ATTACH_OUTPUT")"
|
||||
|
||||
if [[ "${SKIP_DMG_STYLE:-0}" != "1" ]]; then
|
||||
mkdir -p "$MOUNT_POINT/.background"
|
||||
|
||||
@@ -83,6 +83,8 @@ case "$command_name" in
|
||||
mkdir -p "$mountpoint"
|
||||
printf mounted > "$mountpoint/live-volume-file"
|
||||
fi
|
||||
printf '%s' "$mountpoint" > "\${HDIUTIL_LOG}.attached"
|
||||
printf '/dev/disk99\tGUID_partition_scheme\t\n/dev/disk99s1\tApple_HFS\t%s\n' "$mountpoint"
|
||||
;;
|
||||
detach)
|
||||
if [[ "\${HDIUTIL_DETACH_FAIL:-0}" == "1" ]]; then
|
||||
@@ -95,9 +97,16 @@ case "$command_name" in
|
||||
fi
|
||||
detach_attempts=$((detach_attempts + 1))
|
||||
printf '%s' "$detach_attempts" > "$detach_attempts_file"
|
||||
vanish_at="\${HDIUTIL_DETACH_VANISH_AT:-0}"
|
||||
if (( vanish_at > 0 && detach_attempts >= vanish_at )); then
|
||||
rm -f "\${HDIUTIL_LOG}.attached"
|
||||
echo "hdiutil: detach failed - No such file or directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( detach_attempts <= \${HDIUTIL_DETACH_FAIL_COUNT:-0} )); then
|
||||
exit 9
|
||||
fi
|
||||
rm -f "\${HDIUTIL_LOG}.attached"
|
||||
;;
|
||||
resize)
|
||||
if [[ "\${1:-}" == "-limits" ]]; then
|
||||
@@ -132,6 +141,17 @@ esac
|
||||
writeFileSync(tool, `#!/bin/bash\nprintf '${command} %s\\n' "$*" >> "$HDIUTIL_LOG"\n`, "utf8");
|
||||
chmodSync(tool, 0o755);
|
||||
}
|
||||
const mount = path.join(bin, "mount");
|
||||
writeFileSync(
|
||||
mount,
|
||||
`#!/bin/bash
|
||||
echo '/dev/disk1s1 on / (apfs, sealed, local, read-only, journaled)'
|
||||
[[ -f "$HDIUTIL_LOG.attached" ]] && echo "/dev/disk99s1 on $(cat "$HDIUTIL_LOG.attached") (hfs, local, nobrowse)"
|
||||
exit 0
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
chmodSync(mount, 0o755);
|
||||
return {
|
||||
env: {
|
||||
HDIUTIL_LOG: hdiutilLog,
|
||||
@@ -172,7 +192,10 @@ function expectPrivateDmgMount(log: string): string {
|
||||
log.matchAll(/^detach (.+?)(?: -(?:quiet|force))?$/gm),
|
||||
([, target]) => target,
|
||||
);
|
||||
expect(new Set(detachTargets)).toEqual(new Set([mountPoint]));
|
||||
expect(detachTargets).toContain(mountPoint);
|
||||
expect(detachTargets.filter((target) => target !== "/dev/disk99s1")).toEqual(
|
||||
detachTargets.filter((target) => target === mountPoint),
|
||||
);
|
||||
return mountPoint;
|
||||
}
|
||||
|
||||
@@ -396,15 +419,53 @@ describe.runIf(process.platform === "darwin")("create-dmg ownership boundaries",
|
||||
expect(result.stderr).toContain("Failed to detach DMG mount");
|
||||
expect(result.stderr).toContain("Preserving DMG temp root");
|
||||
const log = readFileSync(tools.hdiutilLog, "utf8");
|
||||
const mountPoint = expectPrivateDmgMount(log);
|
||||
expect(log).not.toContain("resize");
|
||||
expect(log).not.toContain("convert");
|
||||
// Ten packaging attempts plus the EXIT trap's last cleanup attempt.
|
||||
expect(log.match(/^detach /gm)).toHaveLength(11);
|
||||
const mountPoint = expectPrivateDmgMount(log);
|
||||
// Nine polite attempts, three forced rounds by device then path, and the
|
||||
// EXIT trap's last cleanup attempt.
|
||||
expect(log.match(/^detach /gm)).toHaveLength(16);
|
||||
expect(log.match(/^detach \/dev\/disk99s1 -force$/gm)).toHaveLength(4);
|
||||
expect(log).toContain(`detach ${mountPoint} -force`);
|
||||
expect(readFileSync(path.join(mountPoint, "live-volume-file"), "utf8")).toBe("mounted");
|
||||
rmSync(path.dirname(mountPoint), { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ vanishAt: 5, detaches: 5, forced: false },
|
||||
{ vanishAt: 10, detaches: 11, forced: true },
|
||||
])(
|
||||
"treats a busy volume that vanished at detach attempt $vanishAt as detached",
|
||||
({ vanishAt, detaches, forced }) => {
|
||||
const app = makeValidApp();
|
||||
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
||||
tempDirs.push(outputDir);
|
||||
const output = path.join(outputDir, "OpenClaw.dmg");
|
||||
const tools = makeFakeDmgTools();
|
||||
|
||||
const result = runScript([app, output], {
|
||||
...tools.env,
|
||||
HDIUTIL_DETACH_FAIL_COUNT: "9",
|
||||
HDIUTIL_DETACH_VANISH_AT: String(vanishAt),
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(readFileSync(output, "utf8")).toBe("converted");
|
||||
expect(result.stderr).not.toContain("Preserving DMG temp root");
|
||||
const log = readFileSync(tools.hdiutilLog, "utf8");
|
||||
const mountPoint = expectPrivateDmgMount(log);
|
||||
expect(existsSync(path.dirname(mountPoint))).toBe(false);
|
||||
const detachLines = log.split("\n").filter((line) => line.startsWith("detach "));
|
||||
expect(detachLines).toHaveLength(detaches);
|
||||
// The forced round tries the device node first, then the mount path.
|
||||
expect(detachLines.filter((line) => line.endsWith("-force"))).toEqual(
|
||||
forced ? ["detach /dev/disk99s1 -force", `detach ${mountPoint} -force`] : [],
|
||||
);
|
||||
expect(log).toContain("resize");
|
||||
expect(log).toContain("convert ");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([6, 9])("retries %i failed DMG detaches before finalizing the artifact", (failures) => {
|
||||
const app = makeValidApp();
|
||||
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
||||
|
||||
Reference in New Issue
Block a user