emacs: Support socket activation via systemd

Add 'services.emacs.socketActivation.enable' for generating an
'emacs.socket' systemd unit.

Emacs since version 26 has supported socket activation, whereby an
external process manager such as systemd listens on a socket and passes
it to the Emacs daemon when the manager launches it. This improves
startup time of the user session and avoids launching the daemon when not
needed, for example when launching the user session via SSH.

This implementation hard-codes the socket path to the default for the
version of 'programs.emacs.finalPackage', because systemd does not
perform shell expansion in the socket unit's 'ListenStream' parameter
and it seems like an advanced use-case to change the socket path. Shell
expansion would be desirable as the socket path usually resides in
directories such as $XDG_RUNTIME_DIR or $TMPDIR.

Tests were added to verify behavior in the following cases:

- Emacs service with socket activation disabled
- Emacs 26 with socket activation enabled
- Emacs 27 with socket activation enabled

PR #1314
This commit is contained in:
Tad Fisher 2020-06-08 14:01:17 -07:00 committed by Robert Helgesson
parent 02c1f8d416
commit 3815248786
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89
13 changed files with 276 additions and 29 deletions

View file

@ -1542,6 +1542,16 @@ in
programs.ssh.matchBlocks.<name>.serverAliveCountMax
'';
}
{
time = "2020-06-11T18:06:37+00:00";
condition = config.services.emacs.enable;
message = ''
The Emacs service now supports systemd socket activation.
It can be enabled through the option 'services.emacs.socketActivation.enable'.
'';
}
];
};
}

View file

@ -7,6 +7,8 @@ let
cfg = config.services.emacs;
emacsCfg = config.programs.emacs;
emacsBinPath = "${emacsCfg.finalPackage}/bin";
emacsVersion = getVersion emacsCfg.finalPackage;
# Adapted from upstream emacs.desktop
clientDesktopItem = pkgs.makeDesktopItem rec {
name = "emacsclient";
@ -27,9 +29,26 @@ let
'';
};
# Match the default socket path for the Emacs version so emacsclient continues
# to work without wrapping it. It might be worthwhile to allow customizing the
# socket path, but we would want to wrap emacsclient in the user profile to
# connect to the alternative socket by default for Emacs 26, and set
# EMACS_SOCKET_NAME for Emacs 27.
#
# As systemd doesn't perform variable expansion for the ListenStream param, we
# would also have to solve the problem of matching the shell path to the path
# used in the socket unit, which would likely involve templating. It seems of
# little value for the most common use case of one Emacs daemon per user
# session.
socketPath = if versionAtLeast emacsVersion "27" then
"%t/emacs/server"
else
"%T/emacs%U/server";
in {
options.services.emacs = {
enable = mkEnableOption "the Emacs daemon";
client = {
enable = mkEnableOption "generation of Emacs client desktop file";
arguments = mkOption {
@ -40,14 +59,29 @@ in {
'';
};
};
# Attrset for forward-compatibility; there may be a need to customize the
# socket path, though allowing for such is not easy to do as systemd socket
# units don't perform variable expansion for 'ListenStream'.
socketActivation = {
enable = mkEnableOption "systemd socket activation for the Emacs service";
};
};
config = mkIf cfg.enable {
assertions = [{
config = mkIf cfg.enable (mkMerge [
{
assertions = [
{
assertion = emacsCfg.enable;
message = "The Emacs service module requires"
+ " 'programs.emacs.enable = true'.";
}];
}
{
assertion = cfg.socketActivation.enable
-> versionAtLeast emacsVersion "26";
message = "Socket activation requires Emacs 26 or newer.";
}
];
systemd.user.services.emacs = {
Unit = {
@ -61,15 +95,42 @@ in {
};
Service = {
ExecStart =
"${pkgs.runtimeShell} -l -c 'exec ${emacsBinPath}/emacs --fg-daemon'";
ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs)'";
ExecStart = "${emacsBinPath}/emacs --fg-daemon${
# In case the user sets 'server-directory' or 'server-name' in
# their Emacs config, we want to specify the socket path explicitly
# so launching 'emacs.service' manually doesn't break emacsclient
# when using socket activation.
optionalString cfg.socketActivation.enable ''="${socketPath}"''
}";
# We use '(kill-emacs 0)' to avoid exiting with a failure code, which
# would restart the service immediately.
ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs 0)'";
Restart = "on-failure";
};
} // optionalAttrs (!cfg.socketActivation.enable) {
Install = { WantedBy = [ "default.target" ]; };
};
home.packages = optional cfg.client.enable clientDesktopItem;
};
}
(mkIf cfg.socketActivation.enable {
systemd.user.sockets.emacs = {
Unit = {
Description = "Emacs: the extensible, self-documenting text editor";
Documentation =
"info:emacs man:emacs(1) https://gnu.org/software/emacs/";
};
Socket = {
ListenStream = socketPath;
FileDescriptorName = "server";
SocketMode = "0600";
DirectoryMode = "0700";
};
Install = { WantedBy = [ "sockets.target" ]; };
};
})
]);
}

View file

