diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 4f962ead..65b2545b 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 c0276262..7b91bcc2 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 2fbbb5fb..e3d50511 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 00000000..af27538d --- /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 00000000..ab9849bb --- /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 00000000..c862e568 --- /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 00000000..06a26c82 --- /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 00000000..8e9daba8 --- /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 00000000..d2fa78e2 --- /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 00000000..7905e19b --- /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 00000000..99bacf29 --- /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 00000000..8fa68bf5 --- /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 00000000..d1ecb954 --- /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} + ''; + }; +}