Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions install-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,11 @@ fi
# no matter what grep says, so the check condemned a working binary.
#
# BOUNDED, because "does not run" and "does not finish" are different failures
# and only one of them used to be handled. A bundle can block in dyld before
# reaching main — Gatekeeper assessment on a bundle in /Applications does
# exactly this (issue #25) — and an unbounded check then hangs the installer
# forever with no output, no error, and no service. A verification step that
# can wedge is worse than no verification step.
# and only one of them used to be handled. A bundle carrying
# com.apple.quarantine blocks in dyld before reaching main (issue #25), and an
# unbounded check then hangs the installer forever with no output, no error,
# and no service. A verification step that can wedge is worse than no
# verification step.
# run_bounded is defined above the source guard so the tests can reach it.

# --help, NOT a bare invocation. `tacet` with no arguments now runs the agent
Expand All @@ -544,10 +544,31 @@ verify_rc=$?
set -e

if [[ "$verify_rc" -eq 124 ]]; then
# The headline goes through err(); the rest does not. err() prefixes every
# line with ERROR:, and fifteen of those is a wall nobody reads at the moment
# they are already stuck.
err "installed ${APP_DST} but it did not finish starting within 20s."
err "The process blocks before reaching main — nothing it logs will say so."
err "A bundle in /Applications does this under Gatekeeper assessment (#25);"
err "installing to ~/Applications (no cask, no TACET_APP_DIR) is known-good."
cat >&2 <<EOF

The process blocks before reaching main, so nothing it logs will say why.

Almost always this is com.apple.quarantine on the bundle (issue #25). Nothing
launches tacet through LaunchServices — launchd starts it and this script execs
it — so the first-launch consent gate has nobody to answer it and the process
waits forever. Notarization does not help; the gate is consent, not assessment.

xattr -p com.apple.quarantine ${APP_DST}

Removing the attribute does NOT recover this path. The first blocked launch
wedges the path itself, and neither deleting the xattr nor replacing the bundle
clears it — only a path that has never wedged:

rm -rf ${APP_DST}
TACET_APP_DIR=~/Applications ./install-server.sh

The Homebrew cask strips quarantine from 0.1.2 on, so a current
'brew install --cask drycodeworks/tap/tacet' should not reach this message.
EOF
exit 1
fi
if [[ "$usage_out" != *"usage: tacet"* ]]; then
Expand Down
19 changes: 17 additions & 2 deletions tests/test_install_server_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ def test_the_usage_text_documents_the_env_overrides(self, tmp_path):
class TestVerificationIsBounded:
"""The installed binary is executed to prove it works. That must not hang.

A bundle can block in dyld before reaching main — Gatekeeper assessment on
a bundle in /Applications does exactly that (#25). Unbounded, the installer
A bundle carrying com.apple.quarantine blocks in dyld before reaching main
(#25) — the consent gate has no UI to answer it. Unbounded, the installer
waits forever: no output, no error, no service, nothing in any log.
"""

Expand All @@ -78,6 +78,21 @@ def test_a_timeout_is_reported_differently_from_a_broken_binary(self):
code = SCRIPT.read_text()
assert 'verify_rc" -eq 124' in code, "the timeout status must be handled"

def test_the_timeout_message_names_quarantine_and_the_real_recovery(self):
# This message is read by someone already stuck, so being wrong here is
# expensive. It used to blame /Applications and call ~/Applications
# "known-good", which is backwards: a quarantined bundle hangs in
# ~/Applications too, and an unquarantined one runs fine in
# /Applications. The variable is the xattr, not the directory.
code = SCRIPT.read_text()
timeout_branch = code[code.index('verify_rc" -eq 124'):code.index("Shared secret")]
assert "com.apple.quarantine" in timeout_branch, \
"the message must name the actual cause"
assert "TACET_APP_DIR" in timeout_branch, \
"the message must give a recovery that works"
assert "known-good" not in timeout_branch, \
"the disproved /Applications-vs-~/Applications advice is back"

def test_the_fallback_exists_because_macos_ships_no_timeout(self):
# A stock macOS has neither timeout(1) nor gtimeout. Relying on
# coreutils would leave the check silently unbounded on exactly the
Expand Down
Loading