diff --git a/modules/launchd/launchd.nix b/modules/launchd/launchd.nix index 9c9b54452..98024c584 100644 --- a/modules/launchd/launchd.nix +++ b/modules/launchd/launchd.nix @@ -25,9 +25,11 @@ { config, lib, ... }: -with lib; +let + inherit (lib) types mkOption; # added by Home Manager -{ + launchdTypes = import ./types.nix { inherit config lib; }; +in { freeformType = with types; attrsOf anything; # added by Home Manager options = { @@ -118,7 +120,7 @@ with lib; }; LimitLoadToSessionType = mkOption { - type = types.nullOr types.str; + type = types.nullOr (types.oneOf [ types.str (types.listOf types.str) ]); default = null; description = '' This configuration file only applies to sessions of the type specified. This key is used in concert @@ -369,60 +371,26 @@ with lib; StartCalendarInterval = mkOption { default = null; - example = { + 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 + This optional key causes the job to be started every calendar interval as specified. The semantics are + much like {manpage}`crontab(5)`: Missing attributes are considered to be wildcard. 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. + one event upon waking from sleep. + + ::: {.important} + The list must not be empty and must not contain duplicate entries (attrsets which compare equally). + ::: + + ::: {.caution} + Since missing attrs become wildcards, an empty attrset effectively means "every minute". + ::: ''; - 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. - ''; - }; - }; - })); + type = types.nullOr launchdTypes.StartCalendarInterval; }; StandardInPath = mkOption { @@ -669,22 +637,22 @@ with lib; 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. + 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. + 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 {manpage}`xpc_transaction_begin(3)` for details. + 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. Interactive - 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. + Interactive + : Interactive jobs run with the same resource limitations as apps, that is to say, none. Interactive + 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. ''; }; @@ -706,6 +674,15 @@ with lib; ''; }; + LowPriorityBackgroundIO = 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 when the process is throttled with the Darwin-background classification. + ''; + }; + LaunchOnlyOnce = mkOption { type = types.nullOr types.bool; default = null; @@ -717,7 +694,7 @@ with lib; MachServices = mkOption { default = null; - example = { ResetAtClose = true; }; + example = { "org.nixos.service" = { 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 @@ -726,31 +703,32 @@ with lib; 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 bootstrap_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. - ''; - }; + type = types.nullOr (types.attrsOf (types.either types.bool + (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 bootstrap_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. - ''; + 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. + ''; + }; }; - }; - }); + }))); }; LaunchEvents = mkOption { @@ -778,6 +756,26 @@ with lib; }; }; + ServiceIPC = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This optional key specifies whether the job participates in advanced + communication with launchd. The default is false. This flag is + incompatible with the inetdCompatibility key. + ''; + }; + + SessionCreate = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + This key specifies that the job should be spawned into a new security + audit session rather than the default session for the context is belongs + to. See auditon(2) for details. + ''; + }; + Sockets = mkOption { default = null; description = '' diff --git a/modules/launchd/types.nix b/modules/launchd/types.nix new file mode 100644 index 000000000..fadf46cb5 --- /dev/null +++ b/modules/launchd/types.nix @@ -0,0 +1,121 @@ +# launchd option type from nix-darwin +# +# Original code from https://github.com/LnL7/nix-darwin/commit/861af0fc94df9454f4e92d6892f75588763164bb + +{ lib, ... }: + +let + inherit (lib) imap1 types mkOption showOption mergeDefinitions; + inherit (builtins) map filter length deepSeq throw toString concatLists; + inherit (lib.options) showDefs; + wildcardText = lib.literalMD "`*`"; + + /* * + A type of list which does not allow duplicate elements. The base/inner + list type to use (e.g. `types.listOf` or `types.nonEmptyListOf`) is passed + via argument `listType`, which must be the final type and not a function. + + NOTE: The extra check for duplicates is quadratic and strict, so use this + type sparingly and only: + + * when needed, and + * when the list is expected to be recursively short (e.g. < 10 elements) + and shallow (i.e. strict evaluation of the list won't take too long) + + The implementation of this function is similar to that of + `types.nonEmptyListOf`. + */ + types'.uniqueList = listType: + listType // { + description = "unique ${ + types.optionDescriptionPhrase (class: class == "noun") listType + }"; + substSubModules = m: types'.uniqueList (listType.substSubModules m); + # This has been taken from the implementation of `types.listOf`, but has + # been modified to throw on duplicates. This check cannot be done in the + # `check` fn as this check is deep/strict, and because `check` runs + # prior to merging. + merge = loc: defs: + let + # Each element of `dupes` is a list. When there are duplicates, + # later lists will be duplicates of earlier lists, so just throw on + # the first set of duplicates found so that we don't have duplicate + # error msgs. + checked = filter (li: + if length li > 1 then + throw '' + The option `${ + showOption loc + }' contains duplicate entries after merging: + ${showDefs li}'' + else + false) dupes; + dupes = + map (def: filter (def': def'.value == def.value) merged) merged; + merged = filter (x: x ? value) (concatLists (imap1 (n: def: + imap1 (m: el: + let + inherit (def) file; + loc' = loc + ++ [ "[definition ${toString n}-entry ${toString m}]" ]; + in (mergeDefinitions loc' listType.nestedTypes.elemType [{ + inherit file; + value = el; + }]).optionalValue // { + inherit loc' file; + }) def.value) defs)); + in deepSeq checked (map (x: x.value) merged); + }; +in { + StartCalendarInterval = let + CalendarIntervalEntry = types.submodule { + options = { + Minute = mkOption { + type = types.nullOr (types.ints.between 0 59); + default = null; + defaultText = wildcardText; + description = '' + The minute on which this job will be run. + ''; + }; + + Hour = mkOption { + type = types.nullOr (types.ints.between 0 23); + default = null; + defaultText = wildcardText; + description = '' + The hour on which this job will be run. + ''; + }; + + Day = mkOption { + type = types.nullOr (types.ints.between 1 31); + default = null; + defaultText = wildcardText; + description = '' + The day on which this job will be run. + ''; + }; + + Weekday = mkOption { + type = types.nullOr (types.ints.between 0 7); + default = null; + defaultText = wildcardText; + description = '' + The weekday on which this job will be run (0 and 7 are Sunday). + ''; + }; + + Month = mkOption { + type = types.nullOr (types.ints.between 1 12); + default = null; + defaultText = wildcardText; + description = '' + The month on which this job will be run. + ''; + }; + }; + }; + in types.either CalendarIntervalEntry + (types'.uniqueList (types.nonEmptyListOf CalendarIntervalEntry)); +}