diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-10-17 17:30:15 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-10-17 18:01:12 +0900 |
| commit | d1a648f9379fe2b4f6a3b2230889d520cae2f868 (patch) | |
| tree | a9d74fc44d7a4bb135af5b2acccdc8b14fb4bcd8 /cmd | |
| parent | 79cd9088c7b541d2a3c9e9cad879f0ae46632ca6 (diff) | |
This runs much faster than the NixOS test suite, and is much more
precise. This also uncovered a recent severe pipewire regression.
This change removes all remaining deprecated nix code.
Closes #22.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/hakurei/testsuite/configuration.nix | 252 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/default.nix | 81 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/flake.lock | 49 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/flake.nix | 75 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/hsu.nix | 23 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/main.go | 832 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/nixos.nix | 407 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/options.nix | 364 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/package.nix | 145 | ||||
| -rw-r--r-- | cmd/hakurei/testsuite/test.py | 315 |
10 files changed, 832 insertions, 1711 deletions
diff --git a/cmd/hakurei/testsuite/configuration.nix b/cmd/hakurei/testsuite/configuration.nix deleted file mode 100644 index 62d8239d..00000000 --- a/cmd/hakurei/testsuite/configuration.nix +++ /dev/null @@ -1,252 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: -{ - users.users = { - alice = { - isNormalUser = true; - description = "Alice Foobar"; - password = "foobar"; - uid = 1000; - }; - untrusted = { - isNormalUser = true; - description = "Untrusted user"; - password = "foobar"; - uid = 1001; - - # For deny unmapped uid test: - packages = [ config.environment.hakurei.package ]; - }; - }; - - home-manager.users.alice.home.stateVersion = "24.11"; - - # Automatically login on tty1 as a normal user: - services.getty.autologinUser = "alice"; - - security.pam.loginLimits = [ - { - domain = "@users"; - item = "rtprio"; - type = "-"; - value = 1; - } - ]; - - environment = { - systemPackages = with pkgs; [ - # For D-Bus tests: - mako - libnotify - ]; - - variables = { - SWAYSOCK = "/tmp/sway-ipc.sock"; - WLR_RENDERER = "pixman"; - }; - - # To help with OCR: - etc."xdg/foot/foot.ini".text = lib.generators.toINI { } { - main = { - font = "inconsolata:size=14"; - }; - colors = rec { - foreground = "000000"; - background = "ffffff"; - regular2 = foreground; - }; - }; - }; - - fonts.packages = [ pkgs.inconsolata ]; - - # Automatically configure and start Sway when logging in on tty1: - programs.bash.loginShellInit = '' - if [ "$(tty)" = "/dev/tty1" ]; then - set -e - - mkdir -p ~/.config/sway - (sed s/Mod4/Mod1/ /etc/sway/config && - echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' && - echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config - - sway --validate - systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok - fi - ''; - - programs.sway.enable = true; - - # For PulseAudio tests: - security.rtkit.enable = true; - services.pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - jack.enable = true; - }; - - virtualisation = { - # Hopefully reduces spurious test failures: - memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192; - - qemu.options = [ - # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch: - "-vga none -device virtio-gpu-pci" - - # Increase Go test compiler performance: - "-smp 16" - ]; - }; - - # Disk image is too small for some tests: - boot.tmp.useTmpfs = true; - - environment.hakurei = { - enable = true; - stateDir = "/var/lib/hakurei"; - users.alice = 0; - - extraHomeConfig = - { config, ... }: - { - # To test merge deduplication: - options._hakurei.stateVersion = lib.mkOption { type = lib.types.str; }; - - config = { - home = { inherit (config._hakurei) stateVersion; }; - _hakurei.stateVersion = "23.05"; - }; - }; - - commonPaths = [ - { - type = "bind"; - src = "/var/tmp"; - write = true; - } - ]; - - apps = { - "cat.gensokyo.extern.bash.linger-timeout" = { - name = "hakurei-check-linger-timeout"; - identity = 9999; - share = pkgs.bash; - packages = [ pkgs.bash ]; - command = '' - sleep infinity & disown - exit - ''; - wait_delay = 1; - enablements = { - wayland = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.noEnablements" = { - name = "ne-foot"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.foot; - packages = with pkgs; [ - foot - - # For wayland-info: - wayland-utils - ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.noEnablements.immediate" = { - name = "ne-foot-immediate"; - identity = 1; - shareUid = true; - verbose = true; - wait_delay = -1; - share = pkgs.foot; - packages = [ ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.pulseaudio" = { - name = "pa-foot"; - identity = 2; - verbose = true; - share = pkgs.foot; - packages = [ pkgs.foot ]; - command = "foot"; - enablements.dbus = false; - }; - - "cat.gensokyo.extern.Alacritty.x11" = { - name = "x11-alacritty"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.alacritty; - packages = with pkgs; [ - # For X11 terminal emulator: - alacritty - - # For glinfo: - mesa-demos - ]; - command = "alacritty"; - enablements = { - wayland = false; - x11 = true; - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.directWayland" = { - name = "da-foot"; - identity = 4; - verbose = true; - insecureWayland = true; - share = pkgs.foot; - packages = with pkgs; [ - foot - - # For wayland-info: - wayland-utils - ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.strace.wantFail" = { - name = "strace-failure"; - identity = 5; - verbose = true; - share = pkgs.strace; - command = "strace true"; - enablements = { - wayland = false; - x11 = false; - dbus = false; - pipewire = false; - }; - }; - }; - }; -} diff --git a/cmd/hakurei/testsuite/default.nix b/cmd/hakurei/testsuite/default.nix deleted file mode 100644 index 81daa0a2..00000000 --- a/cmd/hakurei/testsuite/default.nix +++ /dev/null @@ -1,81 +0,0 @@ -{ - lib, - testers, - buildFHSEnv, - writeShellScriptBin, - - system, - self, - withRace ? false, -}: - -testers.nixosTest { - name = "hakurei" + (if withRace then "-race" else ""); - nodes.machine = - { options, pkgs, ... }: - let - fhs = - let - hakurei = options.environment.hakurei.package.default; - in - buildFHSEnv { - pname = "hakurei-fhs"; - inherit (hakurei) version; - targetPkgs = _: hakurei.targetPkgs; - extraOutputsToInstall = [ "dev" ]; - profile = '' - export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH" - ''; - }; - in - { - environment.systemPackages = [ - # For go tests: - (writeShellScriptBin "hakurei-test" '' - # Assert hst CGO_ENABLED=0: ${ - with pkgs; - runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ self.packages.${system}.hakurei.go ]; } '' - cp -r ${options.environment.hakurei.package.default.src} "$out" - chmod -R +w "$out" - cp ${writeText "hst_cgo_test.go" ''package hakurei_test;import("testing";"hakurei.app/hst");func TestTemplate(t *testing.T){hst.Template()}''} "$out/hst_cgo_test.go" - (cd "$out" && HOME="$(mktemp -d)" CGO_ENABLED=0 go test .) - '' - } - - cd ${self.packages.${system}.hakurei.src} - ${fhs}/bin/hakurei-fhs -c \ - 'CC="clang -O3 -Werror" go test --tags=noskip ${if withRace then "-race" else "-count 16"} ./...' \ - &> /tmp/hakurei-test.log && \ - touch /tmp/hakurei-test-ok - touch /tmp/hakurei-test-done - '') - ]; - - # Run with Go race detector: - environment.hakurei = lib.mkIf withRace rec { - # race detector does not support static linking - package = (pkgs.callPackage ./package.nix { }).overrideAttrs (previousAttrs: { - env = previousAttrs.env // { - GOFLAGS = previousAttrs.env.GOFLAGS + " -race"; - }; - }); - hsuPackage = options.environment.hakurei.hsuPackage.default.override { hakurei = package; }; - }; - - imports = [ - ./configuration.nix - - self.nixosModules.hakurei - self.inputs.home-manager.nixosModules.home-manager - ]; - }; - - # adapted from nixos sway integration tests - - # testScriptWithTypes:49: error: Cannot call function of unknown type - # (machine.succeed if succeed else machine.execute)( - # ^ - # Found 1 error in 1 file (checked 1 source file) - skipTypeCheck = true; - testScript = builtins.readFile ./test.py; -} diff --git a/cmd/hakurei/testsuite/flake.lock b/cmd/hakurei/testsuite/flake.lock deleted file mode 100644 index 5537506a..00000000 --- a/cmd/hakurei/testsuite/flake.lock +++ /dev/null @@ -1,49 +0,0 @@ -{ - "nodes": { - "home-manager": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1787146702, - "narHash": "sha256-YbRcLdU/yK4gWsQg7V8WTKZHfXL33g8+wSFUX3wyevs=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "173b7e8d40fdc8c296a9c99854314f17a3a1704c", - "type": "github" - }, - "original": { - "owner": "nix-community", - "ref": "release-26.05", - "repo": "home-manager", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1787101114, - "narHash": "sha256-gwrPcFf/rDjHPaVflbDZ040ZDmBTRj/7+s8ZmE2SaIM=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b18a4b905f8d028dc4476412e6d6891728695379", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "home-manager": "home-manager", - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/cmd/hakurei/testsuite/flake.nix b/cmd/hakurei/testsuite/flake.nix deleted file mode 100644 index e1df8990..00000000 --- a/cmd/hakurei/testsuite/flake.nix +++ /dev/null @@ -1,75 +0,0 @@ -{ - description = "hakurei container tool and nixos module"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; - - home-manager = { - url = "github:nix-community/home-manager/release-26.05"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; - - outputs = - { - self, - nixpkgs, - home-manager, - }: - let - supportedSystems = [ "x86_64-linux" ]; - - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; - nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); - in - { - nixosModules.hakurei = import ./nixos.nix self.packages; - - checks = forAllSystems ( - system: - let - pkgs = nixpkgsFor.${system}; - - inherit (pkgs) callPackage; - in - { - hakurei = callPackage ./. { inherit system self; }; - race = callPackage ./. { - inherit system self; - withRace = true; - }; - } - ); - - packages = forAllSystems ( - system: - let - inherit (self.packages.${system}) hakurei hsu; - pkgs = nixpkgsFor.${system}; - in - { - default = hakurei; - hakurei = pkgs.pkgsStatic.callPackage ./package.nix { - inherit (pkgs) - # passthru.buildInputs - go_1_27 - clang - - # nativeBuildInputs - pkg-config - wayland-scanner - makeBinaryWrapper - - # appPackages - glibc - xdg-dbus-proxy - - # for check - nettools - ; - }; - hsu = pkgs.callPackage ./hsu.nix { inherit (self.packages.${system}) hakurei; }; - } - ); - }; -} diff --git a/cmd/hakurei/testsuite/hsu.nix b/cmd/hakurei/testsuite/hsu.nix deleted file mode 100644 index d1ddcb77..00000000 --- a/cmd/hakurei/testsuite/hsu.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - lib, - buildGoModule, - hakurei ? abort "hakurei package required", -}: - -buildGoModule { - pname = "${hakurei.pname}-hsu"; - inherit (hakurei) version; - - src = ../../hsu; - inherit (hakurei) vendorHash; - env.CGO_ENABLED = 0; - - preBuild = '' - go mod init hsu >& /dev/null - ''; - - ldflags = lib.attrsets.foldlAttrs ( - ldflags: name: value: - ldflags ++ [ "-X main.${name}=${value}" ] - ) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; }; -} diff --git a/cmd/hakurei/testsuite/main.go b/cmd/hakurei/testsuite/main.go new file mode 100644 index 00000000..ca48be8e --- /dev/null +++ b/cmd/hakurei/testsuite/main.go @@ -0,0 +1,832 @@ +//go:build testsuite + +// The hakurei test program tests cmd/hakurei behaviour against a graphical +// session and applications simulating a desktop workload. +package main + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "hakurei.app/check" + "hakurei.app/ext" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" +) + +// newConfig returns the address of a new bare minimal hakurei configuration. +func newConfig( + name string, + file string, + args ...string, +) *hst.Config { + c := hst.Config{ + ID: "app.hakurei." + name, + Identity: int(testsuite.NewSerial() % hst.IdentityEnd), + + Container: &hst.ContainerConfig{ + Hostname: name, + Filesystem: []hst.FilesystemConfigJSON{ + {FilesystemConfig: &hst.FSBind{ + Target: fhs.AbsRoot, + Source: fhs.AbsRoot, + Special: true, + }}, + }, + Username: "nobody", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + }, + } + + if p, err := exec.LookPath(file); err != nil { + log.Fatal(err) + } else if p, err = filepath.Abs(p); err != nil { + log.Fatal(err) + } else { + c.Container.Path = check.MustAbs(p) + } + c.Container.Args = []string{filepath.Base(file)} + c.Container.Args = append(c.Container.Args, args...) + return &c +} + +// checkHakurei starts a hakurei container and calls f. +func checkHakurei( + ctx context.Context, + wg *sync.WaitGroup, + cred *syscall.Credential, + dbusEnv string, + + name, long string, + scan, verbose, insecure bool, + processConfig func(c *hst.Config), + f func(p hst.State, pid int) error, + + checkOutput func(output string) bool, + wantCode ...int, +) { + if !scan && f != nil { + log.Fatalf("(%s) cannot inspect without scanning", name) + } + if checkOutput == nil { + checkOutput = func(output string) bool { return output == "" } + } + if len(wantCode) == 0 { + wantCode = []int{130} + } + + c := newConfig(name, "sleep", "infinity") + if processConfig != nil { + processConfig(c) + } + + wg.Go(func() { + p, pid := testsuite.MustStartHakurei( + ctx, wg, cred, dbusEnv, func(err error, output string) { + if err == nil { + if !slices.Contains(wantCode, 0) { + log.Fatalf( + "%s test process unexpectedly succeeded\n%s", + long, output, + ) + } + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatalf("%s: %v\n%s", long, err, output) + } else if !slices.Contains(wantCode, e.ExitCode()) { + log.Fatalf("%s: %v\n%s", long, e, output) + } + + if !checkOutput(output) { + log.Fatalf("%s: %q", long, output) + } + + log.Println("validated", long) + }, scan, false, verbose, insecure, c, + ) + + if f != nil { + if err := f(p, pid); err != nil { + log.Fatalf("(%s) %v", name, err) + } + } + + if scan { + testsuite.MustRun( + cred, testsuite.SessionEnv(dbusEnv), + "hakurei", "kill", p.ID.String(), + ) + testsuite.PollPID(long, pid) + } + }) +} + +func main() { + go testsuite.ReceiveSignals() + cred := syscall.Credential{Uid: 1000, Gid: 100} + + // the signal handler does not wait for termination + ctx := context.Background() + + testsuite.MustPrepareUserDirs(&cred) + var swg sync.WaitGroup + defer swg.Wait() + dbusEnv := testsuite.MustStartSessionBus(&cred) + testsuite.MustStartSway(&swg, &cred, dbusEnv) + defer testsuite.TerminateSway(&cred) + pipewireVersion := testsuite.MustStartPipeWire(&cred, dbusEnv) + sessionEnv := testsuite.SessionEnv(dbusEnv) + absNonexistent := fhs.AbsProc.Append("nonexistent") + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Go(func() { + defer log.Println("validated static linking") + + for _, name := range []string{ + "hakurei", + "hsu", + "sharefs", + } { + pathname := fhs.AbsUsr.Append("local/bin", name) + testsuite.AssertOutput( + "validating "+pathname.String()+" static linking", + "", "\tnot a dynamic executable\n", + )(testsuite.MustOutput( + ctx, nil, nil, hst.ExitFailure, + "validate "+pathname.String()+" static linking", + "ldd", pathname.String(), + )) + } + }) + + wg.Go(func() { + defer log.Println("validated deny unmapped uid") + + testsuite.AssertOutput( + "denying unmapped uid", + "", "hsu: uid 65534 is not in the hsurc file\n", + )(testsuite.MustOutput( + ctx, &syscall.Credential{ + Uid: 65534, + Gid: 100, + }, nil, hst.ExitFailure, "deny unmapped uid", + "hakurei", "exec", "true", + )) + }) + + wg.Go(func() { + defer log.Println("validated deny unmapped uid (verbose)") + + testsuite.AssertOutput( + "denying unmapped uid (verbose)", + "", "hsu: uid 65534 is not in the hsurc file\n"+ + "hakurei: *cannot retrieve user id from setuid wrapper: "+ + "current user is not in the hsurc file\n", + )(testsuite.MustOutput( + ctx, &syscall.Credential{ + Uid: 65534, + Gid: 100, + }, nil, hst.ExitFailure, "deny unmapped uid (verbose)", + "hakurei", "-v", "exec", "true", + )) + }) + + wg.Go(func() { + defer log.Println("validated direct hsu call") + + testsuite.AssertOutput( + "calling hsu directly", + "0", "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "direct hsu call", + "hsu", + )) + }) + + wg.Go(func() { + defer log.Println("validated direct hsu deny") + + testsuite.AssertOutput( + "from direct hsu denial", + "", "hsu: uid 65534 is not in the hsurc file\n", + )(testsuite.MustOutput( + ctx, &syscall.Credential{ + Uid: 65534, + Gid: 100, + }, nil, hst.ExitFailure, "direct hsu deny", + "hsu", + )) + }) + + wg.Go(func() { + defer log.Println("validated timeout behaviour") + + testsuite.AssertOutput( + "validating timeout behaviour", + "", "init: timeout exceeded waiting for lingering processes\n", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "timeout lingering process", + "hakurei", "exec", "--no-wait", + "bash", "-c", "sleep infinity & disown", + )) + }) + + wg.Go(func() { + defer log.Println("validated pd shared tmpdir") + + serial := testsuite.NewSerial() + _serial := strconv.FormatUint(serial, 10) + testsuite.AssertOutput( + "validating pd shared tmpdir", + "", "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "pd shared tmpdir", + "hakurei", "exec", "touch", "/tmp/ok."+_serial, + )) + + if err := os.Remove( + testsuite.HakureiPrefix + "tmpdir/0/ok." + _serial, + ); err != nil { + log.Fatal(err) + } + }) + + wg.Go(func() { + defer log.Println("validated graceful failure on bad Wayland display name") + + testsuite.AssertOutput( + "validating Wayland graceful failure", + "", "hakurei: cannot connect to "+ + "/tmp/hakurei.0/compat/wayland-0: no such file or directory\n", + )(testsuite.MustOutput( + ctx, &cred, nil, hst.ExitRequest, "bad Wayland name", + "hakurei", "exec", "--wayland", + "true", + )) + }) + + wg.Go(func() { + if _, _, err := testsuite.StartHakurei( + ctx, &wg, &cred, dbusEnv, func(cmd *exec.Cmd) { + cmd.ExtraFiles = cmd.ExtraFiles[:len(cmd.ExtraFiles)-1] + }, func(err error, output string) { + defer log.Println("validated bad identifier fd") + + if err == nil { + log.Fatal("bad identity fd unexpectedly succeeded") + } + if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if e.ExitCode() != hst.ExitRequest { + fmt.Print(output) + log.Fatal(e) + } + + if output != "hakurei: cannot write identifier: "+ + "bad file descriptor\r\n" { + log.Fatalf( + "bad identity fd unexpected output: %q", + output, + ) + } + }, false, false, false, false, &hst.Config{ + Container: &hst.ContainerConfig{ + Shell: absNonexistent, + Home: absNonexistent, + Path: absNonexistent, + }, + }, + ); err != nil { + log.Fatal(err) + } + }) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "indirect-termination", "indirect termination", + true, false, false, nil, nil, nil, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "immediate-termination", "immediate termination", + true, false, false, func(c *hst.Config) { + c.Container.WaitDelay = -1 + }, nil, nil, 254, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "shim-poke", "unexpected shim notification", + true, true, false, nil, func(p hst.State, pid int) error { + if err := syscall.Kill(p.ShimPID, syscall.SIGCONT); err != nil { + return os.NewSyscallError("kill", err) + } + return nil + }, func(output string) bool { + return strings.Contains( + output, + "shim: got SIGCONT from unexpected process\r\n", + ) + }, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "reject-deprecated", "reject insecure configuration", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EPulse) + }, nil, func(output string) bool { + return output == "hakurei: enablement PulseAudio is insecure "+ + "and no longer supported\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-pulse", "pipewire proxying behaviour", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.Container.Path = fhs.AbsUsrBin.Append("pactl") + c.Container.Args = []string{"pactl", "info"} + }, nil, func(output string) bool { + _output, _, ok := strings.CutLast(output, "Cookie: ") + if !ok { + return false + } + output, _output, ok = strings.Cut(_output, "\r\nClient Index: ") + if !ok { + return false + } + _, _output, ok = strings.Cut(_output, "\r\n") + if !ok { + return false + } + output += "\r\n" + _output + output = strings.Replace( + output, + "@DEFAULT_SINK@", + "auto_null", + 1, + ) + + return output == "Server String: unix:/run/user/65534/pulse/native\r\n"+ + "Library Protocol Version: 35\r\n"+ + "Server Protocol Version: 35\r\n"+ + "Is Local: yes\r\n"+ + "Tile Size: "+strconv.Itoa((os.Getpagesize()-4)*16)+"\r\n"+ + "User Name: nobody\r\n"+ + "Host Name: hakurei-pipewire-pulse\r\n"+ + "Server Name: PulseAudio (on PipeWire "+pipewireVersion+")\r\n"+ + "Server Version: 15.0.0\r\n"+ + "Default Sample Specification: float32le 2ch 48000Hz\r\n"+ + "Default Channel Map: front-left,front-right\r\n"+ + "Default Sink: auto_null\r\n"+ + "Default Source: @DEFAULT_SOURCE@\r\n" + }, 0, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-security", "pipewire SecurityContext", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.Container.Path = fhs.AbsUsrBin.Append("pactl") + c.Container.Args = []string{ + "pactl", + "set-sink-mute", + "auto_null", + "toggle", + } + }, nil, func(output string) bool { + return output == "Failure: Access denied\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-direct-failure", "pipewire cover direct access", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.Container.Path = fhs.AbsUsrBin.Append("pw-dump") + c.Container.Args = []string{"pw-dump"} + }, nil, func(output string) bool { + return output == "can't connect: Host is down\r\n" + }, 255, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-direct", "pipewire direct access", + false, false, true, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.DirectPipeWire = true + c.Container.Path = fhs.AbsUsrBin.Append("pw-dump") + c.Container.Args = []string{"pw-dump", "-RN"} + }, nil, func(output string) bool { + return json.Valid(unsafe.Slice(unsafe.StringData(output), len(output))) + }, 0, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "wayland-client", "wayland client", + true, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.Container.Path = fhs.AbsUsrBin.Append("foot") + c.Container.Args = []string{"foot"} + }, func(p hst.State, pid int) error { + testsuite.PollSwayWindow(ctx, &cred, func(n *testsuite.SwayNode) bool { + return n.Sandbox == "app.hakurei" && + n.ID == "app.hakurei.wayland-client" && + n.Instance == p.ID && + n.AppID == "foot" + }) + return nil + }, func(string) bool { return true }, 230, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "wayland-security-context", "wayland security-context-v1", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.Container.Path = fhs.AbsUsrBin.Append("grim") + c.Container.Args = []string{"grim"} + }, nil, func(output string) bool { + return output == "warning: zxdg_output_manager_v1 isn't available, "+ + "guessing the output layout\r\n"+ + "compositor doesn't support wlr-screencopy-unstable-v1\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "wlr-screencopy-unstable-v1", "direct wlr-screencopy-unstable-v1", + false, false, true, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.DirectWayland = true + c.Container.Path = fhs.AbsUsrBin.Append("grim") + c.Container.Args = []string{"grim", "/dev/null"} + }, nil, nil, 0, + ) + + // this client lacks attached identification + wg.Wait() + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "direct-wayland-client", "direct wayland client", + true, false, true, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.DirectWayland = true + c.Container.Path = fhs.AbsUsrBin.Append("foot") + c.Container.Args = []string{"foot"} + }, func(p hst.State, pid int) error { + testsuite.PollSwayWindow(ctx, &cred, func(n *testsuite.SwayNode) bool { + return n.Sandbox == "" && + n.ID == "" && + n.Instance == hst.ID{} && + n.AppID == "foot" + }) + return nil + }, func(string) bool { return true }, 230, + ) + wg.Wait() + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "xwayland-client", "xwayland client", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EX11) + c.Container.Path = fhs.AbsUsrBin.Append("xset") + c.Container.Args = []string{"xset", "q"} + c.Container.Flags |= hst.FHostAbstract + }, nil, func(output string) bool { + return strings.Contains(output, "DPMS") + }, 0, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "dbus", "proxied dbus call", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EDBus) + c.SessionBus = &hst.BusConfig{ + Talk: []string{"org.freedesktop.Notifications"}, + Filter: true, + } + c.Container.Path = fhs.AbsUsrBin.Append("notify-send") + c.Container.Args = []string{ + "notify-send", + "Test notification", + } + }, nil, func(output string) bool { + return output == "GDBus.Error:"+ + "org.freedesktop.DBus.Error.ServiceUnknown: "+ + "The name org.freedesktop.Notifications "+ + "was not provided by any .service files\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "dbus reject", "rejected dbus call", + false, false, false, func(c *hst.Config) { + c.Enablements = new(hst.EDBus) + c.SessionBus = &hst.BusConfig{Filter: true} + c.Container.Path = fhs.AbsUsrBin.Append("notify-send") + c.Container.Args = []string{ + "notify-send", + "Test notification", + } + }, nil, func(output string) bool { + return output == "GDBus.Error:"+ + "org.freedesktop.DBus.Error.ServiceUnknown: "+ + "org.freedesktop.DBus.Error.ServiceUnknown\r\n" + }, hst.ExitFailure, + ) + + silentOutputInterrupt := func(args ...string) { + wg.Go(func() { + defer log.Printf( + "validated silent output after interrupt (args: %q)", + strings.Join(args, " "), + ) + + serial := testsuite.NewSerial() + _serial := strconv.FormatUint(serial, 10) + cmd := exec.CommandContext(ctx, "hakurei", slices.Concat( + []string{"exec"}, + args, + []string{"sh", "-c", + "touch /tmp/ok." + _serial + " && sleep infinity"}, + )...) + var stdout, stderr strings.Builder + cmd.Stdout, cmd.Stderr = &stdout, &stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: &cred, + Setpgid: true, + } + testsuite.MustAppendEnv(cmd, sessionEnv...) + + done := testsuite.MustStart(cmd) + for { + select { + case err := <-done: + fmt.Print(stdout.String()) + fmt.Print(stderr.String()) + if err == nil { + log.Fatalf( + "test process %d terminated unexpectedly", + serial, + ) + } + log.Fatalf( + "test process %d terminated unexpectedly: %v", + serial, err, + ) + default: + break + } + + time.Sleep(100 * time.Millisecond) + if err := os.Remove( + testsuite.HakureiPrefix + "tmpdir/0/ok." + _serial, + ); err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + log.Fatal(err) + } + break + } + + if err := syscall.Kill( + -cmd.Process.Pid, + syscall.SIGINT, + ); err != nil { + log.Fatalf( + "cannot interrupt process group %d: %v", + serial, err, + ) + } + + if err := <-done; err == nil { + log.Fatal("interrupted process unexpectedly succeeded") + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatalf("validate silent interrupt: %v", err) + } else if code := e.ExitCode(); !slices.Contains([]int{ + hst.ExitFailure, + hst.ExitCancel, + 127, 130, 254, + }, code) { + log.Fatalf("validate silent interrupt: %v", e) + } + + if stdout.Len() != 0 || stderr.Len() != 0 { + log.Fatalf( + "interrupt is not silent (args: %q)"+ + "\nstdout = %q\nstderr = %q", + strings.Join(args, " "), + stdout.String(), + stderr.String(), + ) + } + }) + } + silentOutputInterrupt() + silentOutputInterrupt("--dbus") + silentOutputInterrupt("--wayland", "-X", "--dbus", "--pulse") + + const RLIMIT_RTPRIO = 14 + if err := syscall.Setrlimit(RLIMIT_RTPRIO, &syscall.Rlimit{ + Cur: 49, + Max: 49, + }); err != nil { + log.Fatalf("cannot increase rtprio limit: %v", err) + } + for _, policy := range []ext.SchedPolicy{ + ext.SCHED_NORMAL, + ext.SCHED_IDLE, + ext.SCHED_RR, + } { + _policy := policy + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "setscheduler-"+strconv.Itoa(int(policy)), "setscheduler", + true, false, false, func(c *hst.Config) { + c.SchedPolicy = _policy + }, func(p hst.State, pid int) error { + got := -1 + r, err := os.Open(filepath.Join( + fhs.Proc, + strconv.Itoa(pid), + "sched", + )) + if err != nil { + return err + } + + s := bufio.NewScanner(r) + + for range 2 { + if !s.Scan() { + _ = r.Close() + if err = s.Err(); err != nil { + return err + } + return errors.New("sched header too short") + } + } + + for s.Scan() { + if !strings.HasPrefix(s.Text(), "policy") { + continue + } + + k, v, ok := strings.Cut(s.Text(), ":") + if !ok { + continue + } + if strings.TrimSpace(k) != "policy" { + continue + } + + got, err = strconv.Atoi(strings.TrimSpace(v)) + if err != nil { + _ = r.Close() + return err + } + } + if err = s.Err(); err != nil { + _ = r.Close() + return err + } else if err = r.Close(); err != nil { + return err + } + + if _got := ext.SchedPolicy(got); _got != policy { + return fmt.Errorf("unexpected %q policy %q", policy, _got) + } + return nil + }, nil, + ) + + wg.Go(func() { + defer log.Println("validated revert type selection") + + c := newConfig("rts 0", "sleep", "infinity") + c.Container.Flags |= hst.FShareRuntime + uid := hst.ToUser(0, c.Identity) + + onDone := func(err error, output string) { + if err == nil { + log.Fatal("revert type selection test process " + + "terminated unexpectedly") + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatalf("revert type selection: %v\n%s", err, output) + } else if e.ExitCode() != 130 { + log.Fatalf("revert type selection: %v\n%s", e, output) + } + + if output != "" { + log.Fatalf("unexpected output\n%s", output) + } + } + + validatePresent := func() { + testsuite.AssertOutput( + "validating ACLs for rts present", + "user::rwx\n"+ + "user:"+strconv.Itoa(uid)+":rwx\n"+ + "group::---\n"+ + "mask::rwx\n"+ + "other::---\n"+ + "\n", + "", + )(testsuite.MustOutput( + ctx, nil, nil, 0, "getfacl rts present", + "getfacl", + "--absolute-names", + "--omit-header", + "--numeric", + "/tmp/hakurei.0/runtime/"+strconv.Itoa(c.Identity), + )) + } + validateReverted := func() { + testsuite.AssertOutput( + "validating ACLs for rts reverted", + "user::rwx\n"+ + "group::---\n"+ + "mask::---\n"+ + "other::---\n"+ + "\n", + "", + )(testsuite.MustOutput( + ctx, nil, nil, 0, "getfacl rts present", + "getfacl", + "--absolute-names", + "--omit-header", + "--numeric", + "/tmp/hakurei.0/runtime/"+strconv.Itoa(c.Identity), + )) + } + + p0, pid0 := testsuite.MustStartHakurei( + ctx, &wg, &cred, dbusEnv, onDone, + true, false, false, false, c, + ) + validatePresent() + + p1, pid1 := testsuite.MustStartHakurei( + ctx, &wg, &cred, dbusEnv, onDone, + true, false, false, false, c, + ) + validatePresent() + + testsuite.MustRun( + &cred, testsuite.SessionEnv(dbusEnv), + "hakurei", "kill", p1.ID.String(), + ) + testsuite.PollPID("revert type selection 1", pid1) + validatePresent() + + testsuite.MustRun( + &cred, testsuite.SessionEnv(dbusEnv), + "hakurei", "kill", p0.ID.String(), + ) + testsuite.PollPID("revert type selection 0", pid0) + validateReverted() + }) + + wg.Wait() + + if dents, err := os.ReadDir("/tmp"); err != nil { + log.Fatal(err) + } else { + for _, dent := range dents { + if name := dent.Name(); strings.HasPrefix(name, ".hakurei-shim-") { + log.Fatalf("leftover shim work dir %q", name) + } + } + } + } +} diff --git a/cmd/hakurei/testsuite/nixos.nix b/cmd/hakurei/testsuite/nixos.nix deleted file mode 100644 index 49bfffb6..00000000 --- a/cmd/hakurei/testsuite/nixos.nix +++ /dev/null @@ -1,407 +0,0 @@ -packages: -{ - lib, - pkgs, - config, - ... -}: - -let - inherit (lib) - lists - attrsets - mkMerge - mkIf - mapAttrs - foldlAttrs - optional - optionals - ; - - cfg = config.environment.hakurei; - - # userid*userOffset + appStart + appid - getsubuid = userid: appid: userid * 100000 + 10000 + appid; - getsubname = userid: appid: "u${toString userid}_a${toString appid}"; - getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}"; - - mountpoints = { - ${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) { - depends = [ cfg.sharefs.source ]; - device = "sharefs"; - fsType = "fuse.sharefs"; - noCheck = true; - options = [ - "rw" - "noexec" - "nosuid" - "nodev" - "noatime" - "allow_other" - "mkdir" - "source=${cfg.sharefs.source}" - "setuid=${toString config.users.users.${cfg.sharefs.user}.uid}" - "setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}" - ]; - }; - }; -in - -{ - imports = [ (import ./options.nix packages) ]; - - options = { - # Forward declare a dummy option for VM filesystems since the real one won't exist - # unless the VM module is actually imported. - virtualisation.fileSystems = lib.mkOption { }; - }; - - config = mkIf cfg.enable { - assertions = [ - ( - let - conflictingApps = foldlAttrs ( - acc: id: app: - ( - acc - ++ foldlAttrs ( - acc': id': app': - if id == id' || app.shareUid && app'.shareUid || app.identity != app'.identity then acc' else acc' ++ [ id ] - ) [ ] cfg.apps - ) - ) [ ] cfg.apps; - in - { - assertion = (lists.length conflictingApps) == 0; - message = "the following hakurei apps have conflicting identities: " + (builtins.concatStringsSep ", " conflictingApps); - } - ) - ]; - - security.wrappers.hsu = { - source = "${cfg.hsuPackage}/bin/hsu"; - setuid = true; - owner = "root"; - group = "root"; - }; - - environment.etc.hsurc = { - mode = "0400"; - text = foldlAttrs ( - acc: username: fid: - "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc - ) "" cfg.users; - }; - - environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package; - fileSystems = mountpoints; - virtualisation.fileSystems = mountpoints; - - home-manager = - let - privPackages = mapAttrs (_: userid: { - home.packages = foldlAttrs ( - acc: id: app: - [ - ( - let - extendDBusDefault = id: ext: { - filter = true; - - talk = [ "org.freedesktop.Notifications" ] ++ ext.talk; - own = [ - "${id}.*" - "org.mpris.MediaPlayer2.${id}.*" - ] - ++ ext.own; - - inherit (ext) call broadcast; - }; - dbusConfig = - let - default = { - talk = [ ]; - own = [ ]; - call = { }; - broadcast = { }; - }; - in - { - session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault id)) else (extendDBusDefault id default); - system_bus = app.dbus.system; - }; - command = if app.command == null then app.name else app.command; - script = if app.script == null then ("exec " + command + " $@") else app.script; - isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11; - - conf = { - inherit id; - inherit (app) identity enablements; - inherit (dbusConfig) session_bus system_bus; - direct_wayland = app.insecureWayland; - sched_policy = app.schedPolicy; - sched_priority = app.schedPriority; - groups = app.groups ++ optional (cfg.sharefs.source != null) cfg.sharefs.group; - - container = { - inherit (app) - wait_delay - devel - userns - device - tty - multiarch - env - ; - map_real_uid = app.mapRealUid; - host_net = app.hostNet; - host_abstract = app.hostAbstract; - share_runtime = app.shareRuntime; - share_tmpdir = app.shareTmpdir; - - filesystem = - let - bind = src: { - type = "bind"; - inherit src; - }; - optBind = src: { - type = "bind"; - inherit src; - optional = true; - }; - optDevBind = src: { - type = "bind"; - inherit src; - dev = true; - optional = true; - }; - in - [ - (bind "/bin") - (bind "/usr/bin") - (bind "/nix/store") - (optBind "/sys/block") - (optBind "/sys/bus") - (optBind "/sys/class") - (optBind "/sys/dev") - (optBind "/sys/devices") - ] - ++ optionals app.nix [ - (bind "/nix/var") - ] - ++ optionals isGraphical [ - (optDevBind "/dev/dri") - (optDevBind "/dev/nvidiactl") - (optDevBind "/dev/nvidia-modeset") - (optDevBind "/dev/nvidia-uvm") - (optDevBind "/dev/nvidia-uvm-tools") - (optDevBind "/dev/nvidia0") - ] - ++ optionals app.useCommonPaths cfg.commonPaths - ++ app.extraPaths - ++ [ - { - type = "bind"; - dst = "/etc/"; - src = "/etc/"; - special = true; - } - { - type = "link"; - dst = "/run/current-system"; - linkname = "/run/current-system"; - dereference = true; - } - ] - ++ optionals (isGraphical && config.hardware.graphics.enable) ( - [ - { - type = "link"; - dst = "/run/opengl-driver"; - linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument; - } - ] - ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ - { - type = "link"; - dst = "/run/opengl-driver-32"; - linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument; - } - ] - ) - ++ [ - { - type = "bind"; - src = getsubhome userid app.identity; - write = true; - ensure = true; - } - ]; - - username = getsubname userid app.identity; - inherit (cfg) shell; - home = getsubhome userid app.identity; - - path = - if app.path == null then - pkgs.writeScript "${app.name}-start" '' - #!${pkgs.zsh}${pkgs.zsh.shellPath} - ${script} - '' - else - app.path; - args = if app.args == null then [ "${app.name}-start" ] else app.args; - }; - }; - - checkedConfig = - name: value: - let - file = pkgs.writeText name (builtins.toJSON value); - in - pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } '' - ln -vs ${file} "$out" - hakurei show --no-store ${file} - ''; - in - pkgs.writeShellScriptBin app.name '' - exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ - '' - ) - ] - ++ ( - let - pkg = if app.share != null then app.share else pkgs.${app.name}; - copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; - in - optional (app.enablements.wayland || app.enablements.x11) ( - pkgs.runCommand "${app.name}-share" { } '' - mkdir -p $out/share - ${copy "${pkg}/share/applications"} - ${copy "${pkg}/share/pixmaps"} - ${copy "${pkg}/share/icons"} - ${copy "${pkg}/share/man"} - - if test -d "$out/share/applications"; then - substituteInPlace $out/share/applications/* \ - --replace-warn '${pkg}/bin/' "" \ - --replace-warn '${pkg}/libexec/' "" - fi - '' - ) - ) - ++ acc - ) [ cfg.package ] cfg.apps; - }) cfg.users; - in - { - useUserPackages = false; # prevent users.users entries from being added - - users = - mkMerge - (foldlAttrs - ( - acc: _: fid: - foldlAttrs - ( - acc: _: app: - ( - let - key = getsubname fid app.identity; - in - { - usernames = acc.usernames // { - ${key} = true; - }; - merge = acc.merge ++ [ - { - ${key} = mkMerge ( - [ - app.extraConfig - { home.packages = app.packages; } - ] - ++ lib.optional (!attrsets.hasAttrByPath [ key ] acc.usernames) cfg.extraHomeConfig - ); - } - ]; - } - ) - ) - { - inherit (acc) usernames; - merge = acc.merge ++ [ { ${getsubname fid 0} = cfg.extraHomeConfig; } ]; - } - cfg.apps - ) - { - usernames = { }; - merge = [ privPackages ]; - } - cfg.users - ).merge; - }; - - users = - let - getuser = userid: appid: { - isSystemUser = true; - createHome = true; - description = "Hakurei subordinate user ${toString appid} (u${toString userid})"; - group = getsubname userid appid; - home = getsubhome userid appid; - uid = getsubuid userid appid; - }; - getgroup = userid: appid: { gid = getsubuid userid appid; }; - in - { - users = mkMerge ( - foldlAttrs - ( - acc: username: fid: - acc - ++ - foldlAttrs - ( - acc': _: app: - acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ] - ) - [ - { - ${getsubname fid 0} = getuser fid 0; - ${username}.extraGroups = [ cfg.sharefs.group ]; - } - ] - cfg.apps - ) - (optional (cfg.sharefs.source != null) { - ${cfg.sharefs.user} = { - uid = lib.mkDefault 1023; - inherit (cfg.sharefs) group; - isSystemUser = true; - home = cfg.sharefs.source; - }; - }) - cfg.users - ); - - groups = mkMerge ( - foldlAttrs - ( - acc: _: fid: - acc - ++ foldlAttrs ( - acc': _: app: - acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ] - ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps - ) - (optional (cfg.sharefs.source != null) { - ${cfg.sharefs.group} = { - gid = lib.mkDefault 1023; - }; - }) - cfg.users - ); - }; - }; -} diff --git a/cmd/hakurei/testsuite/options.nix b/cmd/hakurei/testsuite/options.nix deleted file mode 100644 index f624b6f5..00000000 --- a/cmd/hakurei/testsuite/options.nix +++ /dev/null @@ -1,364 +0,0 @@ -packages: -{ - lib, - pkgs, - config, - ... -}: - -let - inherit (lib) types mkOption mkEnableOption; - - cfg = config.environment.hakurei; -in - -{ - options = { - environment.hakurei = { - enable = mkEnableOption "hakurei"; - - package = mkOption { - type = types.package; - default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei; - description = "The hakurei package to use."; - }; - - hsuPackage = mkOption { - type = types.package; - default = packages.${pkgs.stdenv.hostPlatform.system}.hsu; - description = "The hsu package to use."; - }; - - users = mkOption { - type = - let - inherit (types) attrsOf ints; - in - attrsOf (ints.between 0 99); - description = '' - Users allowed to spawn hakurei apps and their corresponding hakurei identity. - ''; - }; - - extraHomeConfig = mkOption { - type = types.anything; - description = '' - Extra home-manager configuration to merge with all target users. - ''; - }; - - sharefs = { - package = mkOption { - type = types.package; - default = pkgs.linkFarm "sharefs" { - "bin/sharefs" = "${cfg.package}/libexec/sharefs"; - "bin/mount.fuse.sharefs" = "${cfg.package}/libexec/sharefs"; - }; - description = "The sharefs package to use."; - }; - - user = mkOption { - type = types.str; - default = "sharefs"; - description = '' - Name of the user to run the sharefs daemon as. - ''; - }; - - group = mkOption { - type = types.str; - default = "sharefs"; - description = '' - Name of the group to run the sharefs daemon as. - ''; - }; - - name = mkOption { - type = types.str; - default = "/sdcard"; - description = '' - Host path to mount sharefs on. - ''; - }; - - source = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Writable backing directory. Setting this to null disables sharefs. - ''; - }; - }; - - apps = mkOption { - type = - let - inherit (types) - int - ints - str - bool - enum - package - anything - submodule - listOf - attrsOf - nullOr - functionTo - ; - in - attrsOf (submodule { - options = { - name = mkOption { - type = str; - description = '' - Name of the app's launcher script. - ''; - }; - - verbose = mkEnableOption "launchers with verbose output"; - - identity = mkOption { - type = ints.between 1 9999; - description = '' - Application identity. Identity 0 is reserved for system services. - ''; - }; - shareUid = mkEnableOption "sharing identity with another application"; - - packages = mkOption { - type = listOf package; - default = [ ]; - description = '' - List of extra packages to install via home-manager. - ''; - }; - - extraConfig = mkOption { - type = anything; - default = { }; - description = '' - Extra home-manager configuration. - ''; - }; - - path = mkOption { - type = nullOr str; - default = null; - description = '' - Custom executable path. - Setting this to null will default to the start script. - ''; - }; - - args = mkOption { - type = nullOr (listOf str); - default = null; - description = '' - Custom args. - Setting this to null will default to script name. - ''; - }; - - script = mkOption { - type = nullOr str; - default = null; - description = '' - Application launch script. - ''; - }; - - command = mkOption { - type = nullOr str; - default = null; - description = '' - Command to run as the target user. - Setting this to null will default command to launcher name. - Has no effect when script is set. - ''; - }; - - groups = mkOption { - type = listOf str; - default = [ ]; - description = '' - List of groups to inherit from the privileged user. - ''; - }; - - shareRuntime = mkEnableOption "sharing of XDG_RUNTIME_DIR between containers under the same identity"; - shareTmpdir = mkEnableOption "sharing of TMPDIR between containers under the same identity"; - - dbus = { - session = mkOption { - type = nullOr (functionTo anything); - default = null; - description = '' - D-Bus session bus custom configuration. - Setting this to null will enable built-in defaults. - ''; - }; - - system = mkOption { - type = nullOr anything; - default = null; - description = '' - D-Bus system bus custom configuration. - Setting this to null will disable the system bus proxy. - ''; - }; - }; - - env = mkOption { - type = nullOr (attrsOf str); - default = null; - description = '' - Environment variables to set for the initial process in the sandbox. - ''; - }; - - wait_delay = mkOption { - type = nullOr int; - default = null; - description = '' - Duration to wait for after interrupting a container's initial process in nanoseconds. - A negative value causes the container to be terminated immediately on cancellation. - Setting this to null defaults to five seconds. - ''; - }; - - devel = mkEnableOption "debugging-related kernel interfaces"; - userns = mkEnableOption "user namespace creation"; - tty = mkEnableOption "access to the controlling terminal"; - multiarch = mkEnableOption "multiarch kernel-level support"; - - hostNet = mkEnableOption "share host net namespace" // { - default = true; - }; - hostAbstract = mkEnableOption "share abstract unix socket scope"; - - schedPolicy = mkOption { - type = nullOr (enum [ - "fifo" - "rr" - "batch" - "idle" - "deadline" - "ext" - ]); - default = null; - description = '' - Scheduling policy to set for the container. - The zero value retains the current scheduling policy. - ''; - }; - schedPriority = mkOption { - type = nullOr (ints.between 1 99); - default = null; - description = '' - Scheduling priority to set for the container. - ''; - }; - - nix = mkEnableOption "nix daemon access"; - mapRealUid = mkEnableOption "mapping to priv-user uid"; - device = mkEnableOption "access to all devices"; - insecureWayland = mkEnableOption "direct access to the Wayland socket"; - - gpu = mkOption { - type = nullOr bool; - default = null; - description = '' - Target process GPU and driver access. - Setting this to null will enable GPU whenever X or Wayland is enabled. - ''; - }; - - useCommonPaths = mkEnableOption "common extra paths" // { - default = true; - }; - - extraPaths = mkOption { - type = listOf (attrsOf anything); - default = [ ]; - description = '' - Extra paths to make available to the container. - ''; - }; - - enablements = { - wayland = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to share the Wayland server via security-context-v1. - ''; - }; - - x11 = mkOption { - type = nullOr bool; - default = false; - description = '' - Whether to share the X11 socket and allow connection. - ''; - }; - - dbus = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to proxy D-Bus. - ''; - }; - - pipewire = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket. - ''; - }; - }; - - share = mkOption { - type = nullOr package; - default = null; - description = '' - Package containing share files. - Setting this to null will default package name to wrapper name. - ''; - }; - }; - }); - default = { }; - description = '' - Declaratively configured hakurei apps. - ''; - }; - - commonPaths = mkOption { - type = types.listOf (types.attrsOf types.anything); - default = [ ]; - description = '' - Common extra paths to make available to the container. - ''; - }; - - shell = mkOption { - type = types.str; - default = "/run/current-system/sw/bin/bash"; - description = '' - Absolute path to preferred shell. - ''; - }; - - stateDir = mkOption { - type = types.str; - description = '' - The state directory where app home directories are stored. - ''; - }; - }; - }; -} diff --git a/cmd/hakurei/testsuite/package.nix b/cmd/hakurei/testsuite/package.nix deleted file mode 100644 index 12196cf6..00000000 --- a/cmd/hakurei/testsuite/package.nix +++ /dev/null @@ -1,145 +0,0 @@ -{ - lib, - stdenv, - buildGo127Module, - makeBinaryWrapper, - xdg-dbus-proxy, - pkg-config, - libffi, - libseccomp, - acl, - wayland, - wayland-protocols, - wayland-scanner, - - libxcb, - libxau, - libxdmcp, - - # for sharefs - fuse3, - - # for passthru.buildInputs - go_1_27, - clang, - xorgproto, - - # for check - util-linux, - nettools, - - glibc, # for ldd - withStatic ? stdenv.hostPlatform.isStatic, -}: - -buildGo127Module rec { - pname = "hakurei"; - version = with lib.strings; removePrefix "v" (trim (builtins.readFile ../../dist/VERSION)); - - srcFiltered = builtins.path { - name = "${pname}-src"; - path = lib.cleanSource ../../../.; - filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/hsu" path); - }; - vendorHash = null; - - src = stdenv.mkDerivation { - name = "${pname}-src-full"; - inherit version; - enableParallelBuilding = true; - src = srcFiltered; - - buildInputs = [ - wayland - wayland-protocols - ]; - - nativeBuildInputs = [ - go_1_27 - pkg-config - wayland-scanner - ]; - - buildPhase = "GOCACHE=$(mktemp -d) go generate ./..."; - installPhase = "cp -r . $out"; - }; - - ldflags = - lib.attrsets.foldlAttrs - ( - ldflags: name: value: - ldflags ++ [ "-X hakurei.app/internal/info.${name}=${value}" ] - ) - ( - [ "-s -w" ] - ++ lib.optionals withStatic [ - "-linkmode external" - "-extldflags \"-static\"" - ] - ) - { - buildVersion = "v${version}"; - hakureiPath = "${placeholder "out"}/libexec/hakurei"; - hsuPath = "/run/wrappers/bin/hsu"; - }; - - env = { - # use clang instead of gcc - CC = "clang -O3 -Werror"; - }; - - buildInputs = [ - libffi - libseccomp - fuse3 - acl - wayland - util-linux - - libxcb - libxau - libxdmcp - ]; - - nativeBuildInputs = [ - pkg-config - makeBinaryWrapper - - # for container example - nettools - ]; - - postInstall = - let - appPackages = [ - glibc - xdg-dbus-proxy - ]; - in - '' - install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/* - - mkdir "$out/libexec" - mv "$out"/bin/* "$out/libexec/" - - makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \ - --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages} - ''; - - passthru = { - go = go_1_27; - - targetPkgs = [ - go_1_27 - clang - xorgproto - util-linux - - # for go generate - wayland-protocols - wayland-scanner - ] - ++ buildInputs - ++ nativeBuildInputs; - }; -} diff --git a/cmd/hakurei/testsuite/test.py b/cmd/hakurei/testsuite/test.py deleted file mode 100644 index 98f08275..00000000 --- a/cmd/hakurei/testsuite/test.py +++ /dev/null @@ -1,315 +0,0 @@ -import json -import shlex - -q = shlex.quote -NODE_GROUPS = ["nodes", "floating_nodes"] - - -def swaymsg(command: str = "", succeed=True, type="command"): - assert command != "" or type != "command", "Must specify command or type" - shell = q(f"swaymsg -t {q(type)} -- {q(command)}") - with machine.nested(f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)): - ret = (machine.succeed if succeed else machine.execute)( - f"su - alice -c {shell}" - ) - - # execute also returns a status code, but disregard. - if not succeed: - _, ret = ret - - if not succeed and not ret: - return None - - parsed = json.loads(ret) - return parsed - - -def walk(tree): - yield tree - for group in NODE_GROUPS: - for node in tree.get(group, []): - yield from walk(node) - - -def wait_for_window(pattern): - def func(last_chance): - nodes = (node["name"] for node in walk(swaymsg(type="get_tree"))) - - if last_chance: - nodes = list(nodes) - machine.log(f"Last call! Current list of windows: {nodes}") - - return any(pattern in name for name in nodes) - - retry(func) - - -def collect_state_ui(name): - swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'") - machine.wait_for_file(f"/tmp/{name}.ps") - machine.copy_from_vm(f"/tmp/{name}.ps", "") - swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'") - machine.wait_for_file(f"/tmp/{name}.json") - machine.copy_from_vm(f"/tmp/{name}.json", "") - machine.screenshot(name) - - -def check_state(name, enablements): - instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps")) - if len(instances) != 1: - raise Exception(f"unexpected state length {len(instances)}") - instance = instances[0] - - command = f"{name}-start" - if not (instance['container']['path'].startswith("/nix/store/")) or not (instance['container']['path'].endswith(command)): - raise Exception(f"unexpected path {instance['path']}") - - if len(instance['container']['args']) != 1 or instance['container']['args'][0] != command: - raise Exception(f"unexpected args {instance['args']}") - - if instance['enablements'] != enablements: - raise Exception(f"unexpected enablements {instance['enablements']['enablements']}") - - -def hakurei(command): - swaymsg(f"exec hakurei {command}") - - -start_all() -machine.wait_for_unit("multi-user.target") - -# To check hakurei's version: -print(machine.succeed("sudo -u alice -i hakurei version")) - -# Wait for Sway to complete startup: -machine.wait_for_file("/run/user/1000/wayland-1") -machine.wait_for_file("/tmp/sway-ipc.sock") - -# Run hakurei Go tests outside of nix build in the background: -swaymsg("exec hakurei-test") - -# Deny unmapped uid: -denyOutput = machine.fail("sudo -u untrusted -i hakurei exec &>/dev/stdout") -print(denyOutput) -denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v exec &>/dev/stdout") -print(denyOutputVerbose) - -# Direct hsu call: -userid = machine.succeed("sudo -u alice -i hsu") -if userid != "0": - raise Exception(f"unexpected userid: {userid}") - -# Verify hsu fault behaviour: -if denyOutput != "hsu: uid 1001 is not in the hsurc file\n": - raise Exception(f"unexpected deny output:\n{denyOutput}") -if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot retrieve user id from setuid wrapper: current user is not in the hsurc file\n": - raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}") - -# Verify timeout behaviour: -machine.succeed('sudo -u alice -i hakurei-check-linger-timeout > /var/tmp/linger-stdout 2> /var/tmp/linger-stderr || (cat /var/tmp/linger-stderr; false)') -linger_stdout = machine.succeed("cat /var/tmp/linger-stdout") -linger_stderr = machine.succeed("cat /var/tmp/linger-stderr") -if linger_stdout != "": - raise Exception(f"unexpected stdout: {linger_stdout}") -if linger_stderr != "init: timeout exceeded waiting for lingering processes\n": - raise Exception(f"unexpected stderr: {linger_stderr}") - -check_offset = 0 - - -def hakurei_identity(offset): - return 1+check_offset+offset - - -# Start hakurei permissive defaults outside Wayland session: -print(machine.succeed("sudo -u alice -i hakurei -v exec -a 0 touch /tmp/pd-bare-ok")) -machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok") - -# Verify silent output permissive defaults: -output = machine.succeed("sudo -u alice -i hakurei exec -a 0 true &>/dev/stdout") -if output != "": - raise Exception(f"unexpected output\n{output}") - -# Verify silent output permissive defaults signal: -def silent_output_interrupt(flags): - swaymsg("exec foot") - wait_for_window("alice@machine") - # identity 0 does not have home-manager - machine.send_chars(f"exec hakurei exec {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n") - machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready") - machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready") - machine.send_key("ctrl-c") - machine.wait_until_fails("pgrep foot") - machine.wait_until_fails(f"pgrep -u alice -f 'hakurei exec {flags}-a 0 '") - output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent") - if output != "": - raise Exception(f"unexpected output\n{output}") - - -silent_output_interrupt("") -silent_output_interrupt("--dbus ") # this one is especially painful as it maintains a helper -silent_output_interrupt("--wayland -X --dbus --pulse ") - -# Verify graceful failure on bad Wayland display name: -print(machine.fail("sudo -u alice -i hakurei -v exec --wayland true")) - -# Start hakurei permissive defaults within Wayland session: -hakurei('-v exec --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok') -machine.wait_for_file("/tmp/dbus-ok") -collect_state_ui("dbus_notify_exited") -# not in pid namespace, verify termination -machine.wait_until_fails("pgrep xdg-dbus-proxy") -machine.succeed("pkill -9 mako") - -# Check revert type selection: -hakurei("-v exec --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok") -wait_for_window("p0@machine") -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -hakurei("-v exec --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok") -wait_for_window("p1@machine") -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -machine.send_chars("exit\n") -machine.wait_for_file("/tmp/p1-exit-ok") -# Verify acl is kept alive: -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -machine.send_chars("exit\n") -machine.wait_for_file("/tmp/p0-exit-ok") -machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000") - -# Check invalid identifier fd behaviour: -machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v run --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd') -machine.wait_for_file("/tmp/invalid-identifier-fd") -print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd')) - -# Check interrupt shim behaviour: -swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/monitor-exit-code") -interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) -if interrupt_exit_code != 230: - raise Exception(f"unexpected exit code {interrupt_exit_code}") - -# Check interrupt shim behaviour immediate termination: -swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/monitor-exit-code") -interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) -if interrupt_exit_code != 254: - raise Exception(f"unexpected exit code {interrupt_exit_code}") - -# Check shim SIGCONT from unexpected process behaviour: -swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -CONT -f 'hakurei shim'") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/shim-cont-unexpected-pid") -print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid')) - -# Check setscheduler: -sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v exec cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_unset != 0: - raise Exception(f"unexpected unset policy: {sched_unset}") -sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_idle != 5: - raise Exception(f"unexpected idle policy: {sched_idle}") -sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_rr != 2: - raise Exception(f"unexpected round-robin policy: {sched_idle}") - -# Start app (foot) with Wayland enablement: -swaymsg("exec ne-foot") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/client-ok\n") -machine.wait_for_file("/var/tmp/client-ok") -collect_state_ui("foot_wayland") -check_state("ne-foot", {"wayland": True}) -# Verify lack of acl on XDG_RUNTIME_DIR: -machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}") -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}") - -# Test pipewire-pulse: -swaymsg("exec pa-foot") -wait_for_window(f"u0_a{hakurei_identity(1)}@machine") -machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n") -machine.wait_for_file("/var/tmp/pulse-ok") -collect_state_ui("pulse_wayland") -check_state("pa-foot", {"wayland": True, "pipewire": True}) -machine.fail("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -machine.wait_until_fails("pgrep -x hakurei") -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") -# Test PipeWire SecurityContext: -machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl info") -machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle") -# Test PipeWire direct access: -machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump") -machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pipewire pw-dump") - -# Test XWayland (foot does not support X): -swaymsg("exec x11-alacritty") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; glinfo && touch /var/tmp/x11-ok\n") -machine.wait_for_file("/var/tmp/x11-ok") -collect_state_ui("alacritty_x11") -check_state("x11-alacritty", {"x11": True}) -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep alacritty") - -# Start app (foot) with direct Wayland access: -swaymsg("exec da-foot") -wait_for_window(f"u0_a{hakurei_identity(3)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/direct-ok\n") -collect_state_ui("foot_direct") -machine.wait_for_file("/var/tmp/direct-ok") -check_state("da-foot", {"wayland": True}) -# Verify acl on XDG_RUNTIME_DIR: -print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}")) -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -# Verify acl cleanup on XDG_RUNTIME_DIR: -machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}") - -# Test syscall filter: -print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure")) - -# Start app (foot) with Wayland enablement from a terminal: -swaymsg("exec foot $SHELL -c '(ne-foot) & disown && exec $SHELL'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/term-ok\n") -machine.wait_for_file("/var/tmp/term-ok") -machine.send_key("alt-h") -machine.send_chars("clear; hakurei show $(hakurei ps --short) && touch /tmp/ps-show-ok && exec cat\n") -machine.wait_for_file("/tmp/ps-show-ok") -collect_state_ui("foot_wayland_term") -check_state("ne-foot", {"wayland": True}) -machine.send_key("alt-l") -machine.send_chars("exit\n") -wait_for_window("alice@machine") -machine.send_key("ctrl-c") -machine.wait_until_fails("pgrep foot") - -# Exit Sway and verify process exit status 0: -machine.wait_until_fails("pgrep -x hakurei") -swaymsg("exit", succeed=False) -machine.wait_for_file("/tmp/sway-exit-ok") - -# Print hakurei share and rundir contents: -print(machine.succeed("find /tmp/hakurei.0 " - + "-path '/tmp/hakurei.0/runtime/*/*' -prune -o " - + "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o " - + "-print")) -print(machine.succeed("find /run/user/1000/hakurei")) -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") - -# Verify go test status: -machine.wait_for_file("/tmp/hakurei-test-done") -print(machine.succeed("cat /tmp/hakurei-test.log")) -machine.wait_for_file("/tmp/hakurei-test-ok") |
