Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/acpid: refactor and add initrd support #277898

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
276 changes: 156 additions & 120 deletions nixos/modules/services/hardware/acpid.nix
Expand Up @@ -3,153 +3,189 @@
with lib;

let
cfg = config.services.acpid;

canonicalHandlers = {
powerEvent = {
event = "button/power.*";
action = cfg.powerEventCommands;
};

lidEvent = {
event = "button/lid.*";
action = cfg.lidEventCommands;
};

acEvent = {
event = "ac_adapter.*";
action = cfg.acEventCommands;
};
};

acpiConfDir = pkgs.runCommand "acpi-events" { preferLocalBuild = true; }
''
mkdir -p $out
${
# Generate a configuration file for each event. (You can't have
# multiple events in one config file...)
let f = name: handler:
''
fn=$out/${name}
echo "event=${handler.event}" > $fn
echo "action=${pkgs.writeShellScriptBin "${name}.sh" handler.action }/bin/${name}.sh '%e'" >> $fn
'';
in concatStringsSep "\n" (mapAttrsToList f (canonicalHandlers // cfg.handlers))
}
stage1Cfg = config.boot.initrd.services.acpid;
stage2Cfg = config.services.acpid;
Comment on lines +6 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be confusing as initrd has stage 1 and 2, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so would initrdCfg / mainCfg work? I'm not sure what to name the one under config.services.


# Created as a single derivation for simpler use in initrd
mkAcpiHandlers = handlers: let
mkScript = name: handler: ''
echo ${lib.escapeShellArg handler.action} > "$out/scripts/${name}.sh"
chmod +x "$out/scripts/${name}.sh"
'';

in

{

###### interface
mkHandler = name: handler: ''
echo "event=${handler.event}" > "$out/handlers/${name}"
echo "action=$out/scripts/${name}.sh" >> "$out/handlers/${name}"
outfoxxed marked this conversation as resolved.
Show resolved Hide resolved
'';
in pkgs.runCommand "acpi-events" { preferLocalBuild = true; } ''
outfoxxed marked this conversation as resolved.
Show resolved Hide resolved
mkdir -p $out/scripts
mkdir -p $out/handlers
outfoxxed marked this conversation as resolved.
Show resolved Hide resolved
${concatStringsSep "\n" (mapAttrsToList mkScript handlers)}
${concatStringsSep "\n" (mapAttrsToList mkHandler handlers)}
'';

acpiConfFor = cfg: let
canonicalHandlers = {
powerEvent = {
event = "button/power.*";
action = cfg.powerEventCommands;
};

options = {
lidEvent = {
event = "button/lid.*";
action = cfg.lidEventCommands;
};

services.acpid = {
acEvent = {
event = "ac_adapter.*";
action = cfg.acEventCommands;
};
};
in mkAcpiHandlers ((lib.filterAttrs (_: handler: handler.action != "") canonicalHandlers) // cfg.handlers);

enable = mkEnableOption (lib.mdDoc "the ACPI daemon");
options = stage2: {
enable = mkEnableOption (lib.mdDoc "the ACPI daemon");
package = mkPackageOption pkgs "acpid" {};

logEvents = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc "Log all event activity.";
};
logEvents = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc "Log all event activity";
};

handlers = mkOption {
type = types.attrsOf (types.submodule {
options = {
event = mkOption {
type = types.str;
example = literalExpression ''"button/power.*" "button/lid.*" "ac_adapter.*" "button/mute.*" "button/volumedown.*" "cd/play.*" "cd/next.*"'';
description = lib.mdDoc "Event type.";
};

action = mkOption {
type = types.lines;
description = lib.mdDoc "Shell commands to execute when the event is triggered.";
};
};
});

description = lib.mdDoc ''
Event handlers.

::: {.note}
Handler can be a single command.
:::
'';
default = {};
example = {
ac-power = {
event = "ac_adapter/*";
action = ''
vals=($1) # space separated string to array of multiple values
case ''${vals[3]} in
00000000)
echo unplugged >> /tmp/acpi.log
;;
00000001)
echo plugged in >> /tmp/acpi.log
;;
*)
echo unknown >> /tmp/acpi.log
;;
esac
handlers = mkOption {
type = types.attrsOf (types.submodule {
options = {
event = mkOption {
type = types.str;
description = lib.mdDoc (''
Event type
'' + lib.optionalString (!stage2) ''

::: {.note}
Many events will require kernel modules to be available via `boot.initrd.availableKernelModules`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other useful information we can give or link how to know which ones are required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:::
'');

example = literalExpression ''
"button/power.*" "button/lid.*" "ac_adapter.*" "button/mute.*" "button/volumedown.*" "cd/play.*" "cd/next.*"
'';
};
};
};

powerEventCommands = mkOption {
type = types.lines;
default = "";
description = lib.mdDoc "Shell commands to execute on a button/power.* event.";
};

lidEventCommands = mkOption {
type = types.lines;
default = "";
description = lib.mdDoc "Shell commands to execute on a button/lid.* event.";
action = mkOption {
type = types.lines;
description = "Shell commands to execute when the event is triggered";
};
};
});

description = lib.mdDoc ''
Event handlers

::: {.note}
Handler can be a single command.
:::
'';

default = {};
example = {
ac-power = {
event = "ac_adapter/*";

action = ''
vals=($1) # space separated string to array of multiple values
case ''${vals[3]} in
00000000)
echo unplugged >> /tmp/acpi.log
;;
00000001)
echo plugged in >> /tmp/acpi.log
;;
*)
echo unknown >> /tmp/acpi.log
;;
esac
'';
};
};
};

acEventCommands = mkOption {
type = types.lines;
default = "";
description = lib.mdDoc "Shell commands to execute on an ac_adapter.* event.";
};
powerEventCommands = mkOption {
type = types.lines;
default = "";
description = "Shell commands to execute on a button/power.* event";
};

lidEventCommands = mkOption {
type = types.lines;
default = "";
description = "Shell commands to execute on a button/lid.* event";
};

acEventCommands = mkOption {
type = types.lines;
default = "";
description = "Shell commands to execute on an ac_adapter.* event";
};
};


