From 4ffeec3004607b76f73e504da220a1490c598348 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Fri, 15 Aug 2025 04:57:37 +0900 Subject: [PATCH] hst/enablement: editor friendly enablement adaptor Having the bit field value here (in decimal, no less) is unfriendly to text editors. Use a bunch of booleans here to improve ease of use. Signed-off-by: Ophestra --- cmd/hakurei/command.go | 10 ++- cmd/hakurei/print.go | 2 +- cmd/hakurei/print_test.go | 18 ++++- cmd/hpkg/app.go | 3 +- cmd/hpkg/build.nix | 7 +- cmd/hpkg/test/test.py | 2 +- hst/config.go | 3 +- hst/enablement.go | 69 +++++++++++++++++ hst/enablement_test.go | 108 +++++++++++++++++++++++++++ hst/template.go | 2 +- hst/template_test.go | 6 +- internal/app/app_nixos_linux_test.go | 2 +- internal/app/app_pd_linux_test.go | 2 +- internal/app/process_linux.go | 2 +- internal/app/seal_linux.go | 10 +-- nixos.nix | 9 +-- options.nix | 10 +-- test/configuration.nix | 12 +-- test/interactive/hakurei.nix | 2 +- test/test.py | 10 +-- 20 files changed, 243 insertions(+), 46 deletions(-) create mode 100644 hst/enablement.go create mode 100644 hst/enablement_test.go diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index d1d6730..2eee5de 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -124,18 +124,20 @@ func buildCommand(out io.Writer) command.Command { config.Data = a } + var e system.Enablement if wayland { - config.Enablements |= system.EWayland + e |= system.EWayland } if x11 { - config.Enablements |= system.EX11 + e |= system.EX11 } if dBus { - config.Enablements |= system.EDBus + e |= system.EDBus } if pulse { - config.Enablements |= system.EPulse + e |= system.EPulse } + config.Enablements = hst.NewEnablements(e) // parse D-Bus config file from flags if applicable if dBus { diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index ff3ce64..78c34be 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -78,7 +78,7 @@ func printShowInstance( } else { t.Printf(" Identity:\t%d\n", config.Identity) } - t.Printf(" Enablements:\t%s\n", config.Enablements.String()) + t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String()) if len(config.Groups) > 0 { t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) } diff --git a/cmd/hakurei/print_test.go b/cmd/hakurei/print_test.go index 0935de2..c2f97ec 100644 --- a/cmd/hakurei/print_test.go +++ b/cmd/hakurei/print_test.go @@ -195,7 +195,11 @@ App "--enable-features=UseOzonePlatform", "--ozone-platform=wayland" ], - "enablements": 13, + "enablements": { + "wayland": true, + "dbus": true, + "pulse": true + }, "session_bus": { "see": null, "talk": [ @@ -339,7 +343,11 @@ App "--enable-features=UseOzonePlatform", "--ozone-platform=wayland" ], - "enablements": 13, + "enablements": { + "wayland": true, + "dbus": true, + "pulse": true + }, "session_bus": { "see": null, "talk": [ @@ -537,7 +545,11 @@ func Test_printPs(t *testing.T) { "--enable-features=UseOzonePlatform", "--ozone-platform=wayland" ], - "enablements": 13, + "enablements": { + "wayland": true, + "dbus": true, + "pulse": true + }, "session_bus": { "see": null, "talk": [ diff --git a/cmd/hpkg/app.go b/cmd/hpkg/app.go index 131f839..4493dad 100644 --- a/cmd/hpkg/app.go +++ b/cmd/hpkg/app.go @@ -8,7 +8,6 @@ import ( "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/hst" - "hakurei.app/system" "hakurei.app/system/dbus" ) @@ -41,7 +40,7 @@ type appInfo struct { // passed through to [hst.Config] SessionBus *dbus.Config `json:"session_bus,omitempty"` // passed through to [hst.Config] - Enablements system.Enablement `json:"enablements"` + Enablements *hst.Enablements `json:"enablements,omitempty"` // passed through to [hst.Config] Multiarch bool `json:"multiarch,omitempty"` diff --git a/cmd/hpkg/build.nix b/cmd/hpkg/build.nix index 13f3144..3733f42 100644 --- a/cmd/hpkg/build.nix +++ b/cmd/hpkg/build.nix @@ -171,7 +171,12 @@ let broadcast = { }; }); - enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0); + enablements = { + wayland = allow_wayland; + x11 = allow_x11; + dbus = allow_dbus; + pulse = allow_pulse; + }; mesa = if gpu then mesaWrappers else null; nix_gl = if gpu then nixGL else null; diff --git a/cmd/hpkg/test/test.py b/cmd/hpkg/test/test.py index c994435..5a199ac 100644 --- a/cmd/hpkg/test/test.py +++ b/cmd/hpkg/test/test.py @@ -92,7 +92,7 @@ wait_for_window("hakurei@machine-foot") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client") collect_state_ui("app_wayland") -check_state("foot", 13) +check_state("foot", {"wayland": True, "dbus": True, "pulse": True}) # Verify acl on XDG_RUNTIME_DIR: print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")) machine.send_chars("exit\n") diff --git a/hst/config.go b/hst/config.go index c34ecb1..44d3638 100644 --- a/hst/config.go +++ b/hst/config.go @@ -3,7 +3,6 @@ package hst import ( "hakurei.app/container" - "hakurei.app/system" "hakurei.app/system/dbus" ) @@ -24,7 +23,7 @@ type Config struct { Args []string `json:"args"` // system services to make available in the container - Enablements system.Enablement `json:"enablements"` + Enablements *Enablements `json:"enablements,omitempty"` // session D-Bus proxy configuration; // nil makes session bus proxy assume built-in defaults diff --git a/hst/enablement.go b/hst/enablement.go new file mode 100644 index 0000000..021288b --- /dev/null +++ b/hst/enablement.go @@ -0,0 +1,69 @@ +package hst + +import ( + "encoding/json" + "syscall" + + "hakurei.app/system" +) + +// NewEnablements returns the address of [system.Enablement] as [Enablements]. +func NewEnablements(e system.Enablement) *Enablements { return (*Enablements)(&e) } + +// enablementsJSON is the [json] representation of the [system.Enablement] bit field. +type enablementsJSON struct { + Wayland bool `json:"wayland,omitempty"` + X11 bool `json:"x11,omitempty"` + DBus bool `json:"dbus,omitempty"` + Pulse bool `json:"pulse,omitempty"` +} + +// Enablements is the [json] adapter for [system.Enablement]. +type Enablements system.Enablement + +// Unwrap returns the underlying [system.Enablement]. +func (e *Enablements) Unwrap() system.Enablement { + if e == nil { + return 0 + } + return system.Enablement(*e) +} + +func (e *Enablements) MarshalJSON() ([]byte, error) { + if e == nil { + return nil, syscall.EINVAL + } + return json.Marshal(&enablementsJSON{ + Wayland: system.Enablement(*e)&system.EWayland != 0, + X11: system.Enablement(*e)&system.EX11 != 0, + DBus: system.Enablement(*e)&system.EDBus != 0, + Pulse: system.Enablement(*e)&system.EPulse != 0, + }) +} + +func (e *Enablements) UnmarshalJSON(data []byte) error { + if e == nil { + return syscall.EINVAL + } + + v := new(enablementsJSON) + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + var ve system.Enablement + if v.Wayland { + ve |= system.EWayland + } + if v.X11 { + ve |= system.EX11 + } + if v.DBus { + ve |= system.EDBus + } + if v.Pulse { + ve |= system.EPulse + } + *e = Enablements(ve) + return nil +} diff --git a/hst/enablement_test.go b/hst/enablement_test.go new file mode 100644 index 0000000..c132dd3 --- /dev/null +++ b/hst/enablement_test.go @@ -0,0 +1,108 @@ +package hst_test + +import ( + "encoding/json" + "errors" + "syscall" + "testing" + + "hakurei.app/hst" + "hakurei.app/system" +) + +func TestEnablements(t *testing.T) { + testCases := []struct { + name string + e *hst.Enablements + data string + sData string + }{ + {"nil", nil, "null", `{"value":null,"magic":3236757504}`}, + {"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`}, + {"wayland", hst.NewEnablements(system.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`}, + {"x11", hst.NewEnablements(system.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`}, + {"dbus", hst.NewEnablements(system.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`}, + {"pulse", hst.NewEnablements(system.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`}, + {"all", hst.NewEnablements(system.EWayland | system.EX11 | system.EDBus | system.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("marshal", func(t *testing.T) { + if got, err := json.Marshal(tc.e); err != nil { + t.Fatalf("Marshal: error = %v", err) + } else if string(got) != tc.data { + t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.data) + } + + if got, err := json.Marshal(struct { + Value *hst.Enablements `json:"value"` + Magic int `json:"magic"` + }{tc.e, syscall.MS_MGC_VAL}); err != nil { + t.Fatalf("Marshal: error = %v", err) + } else if string(got) != tc.sData { + t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.sData) + } + }) + + t.Run("unmarshal", func(t *testing.T) { + { + got := new(hst.Enablements) + if err := json.Unmarshal([]byte(tc.data), &got); err != nil { + t.Fatalf("Unmarshal: error = %v", err) + } + if tc.e == nil { + if got != nil { + t.Errorf("Unmarshal: %v", got) + } + } else if *got != *tc.e { + t.Errorf("Unmarshal: %v, want %v", got, tc.e) + } + } + + { + got := *(new(struct { + Value *hst.Enablements `json:"value"` + Magic int `json:"magic"` + })) + if err := json.Unmarshal([]byte(tc.sData), &got); err != nil { + t.Fatalf("Unmarshal: error = %v", err) + } + if tc.e == nil { + if got.Value != nil { + t.Errorf("Unmarshal: %v", got) + } + } else if *got.Value != *tc.e { + t.Errorf("Unmarshal: %v, want %v", got.Value, tc.e) + } + } + }) + }) + } + + t.Run("unwrap", func(t *testing.T) { + t.Run("nil", func(t *testing.T) { + if got := (*hst.Enablements)(nil).Unwrap(); got != 0 { + t.Errorf("Unwrap: %v", got) + } + }) + + t.Run("val", func(t *testing.T) { + if got := hst.NewEnablements(system.EWayland | system.EPulse).Unwrap(); got != system.EWayland|system.EPulse { + t.Errorf("Unwrap: %v", got) + } + }) + }) + + t.Run("passthrough", func(t *testing.T) { + if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) { + t.Errorf("MarshalJSON: error = %v", err) + } + if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) { + t.Errorf("UnmarshalJSON: error = %v", err) + } + if err := new(hst.Enablements).UnmarshalJSON([]byte{}); err == nil { + t.Errorf("UnmarshalJSON: error = %v", err) + } + }) +} diff --git a/hst/template.go b/hst/template.go index 82e0b7d..ea27fbb 100644 --- a/hst/template.go +++ b/hst/template.go @@ -21,7 +21,7 @@ func Template() *Config { "--ozone-platform=wayland", }, - Enablements: system.EWayland | system.EDBus | system.EPulse, + Enablements: NewEnablements(system.EWayland | system.EDBus | system.EPulse), SessionBus: &dbus.Config{ See: nil, diff --git a/hst/template_test.go b/hst/template_test.go index 7693378..31b9013 100644 --- a/hst/template_test.go +++ b/hst/template_test.go @@ -18,7 +18,11 @@ func TestTemplate(t *testing.T) { "--enable-features=UseOzonePlatform", "--ozone-platform=wayland" ], - "enablements": 13, + "enablements": { + "wayland": true, + "dbus": true, + "pulse": true + }, "session_bus": { "see": null, "talk": [ diff --git a/internal/app/app_nixos_linux_test.go b/internal/app/app_nixos_linux_test.go index ef58722..d762ca0 100644 --- a/internal/app/app_nixos_linux_test.go +++ b/internal/app/app_nixos_linux_test.go @@ -23,7 +23,7 @@ var testCasesNixos = []sealTestCase{ &hst.Config{ ID: "org.chromium.Chromium", Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"), - Enablements: system.EWayland | system.EDBus | system.EPulse, + Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse), Shell: m("/run/current-system/sw/bin/zsh"), Container: &hst.ContainerConfig{ diff --git a/internal/app/app_pd_linux_test.go b/internal/app/app_pd_linux_test.go index cc738ed..109e881 100644 --- a/internal/app/app_pd_linux_test.go +++ b/internal/app/app_pd_linux_test.go @@ -106,7 +106,7 @@ var testCasesPd = []sealTestCase{ }, Filter: true, }, - Enablements: system.EWayland | system.EDBus | system.EPulse, + Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse), }, state.ID{ 0xeb, 0xf0, 0x83, 0xd1, diff --git a/internal/app/process_linux.go b/internal/app/process_linux.go index 11d4c8e..c6df6fe 100644 --- a/internal/app/process_linux.go +++ b/internal/app/process_linux.go @@ -64,7 +64,7 @@ func (seal *outcome) Run(rs *RunState) error { // accumulate enablements of remaining launchers for i, s := range states { if s.Config != nil { - rt |= s.Config.Enablements + rt |= s.Config.Enablements.Unwrap() } else { log.Printf("state entry %d does not contain config", i) } diff --git a/internal/app/seal_linux.go b/internal/app/seal_linux.go index 961447b..d0a8a54 100644 --- a/internal/app/seal_linux.go +++ b/internal/app/seal_linux.go @@ -247,7 +247,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // bind GPU stuff - if config.Enablements&(system.EX11|system.EWayland) != 0 { + if config.Enablements.Unwrap()&(system.EX11|system.EWayland) != 0 { conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Src: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}}) } // opportunistically bind kvm @@ -348,7 +348,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.env[term] = t } - if config.Enablements&system.EWayland != 0 { + if config.Enablements.Unwrap()&system.EWayland != 0 { // outer wayland socket (usually `/run/user/%d/wayland-%d`) var socketPath *container.Absolute if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { @@ -381,7 +381,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } } - if config.Enablements&system.EX11 != 0 { + if config.Enablements.Unwrap()&system.EX11 != 0 { if d, ok := sys.LookupEnv(display); !ok { return hlog.WrapErr(ErrXDisplay, "DISPLAY is not set") @@ -393,7 +393,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } } - if config.Enablements&system.EPulse != 0 { + if config.Enablements.Unwrap()&system.EPulse != 0 { // PulseAudio runtime directory (usually `/run/user/%d/pulse`) pulseRuntimeDir := share.sc.RuntimePath.Append("pulse") // PulseAudio socket (usually `/run/user/%d/pulse/native`) @@ -442,7 +442,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } } - if config.Enablements&system.EDBus != 0 { + if config.Enablements.Unwrap()&system.EDBus != 0 { // ensure dbus session bus defaults if config.SessionBus == nil { config.SessionBus = dbus.NewConfig(config.ID, true, true) diff --git a/nixos.nix b/nixos.nix index 5d05143..4f083bc 100644 --- a/nixos.nix +++ b/nixos.nix @@ -102,8 +102,7 @@ in }; command = if app.command == null then app.name else app.command; script = if app.script == null then ("exec " + command + " $@") else app.script; - enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0); - isGraphical = if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11; + isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11; conf = { path = @@ -116,7 +115,7 @@ in app.path; args = if app.args == null then [ "${app.name}-start" ] else app.args; - inherit id enablements; + inherit id; inherit (dbusConfig) session_bus system_bus; direct_wayland = app.insecureWayland; @@ -125,7 +124,7 @@ in data = getsubhome fid app.identity; inherit (cfg) shell; - inherit (app) identity groups; + inherit (app) identity groups enablements; container = { inherit (app) @@ -226,7 +225,7 @@ in 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.capability.wayland || app.capability.x11) ( + optional (app.enablements.wayland || app.enablements.x11) ( pkgs.runCommand "${app.name}-share" { } '' mkdir -p $out/share ${copy "${pkg}/share/applications"} diff --git a/options.nix b/options.nix index bd45b7b..a51609c 100644 --- a/options.nix +++ b/options.nix @@ -209,9 +209,9 @@ in ''; }; - capability = { + enablements = { wayland = mkOption { - type = bool; + type = nullOr bool; default = true; description = '' Whether to share the Wayland socket. @@ -219,7 +219,7 @@ in }; x11 = mkOption { - type = bool; + type = nullOr bool; default = false; description = '' Whether to share the X11 socket and allow connection. @@ -227,7 +227,7 @@ in }; dbus = mkOption { - type = bool; + type = nullOr bool; default = true; description = '' Whether to proxy D-Bus. @@ -235,7 +235,7 @@ in }; pulse = mkOption { - type = bool; + type = nullOr bool; default = true; description = '' Whether to share the PulseAudio socket and cookie. diff --git a/test/configuration.nix b/test/configuration.nix index 2c848bc..b7970dd 100644 --- a/test/configuration.nix +++ b/test/configuration.nix @@ -126,7 +126,7 @@ wayland-utils ]; command = "foot"; - capability = { + enablements = { dbus = false; pulse = false; }; @@ -141,7 +141,7 @@ share = pkgs.foot; packages = [ ]; command = "foot"; - capability = { + enablements = { dbus = false; pulse = false; }; @@ -154,7 +154,7 @@ share = pkgs.foot; packages = [ pkgs.foot ]; command = "foot"; - capability.dbus = false; + enablements.dbus = false; }; "cat.gensokyo.extern.Alacritty.x11" = { @@ -171,7 +171,7 @@ mesa-demos ]; command = "alacritty"; - capability = { + enablements = { wayland = false; x11 = true; dbus = false; @@ -192,7 +192,7 @@ wayland-utils ]; command = "foot"; - capability = { + enablements = { dbus = false; pulse = false; }; @@ -204,7 +204,7 @@ verbose = true; share = pkgs.strace; command = "strace true"; - capability = { + enablements = { wayland = false; x11 = false; dbus = false; diff --git a/test/interactive/hakurei.nix b/test/interactive/hakurei.nix index 3c96b3c..887ed3e 100644 --- a/test/interactive/hakurei.nix +++ b/test/interactive/hakurei.nix @@ -13,7 +13,7 @@ share = pkgs.foot; packages = [ pkgs.foot ]; command = "foot"; - capability = { + enablements = { dbus = false; pulse = false; }; diff --git a/test/test.py b/test/test.py index d0252cf..c02766c 100644 --- a/test/test.py +++ b/test/test.py @@ -206,7 +206,7 @@ wait_for_window(f"u0_a{aid(0)}@machine") machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n") machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=15) collect_state_ui("foot_wayland") -check_state("ne-foot", 1) +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 {aid(0) + 1000000}") machine.send_chars("exit\n") @@ -219,7 +219,7 @@ wait_for_window(f"u0_a{aid(1)}@machine") machine.send_chars("clear; pactl info && touch /tmp/pulse-ok\n") machine.wait_for_file(tmpdir_path(1, "pulse-ok"), timeout=15) collect_state_ui("pulse_wayland") -check_state("pa-foot", 9) +check_state("pa-foot", {"wayland": True, "pulse": True}) machine.send_chars("exit\n") machine.wait_until_fails("pgrep foot", timeout=5) @@ -229,7 +229,7 @@ wait_for_window(f"u0_a{aid(0)}@machine") machine.send_chars("clear; glinfo && touch /tmp/x11-ok\n") machine.wait_for_file(tmpdir_path(0, "x11-ok"), timeout=15) collect_state_ui("alacritty_x11") -check_state("x11-alacritty", 2) +check_state("x11-alacritty", {"x11": True}) machine.send_chars("exit\n") machine.wait_until_fails("pgrep alacritty", timeout=5) @@ -239,7 +239,7 @@ wait_for_window(f"u0_a{aid(3)}@machine") machine.send_chars("clear; wayland-info && touch /tmp/direct-ok\n") collect_state_ui("foot_direct") machine.wait_for_file(tmpdir_path(3, "direct-ok"), timeout=15) -check_state("da-foot", 1) +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 {aid(3) + 1000000}")) machine.send_chars("exit\n") @@ -259,7 +259,7 @@ 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", timeout=5) collect_state_ui("foot_wayland_term") -check_state("ne-foot", 1) +check_state("ne-foot", {"wayland": True}) machine.send_key("alt-l") machine.send_chars("exit\n") wait_for_window("alice@machine")