From c7a13f76a78bb5c225ca5e08e9a109347d130792 Mon Sep 17 00:00:00 2001 From: midchildan Date: Mon, 22 Nov 2021 01:13:30 +0900 Subject: [PATCH] launchd: initial support for LaunchAgents --- .github/CODEOWNERS | 2 + docs/release-notes/rl-2205.adoc | 3 + format | 1 + modules/launchd/default.nix | 211 +++++ modules/launchd/launchd.nix | 869 +++++++++++++++++++++ modules/misc/news.nix | 10 + modules/modules.nix | 1 + tests/default.nix | 1 + tests/modules/launchd/agents.nix | 25 + tests/modules/launchd/default.nix | 1 + tests/modules/launchd/expected-agent.plist | 23 + 11 files changed, 1147 insertions(+) create mode 100644 modules/launchd/default.nix create mode 100644 modules/launchd/launchd.nix create mode 100644 tests/modules/launchd/agents.nix create mode 100644 tests/modules/launchd/default.nix create mode 100644 tests/modules/launchd/expected-agent.plist diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b8d201937..b3e45d6f4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,6 +7,8 @@ /modules/i18n/input-method @Kranzes /tests/modules/i18n/input-method @Kranzes +/modules/launchd @midchildan + /modules/misc/dconf.nix @rycee /modules/misc/fontconfig.nix @rycee diff --git a/docs/release-notes/rl-2205.adoc b/docs/release-notes/rl-2205.adoc index ee4fc1fdf..48482ddf5 100644 --- a/docs/release-notes/rl-2205.adoc +++ b/docs/release-notes/rl-2205.adoc @@ -21,6 +21,9 @@ such as the `home-manager` command line tool and the activation script. If you would like to contribute to the translation effort then you can do so through the {hm-weblate}[Home Manager Weblate project]. +* A new module, `launchd.agents` was added. +Use this to enable services based on macOS LaunchAgents. + [[sec-release-22.05-state-version-changes]] === State Version Changes diff --git a/format b/format index 0fa620c1a..41bbdee40 100755 --- a/format +++ b/format @@ -19,6 +19,7 @@ find . -name '*.nix' \ ! -path ./modules/default.nix \ ! -path ./modules/files.nix \ ! -path ./modules/home-environment.nix \ + ! -path ./modules/launchd/launchd.nix \ ! -path ./modules/lib/default.nix \ ! -path ./modules/lib/file-type.nix \ ! -path ./modules/manual.nix \ diff --git a/modules/launchd/default.nix b/modules/launchd/default.nix new file mode 100644 index 000000000..375231d09 --- /dev/null +++ b/modules/launchd/default.nix @@ -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 + launchd.plist5 + 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. + home.activation.setupLaunchAgents = + 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" + continue + fi + $DRY_RUN_CMD rm -f $VERBOSE_ARG "$dstPath" + done + } + + setupLaunchAgents + ''; + }) + ]; +} diff --git a/modules/launchd/launchd.nix b/modules/launchd/launchd.nix new file mode 100644 index 000000000..e25c005b2 --- /dev/null +++ b/modules/launchd/launchd.nix @@ -0,0 +1,869 @@ +# launchd option type from nix-darwin +# +# Original Source: +# https://github.com/LnL7/nix-darwin/blob/a34dea2/modules/launchd/launchd.nix + +# Copyright 2017 Daiderd Jordan +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +{ config, lib, ... }: + +with lib; + +{ + options = { + Label = mkOption { + type = types.str; + description = "This required key uniquely identifies the job to launchd."; + }; + + Disabled = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key is used as a hint to launchctl(1) that it should not submit this job to launchd when + loading a job or jobs. The value of this key does NOT reflect the current state of the job on the run-ning running + ning system. If you wish to know whether a job is loaded in launchd, reading this key from a configura-tion configuration + tion file yourself is not a sufficient test. You should query launchd for the presence of the job using + the launchctl(1) list subcommand or use the ServiceManagement framework's + SMJobCopyDictionary() method. + + Note that as of Mac OS X v10.6, this key's value in a configuration file conveys a default value, which + is changed with the [-w] option of the launchctl(1) load and unload subcommands. These subcommands no + longer modify the configuration file, so the value displayed in the configuration file is not necessar-ily necessarily + ily the value that launchctl(1) will apply. See launchctl(1) for more information. + + Please also be mindful that you should only use this key if the provided on-demand and KeepAlive crite-ria criteria + ria are insufficient to describe the conditions under which your job needs to run. The cost to have a + job loaded in launchd is negligible, so there is no harm in loading a job which only runs once or very + rarely. + ''; + }; + + UserName = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key specifies the user to run the job as. This key is only applicable when launchd is + running as root. + ''; + }; + + GroupName = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key specifies the group to run the job as. This key is only applicable when launchd is + running as root. If UserName is set and GroupName is not, the the group will be set to the default + group of the user. + ''; + }; + + inetdCompatibility = mkOption { + default = null; + example = { Wait = true; }; + description = '' + The presence of this key specifies that the daemon expects to be run as if it were launched from inetd. + ''; + type = types.nullOr (types.submodule { + options = { + Wait = mkOption { + type = types.nullOr (types.either types.bool types.str); + default = null; + description = '' + This flag corresponds to the "wait" or "nowait" option of inetd. If true, then the listening + socket is passed via the standard in/out/error file descriptors. If false, then accept(2) is + called on behalf of the job, and the result is passed via the standard in/out/error descriptors. + ''; + }; + }; + }); + }; + + LimitLoadToHosts = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + This configuration file only applies to the hosts listed with this key. Note: One should set kern.host-name kern.hostname + name in sysctl.conf(5) for this feature to work reliably. + ''; + }; + + LimitLoadFromHosts = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + This configuration file only applies to hosts NOT listed with this key. Note: One should set kern.host-name kern.hostname + name in sysctl.conf(5) for this feature to work reliably. + ''; + }; + + LimitLoadToSessionType = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This configuration file only applies to sessions of the type specified. This key is used in concert + with the -S flag to launchctl. + ''; + }; + + Program = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + This key maps to the first argument of execvp(3). If this key is missing, then the first element of + the array of strings provided to the ProgramArguments will be used instead. This key is required in + the absence of the ProgramArguments key. + ''; + }; + + ProgramArguments = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + This key maps to the second argument of execvp(3). This key is required in the absence of the Program + key. Please note: many people are confused by this key. Please read execvp(3) very carefully! + ''; + }; + + EnableGlobbing = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This flag causes launchd to use the glob(3) mechanism to update the program arguments before invoca-tion. invocation. + tion. + ''; + }; + + EnableTransactions = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This flag instructs launchd that the job promises to use vproc_transaction_begin(3) and + vproc_transaction_end(3) to track outstanding transactions that need to be reconciled before the + process can safely terminate. If no outstanding transactions are in progress, then launchd is free to + send the SIGKILL signal. + ''; + }; + + OnDemand = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This key was used in Mac OS X 10.4 to control whether a job was kept alive or not. The default was + true. This key has been deprecated and replaced in Mac OS X 10.5 and later with the more powerful + KeepAlive option. + ''; + }; + + KeepAlive = mkOption { + type = types.nullOr (types.either types.bool (types.submodule { + options = { + + SuccessfulExit = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + If true, the job will be restarted as long as the program exits and with an exit status of zero. + If false, the job will be restarted in the inverse condition. This key implies that "RunAtLoad" + is set to true, since the job needs to run at least once before we can get an exit status. + ''; + }; + + NetworkState = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + If true, the job will be kept alive as long as the network is up, where up is defined as at least + one non-loopback interface being up and having IPv4 or IPv6 addresses assigned to them. If + false, the job will be kept alive in the inverse condition. + ''; + }; + + PathState = mkOption { + type = types.nullOr (types.attrsOf types.bool); + default = null; + description = '' + Each key in this dictionary is a file-system path. If the value of the key is true, then the job + will be kept alive as long as the path exists. If false, the job will be kept alive in the + inverse condition. The intent of this feature is that two or more jobs may create semaphores in + the file-system namespace. + ''; + }; + + OtherJobEnabled = mkOption { + type = types.nullOr (types.attrsOf types.bool); + default = null; + description = '' + Each key in this dictionary is the label of another job. If the value of the key is true, then + this job is kept alive as long as that other job is enabled. Otherwise, if the value is false, + then this job is kept alive as long as the other job is disabled. This feature should not be + considered a substitute for the use of IPC. + ''; + }; + + # NOTE: this was missing in the original source at the time of writing + Crashed = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + If true, the the job will be restarted as long as it exited due to a signal which is typically + associated with a crash (SIGILL, SIGSEGV, etc.). If false, the job will be restarted in the inverse + condition. + ''; + }; + + }; + })); + default = null; + description = '' + This optional key is used to control whether your job is to be kept continuously running or to let + demand and conditions control the invocation. The default is false and therefore only demand will start + the job. The value may be set to true to unconditionally keep the job alive. Alternatively, a dictio-nary dictionary + nary of conditions may be specified to selectively control whether launchd keeps a job alive or not. If + multiple keys are provided, launchd ORs them, thus providing maximum flexibility to the job to refine + the logic and stall if necessary. If launchd finds no reason to restart the job, it falls back on + demand based invocation. Jobs that exit quickly and frequently when configured to be kept alive will + be throttled to converve system resources. + ''; + }; + + RunAtLoad = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key is used to control whether your job is launched once at the time the job is loaded. + The default is false. + ''; + }; + + RootDirectory = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key is used to specify a directory to chroot(2) to before running the job. + ''; + }; + + WorkingDirectory = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key is used to specify a directory to chdir(2) to before running the job. + ''; + }; + + EnvironmentVariables = mkOption { + type = types.nullOr (types.attrsOf types.str); + default = null; + description = '' + This optional key is used to specify additional environment variables to be set before running the + job. + ''; + }; + + Umask = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + This optional key specifies what value should be passed to umask(2) before running the job. Known bug: + Property lists don't support octal, so please convert the value to decimal. + ''; + }; + + TimeOut = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The recommended idle time out (in seconds) to pass to the job. If no value is specified, a default time + out will be supplied by launchd for use by the job at check in time. + ''; + }; + + ExitTimeOut = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The amount of time launchd waits before sending a SIGKILL signal. The default value is 20 seconds. The + value zero is interpreted as infinity. + ''; + }; + + ThrottleInterval = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + This key lets one override the default throttling policy imposed on jobs by launchd. The value is in + seconds, and by default, jobs will not be spawned more than once every 10 seconds. The principle + behind this is that jobs should linger around just in case they are needed again in the near future. + This not only reduces the latency of responses, but it encourages developers to amortize the cost of + program invocation. + ''; + }; + + InitGroups = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies whether initgroups(3) should be called before running the job. The default + is true in 10.5 and false in 10.4. This key will be ignored if the UserName key is not set. + ''; + }; + + WatchPaths = mkOption { + type = types.nullOr (types.listOf types.path); + default = null; + description = '' + This optional key causes the job to be started if any one of the listed paths are modified. + ''; + }; + + QueueDirectories = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + Much like the WatchPaths option, this key will watch the paths for modifications. The difference being + that the job will only be started if the path is a directory and the directory is not empty. + ''; + }; + + StartOnMount = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key causes the job to be started every time a filesystem is mounted. + ''; + }; + + StartInterval = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + This optional key causes the job to be started every N seconds. If the system is asleep, the job will + be started the next time the computer wakes up. If multiple intervals transpire before the computer is + woken, those events will be coalesced into one event upon wake from sleep. + ''; + }; + + StartCalendarInterval = mkOption { + default = null; + example = { Hour = 2; Minute = 30; }; + description = '' + This optional key causes the job to be started every calendar interval as specified. Missing arguments + are considered to be wildcard. The semantics are much like crontab(5). Unlike cron which skips job + invocations when the computer is asleep, launchd will start the job the next time the computer wakes + up. If multiple intervals transpire before the computer is woken, those events will be coalesced into + one event upon wake from sleep. + ''; + type = types.nullOr (types.listOf (types.submodule { + options = { + Minute = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The minute on which this job will be run. + ''; + }; + + Hour = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The hour on which this job will be run. + ''; + }; + + Day = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The day on which this job will be run. + ''; + }; + + Weekday = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The weekday on which this job will be run (0 and 7 are Sunday). + ''; + }; + + Month = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The month on which this job will be run. + ''; + }; + }; + })); + }; + + StandardInPath = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + This optional key specifies what file should be used for data being supplied to stdin when using + stdio(3). + ''; + }; + + StandardOutPath = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + This optional key specifies what file should be used for data being sent to stdout when using stdio(3). + ''; + }; + + StandardErrorPath = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + This optional key specifies what file should be used for data being sent to stderr when using stdio(3). + ''; + }; + + Debug = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies that launchd should adjust its log mask temporarily to LOG_DEBUG while + dealing with this job. + ''; + }; + + WaitForDebugger = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies that launchd should instruct the kernel to have the job wait for a debugger + to attach before any code in the job is executed. + ''; + }; + + SoftResourceLimits = mkOption { + default = null; + description = '' + Resource limits to be imposed on the job. These adjust variables set with setrlimit(2). The following + keys apply: + ''; + type = types.nullOr (types.submodule { + options = { + Core = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The largest size (in bytes) core file that may be created. + ''; + }; + + CPU = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum amount of cpu time (in seconds) to be used by each process. + ''; + }; + + Data = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) of the data segment for a process; this defines how far a program may + extend its break with the sbrk(2) system call. + ''; + }; + + FileSize = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The largest size (in bytes) file that may be created. + ''; + }; + + MemoryLock = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) which a process may lock into memory using the mlock(2) function. + ''; + }; + + NumberOfFiles = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum number of open files for this process. Setting this value in a system wide daemon + will set the sysctl(3) kern.maxfiles (SoftResourceLimits) or kern.maxfilesperproc (HardResource-Limits) (HardResourceLimits) + Limits) value in addition to the setrlimit(2) values. + ''; + }; + + NumberOfProcesses = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum number of simultaneous processes for this user id. Setting this value in a system + wide daemon will set the sysctl(3) kern.maxproc (SoftResourceLimits) or kern.maxprocperuid + (HardResourceLimits) value in addition to the setrlimit(2) values. + ''; + }; + + ResidentSetSize = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) to which a process's resident set size may grow. This imposes a + limit on the amount of physical memory to be given to a process; if memory is tight, the system + will prefer to take memory from processes that are exceeding their declared resident set size. + ''; + }; + + Stack = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) of the stack segment for a process; this defines how far a program's + stack segment may be extended. Stack extension is performed automatically by the system. + ''; + }; + }; + }); + }; + + HardResourceLimits = mkOption { + default = null; + example = { NumberOfFiles = 4096; }; + description = '' + Resource limits to be imposed on the job. These adjust variables set with setrlimit(2). The following + keys apply: + ''; + type = types.nullOr (types.submodule { + options = { + Core = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The largest size (in bytes) core file that may be created. + ''; + }; + + CPU = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum amount of cpu time (in seconds) to be used by each process. + ''; + }; + + Data = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) of the data segment for a process; this defines how far a program may + extend its break with the sbrk(2) system call. + ''; + }; + + FileSize = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The largest size (in bytes) file that may be created. + ''; + }; + + MemoryLock = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) which a process may lock into memory using the mlock(2) function. + ''; + }; + + NumberOfFiles = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum number of open files for this process. Setting this value in a system wide daemon + will set the sysctl(3) kern.maxfiles (SoftResourceLimits) or kern.maxfilesperproc (HardResource-Limits) (HardResourceLimits) + Limits) value in addition to the setrlimit(2) values. + ''; + }; + + NumberOfProcesses = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum number of simultaneous processes for this user id. Setting this value in a system + wide daemon will set the sysctl(3) kern.maxproc (SoftResourceLimits) or kern.maxprocperuid + (HardResourceLimits) value in addition to the setrlimit(2) values. + ''; + }; + + ResidentSetSize = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) to which a process's resident set size may grow. This imposes a + limit on the amount of physical memory to be given to a process; if memory is tight, the system + will prefer to take memory from processes that are exceeding their declared resident set size. + ''; + }; + + Stack = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + The maximum size (in bytes) of the stack segment for a process; this defines how far a program's + stack segment may be extended. Stack extension is performed automatically by the system. + ''; + }; + }; + }); + }; + + Nice = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + This optional key specifies what nice(3) value should be applied to the daemon. + ''; + }; + + ProcessType = mkOption { + type = types.nullOr (types.enum [ "Background" "Standard" "Adaptive" "Interactive" ]); + default = null; + example = "Background"; + description = '' + This optional key describes, at a high level, the intended purpose of the job. The system will apply + resource limits based on what kind of job it is. If left unspecified, the system will apply light + resource limits to the job, throttling its CPU usage and I/O bandwidth. The following are valid values: + + Background + Background jobs are generally processes that do work that was not directly requested by the user. + The resource limits applied to Background jobs are intended to prevent them from disrupting the + user experience. + + Standard + Standard jobs are equivalent to no ProcessType being set. + + Adaptive + Adaptive jobs move between the Background and Interactive classifications based on activity over + XPC connections. See xpc_transaction_begin(3) for details. + + Interactive + Interactive jobs run with the same resource limitations as apps, that is to say, none. Interac-tive Interactive + tive jobs are critical to maintaining a responsive user experience, and this key should only be + used if an app's ability to be responsive depends on it, and cannot be made Adaptive. + ''; + }; + + AbandonProcessGroup = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + When a job dies, launchd kills any remaining processes with the same process group ID as the job. Set-ting Setting + ting this key to true disables that behavior. + ''; + }; + + LowPriorityIO = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies whether the kernel should consider this daemon to be low priority when + doing file system I/O. + ''; + }; + + LaunchOnlyOnce = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies whether the job can only be run once and only once. In other words, if the + job cannot be safely respawned without a full machine reboot, then set this key to be true. + ''; + }; + + MachServices = mkOption { + default = null; + example = { ResetAtClose = true; }; + description = '' + This optional key is used to specify Mach services to be registered with the Mach bootstrap sub-system. + Each key in this dictionary should be the name of service to be advertised. The value of the key must + be a boolean and set to true. Alternatively, a dictionary can be used instead of a simple true value. + + Finally, for the job itself, the values will be replaced with Mach ports at the time of check-in with + launchd. + ''; + type = types.nullOr (types.submodule { + options = { + ResetAtClose = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + If this boolean is false, the port is recycled, thus leaving clients to remain oblivious to the + demand nature of job. If the value is set to true, clients receive port death notifications when + the job lets go of the receive right. The port will be recreated atomically with respect to boot-strap_look_up() bootstrap_look_up() + strap_look_up() calls, so that clients can trust that after receiving a port death notification, + the new port will have already been recreated. Setting the value to true should be done with + care. Not all clients may be able to handle this behavior. The default value is false. + ''; + }; + + HideUntilCheckIn = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Reserve the name in the namespace, but cause bootstrap_look_up() to fail until the job has + checked in with launchd. + ''; + }; + }; + }); + }; + + Sockets = mkOption { + default = null; + description = '' + This optional key is used to specify launch on demand sockets that can be used to let launchd know when + to run the job. The job must check-in to get a copy of the file descriptors using APIs outlined in + launch(3). The keys of the top level Sockets dictionary can be anything. They are meant for the appli-cation application + cation developer to use to differentiate which descriptors correspond to which application level proto-cols protocols + cols (e.g. http vs. ftp vs. DNS...). At check-in time, the value of each Sockets dictionary key will + be an array of descriptors. Daemon/Agent writers should consider all descriptors of a given key to be + to be effectively equivalent, even though each file descriptor likely represents a different networking + protocol which conforms to the criteria specified in the job configuration file. + + The parameters below are used as inputs to call getaddrinfo(3). + ''; + type = types.nullOr (types.attrsOf (types.submodule { + options = { + SockType = mkOption { + type = types.nullOr (types.enum [ "stream" "dgram" "seqpacket" ]); + default = null; + description = '' + This optional key tells launchctl what type of socket to create. The default is "stream" and + other valid values for this key are "dgram" and "seqpacket" respectively. + ''; + }; + + SockPassive = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies whether listen(2) or connect(2) should be called on the created file + descriptor. The default is true ("to listen"). + ''; + }; + + SockNodeName = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key specifies the node to connect(2) or bind(2) to. + ''; + }; + + SockServiceName = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key specifies the service on the node to connect(2) or bind(2) to. + ''; + }; + + SockFamily = mkOption { + type = types.nullOr (types.enum [ "IPv4" "IPv6" ]); + default = null; + description = '' + This optional key can be used to specifically request that "IPv4" or "IPv6" socket(s) be created. + ''; + }; + + SockProtocol = mkOption { + type = types.nullOr (types.enum [ "TCP" ]); + default = null; + description = '' + This optional key specifies the protocol to be passed to socket(2). The only value understood by + this key at the moment is "TCP". + ''; + }; + + SockPathName = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + This optional key implies SockFamily is set to "Unix". It specifies the path to connect(2) or + bind(2) to. + ''; + }; + + SecureSocketWithKey = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key is a variant of SockPathName. Instead of binding to a known path, a securely + generated socket is created and the path is assigned to the environment variable that is inher-ited inherited + ited by all jobs spawned by launchd. + ''; + }; + + SockPathMode = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + This optional key specifies the mode of the socket. Known bug: Property lists don't support + octal, so please convert the value to decimal. + ''; + }; + + Bonjour = mkOption { + type = types.nullOr (types.either types.bool (types.listOf types.str)); + default = null; + description = '' + This optional key can be used to request that the service be registered with the + mDNSResponder(8). If the value is boolean, the service name is inferred from the SockService-Name. SockServiceName. + Name. + ''; + }; + + MulticastGroup = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + This optional key can be used to request that the datagram socket join a multicast group. If the + value is a hostname, then getaddrinfo(3) will be used to join the correct multicast address for a + given socket family. If an explicit IPv4 or IPv6 address is given, it is required that the Sock-Family SockFamily + Family family also be set, otherwise the results are undefined. + ''; + }; + }; + })); + }; + }; + + config = { + }; +} diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 71087ed22..a3005e4a9 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -2432,6 +2432,16 @@ in A new module is available: 'programs.gitui'. ''; } + + { + time = "2022-02-26T09:28:57+00:00"; + condition = hostPlatform.isDarwin; + message = '' + A new module is available: 'launchd.agents' + + Use this to enable services based on macOS LaunchAgents. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index 1df984c2c..a43c9e3d5 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -19,6 +19,7 @@ let ./files.nix ./home-environment.nix ./i18n/input-method/default.nix + ./launchd/default.nix ./manual.nix ./misc/dconf.nix ./misc/debug.nix diff --git a/tests/default.nix b/tests/default.nix index 14c7773c5..346217565 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -104,6 +104,7 @@ import nmt { ./modules/programs/zsh ./modules/xresources ] ++ lib.optionals isDarwin [ + ./modules/launchd ./modules/targets-darwin ] ++ lib.optionals isLinux [ ./modules/config/i18n diff --git a/tests/modules/launchd/agents.nix b/tests/modules/launchd/agents.nix new file mode 100644 index 000000000..cbcff6b5a --- /dev/null +++ b/tests/modules/launchd/agents.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = { + launchd.agents."test-service" = { + enable = true; + config = { + ProgramArguments = [ "/some/command" "--with-arguments" "foo" ]; + KeepAlive = { + Crashed = true; + SuccessfulExit = false; + }; + ProcessType = "Background"; + }; + }; + + nmt.script = '' + serviceFile=LaunchAgents/org.nix-community.home.test-service.plist + assertFileExists $serviceFile + assertFileContent $serviceFile ${./expected-agent.plist} + ''; + }; +} diff --git a/tests/modules/launchd/default.nix b/tests/modules/launchd/default.nix new file mode 100644 index 000000000..09549ee7b --- /dev/null +++ b/tests/modules/launchd/default.nix @@ -0,0 +1 @@ +{ launchd-agents = ./agents.nix; } diff --git a/tests/modules/launchd/expected-agent.plist b/tests/modules/launchd/expected-agent.plist new file mode 100644 index 000000000..c56ad4da4 --- /dev/null +++ b/tests/modules/launchd/expected-agent.plist @@ -0,0 +1,23 @@ + + + + + KeepAlive + + Crashed + + SuccessfulExit + + + Label + org.nix-community.home.test-service + ProcessType + Background + ProgramArguments + + /some/command + --with-arguments + foo + + + \ No newline at end of file