From b431496538b0e294fbe44a1441b24ae8195c63f0 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 6 Mar 2025 16:04:16 -0600 Subject: [PATCH] launchd: refactor setupLaunchAgents --- modules/launchd/default.nix | 207 ++++++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 59 deletions(-) diff --git a/modules/launchd/default.nix b/modules/launchd/default.nix index 2361280a0..4932bd847 100644 --- a/modules/launchd/default.nix +++ b/modules/launchd/default.nix @@ -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 ''; }) ];