-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
launchd: initial support for LaunchAgents #2497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| { config, lib, pkgs, ... }: | ||
|
|
||
| with lib; | ||
|
|
||
| let | ||
| inherit (pkgs.stdenv.hostPlatform) isDarwin; | ||
| inherit (lib.generators) toPlist; | ||
|
|
||
| cfg = config.launchd; | ||
| labelPrefix = "org.nix-community.home."; | ||
| dstDir = "${config.home.homeDirectory}/Library/LaunchAgents"; | ||
|
|
||
| launchdConfig = { config, name, ... }: { | ||
| options = { | ||
| enable = mkEnableOption name; | ||
| config = mkOption { | ||
| type = types.submodule (import ./launchd.nix); | ||
| default = { }; | ||
| example = literalExpression '' | ||
| { | ||
| ProgramArguments = [ "/usr/bin/say" "Good afternoon" ]; | ||
| StartCalendarInterval = { | ||
| Hour = 12; | ||
| Minute = 0; | ||
| }; | ||
| } | ||
| ''; | ||
| description = '' | ||
| Define a launchd job. See <citerefentry> | ||
| <refentrytitle>launchd.plist</refentrytitle><manvolnum>5</manvolnum> | ||
| </citerefentry> for details. | ||
| ''; | ||
| }; | ||
| }; | ||
|
|
||
| config = { config.Label = mkDefault "${labelPrefix}${name}"; }; | ||
| }; | ||
|
|
||
| toAgent = config: pkgs.writeText "${config.Label}.plist" (toPlist { } config); | ||
|
|
||
| agentPlists = | ||
| mapAttrs' (n: v: nameValuePair "${v.config.Label}.plist" (toAgent v.config)) | ||
| (filterAttrs (n: v: v.enable) cfg.agents); | ||
|
|
||
| agentsDrv = pkgs.runCommand "home-manager-agents" { | ||
| srcs = attrValues agentPlists; | ||
| dsts = attrNames agentPlists; | ||
| } '' | ||
| mkdir -p "$out" | ||
|
|
||
| if [[ -n "$srcs" ]]; then | ||
| for (( i=0; i < "''${#srcs[@]}"; i+=1 )); do | ||
| src="''${srcs[i]}" | ||
| dst="''${dsts[i]}" | ||
| ln -s "$src" "$out/$dst" | ||
| done | ||
| fi | ||
| ''; | ||
| in { | ||
| meta.maintainers = with maintainers; [ midchildan ]; | ||
|
|
||
| options.launchd = { | ||
| enable = mkOption { | ||
| type = types.bool; | ||
| default = isDarwin; | ||
| defaultText = literalExpression "pkgs.stdenv.hostPlatform.isDarwin"; | ||
| description = '' | ||
| Whether to enable Home Manager to define per-user daemons by making use | ||
| of launchd's LaunchAgents. | ||
| ''; | ||
| }; | ||
|
|
||
| agents = mkOption { | ||
| type = with types; attrsOf (submodule launchdConfig); | ||
| default = { }; | ||
| description = "Define LaunchAgents."; | ||
| }; | ||
| }; | ||
|
|
||
| config = mkMerge [ | ||
| { | ||
| assertions = [{ | ||
| assertion = (cfg.enable && agentPlists != { }) -> isDarwin; | ||
| message = let names = lib.concatStringsSep ", " (attrNames agentPlists); | ||
| in "Must use Darwin for modules that require Launchd: " + names; | ||
| }]; | ||
| } | ||
|
|
||
| (mkIf isDarwin { | ||
| home.extraBuilderCommands = '' | ||
| ln -s "${agentsDrv}" $out/LaunchAgents | ||
| ''; | ||
|
|
||
| home.activation.checkLaunchAgents = | ||
| hm.dag.entryBefore [ "writeBoundary" ] '' | ||
| checkLaunchAgents() { | ||
| local oldDir newDir dstDir err | ||
| oldDir="" | ||
| if [[ -n "''${oldGenPath:-}" ]]; then | ||
| oldDir="$(readlink -m "$oldGenPath/LaunchAgents")" || err=$? | ||
| if (( err )); then | ||
| oldDir="" | ||
| fi | ||
| fi | ||
| newDir=${escapeShellArg agentsDrv} | ||
| dstDir=${escapeShellArg dstDir} | ||
|
|
||
| local oldSrcPath newSrcPath dstPath agentFile agentName | ||
|
|
||
| find -L "$newDir" -maxdepth 1 -name '*.plist' -type f -print0 \ | ||
| | while IFS= read -rd "" newSrcPath; do | ||
| agentFile="''${newSrcPath##*/}" | ||
| agentName="''${agentFile%.plist}" | ||
| dstPath="$dstDir/$agentFile" | ||
| oldSrcPath="$oldDir/$agentFile" | ||
|
|
||
| if [[ ! -e "$dstPath" ]]; then | ||
| continue | ||
| fi | ||
|
|
||
| if ! cmp --quiet "$oldSrcPath" "$dstPath"; then | ||
| errorEcho "Existing file '$dstPath' is in the way of '$newSrcPath'" | ||
| exit 1 | ||
| fi | ||
| done | ||
| } | ||
|
|
||
| checkLaunchAgents | ||
| ''; | ||
|
|
||
| # NOTE: Launch Agent configurations can't be symlinked from the Nix store | ||
| # because it needs to be owned by the user running it. | ||
|
Comment on lines
+131
to
+132
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LaunchAgent configuration files are copied into the home directory because Launchd refuses to work with files not owned by the user running it. For safety reasons, the activation script would check if any changes are made to configurations files previously deployed by Home Manager. |
||
| home.activation.setupLaunchAgents = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The activation script takes care of starting/restarting/stopping the agents automatically. |
||
| hm.dag.entryAfter [ "writeBoundary" ] '' | ||
| setupLaunchAgents() { | ||
| local oldDir newDir dstDir domain err | ||
| oldDir="" | ||
| if [[ -n "''${oldGenPath:-}" ]]; then | ||
| oldDir="$(readlink -m "$oldGenPath/LaunchAgents")" || err=$? | ||
| if (( err )); then | ||
| oldDir="" | ||
| fi | ||
| fi | ||
| newDir="$(readlink -m "$newGenPath/LaunchAgents")" | ||
| dstDir=${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 | ||
| fi | ||
| if [[ -f "$dstPath" ]]; then | ||
| for (( i = 0; i < bootout_retries; i++ )); do | ||
| $DRY_RUN_CMD launchctl bootout "$domain/$agentName" || err=$? | ||
| if [[ -v DRY_RUN ]]; then | ||
| break | ||
| fi | ||
| if (( err != 9216 )) && | ||
| ! 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 | ||
| $DRY_RUN_CMD install -Dm444 -T "$srcPath" "$dstPath" | ||
| $DRY_RUN_CMD launchctl bootstrap "$domain" "$dstPath" | ||
| done | ||
|
|
||
| if [[ ! -e "$oldDir" ]]; then | ||
| 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 | ||
|
|
||
| $DRY_RUN_CMD 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" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I like the "diverged" wording 👍 |
||
| continue | ||
| fi | ||
| $DRY_RUN_CMD rm -f $VERBOSE_ARG "$dstPath" | ||
| done | ||
| } | ||
|
|
||
| setupLaunchAgents | ||
| ''; | ||
| }) | ||
| ]; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.