1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-03-31 04:04:32 +00:00

launchd: refactor setupLaunchAgents

This commit is contained in:
Austin Horstman 2025-03-06 16:04:16 -06:00
parent 86d2e3b005
commit b431496538

View file

@ -93,81 +93,170 @@ in {
# NOTE: Launch Agent configurations can't be symlinked from the Nix store
# because it needs to be owned by the user running it.
home.activation.setupLaunchAgents =
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
setupLaunchAgents() {
local oldDir newDir dstDir domain err
oldDir=""
err=0
if [[ -n "''${oldGenPath:-}" ]]; then
oldDir="$(readlink -m "$oldGenPath/LaunchAgents")" || err=$?
if (( err )); then
oldDir=""
lib.hm.dag.entryAfter [ "writeBoundary" ] # Bash
''
# Disable errexit to ensure we process all agents even if some fail
set +e
# Stop an agent if it's running
bootoutAgent() {
local domain="$1"
local agentName="$2"
verboseEcho "Stopping agent '$domain/$agentName'..."
local bootout_output
bootout_output=$(run /bin/launchctl bootout "$domain/$agentName" 2>&1) || {
# Only show warning if it's not the common "No such process" error
if [[ "$bootout_output" != *"No such process"* ]]; then
warnEcho "Failed to stop agent '$domain/$agentName': $bootout_output"
else
verboseEcho "Agent '$domain/$agentName' was not running"
fi
}
# Give the system a moment to fully unload the agent
sleep 1
}
installAndBootstrapAgent() {
local srcPath="$1"
local dstPath="$2"
local domain="$3"
local agentName="$4"
verboseEcho "Installing agent file to $dstPath"
if ! run install -Dm444 -T "$srcPath" "$dstPath"; then
errorEcho "Failed to install agent file for '$agentName'"
return 1
fi
verboseEcho "Starting agent '$domain/$agentName'"
local bootstrap_output
bootstrap_output=$(run /bin/launchctl bootstrap "$domain" "$dstPath" 2>&1) || {
local error_code=$?
if [[ "$bootstrap_output" == *"Bootstrap failed: 5: Input/output error"* ]]; then
errorEcho "Failed to start agent '$domain/$agentName' with I/O error (code 5)"
errorEcho "This typically happens when the agent wasn't unloaded before attempting to bootstrap the new agent."
else
errorEcho "Failed to start agent '$domain/$agentName' with error: $bootstrap_output"
fi
return 1
}
verboseEcho "Successfully started agent '$domain/$agentName'"
return 0
}
processAgent() {
local srcPath="$1"
local dstDir="$2"
local domain="$3"
local agentFile="''${srcPath##*/}"
local agentName="''${agentFile%.plist}"
local dstPath="$dstDir/$agentFile"
# Skip if unchanged
if cmp -s "$srcPath" "$dstPath"; then
verboseEcho "Agent '$agentName' is already up-to-date"
return 0
fi
verboseEcho "Processing agent '$agentName'"
# Stop/Unload agent if it's already running
if [[ -f "$dstPath" ]]; then
bootoutAgent "$domain" "$agentName"
fi
installAndBootstrapAgent "$srcPath" "$dstPath" "$domain" "$agentName"
# Note: We continue processing even if this agent fails
return 0
}
removeAgent() {
local srcPath="$1"
local dstDir="$2"
local newDir="$3"
local domain="$4"
local agentFile="''${srcPath##*/}"
local agentName="''${agentFile%.plist}"
local dstPath="$dstDir/$agentFile"
if [[ -e "$newDir/$agentFile" ]]; then
verboseEcho "Agent '$agentName' still exists in new generation, skipping cleanup"
return 0
fi
if [[ ! -e "$dstPath" ]]; then
verboseEcho "Agent file '$dstPath' already removed"
return 0
fi
if ! cmp -s "$srcPath" "$dstPath"; then
warnEcho "Skipping deletion of '$dstPath', since its contents have diverged"
return 0
fi
# Stop and remove the agent
bootoutAgent "$domain" "$agentName"
verboseEcho "Removing agent file '$dstPath'"
if run rm -f $VERBOSE_ARG "$dstPath"; then
verboseEcho "Successfully removed agent file for '$agentName'"
else
warnEcho "Failed to remove agent file '$dstPath'"
fi
return 0
}
setupLaunchAgents() {
local oldDir newDir dstDir domain
newDir="$(readlink -m "$newGenPath/LaunchAgents")"
dstDir=${lib.escapeShellArg dstDir}
domain="gui/$UID"
err=0
local srcPath dstPath agentFile agentName i bootout_retries
bootout_retries=10
find -L "$newDir" -maxdepth 1 -name '*.plist' -type f -print0 \
| while IFS= read -rd "" srcPath; do
agentFile="''${srcPath##*/}"
agentName="''${agentFile%.plist}"
dstPath="$dstDir/$agentFile"
if cmp --quiet "$srcPath" "$dstPath"; then
continue
if [[ -n "''${oldGenPath:-}" ]]; then
oldDir="$(readlink -m "$oldGenPath/LaunchAgents")"
if [[ ! -d "$oldDir" ]]; then
verboseEcho "No previous LaunchAgents directory found"
oldDir=""
fi
if [[ -f "$dstPath" ]]; then
for (( i = 0; i < bootout_retries; i++ )); do
run /bin/launchctl bootout "$domain/$agentName" || err=$?
if [[ -v DRY_RUN ]]; then
break
fi
if (( err != 9216 )) &&
! /bin/launchctl print "$domain/$agentName" &> /dev/null; then
break
fi
sleep 1
done
if (( i == bootout_retries )); then
warnEcho "Failed to stop '$domain/$agentName'"
return 1
fi
fi
run install -Dm444 -T "$srcPath" "$dstPath"
run /bin/launchctl bootstrap "$domain" "$dstPath"
else
oldDir=""
fi
verboseEcho "Setting up LaunchAgents in $dstDir"
[[ -d "$dstDir" ]] || run mkdir -p "$dstDir"
verboseEcho "Processing new/updated LaunchAgents..."
find -L "$newDir" -maxdepth 1 -name '*.plist' -type f | while read -r srcPath; do
processAgent "$srcPath" "$dstDir" "$domain"
done
if [[ ! -e "$oldDir" ]]; then
# Skip cleanup if there's no previous generation
if [[ -z "$oldDir" || ! -d "$oldDir" ]]; then
verboseEcho "LaunchAgents setup complete"
return
fi
find -L "$oldDir" -maxdepth 1 -name '*.plist' -type f -print0 \
| while IFS= read -rd "" srcPath; do
agentFile="''${srcPath##*/}"
agentName="''${agentFile%.plist}"
dstPath="$dstDir/$agentFile"
if [[ -e "$newDir/$agentFile" ]]; then
continue
fi
run /bin/launchctl bootout "$domain/$agentName" || :
if [[ ! -e "$dstPath" ]]; then
continue
fi
if ! cmp --quiet "$srcPath" "$dstPath"; then
warnEcho "Skipping deletion of '$dstPath', since its contents have diverged"
continue
fi
run rm -f $VERBOSE_ARG "$dstPath"
verboseEcho "Cleaning up removed LaunchAgents..."
find -L "$oldDir" -maxdepth 1 -name '*.plist' -type f | while read -r srcPath; do
removeAgent "$srcPath" "$dstDir" "$newDir" "$domain"
done
}
setupLaunchAgents
# Restore errexit
if [[ -o errexit ]]; then
set -e
fi
'';
})
];