Skip to content

Commit

Permalink
Merge pull request #716 from tmillr/fix-launchd-calendar-interval
Browse files Browse the repository at this point in the history
fix(launchd): improve `StartCalendarInterval`
  • Loading branch information
emilazy committed Jun 15, 2024
2 parents 801f8ab + 861af0f commit 58b905e
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 56 deletions.
65 changes: 17 additions & 48 deletions modules/launchd/launchd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

with lib;

let
launchdTypes = import ./types.nix { inherit config lib; };
in

{
options = {
Label = mkOption {
Expand Down Expand Up @@ -344,55 +348,21 @@ with lib;
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
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.
'';
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.
'';
};
one event upon waking from sleep.
Weekday = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
The weekday on which this job will be run (0 and 7 are Sunday).
'';
};
::: {.important}
The list must not be empty and must not contain duplicate entries (attrsets which compare equally).
:::
Month = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
The month on which this job will be run.
'';
};
};
}));
::: {.caution}
Since missing attrs become wildcards, an empty attrset effectively means "every minute".
:::
'';
type = types.nullOr launchdTypes.StartCalendarInterval;
};

StandardInPath = mkOption {
Expand Down Expand Up @@ -895,6 +865,5 @@ with lib;
};
};

config = {
};
config = {};
}
110 changes: 110 additions & 0 deletions modules/launchd/types.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{ lib, ... }:

let
inherit (lib) imap1 types mkOption showOption optionDescriptionPhrase 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:\n${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));
}
13 changes: 9 additions & 4 deletions modules/services/nix-gc/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ with lib;

let
cfg = config.nix.gc;
launchdTypes = import ../../launchd/types.nix { inherit config lib; };
in

{
Expand Down Expand Up @@ -35,9 +36,13 @@ in
};

interval = mkOption {
type = types.attrs;
default = { Hour = 3; Minute = 15; };
description = "The time interval at which the garbage collector will run.";
type = launchdTypes.StartCalendarInterval;
default = [{ Weekday = 7; Hour = 3; Minute = 15; }];
description = ''
The calendar interval at which the garbage collector will run.
See the {option}`serviceConfig.StartCalendarInterval` option of
the {option}`launchd` module for more info.
'';
};

options = mkOption {
Expand All @@ -63,7 +68,7 @@ in
command = "${config.nix.package}/bin/nix-collect-garbage ${cfg.options}";
environment.NIX_REMOTE = optionalString config.nix.useDaemon "daemon";
serviceConfig.RunAtLoad = false;
serviceConfig.StartCalendarInterval = [ cfg.interval ];
serviceConfig.StartCalendarInterval = cfg.interval;
serviceConfig.UserName = cfg.user;
};

Expand Down
13 changes: 9 additions & 4 deletions modules/services/nix-optimise/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let
;

cfg = config.nix.optimise;
launchdTypes = import ../../launchd/types.nix { inherit config lib; };
in

{
Expand Down Expand Up @@ -41,9 +42,13 @@ in
};

interval = mkOption {
type = types.attrs;
default = { Hour = 3; Minute = 15; };
description = "The time interval at which the optimiser will run.";
type = launchdTypes.StartCalendarInterval;
default = [{ Weekday = 7; Hour = 4; Minute = 15; }];
description = ''
The calendar interval at which the optimiser will run.
See the {option}`serviceConfig.StartCalendarInterval` option of
the {option}`launchd` module for more info.
'';
};

};
Expand All @@ -63,7 +68,7 @@ in
"/bin/wait4path ${config.nix.package} &amp;&amp; exec ${config.nix.package}/bin/nix-store --optimise"
];
RunAtLoad = false;
StartCalendarInterval = [ cfg.interval ];
StartCalendarInterval = cfg.interval;
UserName = cfg.user;
};
};
Expand Down

0 comments on commit 58b905e

Please sign in to comment.