Unverified Commit 5bfe2b69 authored by Sandro Jäckel's avatar Sandro Jäckel Committed by GitHub
Browse files

Merge pull request #294641 from devusb/sunshine-module

nixos/sunshine: init
parents 59dc3a46 c44ca352
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -185,6 +185,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m

- [Mealie](https://nightly.mealie.io/), a self-hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in NuxtJS for a pleasant user experience for the whole family. Available as [services.mealie](#opt-services.mealie.enable)

- [Sunshine](https://app.lizardbyte.dev/Sunshine), a self-hosted game stream host for Moonlight. Available as [services.sunshine](#opt-services.sunshine.enable).

- [Uni-Sync](https://github.com/EightB1ts/uni-sync), a synchronization tool for Lian Li Uni Controllers. Available as [hardware.uni-sync](#opt-hardware.uni-sync.enable)

- [prometheus-nats-exporter](https://github.com/nats-io/prometheus-nats-exporter), a Prometheus exporter for NATS. Available as [services.prometheus.exporters.nats](#opt-services.prometheus.exporters.nats.enable).
+1 −0
Original line number Diff line number Diff line
@@ -1161,6 +1161,7 @@
  ./services/networking/strongswan.nix
  ./services/networking/stubby.nix
  ./services/networking/stunnel.nix
  ./services/networking/sunshine.nix
  ./services/networking/supplicant.nix
  ./services/networking/supybot.nix
  ./services/networking/syncplay.nix
+156 −0
Original line number Diff line number Diff line
{ config, lib, pkgs, utils, ... }:
let
  inherit (lib) mkEnableOption mkPackageOption mkOption mkIf mkDefault types optionals getExe;
  inherit (utils) escapeSystemdExecArgs;
  cfg = config.services.sunshine;

  # ports used are offset from a single base port, see https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#port
  generatePorts = port: offsets: map (offset: port + offset) offsets;
  defaultPort = 47989;

  appsFormat = pkgs.formats.json { };
  settingsFormat = pkgs.formats.keyValue { };

  appsFile = appsFormat.generate "apps.json" cfg.applications;
  configFile = settingsFormat.generate "sunshine.conf" cfg.settings;
in
{
  options.services.sunshine = with types; {
    enable = mkEnableOption "Sunshine, a self-hosted game stream host for Moonlight";
    package = mkPackageOption pkgs "sunshine" { };
    openFirewall = mkOption {
      type = bool;
      default = false;
      description = ''
        Whether to automatically open ports in the firewall.
      '';
    };
    capSysAdmin = mkOption {
      type = bool;
      default = false;
      description = ''
        Whether to give the Sunshine binary CAP_SYS_ADMIN, required for DRM/KMS screen capture.
      '';
    };
    settings = mkOption {
      default = { };
      description = ''
        Settings to be rendered into the configuration file. If this is set, no configuration is possible from the web UI.

        See https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#configuration for syntax.
      '';
      example = ''
        {
          sunshine_name = "nixos";
        }
      '';
      type = submodule (settings: {
        freeformType = settingsFormat.type;
        options.port = mkOption {
          type = port;
          default = defaultPort;
          description = ''
            Base port -- others used are offset from this one, see https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/advanced_usage.html#port for details.
          '';
        };
      });
    };
    applications = mkOption {
      default = { };
      description = ''
        Configuration for applications to be exposed to Moonlight. If this is set, no configuration is possible from the web UI, and must be by the `settings` option.
      '';
      example = ''
        {
          env = {
            PATH = "$(PATH):$(HOME)/.local/bin";
          };
          apps = [
            {
              name = "1440p Desktop";
              prep-cmd = [
                {
                  do = "''${pkgs.kdePackages.libkscreen}/bin/kscreen-doctor output.DP-4.mode.2560x1440@144";
                  undo = "''${pkgs.kdePackages.libkscreen}/bin/kscreen-doctor output.DP-4.mode.3440x1440@144";
                }
              ];
              exclude-global-prep-cmd = "false";
              auto-detach = "true";
            }
          ];
        }
      '';
      type = submodule {
        options = {
          env = mkOption {
            default = { };
            description = ''
              Environment variables to be set for the applications.
            '';
            type = attrsOf str;
          };
          apps = mkOption {
            default = [ ];
            description = ''
              Applications to be exposed to Moonlight.
            '';
            type = listOf attrs;
          };
        };
      };
    };
  };

  config = mkIf cfg.enable {
    services.sunshine.settings.file_apps = mkIf (cfg.applications.apps != [ ]) "${appsFile}";

    environment.systemPackages = [
      cfg.package
    ];

    networking.firewall = mkIf cfg.openFirewall {
      allowedTCPPorts = generatePorts cfg.settings.port [ (-5) 0 1 21 ];
      allowedUDPPorts = generatePorts cfg.settings.port [ 9 10 11 13 21 ];
    };

    boot.kernelModules = [ "uinput" ];

    services.udev.packages = [ cfg.package ];

    services.avahi = {
      enable = mkDefault true;
      publish = {
        enable = mkDefault true;
        userServices = mkDefault true;
      };
    };

    security.wrappers.sunshine = mkIf cfg.capSysAdmin {
      owner = "root";
      group = "root";
      capabilities = "cap_sys_admin+p";
      source = getExe cfg.package;
    };

    systemd.user.services.sunshine = {
      description = "Self-hosted game stream host for Moonlight";

      wantedBy = [ "graphical-session.target" ];
      partOf = [ "graphical-session.target" ];
      wants = [ "graphical-session.target" ];
      after = [ "graphical-session.target" ];

      startLimitIntervalSec = 500;
      startLimitBurst = 5;

      serviceConfig = {
        # only add configFile if an application or a setting other than the default port is set to allow configuration from web UI
        ExecStart = escapeSystemdExecArgs ([
          (if cfg.capSysAdmin then "${config.security.wrapperDir}/sunshine" else "${getExe cfg.package}")
        ] ++ optionals (cfg.applications.apps != [ ] || (builtins.length (builtins.attrNames cfg.settings) > 1 || cfg.settings.port != defaultPort)) [ "${configFile}" ]);
        Restart = "on-failure";
        RestartSec = "5s";
      };
    };
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -858,6 +858,7 @@ in {
  stunnel = handleTest ./stunnel.nix {};
  sudo = handleTest ./sudo.nix {};
  sudo-rs = handleTest ./sudo-rs.nix {};
  sunshine = handleTest ./sunshine.nix {};
  suwayomi-server = handleTest ./suwayomi-server.nix {};
  swap-file-btrfs = handleTest ./swap-file-btrfs.nix {};
  swap-partition = handleTest ./swap-partition.nix {};
+70 −0
Original line number Diff line number Diff line
import ./make-test-python.nix ({ pkgs, lib, ... }: {
  name = "sunshine";
  meta = {
    # test is flaky on aarch64
    broken = pkgs.stdenv.isAarch64;
    maintainers = [ lib.maintainers.devusb ];
  };

  nodes.sunshine = { config, pkgs, ... }: {
    imports = [
      ./common/x11.nix
    ];

    services.sunshine = {
      enable = true;
      openFirewall = true;
      settings = {
        capture = "x11";
        encoder = "software";
        output_name = 0;
      };
    };

    environment.systemPackages = with pkgs; [
      gxmessage
    ];

  };

  nodes.moonlight = { config, pkgs, ... }: {
    imports = [
      ./common/x11.nix
    ];

    environment.systemPackages = with pkgs; [
      moonlight-qt
    ];

  };

  enableOCR = true;

  testScript = ''
    # start the tests, wait for sunshine to be up
    start_all()
    sunshine.wait_for_open_port(48010,"localhost")

    # set the admin username/password, restart sunshine
    sunshine.execute("sunshine --creds sunshine sunshine")
    sunshine.systemctl("restart sunshine","root")
    sunshine.wait_for_open_port(48010,"localhost")

    # initiate pairing from moonlight
    moonlight.execute("moonlight pair sunshine --pin 1234 >&2 & disown")
    moonlight.wait_for_console_text("Executing request")

    # respond to pairing request from sunshine
    sunshine.succeed("curl --insecure -u sunshine:sunshine -d '{\"pin\": \"1234\"}' https://localhost:47990/api/pin")

    # close moonlight once pairing complete
    moonlight.send_key("kp_enter")

    # put words on the sunshine screen for moonlight to see
    sunshine.execute("gxmessage 'hello world' -center -font 'sans 75' >&2 & disown")

    # connect to sunshine from moonlight and look for the words
    moonlight.execute("moonlight --video-decoder software stream sunshine 'Desktop' >&2 & disown")
    moonlight.wait_for_text("hello world")
  '';
})
Loading