From 3815248786cb7b911ae1a72d2ec769f93c0b27b6 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Mon, 8 Jun 2020 14:01:17 -0700 Subject: [PATCH] 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 --- modules/misc/news.nix | 10 ++ modules/services/emacs.nix | 119 +++++++++++++----- tests/default.nix | 1 + tests/modules/services/emacs/default.nix | 5 + .../services/emacs/emacs-emacsclient.desktop | 12 ++ .../emacs/emacs-service-emacs.service | 12 ++ .../modules/services/emacs/emacs-service.nix | 32 +++++ .../emacs/emacs-socket-26-emacs.service | 9 ++ .../emacs/emacs-socket-26-emacs.socket | 12 ++ .../services/emacs/emacs-socket-26.nix | 35 ++++++ .../emacs/emacs-socket-27-emacs.service | 9 ++ .../emacs/emacs-socket-27-emacs.socket | 12 ++ .../services/emacs/emacs-socket-27.nix | 37 ++++++ 13 files changed, 276 insertions(+), 29 deletions(-) create mode 100644 tests/modules/services/emacs/default.nix create mode 100644 tests/modules/services/emacs/emacs-emacsclient.desktop create mode 100644 tests/modules/services/emacs/emacs-service-emacs.service create mode 100644 tests/modules/services/emacs/emacs-service.nix create mode 100644 tests/modules/services/emacs/emacs-socket-26-emacs.service create mode 100644 tests/modules/services/emacs/emacs-socket-26-emacs.socket create mode 100644 tests/modules/services/emacs/emacs-socket-26.nix create mode 100644 tests/modules/services/emacs/emacs-socket-27-emacs.service create mode 100644 tests/modules/services/emacs/emacs-socket-27-emacs.socket create mode 100644 tests/modules/services/emacs/emacs-socket-27.nix diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 4f962eadd..65b2545b0 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1542,6 +1542,16 @@ in programs.ssh.matchBlocks..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'. + ''; + } ]; }; } diff --git a/modules/services/emacs.nix b/modules/services/emacs.nix index c02762625..7b91bcc29 100644 --- a/modules/services/emacs.nix +++ b/modules/services/emacs.nix @@ -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,36 +59,78 @@ in { ''; }; }; - }; - config = mkIf cfg.enable { - assertions = [{ - assertion = emacsCfg.enable; - message = "The Emacs service module requires" - + " 'programs.emacs.enable = true'."; - }]; - - systemd.user.services.emacs = { - Unit = { - Description = "Emacs: the extensible, self-documenting text editor"; - Documentation = - "info:emacs man:emacs(1) https://gnu.org/software/emacs/"; - - # Avoid killing the Emacs session, which may be full of - # unsaved buffers. - X-RestartIfChanged = false; - }; - - Service = { - ExecStart = - "${pkgs.runtimeShell} -l -c 'exec ${emacsBinPath}/emacs --fg-daemon'"; - ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs)'"; - Restart = "on-failure"; - }; - - Install = { WantedBy = [ "default.target" ]; }; + # 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"; }; - - home.packages = optional cfg.client.enable clientDesktopItem; }; + + 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 = { + Description = "Emacs: the extensible, self-documenting text editor"; + Documentation = + "info:emacs man:emacs(1) https://gnu.org/software/emacs/"; + + # Avoid killing the Emacs session, which may be full of + # unsaved buffers. + X-RestartIfChanged = false; + }; + + Service = { + 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" ]; }; + }; + }) + ]); } diff --git a/tests/default.nix b/tests/default.nix index 2fbbb5fb4..e3d50511a 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -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 diff --git a/tests/modules/services/emacs/default.nix b/tests/modules/services/emacs/default.nix new file mode 100644 index 000000000..af27538d9 --- /dev/null +++ b/tests/modules/services/emacs/default.nix @@ -0,0 +1,5 @@ +{ + emacs-service = ./emacs-service.nix; + emacs-socket-26 = ./emacs-socket-26.nix; + emacs-socket-27 = ./emacs-socket-27.nix; +} diff --git a/tests/modules/services/emacs/emacs-emacsclient.desktop b/tests/modules/services/emacs/emacs-emacsclient.desktop new file mode 100644 index 000000000..ab9849bb6 --- /dev/null +++ b/tests/modules/services/emacs/emacs-emacsclient.desktop @@ -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 + diff --git a/tests/modules/services/emacs/emacs-service-emacs.service b/tests/modules/services/emacs/emacs-service-emacs.service new file mode 100644 index 000000000..c862e5688 --- /dev/null +++ b/tests/modules/services/emacs/emacs-service-emacs.service @@ -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 diff --git a/tests/modules/services/emacs/emacs-service.nix b/tests/modules/services/emacs/emacs-service.nix new file mode 100644 index 000000000..06a26c823 --- /dev/null +++ b/tests/modules/services/emacs/emacs-service.nix @@ -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} + ''; + }; +} diff --git a/tests/modules/services/emacs/emacs-socket-26-emacs.service b/tests/modules/services/emacs/emacs-socket-26-emacs.service new file mode 100644 index 000000000..8e9daba80 --- /dev/null +++ b/tests/modules/services/emacs/emacs-socket-26-emacs.service @@ -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 diff --git a/tests/modules/services/emacs/emacs-socket-26-emacs.socket b/tests/modules/services/emacs/emacs-socket-26-emacs.socket new file mode 100644 index 000000000..d2fa78e22 --- /dev/null +++ b/tests/modules/services/emacs/emacs-socket-26-emacs.socket @@ -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/ diff --git a/tests/modules/services/emacs/emacs-socket-26.nix b/tests/modules/services/emacs/emacs-socket-26.nix new file mode 100644 index 000000000..7905e19b3 --- /dev/null +++ b/tests/modules/services/emacs/emacs-socket-26.nix @@ -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} + ''; + }; +} diff --git a/tests/modules/services/emacs/emacs-socket-27-emacs.service b/tests/modules/services/emacs/emacs-socket-27-emacs.service new file mode 100644 index 000000000..99bacf290 --- /dev/null +++ b/tests/modules/services/emacs/emacs-socket-27-emacs.service @@ -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 diff --git a/tests/modules/services/emacs/emacs-socket-27-emacs.socket b/tests/modules/services/emacs/emacs-socket-27-emacs.socket new file mode 100644 index 000000000..8fa68bf59 --- /dev/null +++ b/tests/modules/services/emacs/emacs-socket-27-emacs.socket @@ -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/ diff --git a/tests/modules/services/emacs/emacs-socket-27.nix b/tests/modules/services/emacs/emacs-socket-27.nix new file mode 100644 index 000000000..d1ecb954a --- /dev/null +++ b/tests/modules/services/emacs/emacs-socket-27.nix @@ -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} + ''; + }; +}