@ -67,6 +67,7 @@ import nmt {
./modules/misc/xsession
./modules/programs/abook
./modules/programs/autorandr
./modules/services/emacs
./modules/programs/firefox
./modules/programs/getmail
./modules/services/lieer

View file

@ -0,0 +1,5 @@
{
emacs-service = ./emacs-service.nix;
emacs-socket-26 = ./emacs-socket-26.nix;
emacs-socket-27 = ./emacs-socket-27.nix;
}

View file

@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Exec=@emacs@/bin/emacsclient -c %F
Terminal=false
Name=Emacs Client
Icon=emacs
Comment=Edit text
GenericName=Text Editor
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Categories=Utility;TextEditor;
StartupWMClass=Emacs

View file

@ -0,0 +1,12 @@
[Install]
WantedBy=default.target
[Service]
ExecStart=@emacs@/bin/emacs --fg-daemon
ExecStop=@emacs@/bin/emacsclient --eval '(kill-emacs 0)'
Restart=on-failure
[Unit]
Description=Emacs: the extensible, self-documenting text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
X-RestartIfChanged=false

View file

@ -0,0 +1,32 @@
{ config, lib, pkgs, ... }:
with lib;
{
config = {
nixpkgs.overlays = [
(self: super: rec {
emacs = pkgs.writeShellScriptBin "dummy-emacs" "" // {
outPath = "@emacs@";
};
emacsPackagesFor = _:
makeScope super.newScope (_: { emacsWithPackages = _: emacs; });
})
];
programs.emacs.enable = true;
services.emacs.enable = true;
services.emacs.client.enable = true;
nmt.script = ''
assertPathNotExists home-files/.config/systemd/user/emacs.socket
assertFileExists home-files/.config/systemd/user/emacs.service
assertFileExists home-path/share/applications/emacsclient.desktop
assertFileContent home-files/.config/systemd/user/emacs.service \
${./emacs-service-emacs.service}
assertFileContent home-path/share/applications/emacsclient.desktop \
${./emacs-emacsclient.desktop}
'';
};
}

View file

@ -0,0 +1,9 @@
[Service]
ExecStart=@emacs@/bin/emacs --fg-daemon="%T/emacs%U/server"
ExecStop=@emacs@/bin/emacsclient --eval '(kill-emacs 0)'
Restart=on-failure
[Unit]
Description=Emacs: the extensible, self-documenting text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
X-RestartIfChanged=false

View file

@ -0,0 +1,12 @@
[Install]
WantedBy=sockets.target
[Socket]
DirectoryMode=0700
FileDescriptorName=server
ListenStream=%T/emacs%U/server
SocketMode=0600
[Unit]
Description=Emacs: the extensible, self-documenting text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

View file

@ -0,0 +1,35 @@
{ config, lib, pkgs, ... }:
with lib;
{
config = {
nixpkgs.overlays = [
(self: super: rec {
emacs = pkgs.writeShellScriptBin "dummy-emacs-26.3" "" // {
outPath = "@emacs@";
};
emacsPackagesFor = _:
makeScope super.newScope (_: { emacsWithPackages = _: emacs; });
})
];
programs.emacs.enable = true;
services.emacs.enable = true;
services.emacs.client.enable = true;
services.emacs.socketActivation.enable = true;
nmt.script = ''
assertFileExists home-files/.config/systemd/user/emacs.socket
assertFileExists home-files/.config/systemd/user/emacs.service
assertFileExists home-path/share/applications/emacsclient.desktop
assertFileContent home-files/.config/systemd/user/emacs.socket \
${./emacs-socket-26-emacs.socket}
assertFileContent home-files/.config/systemd/user/emacs.service \
${./emacs-socket-26-emacs.service}
assertFileContent home-path/share/applications/emacsclient.desktop \
${./emacs-emacsclient.desktop}
'';
};
}

View file

@ -0,0 +1,9 @@
[Service]
ExecStart=@emacs@/bin/emacs --fg-daemon="%t/emacs/server"
ExecStop=@emacs@/bin/emacsclient --eval '(kill-emacs 0)'
Restart=on-failure
[Unit]
Description=Emacs: the extensible, self-documenting text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
X-RestartIfChanged=false

View file

@ -0,0 +1,12 @@
[Install]
WantedBy=sockets.target
[Socket]
DirectoryMode=0700
FileDescriptorName=server
ListenStream=%t/emacs/server
SocketMode=0600
[Unit]
Description=Emacs: the extensible, self-documenting text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

View file

@ -0,0 +1,37 @@
{ config, lib, pkgs, ... }:
with lib;
let
in {
config = {
nixpkgs.overlays = [
(self: super: rec {
emacs = pkgs.writeShellScriptBin "dummy-emacs-27.0.91" "" // {
outPath = "@emacs@";
};
emacsPackagesFor = _:
makeScope super.newScope (_: { emacsWithPackages = _: emacs; });
})
];
programs.emacs.enable = true;
services.emacs.enable = true;
services.emacs.client.enable = true;
services.emacs.socketActivation.enable = true;
nmt.script = ''
assertFileExists home-files/.config/systemd/user/emacs.socket
assertFileExists home-files/.config/systemd/user/emacs.service
assertFileExists home-path/share/applications/emacsclient.desktop
assertFileContent home-files/.config/systemd/user/emacs.socket \
${./emacs-socket-27-emacs.socket}
assertFileContent home-files/.config/systemd/user/emacs.service \
${./emacs-socket-27-emacs.service}
assertFileContent home-path/share/applications/emacsclient.desktop \
${./emacs-emacsclient.desktop}
'';
};
}