###### implementation

config = mkIf cfg.enable {

mkConfig = cfg: mkIf cfg.enable {
systemd.services.acpid = {
description = "ACPI Daemon";
documentation = [ "man:acpid(8)" ];

wantedBy = [ "multi-user.target" ];
# Acpid requires /var/run to exist but dosen't create it, and no systemd service seems to create it either.
# Note this is only a problem for stage1.
Comment on lines +138 to +139
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rather patch this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patch what here? Making acpid create or link /var/run seems out of scope imo.

script = ''
ln -s /run /var/run || true

Comment on lines +141 to +142
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ln -s /run /var/run || true

${cfg.package}/bin/acpid \
outfoxxed marked this conversation as resolved.
Show resolved Hide resolved
--foreground \
--netlink \
--confdir "${acpiConfFor cfg}/handlers" \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the trailing \ causing problems when logEvents is not appended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it behaves similarly to running it interactively

> echo hi \
>  \
>  \
>  \
>
hi
>

${lib.optionalString cfg.logEvents "--logevents"}
'';

serviceConfig = {
ExecStart = escapeShellArgs
([ "${pkgs.acpid}/bin/acpid"
"--foreground"
"--netlink"
"--confdir" "${acpiConfDir}"
] ++ optional cfg.logEvents "--logevents"
);
};
unitConfig = {
ConditionVirtualization = "!systemd-nspawn";
ConditionPathExists = [ "/proc/acpi" ];
};

};

};
in {
options = {
services.acpid = options true;
boot.initrd.services.acpid = options false;
};

config = mkMerge [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shoudl avoid mkMerge on config. For me that often caused hard to debug infinite recusions but I am not sure if that was only on me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never experienced this issue and mkMerge assists readability a lot. Would you mind linking relevant issues so I can try to reproduce it or work around it?

(mkConfig stage2Cfg)
{ boot.initrd = mkConfig stage1Cfg; }

(mkIf stage2Cfg.enable {
systemd.services.acpid.wantedBy = [ "multi-user.target" ];
})

(mkIf stage1Cfg.enable {
assertions = [ {
assertion = config.boot.initrd.systemd.enable;
message = "the acpid module only works with systemd based initrd";
} ];

boot.initrd = {
# Add the kernel modules required by each canonical command type if present
availableKernelModules = mkMerge [
(mkIf (stage1Cfg.powerEventCommands != "" || stage1Cfg.lidEventCommands != "") [ "button" ])
(mkIf (stage1Cfg.acEventCommands != "") [ "ac" ])
];

systemd = {
initrdBin = [ stage1Cfg.package ];
storePaths = [ (acpiConfFor stage1Cfg) ];

services.acpid.wantedBy = [ "initrd.target" ];
};
};
})
];
}