diff --git a/nixos/modules/hardware/printers.nix b/nixos/modules/hardware/printers.nix index 85e3215127fd..846ff6f3fb4f 100644 --- a/nixos/modules/hardware/printers.nix +++ b/nixos/modules/hardware/printers.nix @@ -110,21 +110,26 @@ in { }; config = mkIf (cfg.ensurePrinters != [] && config.services.printing.enable) { - systemd.services.ensure-printers = let - cupsUnit = if config.services.printing.startWhenNeeded then "cups.socket" else "cups.service"; - in { + systemd.services.ensure-printers = { description = "Ensure NixOS-configured CUPS printers"; wantedBy = [ "multi-user.target" ]; - requires = [ cupsUnit ]; - after = [ cupsUnit ]; + wants = [ "cups.service" ]; + after = [ "cups.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; - script = concatMapStringsSep "\n" ensurePrinter cfg.ensurePrinters - + optionalString (cfg.ensureDefaultPrinter != null) (ensureDefaultPrinter cfg.ensureDefaultPrinter); + script = concatStringsSep "\n" [ + (concatMapStrings ensurePrinter cfg.ensurePrinters) + (optionalString (cfg.ensureDefaultPrinter != null) + (ensureDefaultPrinter cfg.ensureDefaultPrinter)) + # Note: if cupsd is "stateless" the service can't be stopped, + # otherwise the configuration will be wiped on the next start. + (optionalString (with config.services.printing; startWhenNeeded && !stateless) + "systemctl stop cups.service") + ]; }; }; } diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix index ae59dcc226de..9ac89e057620 100644 --- a/nixos/modules/services/printing/cupsd.nix +++ b/nixos/modules/services/printing/cupsd.nix @@ -341,7 +341,7 @@ in systemd.sockets.cups = mkIf cfg.startWhenNeeded { wantedBy = [ "sockets.target" ]; - listenStreams = [ "/run/cups/cups.sock" ] + listenStreams = [ "" "/run/cups/cups.sock" ] ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses; }; @@ -395,10 +395,7 @@ in ''} ''; - serviceConfig = { - PrivateTmp = true; - RuntimeDirectory = [ "cups" ]; - }; + serviceConfig.PrivateTmp = true; }; systemd.services.cups-browsed = mkIf avahiEnabled diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix index 337b5192776f..ed698b63ee63 100644 --- a/nixos/release-combined.nix +++ b/nixos/release-combined.nix @@ -146,7 +146,8 @@ in rec { (onFullSupported "nixos.tests.predictable-interface-names.predictable") (onFullSupported "nixos.tests.predictable-interface-names.unpredictableNetworkd") (onFullSupported "nixos.tests.predictable-interface-names.unpredictable") - (onFullSupported "nixos.tests.printing") + (onFullSupported "nixos.tests.printing-service") + (onFullSupported "nixos.tests.printing-socket") (onFullSupported "nixos.tests.proxy") (onFullSupported "nixos.tests.sddm.default") (onFullSupported "nixos.tests.shadow") diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 1956d3c9e8c7..7a005ff3d9ea 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -531,7 +531,8 @@ in { power-profiles-daemon = handleTest ./power-profiles-daemon.nix {}; pppd = handleTest ./pppd.nix {}; predictable-interface-names = handleTest ./predictable-interface-names.nix {}; - printing = handleTest ./printing.nix {}; + printing-socket = handleTest ./printing.nix { socket = true; }; + printing-service = handleTest ./printing.nix { socket = false; }; privacyidea = handleTest ./privacyidea.nix {}; privoxy = handleTest ./privoxy.nix {}; prometheus = handleTest ./prometheus.nix {}; diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix index cfebe232d92a..7df042e72e90 100644 --- a/nixos/tests/printing.nix +++ b/nixos/tests/printing.nix @@ -1,19 +1,31 @@ # Test printing via CUPS. -import ./make-test-python.nix ({pkgs, ... }: -let - printingServer = startWhenNeeded: { - services.printing.enable = true; - services.printing.stateless = true; - services.printing.startWhenNeeded = startWhenNeeded; - services.printing.listenAddresses = [ "*:631" ]; - services.printing.defaultShared = true; - services.printing.extraConf = '' - - Order allow,deny - Allow from all - - ''; +import ./make-test-python.nix ( +{ pkgs +, socket ? true # whether to use socket activation +, ... +}: + +{ + name = "printing"; + meta = with pkgs.lib.maintainers; { + maintainers = [ domenkozar eelco matthewbauer ]; + }; + + nodes.server = { ... }: { + services.printing = { + enable = true; + stateless = true; + startWhenNeeded = socket; + listenAddresses = [ "*:631" ]; + defaultShared = true; + extraConf = '' + + Order allow,deny + Allow from all + + ''; + }; networking.firewall.allowedTCPPorts = [ 631 ]; # Add a HP Deskjet printer connected via USB to the server. hardware.printers.ensurePrinters = [{ @@ -22,32 +34,19 @@ let model = "drv:///sample.drv/deskjet.ppd"; }]; }; - printingClient = startWhenNeeded: { + + nodes.client = { ... }: { services.printing.enable = true; - services.printing.startWhenNeeded = startWhenNeeded; + services.printing.startWhenNeeded = socket; # Add printer to the client as well, via IPP. hardware.printers.ensurePrinters = [{ name = "DeskjetRemote"; - deviceUri = "ipp://${if startWhenNeeded then "socketActivatedServer" else "serviceServer"}/printers/DeskjetLocal"; + deviceUri = "ipp://server/printers/DeskjetLocal"; model = "drv:///sample.drv/deskjet.ppd"; }]; hardware.printers.ensureDefaultPrinter = "DeskjetRemote"; }; -in { - name = "printing"; - meta = with pkgs.lib.maintainers; { - maintainers = [ domenkozar eelco matthewbauer ]; - }; - - nodes = { - socketActivatedServer = { ... }: (printingServer true); - serviceServer = { ... }: (printingServer false); - - socketActivatedClient = { ... }: (printingClient true); - serviceClient = { ... }: (printingClient false); - }; - testScript = '' import os import re @@ -55,75 +54,69 @@ in { start_all() with subtest("Make sure that cups is up on both sides and printers are set up"): - serviceServer.wait_for_unit("cups.service") - serviceClient.wait_for_unit("cups.service") - socketActivatedClient.wait_for_unit("ensure-printers.service") + server.wait_for_unit("cups.${if socket then "socket" else "service"}") + client.wait_for_unit("cups.${if socket then "socket" else "service"}") + assert "scheduler is running" in client.succeed("lpstat -r") - def test_printing(client, server): - assert "scheduler is running" in client.succeed("lpstat -r") + with subtest("UNIX socket is used for connections"): + assert "/var/run/cups/cups.sock" in client.succeed("lpstat -H") - with subtest("UNIX socket is used for connections"): - assert "/var/run/cups/cups.sock" in client.succeed("lpstat -H") - with subtest("HTTP server is available too"): - client.succeed("curl --fail http://localhost:631/") - client.succeed(f"curl --fail http://{server.name}:631/") - server.fail(f"curl --fail --connect-timeout 2 http://{client.name}:631/") + with subtest("HTTP server is available too"): + client.succeed("curl --fail http://localhost:631/") + client.succeed(f"curl --fail http://{server.name}:631/") + server.fail(f"curl --fail --connect-timeout 2 http://{client.name}:631/") - with subtest("LP status checks"): - assert "DeskjetRemote accepting requests" in client.succeed("lpstat -a") - assert "DeskjetLocal accepting requests" in client.succeed( - f"lpstat -h {server.name}:631 -a" - ) - client.succeed("cupsdisable DeskjetRemote") - out = client.succeed("lpq") - print(out) - assert re.search( - "DeskjetRemote is not ready.*no entries", - client.succeed("lpq"), - flags=re.DOTALL, - ) - client.succeed("cupsenable DeskjetRemote") - assert re.match( - "DeskjetRemote is ready.*no entries", client.succeed("lpq"), flags=re.DOTALL + with subtest("LP status checks"): + assert "DeskjetRemote accepting requests" in client.succeed("lpstat -a") + assert "DeskjetLocal accepting requests" in client.succeed( + f"lpstat -h {server.name}:631 -a" + ) + client.succeed("cupsdisable DeskjetRemote") + out = client.succeed("lpq") + print(out) + assert re.search( + "DeskjetRemote is not ready.*no entries", + client.succeed("lpq"), + flags=re.DOTALL, + ) + client.succeed("cupsenable DeskjetRemote") + assert re.match( + "DeskjetRemote is ready.*no entries", client.succeed("lpq"), flags=re.DOTALL + ) + + # Test printing various file types. + for file in [ + "${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf", + "${pkgs.groff.doc}/share/doc/*/meref.ps", + "${pkgs.cups.out}/share/doc/cups/images/cups.png", + "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt", + ]: + file_name = os.path.basename(file) + with subtest(f"print {file_name}"): + # Print the file on the client. + print(client.succeed("lpq")) + client.succeed(f"lp {file}") + client.wait_until_succeeds( + f"lpq; lpq | grep -q -E 'active.*root.*{file_name}'" ) - # Test printing various file types. - for file in [ - "${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf", - "${pkgs.groff.doc}/share/doc/*/meref.ps", - "${pkgs.cups.out}/share/doc/cups/images/cups.png", - "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt", - ]: - file_name = os.path.basename(file) - with subtest(f"print {file_name}"): - # Print the file on the client. - print(client.succeed("lpq")) - client.succeed(f"lp {file}") - client.wait_until_succeeds( - f"lpq; lpq | grep -q -E 'active.*root.*{file_name}'" - ) + # Ensure that a raw PCL file appeared in the server's queue + # (showing that the right filters have been applied). Of + # course, since there is no actual USB printer attached, the + # file will stay in the queue forever. + server.wait_for_file("/var/spool/cups/d*-001") + server.wait_until_succeeds(f"lpq -a | grep -q -E '{file_name}'") - # Ensure that a raw PCL file appeared in the server's queue - # (showing that the right filters have been applied). Of - # course, since there is no actual USB printer attached, the - # file will stay in the queue forever. - server.wait_for_file("/var/spool/cups/d*-001") - server.wait_until_succeeds(f"lpq -a | grep -q -E '{file_name}'") + # Delete the job on the client. It should disappear on the + # server as well. + client.succeed("lprm") + client.wait_until_succeeds("lpq -a | grep -q -E 'no entries'") - # Delete the job on the client. It should disappear on the - # server as well. - client.succeed("lprm") - client.wait_until_succeeds("lpq -a | grep -q -E 'no entries'") + retry(lambda _: "no entries" in server.succeed("lpq -a")) - retry(lambda _: "no entries" in server.succeed("lpq -a")) - - # The queue is empty already, so this should be safe. - # Otherwise, pairs of "c*"-"d*-001" files might persist. - server.execute("rm /var/spool/cups/*") - - - test_printing(serviceClient, serviceServer) - test_printing(socketActivatedClient, socketActivatedServer) + # The queue is empty already, so this should be safe. + # Otherwise, pairs of "c*"-"d*-001" files might persist. + server.execute("rm /var/spool/cups/*") ''; }) diff --git a/pkgs/misc/cups/default.nix b/pkgs/misc/cups/default.nix index 30e0ce44ca4a..b707647377ca 100644 --- a/pkgs/misc/cups/default.nix +++ b/pkgs/misc/cups/default.nix @@ -38,6 +38,11 @@ stdenv.mkDerivation rec { postPatch = '' substituteInPlace cups/testfile.c \ --replace 'cupsFileFind("cat", "/bin' 'cupsFileFind("cat", "${coreutils}/bin' + + # The cups.socket unit shouldn't be part of cups.service: stopping the + # service would stop the socket and break subsequent socket activations. + # See https://github.com/apple/cups/issues/6005 + sed -i '/PartOf=cups.service/d' scheduler/cups.socket.in ''; nativeBuildInputs = [ pkg-config removeReferencesTo ];