Compare commits
141 Commits
16e674782a
...
v0.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
8cb0b433b2
|
|||
|
767f1844d2
|
|||
|
54610aaddc
|
|||
|
2e80660169
|
|||
|
d0a3c6a2f3
|
|||
|
0c0e3d6fc2
|
|||
|
fae910a1ad
|
|||
|
178c8bc28b
|
|||
|
30dcab0734
|
|||
|
0ea051062b
|
|||
|
b0f2ab6fff
|
|||
|
00a5bdf006
|
|||
|
a27dfdc058
|
|||
|
6d0d9cecd1
|
|||
|
17248d7d61
|
|||
|
41e5628c67
|
|||
|
ffbec828e1
|
|||
|
de0467a65e
|
|||
|
b5999b8814
|
|||
|
ebc67bb8ad
|
|||
|
e60ff660f6
|
|||
|
47db461546
|
|||
|
0a3fe5f907
|
|||
|
b72d502f1c
|
|||
|
f8b3db3f66
|
|||
|
0e2fb1788f
|
|||
|
d8417e2927
|
|||
|
ccc0d98bd7
|
|||
|
a3fd05765e
|
|||
|
c538df7daa
|
|||
|
44e5aa1a36
|
|||
|
cf0e7d8c27
|
|||
|
130add21e5
|
|||
|
5ec4045e24
|
|||
|
be2075f169
|
|||
|
e9fb1d7be5
|
|||
|
dafe9f8efc
|
|||
|
96dd7abd80
|
|||
|
d5fb179012
|
|||
|
462863e290
|
|||
|
2786611b88
|
|||
|
791a1dfa55
|
|||
|
564db6863b
|
|||
|
87781c7658
|
|||
|
0c38fb7b6a
|
|||
|
357cfcddee
|
|||
|
6bf245cf1b
|
|||
|
c8eeb4a4d1
|
|||
|
5785714b64
|
|||
|
422efcf258
|
|||
|
104eeecf65
|
|||
|
bf856f06e5
|
|||
|
1931b54600
|
|||
|
093e30c788
|
|||
|
1b17ccda91
|
|||
|
7c6fc1128b
|
|||
|
8cdd659239
|
|||
|
15c2839a09
|
|||
|
b9b9705b52
|
|||
|
246e04214a
|
|||
|
503bfc6468
|
|||
|
d837628b4c
|
|||
|
3cb58b4b72
|
|||
|
bb1fc4c7bc
|
|||
|
f44923da29
|
|||
|
5e7861bb00
|
|||
|
7cb3308a53
|
|||
|
490093a659
|
|||
|
2b22efcdf1
|
|||
|
8a2f9edcf9
|
|||
|
0d3f332d45
|
|||
|
d5509cc6e5
|
|||
|
0d3ae6cb23
|
|||
|
69b1131d66
|
|||
|
2c0b92771a
|
|||
|
054c91879f
|
|||
|
c34439fc5f
|
|||
|
32fb137bb2
|
|||
|
e7a665e043
|
|||
|
af741f20a0
|
|||
|
39c6716fb0
|
|||
|
7bc73afadd
|
|||
|
647aa9d02f
|
|||
|
91aaabaa1b
|
|||
|
3d4c7cdd9e
|
|||
|
4fd6d6c037
|
|||
|
de3fc7ba38
|
|||
|
5a5c4705dd
|
|||
|
f703aa20a5
|
|||
|
5c12425d48
|
|||
|
cbe86dc4f0
|
|||
|
d08a1081bd
|
|||
|
72a2601d74
|
|||
|
1dab87aaf0
|
|||
|
2bafde99e3
|
|||
|
91efeb101a
|
|||
|
dcb22a61c0
|
|||
|
e028a61fc1
|
|||
|
73987be7d4
|
|||
|
563b5e66fc
|
|||
|
2edcfe1e68
|
|||
|
2698ca00e8
|
|||
|
1d0143386d
|
|||
|
a55c209099
|
|||
|
10ff276da1
|
|||
|
fd4d379b67
|
|||
|
77f5b89a41
|
|||
|
14e33f17e5
|
|||
|
cfeb7818eb
|
|||
|
05391da556
|
|||
|
463f8836e6
|
|||
|
2e465c94da
|
|||
|
26009fd3f7
|
|||
|
2d7b896a8c
|
|||
|
a0eb010aab
|
|||
|
b1b27ac1df
|
|||
|
fc3d78fe01
|
|||
|
591637264a
|
|||
|
e77652bf89
|
|||
|
88d3e46413
|
|||
|
e51e81bb22
|
|||
|
8f4a3bcf9f
|
|||
|
827dc9e1ba
|
|||
|
d92de1c709
|
|||
|
5bcafcf734
|
|||
|
9f7b0c2f46
|
|||
|
3e87187c4c
|
|||
|
b651d95e77
|
|||
|
aab92ce3c1
|
|||
|
a495e09a8f
|
|||
|
3afca2bd5b
|
|||
|
b73a789dfe
|
|||
|
38b5ff0cec
|
|||
|
3c204b9b40
|
|||
|
00771efeb4
|
|||
|
61972d61f6
|
|||
|
fe40af7b7e
|
|||
|
12751932d1
|
|||
|
41b49137a8
|
|||
|
c761e1de4d
|
|||
|
a91920310d
|
2
.clang-format
Normal file
2
.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
ColumnLimit: 0
|
||||
IndentWidth: 4
|
||||
@@ -2,7 +2,6 @@ name: Test
|
||||
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
hakurei:
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -91,7 +91,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
|
||||
flagPrivateRuntime, flagPrivateTmpdir bool
|
||||
|
||||
flagWayland, flagX11, flagDBus, flagPulse bool
|
||||
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
|
||||
)
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
|
||||
@@ -146,8 +146,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
if flagDBus {
|
||||
et |= hst.EDBus
|
||||
}
|
||||
if flagPulse {
|
||||
et |= hst.EPulse
|
||||
if flagPipeWire || flagPulse {
|
||||
et |= hst.EPipeWire
|
||||
}
|
||||
|
||||
config := &hst.Config{
|
||||
@@ -297,8 +297,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
"Enable direct connection to X11").
|
||||
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
|
||||
"Enable connection to PipeWire via SecurityContext").
|
||||
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||
"Enable direct connection to PulseAudio")
|
||||
"Enable PulseAudio compatibility daemon")
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ Commands:
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Enable direct connection to X11
|
||||
@@ -58,12 +58,14 @@ Flags:
|
||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pipewire
|
||||
Enable connection to PipeWire via SecurityContext
|
||||
-private-runtime
|
||||
Do not share XDG_RUNTIME_DIR between containers under the same identity
|
||||
-private-tmpdir
|
||||
Do not share TMPDIR between containers under the same identity
|
||||
-pulse
|
||||
Enable direct connection to PulseAudio
|
||||
Enable PulseAudio compatibility daemon
|
||||
-u string
|
||||
Passwd user name within sandbox (default "chronos")
|
||||
-wayland
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/internal/store"
|
||||
"hakurei.app/message"
|
||||
@@ -23,16 +21,14 @@ import (
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
hi := &hst.Info{Version: info.Version(), User: new(outcome.Hsu).MustID(nil)}
|
||||
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
||||
hi := outcome.Info()
|
||||
|
||||
if flagJSON {
|
||||
encodeJSON(log.Fatal, output, short, hi)
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Version:\t%s\n", hi.Version)
|
||||
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
||||
t.Printf("User:\t%d\n", hi.User)
|
||||
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
||||
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
PID: 0xbeef,
|
||||
ShimPID: 0xcafe,
|
||||
Config: &hst.Config{
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse),
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
||||
Identity: 1,
|
||||
Container: &hst.ContainerConfig{
|
||||
Shell: check.MustAbs("/bin/sh"),
|
||||
@@ -62,7 +62,7 @@ func TestPrintShowInstance(t *testing.T) {
|
||||
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
|
||||
{"config", nil, hst.Template(), false, false, `App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Enablements: wayland, dbus, pipewire
|
||||
Groups: video, dialout, plugdev
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
@@ -159,7 +159,7 @@ Session bus
|
||||
|
||||
App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Enablements: wayland, dbus, pipewire
|
||||
Groups: video, dialout, plugdev
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
@@ -215,7 +215,7 @@ App
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
"pipewire": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
@@ -366,7 +366,7 @@ App
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
"pipewire": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
@@ -564,7 +564,7 @@ func TestPrintPs(t *testing.T) {
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
"pipewire": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
@@ -715,7 +715,7 @@ func TestPrintPs(t *testing.T) {
|
||||
"shim_pid": 51966,
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"pulse": true
|
||||
"pipewire": true
|
||||
},
|
||||
"identity": 1,
|
||||
"groups": null,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
allow_wayland ? true,
|
||||
allow_x11 ? false,
|
||||
allow_dbus ? true,
|
||||
allow_pulse ? true,
|
||||
allow_audio ? true,
|
||||
gpu ? allow_wayland || allow_x11,
|
||||
}:
|
||||
|
||||
@@ -175,7 +175,7 @@ let
|
||||
wayland = allow_wayland;
|
||||
x11 = allow_x11;
|
||||
dbus = allow_dbus;
|
||||
pulse = allow_pulse;
|
||||
pipewire = allow_audio;
|
||||
};
|
||||
|
||||
mesa = if gpu then mesaWrappers else null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
nixosTest,
|
||||
testers,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
@@ -8,7 +8,7 @@
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
testers.nixosTest {
|
||||
name = "hpkg";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
|
||||
@@ -90,13 +90,13 @@ wait_for_window("hakurei@machine-foot")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
||||
collect_state_ui("app_wayland")
|
||||
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
||||
check_state("foot", {"wayland": True, "dbus": True, "pipewire": True})
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002"))
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
@@ -107,4 +107,4 @@ 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"))
|
||||
print(machine.fail("ls /run/user/1000/hakurei"))
|
||||
|
||||
@@ -59,6 +59,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
|
||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||
|
||||
@@ -69,6 +69,7 @@ func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (r *AutoRootOp) Is(op Op) bool {
|
||||
vr, ok := op.(*AutoRootOp)
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) {
|
||||
t.Parallel()
|
||||
var msg message.Msg
|
||||
if tc.log {
|
||||
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
||||
}})}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,8 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||
t.Parallel()
|
||||
|
||||
wait4signal := make(chan struct{})
|
||||
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
||||
lockNotify := make(chan struct{})
|
||||
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
|
||||
defer stub.HandleExit(t)
|
||||
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||
@@ -200,8 +201,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
k := &kstub{nil, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
||||
k := &kstub{nil, nil, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
|
||||
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||
)}
|
||||
state := &setupState{Params: tc.params, Msg: k}
|
||||
@@ -322,12 +323,19 @@ const (
|
||||
|
||||
type kstub struct {
|
||||
wait4signal chan struct{}
|
||||
lockNotify chan struct{}
|
||||
*stub.Stub[syscallDispatcher]
|
||||
}
|
||||
|
||||
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||
|
||||
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
||||
func (k *kstub) lockOSThread() {
|
||||
k.Helper()
|
||||
expect := k.Expects("lockOSThread")
|
||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||
<-k.lockNotify
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) setPtracer(pid uintptr) error {
|
||||
k.Helper()
|
||||
@@ -472,6 +480,10 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
||||
k.FailNow()
|
||||
}
|
||||
|
||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||
defer close(k.lockNotify)
|
||||
}
|
||||
|
||||
// export channel for external instrumentation
|
||||
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||
chanf(c)
|
||||
@@ -759,7 +771,8 @@ func (k *kstub) checkMsg(msg message.Msg) {
|
||||
}
|
||||
|
||||
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
|
||||
func (k *kstub) IsVerbose() bool { panic("unreachable") }
|
||||
|
||||
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
||||
|
||||
func (k *kstub) SwapVerbose(verbose bool) bool {
|
||||
k.Helper()
|
||||
|
||||
@@ -7,31 +7,36 @@ import (
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/vfs"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// messageFromError returns a printable error message for a supported concrete type.
|
||||
func messageFromError(err error) (string, bool) {
|
||||
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
|
||||
return m, ok
|
||||
func messageFromError(err error) (m string, ok bool) {
|
||||
if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
|
||||
return
|
||||
}
|
||||
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||
return m, ok
|
||||
if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||
return
|
||||
}
|
||||
if m, ok := messagePrefixP[check.AbsoluteError]("", err); ok {
|
||||
return m, ok
|
||||
if m, ok = messagePrefixP[check.AbsoluteError](zeroString, err); ok {
|
||||
return
|
||||
}
|
||||
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
|
||||
return m, ok
|
||||
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
|
||||
return
|
||||
}
|
||||
if m, ok := messagePrefix[OpStateError]("", err); ok {
|
||||
return m, ok
|
||||
if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||
return m, ok
|
||||
if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||
return
|
||||
}
|
||||
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
|
||||
return m, ok
|
||||
if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
if m, ok = message.GetMessage(err); ok {
|
||||
return
|
||||
}
|
||||
|
||||
return zeroString, false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
@@ -18,24 +21,28 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
/* intermediate tmpfs mount point
|
||||
/* intermediateHostPath is the pathname of the intermediate tmpfs mount point.
|
||||
|
||||
this path might seem like a weird choice, however there are many good reasons to use it:
|
||||
- the contents of this path is never exposed to the container:
|
||||
the tmpfs root established here effectively becomes anonymous after pivot_root
|
||||
- it is safe to assume this path exists and is a directory:
|
||||
this program will not work correctly without a proper /proc and neither will most others
|
||||
- this path belongs to the container init:
|
||||
the container init is not any more privileged or trusted than the rest of the container
|
||||
- this path is only accessible by init and root:
|
||||
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
|
||||
This path might seem like a weird choice, however there are many good reasons to use it:
|
||||
- The contents of this path is never exposed to the container:
|
||||
The tmpfs root established here effectively becomes anonymous after pivot_root.
|
||||
- It is safe to assume this path exists and is a directory:
|
||||
This program will not work correctly without a proper /proc and neither will most others.
|
||||
- This path belongs to the container init:
|
||||
The container init is not any more privileged or trusted than the rest of the container.
|
||||
- This path is only accessible by init and root:
|
||||
The container init sets SUID_DUMP_DISABLE and terminates if that fails.
|
||||
|
||||
it should be noted that none of this should become relevant at any point since the resulting
|
||||
intermediate root tmpfs should be effectively anonymous */
|
||||
It should be noted that none of this should become relevant at any point since the resulting
|
||||
intermediate root tmpfs should be effectively anonymous. */
|
||||
intermediateHostPath = fhs.Proc + "self/fd"
|
||||
|
||||
// setup params file descriptor
|
||||
// setupEnv is the name of the environment variable holding the string representation of
|
||||
// the read end file descriptor of the setup params pipe.
|
||||
setupEnv = "HAKUREI_SETUP"
|
||||
|
||||
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
|
||||
exitUnexpectedWait4 = 2
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -49,6 +56,8 @@ type (
|
||||
early(state *setupState, k syscallDispatcher) error
|
||||
// apply is called in intermediate root.
|
||||
apply(state *setupState, k syscallDispatcher) error
|
||||
// late is called right before starting the initial process.
|
||||
late(state *setupState, k syscallDispatcher) error
|
||||
|
||||
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
|
||||
prefix() (string, bool)
|
||||
@@ -61,11 +70,29 @@ type (
|
||||
// setupState persists context between Ops.
|
||||
setupState struct {
|
||||
nonrepeatable uintptr
|
||||
|
||||
// Whether early reaping has concluded. Must only be accessed in the wait4 loop.
|
||||
processConcluded bool
|
||||
// Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes.
|
||||
process map[int]WaitStatus
|
||||
// Synchronises access to process.
|
||||
processMu sync.RWMutex
|
||||
|
||||
*Params
|
||||
context.Context
|
||||
message.Msg
|
||||
}
|
||||
)
|
||||
|
||||
// terminated returns whether the specified pid has been reaped, and its
|
||||
// syscall.WaitStatus if it had. This is only usable by [Op].
|
||||
func (state *setupState) terminated(pid int) (wstatus WaitStatus, ok bool) {
|
||||
state.processMu.RLock()
|
||||
wstatus, ok = state.process[pid]
|
||||
state.processMu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Grow grows the slice Ops points to using [slices.Grow].
|
||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||
|
||||
@@ -180,7 +207,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
|
||||
}
|
||||
|
||||
state := &setupState{Params: ¶ms.Params, Msg: msg}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
||||
defer cancel()
|
||||
|
||||
/* early is called right before pivot_root into intermediate root;
|
||||
this step is mostly for gathering information that would otherwise be difficult to obtain
|
||||
@@ -330,6 +359,97 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
k.umask(oldmask)
|
||||
|
||||
// winfo represents an exited process from wait4.
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus WaitStatus
|
||||
}
|
||||
|
||||
// info is closed as the wait4 thread terminates
|
||||
// when there are no longer any processes left to reap
|
||||
info := make(chan winfo, 1)
|
||||
|
||||
// whether initial process has started
|
||||
var initialProcessStarted atomic.Bool
|
||||
|
||||
k.new(func(k syscallDispatcher) {
|
||||
k.lockOSThread()
|
||||
|
||||
wait4:
|
||||
var (
|
||||
err error
|
||||
wpid = -2
|
||||
wstatus WaitStatus
|
||||
|
||||
// whether initial process has started
|
||||
started bool
|
||||
)
|
||||
|
||||
// keep going until no child process is left
|
||||
for wpid != -1 {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if wpid != -2 {
|
||||
if !state.processConcluded {
|
||||
state.processMu.Lock()
|
||||
if state.process == nil {
|
||||
// early reaping has already concluded at this point
|
||||
state.processConcluded = true
|
||||
info <- winfo{wpid, wstatus}
|
||||
} else {
|
||||
// initial process has not yet been created, and the
|
||||
// info channel is not yet being received from
|
||||
state.process[wpid] = wstatus
|
||||
}
|
||||
state.processMu.Unlock()
|
||||
} else {
|
||||
info <- winfo{wpid, wstatus}
|
||||
}
|
||||
}
|
||||
|
||||
if !started {
|
||||
started = initialProcessStarted.Load()
|
||||
}
|
||||
|
||||
err = EINTR
|
||||
for errors.Is(err, EINTR) {
|
||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.Is(err, ECHILD) {
|
||||
k.printf(msg, "unexpected wait4 response: %v", err)
|
||||
} else if !started {
|
||||
// initial process has not yet been reached and all daemons
|
||||
// terminated or none were started in the first place
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
goto wait4
|
||||
}
|
||||
|
||||
close(info)
|
||||
})
|
||||
|
||||
// called right before startup of initial process, all state changes to the
|
||||
// current process is prohibited during late
|
||||
for i, op := range *params.Ops {
|
||||
// ops already checked during early setup
|
||||
if err := op.late(state, k); err != nil {
|
||||
if m, ok := messageFromError(err); ok {
|
||||
k.fatal(msg, m)
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
k.fatalf(msg, "%s deadline exceeded", op.String())
|
||||
} else {
|
||||
k.fatalf(msg, "cannot complete op at index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// early reaping has concluded, this must happen before initial process is created
|
||||
state.processMu.Lock()
|
||||
state.process = nil
|
||||
state.processMu.Unlock()
|
||||
|
||||
if err := closeSetup(); err != nil {
|
||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||
}
|
||||
@@ -341,50 +461,11 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Dir = params.Dir.String()
|
||||
|
||||
msg.Verbosef("starting initial program %s", params.Path)
|
||||
msg.Verbosef("starting initial process %s", params.Path)
|
||||
if err := k.start(cmd); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus WaitStatus
|
||||
}
|
||||
|
||||
// info is closed as the wait4 thread terminates
|
||||
// when there are no longer any processes left to reap
|
||||
info := make(chan winfo, 1)
|
||||
|
||||
k.new(func(k syscallDispatcher) {
|
||||
k.lockOSThread()
|
||||
|
||||
var (
|
||||
err error
|
||||
wpid = -2
|
||||
wstatus WaitStatus
|
||||
)
|
||||
|
||||
// keep going until no child process is left
|
||||
for wpid != -1 {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if wpid != -2 {
|
||||
info <- winfo{wpid, wstatus}
|
||||
}
|
||||
|
||||
err = EINTR
|
||||
for errors.Is(err, EINTR) {
|
||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
if !errors.Is(err, ECHILD) {
|
||||
k.printf(msg, "unexpected wait4 response: %v", err)
|
||||
}
|
||||
|
||||
close(info)
|
||||
})
|
||||
initialProcessStarted.Store(true)
|
||||
|
||||
// handle signals to dump withheld messages
|
||||
sig := make(chan os.Signal, 2)
|
||||
@@ -394,7 +475,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
// closed after residualProcessTimeout has elapsed after initial process death
|
||||
timeout := make(chan struct{})
|
||||
|
||||
r := 2
|
||||
r := exitUnexpectedWait4
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
@@ -426,6 +507,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
if w.wpid == cmd.Process.Pid {
|
||||
// cancel Op context early
|
||||
cancel()
|
||||
|
||||
// start timeout early
|
||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||
|
||||
|
||||
@@ -1983,11 +1983,20 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(13)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, nil, stub.UniqueError(12)),
|
||||
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(12)}}, nil, nil),
|
||||
},
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||
}}},
|
||||
}, nil},
|
||||
|
||||
{"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
|
||||
@@ -2062,11 +2071,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
|
||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
|
||||
@@ -2081,7 +2090,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||
@@ -2162,11 +2171,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
|
||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
|
||||
@@ -2181,7 +2190,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||
@@ -2262,11 +2271,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
|
||||
call("beforeExit", stub.ExpectArgs{}, nil, nil),
|
||||
call("exit", stub.ExpectArgs{0}, nil, nil),
|
||||
@@ -2274,7 +2283,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||
@@ -2353,11 +2362,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||
@@ -2368,7 +2377,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
@@ -2448,11 +2457,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||
@@ -2462,7 +2471,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
@@ -2586,11 +2595,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
|
||||
@@ -2600,7 +2609,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
@@ -2728,11 +2737,11 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("newFile", stub.ExpectArgs{uintptr(11), "extra file 1"}, (*os.File)(nil), nil),
|
||||
call("newFile", stub.ExpectArgs{uintptr(12), "extra file 2"}, (*os.File)(nil), nil),
|
||||
call("umask", stub.ExpectArgs{022}, 0, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
@@ -2743,7 +2752,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
|
||||
@@ -90,6 +90,7 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.bindMount(state, source, target, flags)
|
||||
}
|
||||
func (b *BindMountOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (b *BindMountOp) Is(op Op) bool {
|
||||
vb, ok := op.(*BindMountOp)
|
||||
|
||||
134
container/initdaemon.go
Normal file
134
container/initdaemon.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(DaemonOp)) }
|
||||
|
||||
const (
|
||||
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
|
||||
// [DaemonOp.Target] marker becomes available.
|
||||
daemonTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// Daemon appends an [Op] that starts a daemon in the container and blocks until
|
||||
// [DaemonOp.Target] appears.
|
||||
func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops {
|
||||
*f = append(*f, &DaemonOp{target, path, args})
|
||||
return f
|
||||
}
|
||||
|
||||
// DaemonOp starts a daemon in the container and blocks until Target appears.
|
||||
type DaemonOp struct {
|
||||
// Pathname indicating readiness of daemon.
|
||||
Target *check.Absolute
|
||||
// Absolute pathname passed to [exec.Cmd].
|
||||
Path *check.Absolute
|
||||
// Arguments (excl. first) passed to [exec.Cmd].
|
||||
Args []string
|
||||
}
|
||||
|
||||
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
|
||||
// before [DaemonOp.Target] appears.
|
||||
type earlyTerminationError struct {
|
||||
// Returned by [DaemonOp.String].
|
||||
op string
|
||||
// Copied from wait4 loop.
|
||||
wstatus syscall.WaitStatus
|
||||
}
|
||||
|
||||
func (e *earlyTerminationError) Error() string {
|
||||
res := ""
|
||||
switch {
|
||||
case e.wstatus.Exited():
|
||||
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
|
||||
case e.wstatus.Signaled():
|
||||
res = "signal: " + e.wstatus.Signal().String()
|
||||
case e.wstatus.Stopped():
|
||||
res = "stop signal: " + e.wstatus.StopSignal().String()
|
||||
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
|
||||
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
|
||||
}
|
||||
case e.wstatus.Continued():
|
||||
res = "continued"
|
||||
}
|
||||
if e.wstatus.CoreDump() {
|
||||
res += " (core dumped)"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
|
||||
|
||||
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
|
||||
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
|
||||
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
|
||||
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
|
||||
cmd.Env = state.Env
|
||||
cmd.Dir = fhs.Root
|
||||
if state.IsVerbose() {
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
}
|
||||
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||
|
||||
state.Verbosef("starting %s", d.String())
|
||||
if err := k.start(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(daemonTimeout)
|
||||
var wstatusErr error
|
||||
|
||||
for {
|
||||
if _, err := k.stat(d.Target.String()); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
_ = k.signal(cmd, os.Kill)
|
||||
return err
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
_ = k.signal(cmd, os.Kill)
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
if wstatusErr != nil {
|
||||
return wstatusErr
|
||||
}
|
||||
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
|
||||
// check once again: process could have satisfied Target between stat and the lookup
|
||||
wstatusErr = &earlyTerminationError{d.String(), wstatus}
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
continue
|
||||
}
|
||||
|
||||
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DaemonOp) Is(op Op) bool {
|
||||
vd, ok := op.(*DaemonOp)
|
||||
return ok && d.Valid() && vd.Valid() &&
|
||||
d.Target.Is(vd.Target) && d.Path.Is(vd.Path) &&
|
||||
slices.Equal(d.Args, vd.Args)
|
||||
}
|
||||
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
|
||||
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }
|
||||
127
container/initdaemon_test.go
Normal file
127
container/initdaemon_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestEarlyTerminationError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
msg string
|
||||
}{
|
||||
{"exited", &earlyTerminationError{
|
||||
`daemon providing "/run/user/1971/pulse/native"`, 127 << 8,
|
||||
}, "exit status 127", `daemon providing "/run/user/1971/pulse/native" exit status 127`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||
}
|
||||
if got := tc.err.(message.Error).Message(); got != tc.msg {
|
||||
t.Errorf("Message: %s, want %s", got, tc.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkSimple(t, "DaemonOp.late", []simpleTestCase{
|
||||
{"success", func(k *kstub) error {
|
||||
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
|
||||
return (&DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}).late(&state, k)
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
|
||||
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
|
||||
}}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*DaemonOp)(nil), false},
|
||||
{"zero", new(DaemonOp), false},
|
||||
{"valid", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pipewire-pulse", new(Ops).Daemon(
|
||||
check.MustAbs("/run/user/1971/pulse/native"),
|
||||
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
|
||||
), Ops{
|
||||
&DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(DaemonOp), new(DaemonOp), false},
|
||||
|
||||
{"args differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"path differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"target differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/65534/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"equals", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"pipewire-pulse", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
|
||||
})
|
||||
}
|
||||
@@ -126,6 +126,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
||||
}
|
||||
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (d *MountDevOp) Is(op Op) bool {
|
||||
vd, ok := op.(*MountDevOp)
|
||||
|
||||
@@ -27,6 +27,7 @@ func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm)
|
||||
}
|
||||
func (m *MkdirOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (m *MkdirOp) Is(op Op) bool {
|
||||
vm, ok := op.(*MkdirOp)
|
||||
|
||||
@@ -205,6 +205,8 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (o *MountOverlayOp) Is(op Op) bool {
|
||||
vo, ok := op.(*MountOverlayOp)
|
||||
return ok && o.Valid() && vo.Valid() &&
|
||||
|
||||
@@ -57,6 +57,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *TmpfileOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (t *TmpfileOp) Is(op Op) bool {
|
||||
vt, ok := op.(*TmpfileOp)
|
||||
|
||||
@@ -28,6 +28,7 @@ func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
|
||||
}
|
||||
func (p *MountProcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (p *MountProcOp) Is(op Op) bool {
|
||||
vp, ok := op.(*MountProcOp)
|
||||
|
||||
@@ -26,6 +26,7 @@ func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
return k.remount(state, toSysroot(r.Target.String()), r.Flags)
|
||||
}
|
||||
func (r *RemountOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (r *RemountOp) Is(op Op) bool {
|
||||
vr, ok := op.(*RemountOp)
|
||||
|
||||
@@ -50,6 +50,8 @@ func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
return k.symlink(l.LinkName, target)
|
||||
}
|
||||
|
||||
func (l *SymlinkOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (l *SymlinkOp) Is(op Op) bool {
|
||||
vl, ok := op.(*SymlinkOp)
|
||||
return ok && l.Valid() && vl.Valid() &&
|
||||
|
||||
@@ -48,6 +48,7 @@ func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
||||
}
|
||||
func (t *MountTmpfsOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (t *MountTmpfsOp) Is(op Op) bool {
|
||||
vt, ok := op.(*MountTmpfsOp)
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||
|
||||
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
||||
int32_t hakurei_scmp_make_filter(
|
||||
int *ret_p, uintptr_t allocate_p,
|
||||
uint32_t arch, uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags) {
|
||||
@@ -72,11 +73,9 @@ int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
||||
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
||||
|
||||
if (rule->arg)
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 1, *rule->arg);
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 1, *rule->arg);
|
||||
else
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 0);
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0);
|
||||
|
||||
if (*ret_p == -EFAULT) {
|
||||
res = 4;
|
||||
@@ -93,22 +92,17 @@ int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
||||
last_allowed_family = -1;
|
||||
for (i = 0; i < LEN(socket_family_allowlist); i++) {
|
||||
if (socket_family_allowlist[i].flags_mask != 0 &&
|
||||
(socket_family_allowlist[i].flags_mask & flags) !=
|
||||
socket_family_allowlist[i].flags_mask)
|
||||
(socket_family_allowlist[i].flags_mask & flags) != socket_family_allowlist[i].flags_mask)
|
||||
continue;
|
||||
|
||||
for (disallowed = last_allowed_family + 1;
|
||||
disallowed < socket_family_allowlist[i].family; disallowed++) {
|
||||
for (disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
|
||||
/* Blocklist the in-between valid families */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
|
||||
SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_EQ, disallowed));
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
|
||||
}
|
||||
last_allowed_family = socket_family_allowlist[i].family;
|
||||
}
|
||||
/* Blocklist the rest */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||
|
||||
if (allocate_p == 0) {
|
||||
*ret_p = seccomp_load(ctx);
|
||||
|
||||
@@ -19,7 +19,8 @@ struct hakurei_syscall_rule {
|
||||
};
|
||||
|
||||
extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
|
||||
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
||||
int32_t hakurei_scmp_make_filter(
|
||||
int *ret_p, uintptr_t allocate_p,
|
||||
uint32_t arch, uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags);
|
||||
3
dist/comp/_hakurei
vendored
3
dist/comp/_hakurei
vendored
@@ -17,7 +17,8 @@ _hakurei_run() {
|
||||
'--wayland[Enable connection to Wayland via security-context-v1]' \
|
||||
'-X[Enable direct connection to X11]' \
|
||||
'--dbus[Enable proxied connection to D-Bus]' \
|
||||
'--pulse[Enable direct connection to PulseAudio]' \
|
||||
'--pipewire[Enable connection to PipeWire via SecurityContext]' \
|
||||
'--pulse[Enable PulseAudio compatibility daemon]' \
|
||||
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
|
||||
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
|
||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||
|
||||
16
flake.lock
generated
16
flake.lock
generated
@@ -7,32 +7,32 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756679287,
|
||||
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
|
||||
"lastModified": 1765384171,
|
||||
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
|
||||
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-25.05",
|
||||
"ref": "release-25.11",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1757020766,
|
||||
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
|
||||
"lastModified": 1765311797,
|
||||
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
|
||||
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
16
flake.nix
16
flake.nix
@@ -2,10 +2,10 @@
|
||||
description = "hakurei container tool and nixos module";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager/release-25.05";
|
||||
url = "github:nix-community/home-manager/release-25.11";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
@@ -114,7 +114,7 @@
|
||||
inherit (pkgs)
|
||||
# passthru.buildInputs
|
||||
go
|
||||
gcc
|
||||
clang
|
||||
|
||||
# nativeBuildInputs
|
||||
pkg-config
|
||||
@@ -129,6 +129,10 @@
|
||||
zstd
|
||||
gnutar
|
||||
coreutils
|
||||
|
||||
# for check
|
||||
util-linux
|
||||
nettools
|
||||
;
|
||||
};
|
||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||
@@ -144,7 +148,7 @@
|
||||
&& chmod -R +w .
|
||||
|
||||
export HAKUREI_VERSION="v${hakurei.version}"
|
||||
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||
CC="clang -O3 -Werror" ./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||
'';
|
||||
}
|
||||
);
|
||||
@@ -181,13 +185,13 @@
|
||||
hakurei =
|
||||
let
|
||||
# this is used for interactive vm testing during development, where tests might be broken
|
||||
package = self.packages.${pkgs.system}.hakurei.override {
|
||||
package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
|
||||
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit package;
|
||||
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
|
||||
hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,9 +23,54 @@ type Config struct {
|
||||
// System D-Bus proxy configuration.
|
||||
// If set to nil, system bus proxy is disabled.
|
||||
SystemBus *BusConfig `json:"system_bus,omitempty"`
|
||||
|
||||
// Direct access to wayland socket, no attempt is made to attach security-context-v1
|
||||
// and the bare socket is made available to the container.
|
||||
//
|
||||
// This option is unsupported and most likely enables full control over the Wayland
|
||||
// session. Do not set this to true unless you are sure you know what you are doing.
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// Direct access to the PipeWire socket established via SecurityContext::Create, no
|
||||
// attempt is made to start the pipewire-pulse server.
|
||||
//
|
||||
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
|
||||
// bits on all objects for clients with the lowest achievable privilege level (by
|
||||
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
|
||||
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
|
||||
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
|
||||
// is implemented separately in media-session and wireplumber, with the wireplumber
|
||||
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
|
||||
// in use, and there is no known way to change its behaviour and set permissions
|
||||
// differently without replacing the Lua script. Also, since PipeWire relies on these
|
||||
// permissions to work, reducing them is not possible.
|
||||
//
|
||||
// Currently, the only other sandboxed use case is flatpak, which is not aware of
|
||||
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
|
||||
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
|
||||
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
|
||||
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
|
||||
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
|
||||
// since the client has no direct access to PipeWire, insecure parts of the protocol are
|
||||
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
|
||||
// described above.
|
||||
//
|
||||
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
|
||||
// SecurityContext. A pipewire-pulse server connected through it achieves the same
|
||||
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
|
||||
// life of the container.
|
||||
//
|
||||
// This option is unsupported and enables a denial-of-service attack as the sandboxed
|
||||
// client is able to destroy any client object and thus disconnecting them from PipeWire,
|
||||
// or destroy the SecurityContext object preventing any further container creation.
|
||||
// Do not set this to true, it is insecure under any configuration.
|
||||
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
|
||||
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
|
||||
// server via a PipeWire socket with a SecurityContext attached and the bare socket
|
||||
// is made available to the container.
|
||||
//
|
||||
// This option is unsupported and enables arbitrary code execution as the PulseAudio
|
||||
// server. Do not set this to true, it is insecure under any configuration.
|
||||
DirectPulse bool `json:"direct_pulse,omitempty"`
|
||||
|
||||
// Extra acl updates to perform before setuid.
|
||||
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
|
||||
@@ -49,6 +94,9 @@ var (
|
||||
|
||||
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
|
||||
ErrEnviron = errors.New("invalid environment variable name")
|
||||
|
||||
// ErrInsecure is returned by [Config.Validate] if the configuration is considered insecure.
|
||||
ErrInsecure = errors.New("configuration is insecure")
|
||||
)
|
||||
|
||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||
@@ -95,6 +143,11 @@ func (config *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ func TestConfigValidate(t *testing.T) {
|
||||
Env: map[string]string{"TERM\x00": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM\x00"`}},
|
||||
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
|
||||
@@ -17,6 +17,8 @@ const (
|
||||
EX11
|
||||
// EDBus enables the per-container xdg-dbus-proxy daemon.
|
||||
EDBus
|
||||
// EPipeWire exposes a pipewire pathname socket via SecurityContext.
|
||||
EPipeWire
|
||||
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
|
||||
EPulse
|
||||
|
||||
@@ -35,6 +37,8 @@ func (e Enablement) String() string {
|
||||
return "x11"
|
||||
case EDBus:
|
||||
return "dbus"
|
||||
case EPipeWire:
|
||||
return "pipewire"
|
||||
case EPulse:
|
||||
return "pulseaudio"
|
||||
default:
|
||||
@@ -65,6 +69,7 @@ type enablementsJSON = struct {
|
||||
Wayland bool `json:"wayland,omitempty"`
|
||||
X11 bool `json:"x11,omitempty"`
|
||||
DBus bool `json:"dbus,omitempty"`
|
||||
PipeWire bool `json:"pipewire,omitempty"`
|
||||
Pulse bool `json:"pulse,omitempty"`
|
||||
}
|
||||
|
||||
@@ -84,6 +89,7 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
|
||||
Wayland: Enablement(*e)&EWayland != 0,
|
||||
X11: Enablement(*e)&EX11 != 0,
|
||||
DBus: Enablement(*e)&EDBus != 0,
|
||||
PipeWire: Enablement(*e)&EPipeWire != 0,
|
||||
Pulse: Enablement(*e)&EPulse != 0,
|
||||
})
|
||||
}
|
||||
@@ -108,6 +114,9 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
||||
if v.DBus {
|
||||
ve |= EDBus
|
||||
}
|
||||
if v.PipeWire {
|
||||
ve |= EPipeWire
|
||||
}
|
||||
if v.Pulse {
|
||||
ve |= EPulse
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestEnablementString(t *testing.T) {
|
||||
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
|
||||
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
|
||||
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
|
||||
{hst.EM - 1, "wayland, x11, dbus, pipewire, pulseaudio"},
|
||||
|
||||
{1 << 5, "e20"},
|
||||
{1 << 6, "e40"},
|
||||
@@ -62,8 +63,9 @@ func TestEnablements(t *testing.T) {
|
||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"all", hst.NewEnablements(hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
|
||||
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
12
hst/fs.go
12
hst/fs.go
@@ -45,6 +45,9 @@ type Ops interface {
|
||||
Root(host *check.Absolute, flags int) Ops
|
||||
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
Etc(host *check.Absolute, prefix string) Ops
|
||||
|
||||
// Daemon appends an op that starts a daemon in the container and blocks until target appears.
|
||||
Daemon(target, path *check.Absolute, args ...string) Ops
|
||||
}
|
||||
|
||||
// ApplyState holds the address of [Ops] and any relevant application state.
|
||||
@@ -124,6 +127,12 @@ func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
|
||||
*FSLink
|
||||
}{fsType{FilesystemLink}, cv}
|
||||
|
||||
case *FSDaemon:
|
||||
v = &struct {
|
||||
fsType
|
||||
*FSDaemon
|
||||
}{fsType{FilesystemDaemon}, cv}
|
||||
|
||||
default:
|
||||
return nil, FSImplError{f.FilesystemConfig}
|
||||
}
|
||||
@@ -152,6 +161,9 @@ func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
|
||||
case FilesystemLink:
|
||||
*f = FilesystemConfigJSON{new(FSLink)}
|
||||
|
||||
case FilesystemDaemon:
|
||||
*f = FilesystemConfigJSON{new(FSDaemon)}
|
||||
|
||||
default:
|
||||
return FSTypeError(t.Type)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,16 @@ func TestFilesystemConfigJSON(t *testing.T) {
|
||||
}, nil,
|
||||
`{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`,
|
||||
`{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`},
|
||||
|
||||
{"daemon", hst.FilesystemConfigJSON{
|
||||
FilesystemConfig: &hst.FSDaemon{
|
||||
Target: m("/run/user/1971/pulse/native"),
|
||||
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}, nil,
|
||||
`{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]}`,
|
||||
`{"fs":{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]},"magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -345,6 +355,10 @@ func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Etc(host, prefix)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Daemon(target, path *check.Absolute, args ...string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Daemon(target, path, args...)}
|
||||
}
|
||||
|
||||
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
|
||||
func ms(pathnames ...string) []*check.Absolute {
|
||||
as := make([]*check.Absolute, len(pathnames))
|
||||
|
||||
48
hst/fsdaemon.go
Normal file
48
hst/fsdaemon.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package hst
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(FSDaemon)) }
|
||||
|
||||
// FilesystemDaemon is the type string of a daemon.
|
||||
const FilesystemDaemon = "daemon"
|
||||
|
||||
// FSDaemon represents a daemon to be started in the container.
|
||||
type FSDaemon struct {
|
||||
// Pathname indicating readiness of daemon.
|
||||
Target *check.Absolute `json:"dst"`
|
||||
// Absolute pathname to daemon executable file.
|
||||
Exec *check.Absolute `json:"path"`
|
||||
// Arguments (excl. first) passed to daemon.
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func (d *FSDaemon) Valid() bool { return d != nil && d.Target != nil && d.Exec != nil }
|
||||
|
||||
func (d *FSDaemon) Path() *check.Absolute {
|
||||
if !d.Valid() {
|
||||
return nil
|
||||
}
|
||||
return d.Target
|
||||
}
|
||||
|
||||
func (d *FSDaemon) Host() []*check.Absolute { return nil }
|
||||
|
||||
func (d *FSDaemon) Apply(z *ApplyState) {
|
||||
if !d.Valid() {
|
||||
return
|
||||
}
|
||||
z.Daemon(d.Target, d.Exec, d.Args...)
|
||||
}
|
||||
|
||||
func (d *FSDaemon) String() string {
|
||||
if !d.Valid() {
|
||||
return "<invalid>"
|
||||
}
|
||||
|
||||
return "daemon:" + d.Target.String()
|
||||
}
|
||||
29
hst/fsdaemon_test.go
Normal file
29
hst/fsdaemon_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package hst_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func TestFSDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkFs(t, []fsTestCase{
|
||||
{"nil", (*hst.FSDaemon)(nil), false, nil, nil, nil, "<invalid>"},
|
||||
{"zero", new(hst.FSDaemon), false, nil, nil, nil, "<invalid>"},
|
||||
|
||||
{"pipewire-pulse", &hst.FSDaemon{
|
||||
Target: m("/run/user/1971/pulse/native"),
|
||||
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, true, container.Ops{
|
||||
&container.DaemonOp{
|
||||
Target: m("/run/user/1971/pulse/native"),
|
||||
Path: m("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}, m("/run/user/1971/pulse/native"), nil, `daemon:/run/user/1971/pulse/native`},
|
||||
})
|
||||
}
|
||||
@@ -54,6 +54,9 @@ type Paths struct {
|
||||
|
||||
// Info holds basic system information collected from the implementation.
|
||||
type Info struct {
|
||||
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
|
||||
WaylandVersion string `json:"WAYLAND_VERSION"`
|
||||
|
||||
// Version is a hardcoded version string.
|
||||
Version string `json:"version"`
|
||||
// User is the userid according to hsu.
|
||||
@@ -67,7 +70,7 @@ func Template() *Config {
|
||||
return &Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPulse),
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
||||
|
||||
SessionBus: &BusConfig{
|
||||
See: nil,
|
||||
@@ -89,7 +92,6 @@ func Template() *Config {
|
||||
Log: false,
|
||||
Filter: true,
|
||||
},
|
||||
DirectWayland: false,
|
||||
|
||||
ExtraPerms: []ExtraPermConfig{
|
||||
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestTemplate(t *testing.T) {
|
||||
"enablements": {
|
||||
"wayland": true,
|
||||
"dbus": true,
|
||||
"pulse": true
|
||||
"pipewire": true
|
||||
},
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
const testFileName = "acl.test"
|
||||
90
internal/acl/libacl-helper.c
Normal file
90
internal/acl/libacl-helper.c
Normal file
@@ -0,0 +1,90 @@
|
||||
#include "libacl-helper.h"
|
||||
#include <acl/libacl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/acl.h>
|
||||
|
||||
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
|
||||
acl_perm_t *perms, size_t plen) {
|
||||
int ret;
|
||||
bool v;
|
||||
int i;
|
||||
acl_t acl;
|
||||
acl_entry_t entry;
|
||||
acl_tag_t tag_type;
|
||||
void *qualifier_p;
|
||||
acl_permset_t permset;
|
||||
|
||||
ret = -1; /* acl_get_file */
|
||||
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
||||
if (acl == NULL)
|
||||
goto out;
|
||||
|
||||
/* prune entries by uid */
|
||||
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
|
||||
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
||||
ret = -2; /* acl_get_tag_type */
|
||||
if (acl_get_tag_type(entry, &tag_type) != 0)
|
||||
goto out;
|
||||
if (tag_type != ACL_USER)
|
||||
continue;
|
||||
|
||||
ret = -3; /* acl_get_qualifier */
|
||||
qualifier_p = acl_get_qualifier(entry);
|
||||
if (qualifier_p == NULL)
|
||||
goto out;
|
||||
v = *(uid_t *)qualifier_p == uid;
|
||||
acl_free(qualifier_p);
|
||||
|
||||
if (!v)
|
||||
continue;
|
||||
|
||||
ret = -4; /* acl_delete_entry */
|
||||
if (acl_delete_entry(acl, entry) != 0)
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (plen == 0)
|
||||
goto set;
|
||||
|
||||
ret = -5; /* acl_create_entry */
|
||||
if (acl_create_entry(&acl, &entry) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -6; /* acl_get_permset */
|
||||
if (acl_get_permset(entry, &permset) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -7; /* acl_add_perm */
|
||||
for (i = 0; i < plen; i++) {
|
||||
if (acl_add_perm(permset, perms[i]) != 0)
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = -8; /* acl_set_tag_type */
|
||||
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -9; /* acl_set_qualifier */
|
||||
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
||||
goto out;
|
||||
|
||||
set:
|
||||
ret = -10; /* acl_calc_mask */
|
||||
if (acl_calc_mask(&acl) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -11; /* acl_valid */
|
||||
if (acl_valid(acl) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -12; /* acl_set_file */
|
||||
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
free((void *)path_p);
|
||||
if (acl != NULL)
|
||||
acl_free((void *)acl);
|
||||
return ret;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package acl_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
func TestPerms(t *testing.T) {
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
func TestConfigArgs(t *testing.T) {
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -53,6 +53,10 @@ type syscallDispatcher interface {
|
||||
readdir(name string) ([]os.DirEntry, error)
|
||||
// tempdir provides [os.TempDir].
|
||||
tempdir() string
|
||||
// mkdir provides [os.Mkdir].
|
||||
mkdir(name string, perm os.FileMode) error
|
||||
// removeAll provides [os.RemoveAll].
|
||||
removeAll(path string) error
|
||||
// exit provides [os.Exit].
|
||||
exit(code int)
|
||||
|
||||
@@ -62,6 +66,8 @@ type syscallDispatcher interface {
|
||||
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
|
||||
lookupGroupId(name string) (string, error)
|
||||
|
||||
// lookPath provides exec.LookPath.
|
||||
lookPath(file string) (string, error)
|
||||
// cmdOutput provides the Output method of [exec.Cmd].
|
||||
cmdOutput(cmd *exec.Cmd) ([]byte, error)
|
||||
|
||||
@@ -121,6 +127,8 @@ func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name)
|
||||
func (direct) open(name string) (osFile, error) { return os.Open(name) }
|
||||
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
||||
func (direct) tempdir() string { return os.TempDir() }
|
||||
func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
|
||||
func (direct) removeAll(path string) error { return os.RemoveAll(path) }
|
||||
func (direct) exit(code int) { os.Exit(code) }
|
||||
|
||||
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
|
||||
@@ -134,6 +142,7 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) }
|
||||
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
|
||||
|
||||
func (direct) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
|
||||
|
||||
@@ -701,10 +701,13 @@ func (panicDispatcher) stat(string) (os.FileInfo, error) { pa
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
|
||||
@@ -9,12 +9,24 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/wayland"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// Info returns the address to a populated [hst.Info].
|
||||
//
|
||||
// This must not be called from within package outcome.
|
||||
func Info() *hst.Info {
|
||||
hi := hst.Info{WaylandVersion: wayland.Version,
|
||||
Version: info.Version(), User: new(Hsu).MustID(nil)}
|
||||
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
||||
return &hi
|
||||
}
|
||||
|
||||
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
|
||||
// It should be large enough to fit all insertions by outcomeOp.toContainer.
|
||||
const envAllocSize = 1 << 6
|
||||
@@ -58,7 +70,7 @@ type outcomeState struct {
|
||||
// Copied from their respective exported values.
|
||||
mapuid, mapgid *stringPair[int]
|
||||
|
||||
// Copied from [EnvPaths] per-process.
|
||||
// Copied from [env.Paths] per-process.
|
||||
sc hst.Paths
|
||||
*env.Paths
|
||||
|
||||
@@ -160,6 +172,10 @@ type outcomeStateSys struct {
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
// Copied from [hst.Config]. Safe for read by spPipeWireOp.toSystem only.
|
||||
directPipeWire bool
|
||||
// Copied from [hst.Config]. Safe for read by spPulseOp.toSystem only.
|
||||
directPulse bool
|
||||
// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
|
||||
extraPerms []hst.ExtraPermConfig
|
||||
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
|
||||
@@ -173,8 +189,8 @@ type outcomeStateSys struct {
|
||||
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
|
||||
return &outcomeStateSys{
|
||||
appId: config.ID, et: config.Enablements.Unwrap(),
|
||||
directWayland: config.DirectWayland, extraPerms: config.ExtraPerms,
|
||||
sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
directWayland: config.DirectWayland, directPipeWire: config.DirectPipeWire, directPulse: config.DirectPulse,
|
||||
extraPerms: config.ExtraPerms, sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
sys: sys, outcomeState: s,
|
||||
}
|
||||
}
|
||||
@@ -241,6 +257,10 @@ type outcomeStateParams struct {
|
||||
// Populated by spRuntimeOp.
|
||||
runtimeDir *check.Absolute
|
||||
|
||||
// Path to pipewire-pulse server.
|
||||
// Populated by spPipeWireOp if DirectPipeWire is false.
|
||||
pipewirePulsePath *check.Absolute
|
||||
|
||||
as hst.ApplyState
|
||||
*outcomeState
|
||||
}
|
||||
@@ -280,6 +300,7 @@ func (state *outcomeStateSys) toSystem() error {
|
||||
// optional via enablements
|
||||
&spWaylandOp{},
|
||||
&spX11Op{},
|
||||
&spPipeWireOp{},
|
||||
&spPulseOp{},
|
||||
&spDBusOp{},
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestOutcomeMain(t *testing.T) {
|
||||
func TestOutcomeRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := message.New(nil)
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
@@ -67,18 +67,12 @@ func TestOutcomeMain(t *testing.T) {
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
).
|
||||
|
||||
// ensureRuntimeDir
|
||||
Ensure(m("/run/user/1971"), 0700).
|
||||
UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).
|
||||
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
|
||||
// runtime
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
|
||||
UpdatePerm(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), acl.Execute).
|
||||
|
||||
// spPulseOp
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse")).
|
||||
// spPipeWireOp
|
||||
PipeWire(
|
||||
m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
).
|
||||
|
||||
// spDBusOp
|
||||
MustProxyDBus(
|
||||
@@ -106,8 +100,6 @@ func TestOutcomeMain(t *testing.T) {
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
"HOME=/data/data/org.chromium.Chromium",
|
||||
"PULSE_COOKIE=/.hakurei/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
@@ -144,7 +136,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable).
|
||||
|
||||
// spTmpdirOp
|
||||
@@ -157,10 +149,6 @@ func TestOutcomeMain(t *testing.T) {
|
||||
// spWaylandOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
|
||||
|
||||
// spPulseOp
|
||||
Bind(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||
Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
|
||||
// spDBusOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
@@ -182,7 +170,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{Container: &hst.ContainerConfig{
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsRoot,
|
||||
@@ -244,7 +232,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), std.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
@@ -264,6 +252,8 @@ func TestOutcomeMain(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults chromium", new(stubNixOS), &hst.Config{
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Identity: 9,
|
||||
Groups: []string{"video"},
|
||||
@@ -298,7 +288,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -347,10 +337,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
|
||||
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
|
||||
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
@@ -397,8 +384,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"HOME=/home/chronos",
|
||||
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
@@ -413,14 +399,13 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), std.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
|
||||
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
|
||||
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), m("/run/user/65534/pipewire-0"), 0).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
Bind(m("/dev/dri"), m("/dev/dri"), std.BindWritable|std.BindDevice|std.BindOptional).
|
||||
@@ -439,8 +424,10 @@ func TestOutcomeMain(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: nil,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -502,9 +489,8 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
|
||||
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), "org.chromium.Chromium", "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
@@ -544,8 +530,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"HOME=/var/lib/persist/module/hakurei/0/1",
|
||||
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=u0_a1",
|
||||
@@ -559,14 +544,13 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), std.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
|
||||
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
|
||||
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), m("/run/user/1971/pipewire-0"), 0).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
Bind(m("/bin"), m("/bin"), 0).
|
||||
@@ -899,6 +883,16 @@ func (k *stubNixOS) lookupGroupId(name string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) lookPath(file string) (string, error) {
|
||||
switch file {
|
||||
case "pipewire-pulse":
|
||||
return "/run/current-system/sw/bin/pipewire-pulse", nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected file %q", file))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
switch cmd.Path {
|
||||
case "/proc/nonexistent/hsu":
|
||||
|
||||
@@ -38,7 +38,7 @@ static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
|
||||
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
|
||||
*(int *)NULL = 0; /* unreachable */
|
||||
*(volatile int *)NULL = 0; /* unreachable */
|
||||
|
||||
struct sigaction new_action = {0}, old_action = {0};
|
||||
if (sigaction(SIGCONT, NULL, &old_action) != 0)
|
||||
|
||||
@@ -14,9 +14,12 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -83,6 +86,55 @@ func Shim(msg message.Msg) {
|
||||
shimEntrypoint(direct{msg})
|
||||
}
|
||||
|
||||
// A shimPrivate holds state of the private work directory owned by shim.
|
||||
type shimPrivate struct {
|
||||
// Path to directory if created.
|
||||
pathname *check.Absolute
|
||||
|
||||
k syscallDispatcher
|
||||
id *stringPair[hst.ID]
|
||||
}
|
||||
|
||||
// unwrap returns the underlying pathname.
|
||||
func (sp *shimPrivate) unwrap() *check.Absolute {
|
||||
if sp.pathname == nil {
|
||||
if a, err := check.NewAbs(sp.k.tempdir()); err != nil {
|
||||
sp.k.fatal(err)
|
||||
panic("unreachable")
|
||||
} else {
|
||||
pathname := a.Append(".hakurei-shim-" + sp.id.String())
|
||||
sp.k.getMsg().Verbosef("creating private work directory %q", pathname)
|
||||
if err = sp.k.mkdir(pathname.String(), 0700); err != nil {
|
||||
sp.k.fatal(err)
|
||||
panic("unreachable")
|
||||
}
|
||||
sp.pathname = pathname
|
||||
return sp.unwrap()
|
||||
}
|
||||
} else {
|
||||
return sp.pathname
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the absolute pathname to the directory held by shimPrivate.
|
||||
func (sp *shimPrivate) String() string { return sp.unwrap().String() }
|
||||
|
||||
// destroy removes the directory held by shimPrivate.
|
||||
func (sp *shimPrivate) destroy() {
|
||||
defer func() { sp.pathname = nil }()
|
||||
if sp.pathname != nil {
|
||||
sp.k.getMsg().Verbosef("destroying private work directory %q", sp.pathname)
|
||||
if err := sp.k.removeAll(sp.pathname.String()); err != nil {
|
||||
sp.k.getMsg().GetLogger().Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// shimPipeWireTimeout is the duration pipewire-pulse is allowed to run before its socket becomes available.
|
||||
shimPipeWireTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func shimEntrypoint(k syscallDispatcher) {
|
||||
msg := k.getMsg()
|
||||
if msg == nil {
|
||||
@@ -208,6 +260,7 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
|
||||
ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
cancelContainer.Store(&stop)
|
||||
sp := shimPrivate{k: k, id: state.id}
|
||||
z := container.New(ctx, msg)
|
||||
z.Params = *stateParams.params
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
@@ -215,6 +268,79 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
// bounds and default enforced in finalise.go
|
||||
z.WaitDelay = state.Shim.WaitDelay
|
||||
|
||||
if stateParams.pipewirePulsePath != nil {
|
||||
zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName)
|
||||
zpw.Hostname = "hakurei-" + pipewirePulseName
|
||||
zpw.SeccompFlags |= seccomp.AllowMultiarch
|
||||
zpw.SeccompPresets |= std.PresetStrict
|
||||
zpw.Env = []string{
|
||||
// pipewire SecurityContext socket path
|
||||
pipewire.Remote + "=" + stateParams.instancePath().Append("pipewire").String(),
|
||||
// pipewire-pulse socket directory path
|
||||
envXDGRuntimeDir + "=" + sp.String(),
|
||||
}
|
||||
if msg.IsVerbose() {
|
||||
zpw.Stdin, zpw.Stdout, zpw.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
}
|
||||
zpw.
|
||||
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
|
||||
Bind(sp.unwrap(), sp.unwrap(), std.BindWritable).
|
||||
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
||||
socketPath := sp.unwrap().Append("pulse", "native")
|
||||
innerSocketPath := stateParams.runtimeDir.Append("pulse", "native")
|
||||
|
||||
if err := k.containerStart(zpw); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot start "+pipewirePulseName+" container:", err)
|
||||
}
|
||||
if err := k.containerServe(zpw); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot configure "+pipewirePulseName+" container:", err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
k.new(func(k syscallDispatcher, msg message.Msg) { done <- k.containerWait(zpw) })
|
||||
|
||||
socketTimer := time.NewTimer(shimPipeWireTimeout)
|
||||
for {
|
||||
select {
|
||||
case <-socketTimer.C:
|
||||
sp.destroy()
|
||||
k.fatal(pipewirePulseName + " exceeded deadline before socket appeared")
|
||||
break
|
||||
|
||||
case err := <-done:
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
msg.Verbosef("cannot wait: %v", err)
|
||||
k.exit(127)
|
||||
}
|
||||
sp.destroy()
|
||||
k.fatal(pipewirePulseName + " " + exitError.ProcessState.String())
|
||||
break
|
||||
|
||||
default:
|
||||
if _, err := k.stat(socketPath.String()); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
sp.destroy()
|
||||
k.fatal(err)
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
z.Bind(socketPath, innerSocketPath, 0)
|
||||
z.Env = append(z.Env, "PULSE_SERVER=unix:"+innerSocketPath.String())
|
||||
}
|
||||
|
||||
if err := k.containerStart(z); err != nil {
|
||||
var f func(v ...any)
|
||||
if logger := msg.GetLogger(); logger != nil {
|
||||
@@ -225,9 +351,11 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
}
|
||||
}
|
||||
printMessageError(f, "cannot start container:", err)
|
||||
sp.destroy()
|
||||
k.exit(hst.ExitFailure)
|
||||
}
|
||||
if err := k.containerServe(z); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot configure container:", err)
|
||||
}
|
||||
@@ -236,10 +364,13 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
seccomp.Preset(std.PresetStrict, seccomp.AllowMultiarch),
|
||||
seccomp.AllowMultiarch,
|
||||
); err != nil {
|
||||
sp.destroy()
|
||||
k.fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
|
||||
if err := k.containerWait(z); err != nil {
|
||||
sp.destroy()
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
@@ -250,4 +381,5 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
}
|
||||
k.exit(exitError.ExitCode())
|
||||
}
|
||||
sp.destroy()
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestShimEntrypoint(t *testing.T) {
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable).
|
||||
|
||||
// spTmpdirOp
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/internal/validate"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
@@ -382,6 +382,10 @@ func (p opsAdapter) Link(target *check.Absolute, linkName string, dereference bo
|
||||
return opsAdapter{p.Ops.Link(target, linkName, dereference)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Daemon(target, path *check.Absolute, args ...string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Daemon(target, path, args...)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Root(host *check.Absolute, flags int) hst.Ops {
|
||||
return opsAdapter{p.Ops.Root(host, flags)}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
)
|
||||
|
||||
func TestSpParamsOp(t *testing.T) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spDBusOp)) }
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/dbus"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
|
||||
53
internal/outcome/sppipewire.go
Normal file
53
internal/outcome/sppipewire.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
const pipewirePulseName = "pipewire-pulse"
|
||||
|
||||
func init() { gob.Register(new(spPipeWireOp)) }
|
||||
|
||||
// spPipeWireOp exports the PipeWire server to the container via SecurityContext.
|
||||
// Runs after spRuntimeOp.
|
||||
type spPipeWireOp struct {
|
||||
// Path to pipewire-pulse server. Populated during toSystem if DirectPipeWire is false.
|
||||
CompatServerPath *check.Absolute
|
||||
}
|
||||
|
||||
func (s *spPipeWireOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EPipeWire == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
if !state.directPipeWire {
|
||||
if n, err := state.k.lookPath(pipewirePulseName); err != nil {
|
||||
return &hst.AppError{Step: "look up " + pipewirePulseName, Err: err}
|
||||
} else if s.CompatServerPath, err = check.NewAbs(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
appId := state.appId
|
||||
if appId == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appId = "app.hakurei." + state.id.String()
|
||||
}
|
||||
state.sys.PipeWire(state.instance().Append("pipewire"), appId, state.id.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPipeWireOp) toContainer(state *outcomeStateParams) error {
|
||||
if s.CompatServerPath == nil {
|
||||
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
|
||||
state.env[pipewire.Remote] = innerPath.String()
|
||||
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
|
||||
}
|
||||
|
||||
// pipewire-pulse behaviour implemented in shim.go
|
||||
state.pipewirePulsePath = s.CompatServerPath
|
||||
return nil
|
||||
}
|
||||
49
internal/outcome/sppipewire_test.go
Normal file
49
internal/outcome/sppipewire_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func TestSpPipeWireOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := hst.Template()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"not enabled", func(bool, bool) outcomeOp {
|
||||
return new(spPipeWireOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return new(spPipeWireOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.DirectPipeWire = true
|
||||
return c
|
||||
}, nil, []stub.Call{}, newI().
|
||||
// state.instance
|
||||
Ephemeral(system.Process, m(wantInstancePrefix), 0711).
|
||||
// toSystem
|
||||
PipeWire(
|
||||
m(wantInstancePrefix+"/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Bind(m(wantInstancePrefix+"/pipewire"), m("/run/user/1000/pipewire-0"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
pipewire.Remote: "/run/user/1000/pipewire-0",
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
@@ -29,7 +29,7 @@ type spPulseOp struct {
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EPulse == 0 {
|
||||
if !state.directPulse || state.et&hst.EPulse == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
|
||||
@@ -11,31 +11,47 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
)
|
||||
|
||||
func TestSpPulseOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newConfig := func() *hst.Config {
|
||||
config := hst.Template()
|
||||
config.DirectPulse = true
|
||||
config.Enablements = hst.NewEnablements(hst.EPulse)
|
||||
return config
|
||||
}
|
||||
|
||||
config := newConfig()
|
||||
sampleCookie := bytes.Repeat([]byte{0xfc}, pulseCookieSizeMax)
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"not enabled", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c := newConfig()
|
||||
c.DirectPulse = true
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"not enabled direct", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, func() *hst.Config {
|
||||
c := newConfig()
|
||||
c.DirectPulse = false
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"socketDir stat", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spPulseOp)
|
||||
}
|
||||
return &spPulseOp{Cookie: (*[256]byte)(sampleCookie)}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), stub.UniqueError(2)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: `access PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse"`,
|
||||
@@ -44,7 +60,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socketDir nonexistent", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), os.ErrNotExist),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
@@ -54,7 +70,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socket stat", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), stub.UniqueError(1)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
@@ -64,7 +80,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socket nonexistent", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), os.ErrNotExist),
|
||||
}, nil, nil, &hst.AppError{
|
||||
@@ -75,7 +91,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"socket mode", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0660}, nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
@@ -86,7 +102,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"cookie notAbs", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "proc/nonexistent/cookie", nil),
|
||||
@@ -97,7 +113,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"cookie loadFile", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -118,7 +134,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
op.CookieSize += +0xfd
|
||||
}
|
||||
return op
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -150,7 +166,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
sampleCookieTrunc := make([]byte, pulseCookieSizeMax)
|
||||
copy(sampleCookieTrunc, sampleCookie[:len(sampleCookie)-0xe])
|
||||
return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookieTrunc), CookieSize: pulseCookieSizeMax - 0xe}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -183,7 +199,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
return new(spPulseOp)
|
||||
}
|
||||
return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookie), CookieSize: pulseCookieSizeMax}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
|
||||
@@ -213,7 +229,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return new(spPulseOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
}, newConfig, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
|
||||
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -91,6 +91,9 @@ func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// xdgRuntimeDirSize is the size of the filesystem mounted on inner XDG_RUNTIME_DIR.
|
||||
const xdgRuntimeDirSize = 1 << 24
|
||||
|
||||
func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
|
||||
state.runtimeDir = fhs.AbsRunUser.Append(state.mapuid.String())
|
||||
state.env[envXDGRuntimeDir] = state.runtimeDir.String()
|
||||
@@ -108,7 +111,7 @@ func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
|
||||
|
||||
}
|
||||
|
||||
state.params.Tmpfs(fhs.AbsRunUser, 1<<12, 0755)
|
||||
state.params.Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755)
|
||||
if state.Container.Flags&hst.FShareRuntime != 0 {
|
||||
_, runtimeDirInst := s.commonPaths(state.outcomeState)
|
||||
state.params.Bind(runtimeDirInst, state.runtimeDir, std.BindWritable)
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
)
|
||||
|
||||
func TestSpRuntimeOp(t *testing.T) {
|
||||
@@ -40,7 +40,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
@@ -67,7 +67,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
@@ -94,7 +94,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
@@ -117,7 +117,7 @@ func TestSpRuntimeOp(t *testing.T) {
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
)
|
||||
|
||||
func init() { gob.Register(spTmpdirOp{}) }
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
)
|
||||
|
||||
func TestSpTmpdirOp(t *testing.T) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/wayland"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/wayland"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spWaylandOp)) }
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/system/wayland"
|
||||
"hakurei.app/internal/wayland"
|
||||
)
|
||||
|
||||
func TestSpWaylandOp(t *testing.T) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
var absX11SocketDir = fhs.AbsTmp.Append(".X11-unix")
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
func TestSpX11Op(t *testing.T) {
|
||||
|
||||
129
internal/pipewire/client.go
Normal file
129
internal/pipewire/client.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package pipewire
|
||||
|
||||
/* pipewire/client.h */
|
||||
|
||||
const (
|
||||
PW_TYPE_INTERFACE_Client = PW_TYPE_INFO_INTERFACE_BASE + "Client"
|
||||
PW_CLIENT_PERM_MASK = PW_PERM_RWXM
|
||||
PW_VERSION_CLIENT = 3
|
||||
|
||||
PW_ID_CLIENT = 1
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CLIENT_CHANGE_MASK_PROPS = 1 << iota
|
||||
|
||||
PW_CLIENT_CHANGE_MASK_ALL = 1<<iota - 1
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CLIENT_EVENT_INFO = iota
|
||||
PW_CLIENT_EVENT_PERMISSIONS
|
||||
PW_CLIENT_EVENT_NUM
|
||||
|
||||
PW_VERSION_CLIENT_EVENTS = 0
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CLIENT_METHOD_ADD_LISTENER = iota
|
||||
PW_CLIENT_METHOD_ERROR
|
||||
PW_CLIENT_METHOD_UPDATE_PROPERTIES
|
||||
PW_CLIENT_METHOD_GET_PERMISSIONS
|
||||
PW_CLIENT_METHOD_UPDATE_PERMISSIONS
|
||||
PW_CLIENT_METHOD_NUM
|
||||
|
||||
PW_VERSION_CLIENT_METHODS = 0
|
||||
)
|
||||
|
||||
// The ClientInfo event provides client information updates.
|
||||
// This is emitted when binding to a client or when the client info is updated later.
|
||||
type ClientInfo struct {
|
||||
// The global id of the client.
|
||||
ID Int `json:"id"`
|
||||
// The changes emitted by this event.
|
||||
ChangeMask Long `json:"change_mask"`
|
||||
// Properties of this object, valid when change_mask has PW_CLIENT_CHANGE_MASK_PROPS.
|
||||
Properties *SPADict `json:"props"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *ClientInfo) Opcode() byte { return PW_CLIENT_EVENT_INFO }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *ClientInfo) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *ClientInfo) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeInt) +
|
||||
Size(SizeLong) +
|
||||
c.Properties.Size()
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *ClientInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *ClientInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// ClientUpdateProperties is used to update the properties of a client.
|
||||
type ClientUpdateProperties struct {
|
||||
// Properties to update on the client.
|
||||
Properties *SPADict `json:"props"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *ClientUpdateProperties) Opcode() byte { return PW_CLIENT_METHOD_UPDATE_PROPERTIES }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *ClientUpdateProperties) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *ClientUpdateProperties) Size() Word { return SizePrefix + c.Properties.Size() }
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *ClientUpdateProperties) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *ClientUpdateProperties) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// clientUpdateProperties queues a [ClientUpdateProperties] message for the PipeWire server.
|
||||
// This method should not be called directly, the New function queues this message.
|
||||
func (ctx *Context) clientUpdateProperties(props SPADict) error {
|
||||
return ctx.writeMessage(
|
||||
PW_ID_CLIENT,
|
||||
&ClientUpdateProperties{&props},
|
||||
)
|
||||
}
|
||||
|
||||
// Client holds state of [PW_TYPE_INTERFACE_Client].
|
||||
type Client struct {
|
||||
// Additional information from the server, populated or updated during [Context.Roundtrip].
|
||||
Info *ClientInfo `json:"info"`
|
||||
|
||||
// Populated by [CoreBoundProps] events targeting [Client].
|
||||
Properties SPADict `json:"props"`
|
||||
|
||||
noRemove
|
||||
}
|
||||
|
||||
func (client *Client) consume(opcode byte, files []int, unmarshal func(v any)) error {
|
||||
closeReceivedFiles(files...)
|
||||
switch opcode {
|
||||
case PW_CLIENT_EVENT_INFO:
|
||||
unmarshal(&client.Info)
|
||||
return nil
|
||||
|
||||
default:
|
||||
panic(&UnsupportedOpcodeError{opcode, client.String()})
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) setBoundProps(event *CoreBoundProps) error {
|
||||
if event.Properties != nil {
|
||||
client.Properties = *event.Properties
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) String() string { return PW_TYPE_INTERFACE_Registry }
|
||||
157
internal/pipewire/client_test.go
Normal file
157
internal/pipewire/client_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestClientInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.ClientInfo, *pipewire.ClientInfo]{
|
||||
{"sample", samplePWContainer[1][2][1], pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
}}, nil},
|
||||
|
||||
{"sample*", samplePWContainer[1][3][1], pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
}}, nil},
|
||||
|
||||
{"sample**", samplePWContainer[1][4][1], pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestClientUpdateProperties(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.ClientUpdateProperties, *pipewire.ClientUpdateProperties]{
|
||||
{"sample", samplePWContainer[0][1][1], pipewire.ClientUpdateProperties{Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
}}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
1054
internal/pipewire/core.go
Normal file
1054
internal/pipewire/core.go
Normal file
File diff suppressed because it is too large
Load Diff
888
internal/pipewire/core_test.go
Normal file
888
internal/pipewire/core_test.go
Normal file
@@ -0,0 +1,888 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestFooterCoreGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.Footer[pipewire.FooterCoreGeneration], *pipewire.Footer[pipewire.FooterCoreGeneration]]{
|
||||
|
||||
/* recvmsg 0 */
|
||||
|
||||
{"sample0", samplePWContainer[1][0][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x22},
|
||||
}, nil},
|
||||
|
||||
{"sample1", samplePWContainer[1][5][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x23},
|
||||
}, nil},
|
||||
|
||||
// happens on the last message, client footer sent in the next roundtrip
|
||||
{"sample2", samplePWContainer[1][42][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x24},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
|
||||
encodingTestCases[pipewire.Footer[pipewire.FooterClientGeneration], *pipewire.Footer[pipewire.FooterClientGeneration]]{
|
||||
|
||||
/* sendmsg 1 */
|
||||
|
||||
{"sample0", samplePWContainer[3][0][2], pipewire.Footer[pipewire.FooterClientGeneration]{
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
// triggered by difference in sample1, sample0 is overwritten in the same roundtrip
|
||||
Payload: pipewire.FooterClientGeneration{ClientGeneration: 0x23},
|
||||
}, nil},
|
||||
|
||||
/* sendmsg 2 */
|
||||
|
||||
{"sample1", samplePWContainer[6][0][2], pipewire.Footer[pipewire.FooterClientGeneration]{
|
||||
// triggered by difference in sample2, last footer in the previous roundtrip
|
||||
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||
Payload: pipewire.FooterClientGeneration{ClientGeneration: 0x24},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreInfo, *pipewire.CoreInfo]{
|
||||
{"sample", samplePWContainer[1][0][1], pipewire.CoreInfo{
|
||||
ID: 0,
|
||||
Cookie: -2069267610,
|
||||
UserName: "alice",
|
||||
HostName: "nixos",
|
||||
Version: "1.4.7",
|
||||
Name: "pipewire-0",
|
||||
ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_CONFIG_NAME, Value: "pipewire.conf"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pipewire"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pipewire"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1446"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "link.max-buffers", Value: "16"},
|
||||
{Key: pipewire.PW_KEY_CORE_DAEMON, Value: "true"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
{Key: "default.clock.min-quantum", Value: "1024"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "log.level", Value: "2"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"}},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreDone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreDone, *pipewire.CoreDone]{
|
||||
{"sample0", samplePWContainer[1][5][1], pipewire.CoreDone{
|
||||
ID: -1,
|
||||
Sequence: 0,
|
||||
}, nil},
|
||||
|
||||
// matches the Core::Sync sample
|
||||
{"sample1", samplePWContainer[1][41][1], pipewire.CoreDone{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 3,
|
||||
}, nil},
|
||||
|
||||
// matches the second Core::Sync sample
|
||||
{"sample2", samplePWContainer[7][0][1], pipewire.CoreDone{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 6,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCorePing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CorePing, *pipewire.CorePing]{
|
||||
// handmade sample
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x20, 0, 0, 0,
|
||||
/* type: Struct */ byte(pipewire.SPA_TYPE_Struct), 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: -1 */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CorePing{
|
||||
ID: -1,
|
||||
Sequence: 0,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreError, *pipewire.CoreError]{
|
||||
// captured from pw-cli
|
||||
{"pw-cli", []byte{
|
||||
/* size: rest of data */ 0x58, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 2 */ 2, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0x67 */ 0x67, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: -1 */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 0x1b bytes */ 0x1b, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "no permission to destroy 0\x00"
|
||||
0x6e, 0x6f, 0x20, 0x70,
|
||||
0x65, 0x72, 0x6d, 0x69,
|
||||
0x73, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x20, 0x74, 0x6f,
|
||||
0x20, 0x64, 0x65, 0x73,
|
||||
0x74, 0x72, 0x6f, 0x79,
|
||||
0x20, 0x30, 0,
|
||||
|
||||
/* padding */ 0, 0, 0, 0, 0,
|
||||
}, pipewire.CoreError{
|
||||
ID: 2,
|
||||
Sequence: 0x67,
|
||||
Result: -1,
|
||||
Message: "no permission to destroy 0",
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreRemoveId(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreRemoveId, *pipewire.CoreRemoveId]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreRemoveId{
|
||||
ID: 3,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreBoundProps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreBoundProps, *pipewire.CoreBoundProps]{
|
||||
|
||||
/* recvmsg 0 */
|
||||
|
||||
{"sample0", samplePWContainer[1][1][1], pipewire.CoreBoundProps{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
GlobalID: 34,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 1 */
|
||||
|
||||
{"sample1", samplePWContainer[4][0][1], pipewire.CoreBoundProps{
|
||||
ID: 3,
|
||||
GlobalID: 3,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreHello, *pipewire.CoreHello]{
|
||||
{"sample", samplePWContainer[0][0][1], pipewire.CoreHello{
|
||||
Version: pipewire.PW_VERSION_CORE,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreSync(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreSync, *pipewire.CoreSync]{
|
||||
{"sample0", samplePWContainer[0][3][1], pipewire.CoreSync{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 3,
|
||||
}, nil},
|
||||
|
||||
{"sample1", samplePWContainer[6][1][1], pipewire.CoreSync{
|
||||
ID: 0,
|
||||
Sequence: pipewire.CoreSyncSequenceOffset + 6,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCorePong(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CorePong, *pipewire.CorePong]{
|
||||
// handmade sample
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x20, 0, 0, 0,
|
||||
/* type: Struct */ byte(pipewire.SPA_TYPE_Struct), 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: -1 */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ byte(pipewire.SPA_TYPE_Int), 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CorePong{
|
||||
ID: -1,
|
||||
Sequence: 0,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreGetRegistry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreGetRegistry, *pipewire.CoreGetRegistry]{
|
||||
{"sample", samplePWContainer[0][2][1], pipewire.CoreGetRegistry{
|
||||
Version: pipewire.PW_VERSION_REGISTRY,
|
||||
// this ends up as the Id of PW_TYPE_INTERFACE_Registry
|
||||
NewID: 2,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreCreateObject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreCreateObject, *pipewire.CoreCreateObject]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x80, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 0x13 bytes */ 0x13, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "spa-device-factory\x00"
|
||||
0x73, 0x70, 0x61, 0x2d,
|
||||
0x64, 0x65, 0x76, 0x69,
|
||||
0x63, 0x65, 0x2d, 0x66,
|
||||
0x61, 0x63, 0x74, 0x6f,
|
||||
0x72, 0x79, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
|
||||
/* size: 0x1a bytes */ 0x1a, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "PipeWire:Interface:Device\x00"
|
||||
0x50, 0x69, 0x70, 0x65,
|
||||
0x57, 0x69, 0x72, 0x65,
|
||||
0x3a, 0x49, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x66, 0x61,
|
||||
0x63, 0x65, 0x3a, 0x44,
|
||||
0x65, 0x76, 0x69, 0x63,
|
||||
0x65, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreCreateObject{
|
||||
FactoryName: "spa-device-factory",
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Device,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{},
|
||||
NewID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreDestroy, *pipewire.CoreDestroy]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreDestroy{
|
||||
ID: 3,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryGlobal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryGlobal, *pipewire.RegistryGlobal]{
|
||||
{"sample0", samplePWContainer[1][6][1], pipewire.RegistryGlobal{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Permissions: pipewire.PW_CORE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Core,
|
||||
Version: pipewire.PW_VERSION_CORE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample1", samplePWContainer[1][7][1], pipewire.RegistryGlobal{
|
||||
ID: 1,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample2", samplePWContainer[1][8][1], pipewire.RegistryGlobal{
|
||||
ID: 3,
|
||||
Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
|
||||
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample3", samplePWContainer[1][9][1], pipewire.RegistryGlobal{
|
||||
ID: 2,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample4", samplePWContainer[1][10][1], pipewire.RegistryGlobal{
|
||||
ID: 5,
|
||||
Permissions: pipewire.PW_PROFILER_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Profiler,
|
||||
Version: pipewire.PW_VERSION_PROFILER,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "5"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample5", samplePWContainer[1][11][1], pipewire.RegistryGlobal{
|
||||
ID: 4,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "4"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-profiler"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample6", samplePWContainer[1][12][1], pipewire.RegistryGlobal{
|
||||
ID: 6,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "6"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-metadata"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample7", samplePWContainer[1][13][1], pipewire.RegistryGlobal{
|
||||
ID: 7,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "7"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "6"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "metadata"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Metadata},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample8", samplePWContainer[1][14][1], pipewire.RegistryGlobal{
|
||||
ID: 8,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "8"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-device-factory"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample9", samplePWContainer[1][15][1], pipewire.RegistryGlobal{
|
||||
ID: 9,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "9"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "8"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "spa-device-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Device},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample10", samplePWContainer[1][16][1], pipewire.RegistryGlobal{
|
||||
ID: 10,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "10"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-node-factory"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample11", samplePWContainer[1][17][1], pipewire.RegistryGlobal{
|
||||
ID: 11,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "10"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "spa-node-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample12", samplePWContainer[1][18][1], pipewire.RegistryGlobal{
|
||||
ID: 12,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "12"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-node"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample13", samplePWContainer[1][19][1], pipewire.RegistryGlobal{
|
||||
ID: 13,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "13"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "12"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-node"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_ClientNode},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "6"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample14", samplePWContainer[1][20][1], pipewire.RegistryGlobal{
|
||||
ID: 14,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "14"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-device"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample15", samplePWContainer[1][21][1], pipewire.RegistryGlobal{
|
||||
ID: 15,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "15"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "14"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-device"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "Spa:Pointer:Interface:Device"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample16", samplePWContainer[1][22][1], pipewire.RegistryGlobal{
|
||||
ID: 16,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "16"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-portal"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample17", samplePWContainer[1][23][1], pipewire.RegistryGlobal{
|
||||
ID: 17,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "17"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-access"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample18", samplePWContainer[1][24][1], pipewire.RegistryGlobal{
|
||||
ID: 18,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "18"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-adapter"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample19", samplePWContainer[1][25][1], pipewire.RegistryGlobal{
|
||||
ID: 19,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "19"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "18"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "adapter"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample20", samplePWContainer[1][26][1], pipewire.RegistryGlobal{
|
||||
ID: 20,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "20"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-link-factory"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample21", samplePWContainer[1][27][1], pipewire.RegistryGlobal{
|
||||
ID: 21,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "21"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "20"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "link-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Link},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample22", samplePWContainer[1][28][1], pipewire.RegistryGlobal{
|
||||
ID: 22,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-session-manager"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample23", samplePWContainer[1][29][1], pipewire.RegistryGlobal{
|
||||
ID: 23,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "23"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:ClientEndpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample24", samplePWContainer[1][30][1], pipewire.RegistryGlobal{
|
||||
ID: 24,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "24"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:ClientSession"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample25", samplePWContainer[1][31][1], pipewire.RegistryGlobal{
|
||||
ID: 25,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "25"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:Session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample26", samplePWContainer[1][32][1], pipewire.RegistryGlobal{
|
||||
ID: 26,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "26"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:Endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample27", samplePWContainer[1][33][1], pipewire.RegistryGlobal{
|
||||
ID: 27,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "27"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint-stream"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:EndpointStream"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample28", samplePWContainer[1][34][1], pipewire.RegistryGlobal{
|
||||
ID: 28,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "28"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint-link"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:EndpointLink"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample29", samplePWContainer[1][35][1], pipewire.RegistryGlobal{
|
||||
ID: 29,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "29"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-x11-bell"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample30", samplePWContainer[1][36][1], pipewire.RegistryGlobal{
|
||||
ID: 30,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "30"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-jackdbus-detect"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample31", samplePWContainer[1][37][1], pipewire.RegistryGlobal{
|
||||
ID: 31,
|
||||
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||
Version: pipewire.PW_VERSION_NODE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "31"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_ID, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_PRIORITY_DRIVER, Value: "200000"},
|
||||
{Key: pipewire.PW_KEY_NODE_NAME, Value: "Dummy-Driver"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample32", samplePWContainer[1][38][1], pipewire.RegistryGlobal{
|
||||
ID: 32,
|
||||
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||
Version: pipewire.PW_VERSION_NODE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "32"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_ID, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_PRIORITY_DRIVER, Value: "190000"},
|
||||
{Key: pipewire.PW_KEY_NODE_NAME, Value: "Freewheel-Driver"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample33", samplePWContainer[1][39][1], pipewire.RegistryGlobal{
|
||||
ID: 33,
|
||||
Permissions: pipewire.PW_METADATA_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Metadata,
|
||||
Version: pipewire.PW_VERSION_METADATA,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "33"},
|
||||
{Key: "metadata.name", Value: "settings"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample34", samplePWContainer[1][40][1], pipewire.RegistryGlobal{
|
||||
ID: 34,
|
||||
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||
Version: pipewire.PW_VERSION_CLIENT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
},
|
||||
}, nil},
|
||||
|
||||
{"sample35", samplePWContainer[1][42][1], pipewire.RegistryGlobal{
|
||||
ID: 35,
|
||||
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||
Version: pipewire.PW_VERSION_CLIENT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "35"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1447"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "WirePlumber"},
|
||||
},
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryGlobalRemove(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryGlobalRemove, *pipewire.RegistryGlobalRemove]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data*/ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.RegistryGlobalRemove{
|
||||
ID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryBind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryBind, *pipewire.RegistryBind]{
|
||||
{"sample", samplePWContainer[3][0][1], pipewire.RegistryBind{
|
||||
ID: 3,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
|
||||
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
|
||||
NewID: 3, // registry takes up 2
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryDestroy, *pipewire.RegistryDestroy]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data*/ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.RegistryDestroy{
|
||||
ID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
73
internal/pipewire/header.go
Normal file
73
internal/pipewire/header.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// SizeHeader is the fixed size of [Header].
|
||||
SizeHeader = 16
|
||||
// SizeMax is the largest value of [Header.Size] that can be represented in its 3-byte segment.
|
||||
SizeMax = 0x00ffffff
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSizeRange indicates that the value of [Header.Size] cannot be represented in its 3-byte segment.
|
||||
ErrSizeRange = errors.New("size out of range")
|
||||
// ErrBadHeader indicates that the header slice does not have length [HeaderSize].
|
||||
ErrBadHeader = errors.New("incorrect header size")
|
||||
)
|
||||
|
||||
// A Header is the fixed-size message header described in protocol native.
|
||||
type Header struct {
|
||||
// The message id this is the destination resource/proxy id.
|
||||
ID Int `json:"Id"`
|
||||
// The opcode on the resource/proxy interface.
|
||||
Opcode byte `json:"opcode"`
|
||||
// The size of the payload and optional footer of the message.
|
||||
// Note: this value is only 24 bits long in the format.
|
||||
Size uint32 `json:"size"`
|
||||
// An increasing sequence number for each message.
|
||||
Sequence Int `json:"seq"`
|
||||
// Number of file descriptors in this message.
|
||||
FileCount Int `json:"n_fds"`
|
||||
}
|
||||
|
||||
// append appends the protocol native message header to data.
|
||||
//
|
||||
// Callers must perform bounds check on [Header.Size].
|
||||
func (h *Header) append(data []byte) []byte {
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.ID))
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.Opcode)<<24|h.Size)
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.Sequence))
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(h.FileCount))
|
||||
return data
|
||||
}
|
||||
|
||||
// MarshalBinary encodes the protocol native message header.
|
||||
func (h *Header) MarshalBinary() (data []byte, err error) {
|
||||
if h.Size&^SizeMax != 0 {
|
||||
return nil, ErrSizeRange
|
||||
}
|
||||
return h.append(make([]byte, 0, SizeHeader)), nil
|
||||
}
|
||||
|
||||
// unmarshalBinary decodes the protocol native message header.
|
||||
func (h *Header) unmarshalBinary(data [SizeHeader]byte) {
|
||||
h.ID = Int(binary.NativeEndian.Uint32(data[0:4]))
|
||||
h.Size = binary.NativeEndian.Uint32(data[4:8])
|
||||
h.Opcode = byte(h.Size >> 24)
|
||||
h.Size &= SizeMax
|
||||
h.Sequence = Int(binary.NativeEndian.Uint32(data[8:]))
|
||||
h.FileCount = Int(binary.NativeEndian.Uint32(data[12:]))
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes the protocol native message header.
|
||||
func (h *Header) UnmarshalBinary(data []byte) error {
|
||||
if len(data) != SizeHeader {
|
||||
return ErrBadHeader
|
||||
}
|
||||
h.unmarshalBinary(([SizeHeader]byte)(data))
|
||||
return nil
|
||||
}
|
||||
407
internal/pipewire/header_test.go
Normal file
407
internal/pipewire/header_test.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.Header, *pipewire.Header]{
|
||||
|
||||
/* sendmsg 0 */
|
||||
|
||||
{"PW_CORE_METHOD_HELLO", samplePWContainer[0][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_HELLO,
|
||||
Size: 0x18, Sequence: 0, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_METHOD_UPDATE_PROPERTIES", samplePWContainer[0][1][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_METHOD_UPDATE_PROPERTIES,
|
||||
Size: 0x600, Sequence: 1, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_GET_REGISTRY", samplePWContainer[0][2][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_GET_REGISTRY,
|
||||
Size: 0x28, Sequence: 2, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_SYNC 0", samplePWContainer[0][3][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_SYNC,
|
||||
Size: 0x28, Sequence: 3, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 0 */
|
||||
|
||||
{"PW_CORE_EVENT_INFO", samplePWContainer[1][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_INFO,
|
||||
Size: 0x6b8, Sequence: 0, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_EVENT_BOUND_PROPS 0", samplePWContainer[1][1][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_BOUND_PROPS,
|
||||
Size: 0x198, Sequence: 1, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_EVENT_INFO 0", samplePWContainer[1][2][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||
Size: 0x1f0, Sequence: 2, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_EVENT_INFO 1", samplePWContainer[1][3][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||
Size: 0x7a0, Sequence: 3, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CLIENT_EVENT_INFO 2", samplePWContainer[1][4][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CLIENT,
|
||||
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||
Size: 0x7d0, Sequence: 4, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_EVENT_DONE 0", samplePWContainer[1][5][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||
Size: 0x58, Sequence: 5, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 0", samplePWContainer[1][6][0], pipewire.Header{
|
||||
ID: 2, // this is specified by Core::GetRegistry in samplePWContainer[0][2][1]
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xc8, Sequence: 6, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 1", samplePWContainer[1][7][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xd8, Sequence: 7, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 2", samplePWContainer[1][8][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xa8, Sequence: 8, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 3", samplePWContainer[1][9][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 9, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 4", samplePWContainer[1][10][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xa0, Sequence: 10, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 5", samplePWContainer[1][11][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 11, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 6", samplePWContainer[1][12][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 12, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 7", samplePWContainer[1][13][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 13, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 8", samplePWContainer[1][14][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 14, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 9", samplePWContainer[1][15][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 15, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 10", samplePWContainer[1][16][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 16, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 11", samplePWContainer[1][17][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 17, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 12", samplePWContainer[1][18][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 18, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 13", samplePWContainer[1][19][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 19, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 14", samplePWContainer[1][20][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 20, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 15", samplePWContainer[1][21][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 21, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 16", samplePWContainer[1][22][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 22, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 17", samplePWContainer[1][23][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 23, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 18", samplePWContainer[1][24][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 24, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 19", samplePWContainer[1][25][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x160, Sequence: 25, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 20", samplePWContainer[1][26][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 26, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 21", samplePWContainer[1][27][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x168, Sequence: 27, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 22", samplePWContainer[1][28][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 28, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 23", samplePWContainer[1][29][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 29, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 24", samplePWContainer[1][30][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 30, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 25", samplePWContainer[1][31][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x168, Sequence: 31, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 26", samplePWContainer[1][32][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 32, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 27", samplePWContainer[1][33][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x178, Sequence: 33, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 28", samplePWContainer[1][34][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x170, Sequence: 34, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 29", samplePWContainer[1][35][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe0, Sequence: 35, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 30", samplePWContainer[1][36][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xe8, Sequence: 36, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 31", samplePWContainer[1][37][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x118, Sequence: 37, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 32", samplePWContainer[1][38][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x120, Sequence: 38, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 33", samplePWContainer[1][39][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0xd0, Sequence: 39, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 34", samplePWContainer[1][40][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x238, Sequence: 40, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_EVENT_DONE 1", samplePWContainer[1][41][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||
Size: 0x28, Sequence: 41, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
{"PW_REGISTRY_EVENT_GLOBAL 35", samplePWContainer[1][42][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||
Size: 0x268, Sequence: 42, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* sendmsg 1 */
|
||||
|
||||
{"PW_REGISTRY_METHOD_BIND", samplePWContainer[3][0][0], pipewire.Header{
|
||||
ID: 2,
|
||||
Opcode: pipewire.PW_REGISTRY_METHOD_BIND,
|
||||
Size: 0x98, Sequence: 4, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 1 */
|
||||
|
||||
{"PW_CORE_EVENT_BOUND_PROPS 1", samplePWContainer[4][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_BOUND_PROPS,
|
||||
Size: 0x68, Sequence: 43, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* sendmsg 2 */
|
||||
|
||||
{"PW_SECURITY_CONTEXT_METHOD_CREATE", samplePWContainer[6][0][0], pipewire.Header{
|
||||
ID: 3,
|
||||
Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_CREATE,
|
||||
Size: 0xd8, Sequence: 5, FileCount: 2,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_SYNC 1", samplePWContainer[6][1][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_SYNC,
|
||||
Size: 0x28, Sequence: 6, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* recvmsg 2 */
|
||||
|
||||
{"PW_CORE_EVENT_DONE 2", samplePWContainer[7][0][0], pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||
Size: 0x28, Sequence: 44, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* excerpts */
|
||||
|
||||
{"PW_CORE_EVENT_ERROR", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0x60, 0, 0,
|
||||
/* opcode: */ 3,
|
||||
/* seq: */ 0xf5, 0, 0, 0,
|
||||
/* n_fds: */ 0, 0, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_ERROR,
|
||||
Size: 0x60, Sequence: 0xf5, FileCount: 0,
|
||||
}, nil},
|
||||
|
||||
/* handmade samples */
|
||||
|
||||
{"PW_CORE_EVENT_PING", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0xed, 0xb, 0,
|
||||
/* opcode: */ 2,
|
||||
/* seq: */ 0xff, 0xff, 0, 0,
|
||||
/* n_fds: */ 0xfe, 0xca, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_EVENT_PING,
|
||||
Size: 0xbed, Sequence: 0xffff, FileCount: 0xcafe,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_PONG", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0xed, 0xb, 0,
|
||||
/* opcode: */ 3,
|
||||
/* seq: */ 0xff, 0xff, 0, 0,
|
||||
/* n_fds: */ 0xfe, 0xca, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_PONG,
|
||||
Size: 0xbed, Sequence: 0xffff, FileCount: 0xcafe,
|
||||
}, nil},
|
||||
|
||||
{"PW_CORE_METHOD_ERROR", []byte{
|
||||
/* Id: */ 0, 0, 0, 0,
|
||||
/* size: */ 0xad, 0xb, 0,
|
||||
/* opcode: */ 4,
|
||||
/* seq: */ 0xfe, 0xfe, 0, 0,
|
||||
/* n_fds: */ 0xfe, 0xca, 0, 0,
|
||||
}, pipewire.Header{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Opcode: pipewire.PW_CORE_METHOD_ERROR,
|
||||
Size: 0xbad, Sequence: 0xfefe, FileCount: 0xcafe,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
|
||||
t.Run("size range", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := (&pipewire.Header{Size: 0xff000000}).MarshalBinary(); !reflect.DeepEqual(err, pipewire.ErrSizeRange) {
|
||||
t.Errorf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("header size", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err := (*pipewire.Header)(nil).UnmarshalBinary(nil); !reflect.DeepEqual(err, pipewire.ErrBadHeader) {
|
||||
t.Errorf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
915
internal/pipewire/pipewire.go
Normal file
915
internal/pipewire/pipewire.go
Normal file
@@ -0,0 +1,915 @@
|
||||
// Package pipewire provides a partial implementation of the PipeWire protocol native.
|
||||
//
|
||||
// This implementation is created based on black box analysis and very limited static
|
||||
// analysis. The PipeWire documentation is vague and mostly nonexistent, and source code
|
||||
// readability is not great due to frequent macro abuse, confusing and inconsistent naming
|
||||
// schemes, almost complete absence of comments and the multiple layers of abstractions
|
||||
// even internal to the library. The convoluted build system and frequent (mis)use of
|
||||
// dlopen(3) further complicates static analysis efforts.
|
||||
//
|
||||
// Because of this, extreme care must be taken when reusing any code found in this package.
|
||||
// While it is extensively tested to be correct for its role within Hakurei, remember that
|
||||
// work is only done against PipeWire behaviour specific to this use case, and it is nearly
|
||||
// impossible to guarantee that this interpretation of its behaviour is intended, or correct
|
||||
// for any other uses of the protocol.
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Conn is a low level unix socket interface used by [Context].
|
||||
type Conn interface {
|
||||
// Recvmsg calls syscall.Recvmsg on the underlying socket.
|
||||
Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error)
|
||||
|
||||
// Sendmsg calls syscall.SendmsgN on the underlying socket.
|
||||
Sendmsg(p, oob []byte, flags int) (n int, err error)
|
||||
|
||||
// Close closes the connection.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// The kernel constant SCM_MAX_FD defines a limit on the number of file descriptors in the array.
|
||||
// Attempting to send an array larger than this limit causes sendmsg(2) to fail with the error
|
||||
// EINVAL. SCM_MAX_FD has the value 253 (or 255 before Linux 2.6.38).
|
||||
const _SCM_MAX_FD = 253
|
||||
|
||||
// A Context holds state of a connection to PipeWire.
|
||||
type Context struct {
|
||||
// Pending message data, committed via a call to Roundtrip.
|
||||
buf []byte
|
||||
// Current [Header.Sequence] value, incremented every write.
|
||||
sequence Int
|
||||
// Pending file descriptors to be sent with the next message.
|
||||
pendingFiles []int
|
||||
// File count already kept track of in [Header].
|
||||
headerFiles int
|
||||
|
||||
// Proxy id associations.
|
||||
proxy map[Int]eventProxy
|
||||
// Newly allocated proxies pending acknowledgement from the server.
|
||||
pendingIds map[Int]struct{}
|
||||
// Smallest available Id for the next proxy.
|
||||
nextId Int
|
||||
// Proxies targeted by the [CoreDestroy] event pending until next [CoreSync].
|
||||
pendingDestruction map[Int]struct{}
|
||||
|
||||
// Proxy for built-in core events.
|
||||
core Core
|
||||
// Proxy for built-in client events.
|
||||
client Client
|
||||
|
||||
// Current server-side [Header.Sequence] value, incremented on every event processed.
|
||||
remoteSequence Int
|
||||
// Files from the server. This is discarded on every Roundtrip so eventProxy
|
||||
// implementations must make sure to close them to avoid leaking fds.
|
||||
//
|
||||
// These are not automatically set up as [os.File] because it is impossible
|
||||
// to undo the effects of os.NewFile, which can be inconvenient for some uses.
|
||||
receivedFiles []int
|
||||
// Non-protocol errors encountered during event handling of the current Roundtrip;
|
||||
// errors that prevent event processing from continuing must be panicked.
|
||||
proxyErrors ProxyConsumeError
|
||||
// Pending footer value for the next outgoing message.
|
||||
// Newer footers appear to simply replace the existing one.
|
||||
pendingFooter KnownSize
|
||||
// Pending footer value deferred to the next round trip,
|
||||
// sent if pendingFooter is nil. This is for emulating upstream behaviour
|
||||
deferredPendingFooter KnownSize
|
||||
// Server side registry generation number.
|
||||
generation Long
|
||||
// Deferred operations ran after a [Core.Sync] completes or Close is called. Errors
|
||||
//are reported as part of [ProxyConsumeError] and is not considered fatal unless panicked.
|
||||
syncComplete []func() error
|
||||
|
||||
// Passed to [Conn.Recvmsg]. Not copied if sufficient for all received messages.
|
||||
iovecBuf [1 << 15]byte
|
||||
// Passed to [Conn.Recvmsg] for ancillary messages and is never copied.
|
||||
oobBuf [(_SCM_MAX_FD/2+_SCM_MAX_FD%2+2)<<3 + 1]byte
|
||||
// Underlying connection, usually implemented by [net.UnixConn]
|
||||
// via the [SyscallConn] adapter.
|
||||
conn Conn
|
||||
}
|
||||
|
||||
// cleanup arranges for f to be called after the next [CoreDone] event
|
||||
// or when [Context] is closed.
|
||||
func (ctx *Context) cleanup(f func() error) { ctx.syncComplete = append(ctx.syncComplete, f) }
|
||||
|
||||
// GetCore returns the address of [Core] held by this [Context].
|
||||
func (ctx *Context) GetCore() *Core { return &ctx.core }
|
||||
|
||||
// GetClient returns the address of [Client] held by this [Context].
|
||||
func (ctx *Context) GetClient() *Client { return &ctx.client }
|
||||
|
||||
// New initialises [Context] for an already established connection and returns its address.
|
||||
// The caller must not call any method of the underlying [Conn] after this function returns.
|
||||
func New(conn Conn, props SPADict) (*Context, error) {
|
||||
ctx := Context{conn: conn}
|
||||
ctx.core.ctx = &ctx
|
||||
ctx.proxy = map[Int]eventProxy{
|
||||
PW_ID_CORE: &ctx.core,
|
||||
PW_ID_CLIENT: &ctx.client,
|
||||
}
|
||||
ctx.pendingIds = map[Int]struct{}{
|
||||
PW_ID_CLIENT: {},
|
||||
}
|
||||
ctx.nextId = Int(len(ctx.proxy))
|
||||
ctx.pendingDestruction = make(map[Int]struct{})
|
||||
|
||||
if err := ctx.core.hello(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.clientUpdateProperties(props); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ctx, nil
|
||||
}
|
||||
|
||||
// A SyscallConnCloser is a [syscall.Conn] that implements [io.Closer].
|
||||
type SyscallConnCloser interface {
|
||||
syscall.Conn
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// A SyscallConn is a [Conn] adapter for [syscall.Conn].
|
||||
type SyscallConn struct{ SyscallConnCloser }
|
||||
|
||||
// Recvmsg implements [Conn.Recvmsg] via [syscall.Conn.SyscallConn].
|
||||
func (conn SyscallConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) {
|
||||
var rc syscall.RawConn
|
||||
if rc, err = conn.SyscallConn(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if controlErr := rc.Control(func(fd uintptr) {
|
||||
n, oobn, recvflags, _, err = syscall.Recvmsg(int(fd), p, oob, flags)
|
||||
}); controlErr != nil && err == nil {
|
||||
err = controlErr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sendmsg implements [Conn.Sendmsg] via [syscall.Conn.SyscallConn].
|
||||
func (conn SyscallConn) Sendmsg(p, oob []byte, flags int) (n int, err error) {
|
||||
var rc syscall.RawConn
|
||||
if rc, err = conn.SyscallConn(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if controlErr := rc.Control(func(fd uintptr) {
|
||||
n, err = syscall.SendmsgN(int(fd), p, oob, nil, flags)
|
||||
}); controlErr != nil && err == nil {
|
||||
err = controlErr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MustNew calls [New](conn, props) and panics on error.
|
||||
// It is intended for use in tests with hard-coded strings.
|
||||
func MustNew(conn Conn, props SPADict) *Context {
|
||||
if ctx, err := New(conn, props); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
// free releases the underlying storage of buf.
|
||||
func (ctx *Context) free() { ctx.buf = make([]byte, 0) }
|
||||
|
||||
// queueFiles queues some file descriptors to be sent for the next message.
|
||||
// It returns the offset of their index for the syscall.SCM_RIGHTS message.
|
||||
func (ctx *Context) queueFiles(fds ...int) (offset Fd) {
|
||||
offset = Fd(len(ctx.pendingFiles))
|
||||
ctx.pendingFiles = append(ctx.pendingFiles, fds...)
|
||||
return
|
||||
}
|
||||
|
||||
// InconsistentFilesError describes an implementation error where an incorrect amount
|
||||
// of files is queued between two messages.
|
||||
type InconsistentFilesError [2]Int
|
||||
|
||||
func (e *InconsistentFilesError) Error() string {
|
||||
return "queued " + strconv.Itoa(int(e[0])) + " files instead of the expected " + strconv.Itoa(int(e[1]))
|
||||
}
|
||||
|
||||
// writeMessage appends the POD representation of v and an optional footer to buf.
|
||||
func (ctx *Context) writeMessage(Id Int, v Message) (err error) {
|
||||
if fileCount := Int(len(ctx.pendingFiles) - ctx.headerFiles); fileCount != v.FileCount() {
|
||||
return &InconsistentFilesError{fileCount, v.FileCount()}
|
||||
}
|
||||
|
||||
if ctx.pendingFooter == nil && ctx.deferredPendingFooter != nil {
|
||||
ctx.pendingFooter, ctx.deferredPendingFooter = ctx.deferredPendingFooter, nil
|
||||
}
|
||||
|
||||
ctx.buf, err = MessageEncoder{v}.AppendMessage(ctx.buf, Id, ctx.sequence, ctx.pendingFooter)
|
||||
if err == nil {
|
||||
ctx.headerFiles = len(ctx.pendingFiles)
|
||||
ctx.pendingFooter = nil
|
||||
ctx.sequence++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// mustWriteMessage calls writeMessage and panics if a non-nil error is returned.
|
||||
// This must only be called from eventProxy.consume.
|
||||
func (ctx *Context) mustWriteMessage(Id Int, v Message) {
|
||||
if err := ctx.writeMessage(Id, v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// newProxyId returns a newly allocated proxy Id for the specified type.
|
||||
func (ctx *Context) newProxyId(proxy eventProxy, ack bool) Int {
|
||||
newId := ctx.nextId
|
||||
ctx.proxy[newId] = proxy
|
||||
if ack {
|
||||
ctx.pendingIds[newId] = struct{}{}
|
||||
}
|
||||
|
||||
increment:
|
||||
ctx.nextId++
|
||||
|
||||
if _, ok := ctx.proxy[ctx.nextId]; ok {
|
||||
goto increment
|
||||
}
|
||||
return newId
|
||||
}
|
||||
|
||||
// closeReceivedFiles closes all receivedFiles. This is only during protocol error
|
||||
// where [Context] is rendered unusable.
|
||||
func (ctx *Context) closeReceivedFiles() {
|
||||
slices.Sort(ctx.receivedFiles)
|
||||
ctx.receivedFiles = slices.Compact(ctx.receivedFiles)
|
||||
for _, fd := range ctx.receivedFiles {
|
||||
_ = syscall.Close(fd)
|
||||
}
|
||||
ctx.receivedFiles = ctx.receivedFiles[:0]
|
||||
}
|
||||
|
||||
// recvmsgFlags are flags passed to [Conn.Recvmsg] during Context.recvmsg.
|
||||
const recvmsgFlags = syscall.MSG_CMSG_CLOEXEC | syscall.MSG_DONTWAIT
|
||||
|
||||
// recvmsg receives from conn and returns the received payload backed by
|
||||
// iovecBuf. The returned slice is valid until the next call to recvmsg.
|
||||
func (ctx *Context) recvmsg(remaining []byte) (payload []byte, err error) {
|
||||
if copy(ctx.iovecBuf[:], remaining) != len(remaining) {
|
||||
// should not be reachable with correct internal usage
|
||||
return remaining, syscall.ENOMEM
|
||||
}
|
||||
|
||||
var n, oobn, recvflags int
|
||||
|
||||
for {
|
||||
n, oobn, recvflags, err = ctx.conn.Recvmsg(ctx.iovecBuf[len(remaining):], ctx.oobBuf[:], recvmsgFlags)
|
||||
|
||||
if oob := ctx.oobBuf[:oobn]; len(oob) > 0 {
|
||||
var oobErr error
|
||||
|
||||
var scm []syscall.SocketControlMessage
|
||||
if scm, oobErr = syscall.ParseSocketControlMessage(oob); oobErr != nil {
|
||||
ctx.closeReceivedFiles()
|
||||
err = oobErr
|
||||
return
|
||||
}
|
||||
|
||||
var fds []int
|
||||
for i := range scm {
|
||||
if fds, oobErr = syscall.ParseUnixRights(&scm[i]); oobErr != nil {
|
||||
ctx.closeReceivedFiles()
|
||||
err = oobErr
|
||||
return
|
||||
}
|
||||
ctx.receivedFiles = append(ctx.receivedFiles, fds...)
|
||||
}
|
||||
}
|
||||
|
||||
if recvflags&syscall.MSG_CTRUNC != 0 {
|
||||
// unreachable
|
||||
ctx.closeReceivedFiles()
|
||||
return nil, syscall.ENOMEM
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
if err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
||||
ctx.closeReceivedFiles()
|
||||
return nil, os.NewSyscallError("recvmsg", err)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if n == 0 && len(remaining) != len(ctx.iovecBuf) && err == nil {
|
||||
err = syscall.EPIPE // not wrapped as it did not come from the syscall
|
||||
}
|
||||
if n > 0 {
|
||||
payload = ctx.iovecBuf[:len(remaining)+n]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// sendmsgFlags are flags passed to [Conn.Sendmsg] during Context.sendmsg.
|
||||
const sendmsgFlags = syscall.MSG_NOSIGNAL | syscall.MSG_DONTWAIT
|
||||
|
||||
// sendmsg sends p to conn. sendmsg does not retain p.
|
||||
func (ctx *Context) sendmsg(p []byte, fds ...int) error {
|
||||
var oob []byte
|
||||
if len(fds) > 0 {
|
||||
oob = syscall.UnixRights(fds...)
|
||||
}
|
||||
|
||||
for {
|
||||
n, err := ctx.conn.Sendmsg(p, oob, sendmsgFlags)
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
|
||||
if err == nil && n != len(p) {
|
||||
err = syscall.EMSGSIZE
|
||||
}
|
||||
|
||||
if err != nil && err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
||||
return os.NewSyscallError("sendmsg", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// An UnknownIdError describes a server message with an Id unknown to [Context].
|
||||
type UnknownIdError struct {
|
||||
// Offending id decoded from Data.
|
||||
Id Int
|
||||
// Message received from the server.
|
||||
Data string
|
||||
}
|
||||
|
||||
func (e *UnknownIdError) Error() string { return "unknown proxy id " + strconv.Itoa(int(e.Id)) }
|
||||
|
||||
// UnsupportedOpcodeError describes a message with an unsupported opcode.
|
||||
type UnsupportedOpcodeError struct {
|
||||
// Offending opcode.
|
||||
Opcode byte
|
||||
// Name of interface processed by the proxy.
|
||||
Interface string
|
||||
}
|
||||
|
||||
func (e *UnsupportedOpcodeError) Error() string {
|
||||
return "unsupported " + e.Interface + " opcode " + strconv.Itoa(int(e.Opcode))
|
||||
}
|
||||
|
||||
// UnsupportedFooterOpcodeError describes a [Footer] with an unsupported opcode.
|
||||
type UnsupportedFooterOpcodeError Id
|
||||
|
||||
func (e UnsupportedFooterOpcodeError) Error() string {
|
||||
return "unsupported footer opcode " + strconv.Itoa(int(e))
|
||||
}
|
||||
|
||||
// A RoundtripUnexpectedEOFError describes an unexpected EOF encountered during [Context.Roundtrip].
|
||||
type RoundtripUnexpectedEOFError uintptr
|
||||
|
||||
const (
|
||||
// ErrRoundtripEOFHeader is returned when unexpectedly encountering EOF
|
||||
// decoding the message header.
|
||||
ErrRoundtripEOFHeader RoundtripUnexpectedEOFError = iota
|
||||
// ErrRoundtripEOFBody is returned when unexpectedly encountering EOF
|
||||
// establishing message body bounds.
|
||||
ErrRoundtripEOFBody
|
||||
// ErrRoundtripEOFFooter is like [ErrRoundtripEOFBody], but for when establishing
|
||||
// bounds for the footer instead.
|
||||
ErrRoundtripEOFFooter
|
||||
// ErrRoundtripEOFFooterOpcode is returned when unexpectedly encountering EOF
|
||||
// during the footer opcode hack.
|
||||
ErrRoundtripEOFFooterOpcode
|
||||
)
|
||||
|
||||
func (RoundtripUnexpectedEOFError) Unwrap() error { return io.ErrUnexpectedEOF }
|
||||
func (e RoundtripUnexpectedEOFError) Error() string {
|
||||
var suffix string
|
||||
switch e {
|
||||
case ErrRoundtripEOFHeader:
|
||||
suffix = "decoding message header"
|
||||
case ErrRoundtripEOFBody:
|
||||
suffix = "establishing message body bounds"
|
||||
case ErrRoundtripEOFFooter:
|
||||
suffix = "establishing message footer bounds"
|
||||
case ErrRoundtripEOFFooterOpcode:
|
||||
suffix = "decoding message footer opcode"
|
||||
|
||||
default:
|
||||
return "unexpected EOF"
|
||||
}
|
||||
|
||||
return "unexpected EOF " + suffix
|
||||
}
|
||||
|
||||
// eventProxy consumes events during a [Context.Roundtrip].
|
||||
type eventProxy interface {
|
||||
// consume consumes an event and its optional footer.
|
||||
consume(opcode byte, files []int, unmarshal func(v any)) error
|
||||
// setBoundProps stores a [CoreBoundProps] event received from the server.
|
||||
setBoundProps(event *CoreBoundProps) error
|
||||
// remove is called when the proxy is removed for any reason, usually from
|
||||
// being targeted by a [PW_CORE_EVENT_REMOVE_ID] event.
|
||||
remove() error
|
||||
|
||||
// Stringer returns the PipeWire interface name.
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// unmarshal is like [Unmarshal] but handles footer if present.
|
||||
func (ctx *Context) unmarshal(header *Header, data []byte, v any) error {
|
||||
n, err := UnmarshalNext(data, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) < int(header.Size) || header.Size < n {
|
||||
return ErrRoundtripEOFFooter
|
||||
}
|
||||
isLastMessage := len(data) == int(header.Size)
|
||||
|
||||
data = data[n:header.Size]
|
||||
if len(data) > 0 {
|
||||
/* the footer concrete type is determined by opcode, which cannot be
|
||||
decoded directly before the type is known, so this hack is required:
|
||||
skip the struct prefix, then the integer prefix, and the next SizeId
|
||||
bytes are the encoded opcode value */
|
||||
if len(data) < int(SizePrefix*2+SizeId) {
|
||||
return ErrRoundtripEOFFooterOpcode
|
||||
}
|
||||
switch opcode := binary.NativeEndian.Uint32(data[SizePrefix*2:]); opcode {
|
||||
case FOOTER_CORE_OPCODE_GENERATION:
|
||||
var footer Footer[FooterCoreGeneration]
|
||||
if err = Unmarshal(data, &footer); err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.generation != footer.Payload.RegistryGeneration {
|
||||
var pendingFooter = Footer[FooterClientGeneration]{
|
||||
FOOTER_CORE_OPCODE_GENERATION,
|
||||
FooterClientGeneration{ClientGeneration: footer.Payload.RegistryGeneration},
|
||||
}
|
||||
|
||||
// this emulates upstream behaviour that pending footer updated on the last message
|
||||
// during a roundtrip is pushed back to the first message of the next roundtrip
|
||||
if isLastMessage {
|
||||
ctx.deferredPendingFooter = &pendingFooter
|
||||
} else {
|
||||
ctx.pendingFooter = &pendingFooter
|
||||
}
|
||||
}
|
||||
ctx.generation = footer.Payload.RegistryGeneration
|
||||
return nil
|
||||
|
||||
default:
|
||||
return UnsupportedFooterOpcodeError(opcode)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// An UnexpectedSequenceError is a server-side sequence number that does not
|
||||
// match its counterpart tracked by the client. This indicates that either
|
||||
// the client has somehow missed events, or data being interpreted as [Header]
|
||||
// is, in fact, not the message header.
|
||||
type UnexpectedSequenceError Int
|
||||
|
||||
func (e UnexpectedSequenceError) Error() string { return "unexpected seq " + strconv.Itoa(int(e)) }
|
||||
|
||||
// An UnexpectedFilesError describes an inconsistent state where file count claimed by
|
||||
// [Header] accumulates to a value greater than the total number of files received.
|
||||
type UnexpectedFilesError int
|
||||
|
||||
func (e UnexpectedFilesError) Error() string {
|
||||
return "server message headers claim to have sent more files than actually received"
|
||||
}
|
||||
|
||||
// A DanglingFilesError holds onto files that were sent by the server but no [Header]
|
||||
// accounts for. These are closed by their finalizers if discarded.
|
||||
type DanglingFilesError []*os.File
|
||||
|
||||
func (e DanglingFilesError) Error() string {
|
||||
return "received " + strconv.Itoa(len(e)) + " dangling files"
|
||||
}
|
||||
|
||||
// An UnacknowledgedProxyError holds newly allocated proxy ids that the server failed
|
||||
// to acknowledge after an otherwise successful [Core.Sync].
|
||||
type UnacknowledgedProxyError []Int
|
||||
|
||||
func (e UnacknowledgedProxyError) Error() string {
|
||||
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxies"
|
||||
}
|
||||
|
||||
// An UnacknowledgedProxyDestructionError holds destroyed proxy ids that the server failed
|
||||
// to acknowledge after an otherwise successful [Core.Sync].
|
||||
type UnacknowledgedProxyDestructionError []Int
|
||||
|
||||
func (e UnacknowledgedProxyDestructionError) Error() string {
|
||||
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxy destructions"
|
||||
}
|
||||
|
||||
// A ProxyFatalError describes an error that terminates event handling during a
|
||||
// [Context.Roundtrip] and makes further event processing no longer possible.
|
||||
type ProxyFatalError struct {
|
||||
// The fatal error causing the termination of event processing.
|
||||
Err error
|
||||
// Previous non-fatal proxy errors.
|
||||
ProxyErrs []error
|
||||
}
|
||||
|
||||
func (e *ProxyFatalError) Unwrap() []error { return append(e.ProxyErrs, e.Err) }
|
||||
func (e *ProxyFatalError) Error() string {
|
||||
s := e.Err.Error()
|
||||
if len(e.ProxyErrs) > 0 {
|
||||
s += "; " + strconv.Itoa(len(e.ProxyErrs)) + " additional proxy errors occurred before this point"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// A ProxyConsumeError is a collection of non-protocol errors returned by proxies
|
||||
// during event processing. These do not prevent event handling from continuing but
|
||||
// may be considered fatal to the application.
|
||||
type ProxyConsumeError []error
|
||||
|
||||
func (e ProxyConsumeError) Unwrap() []error { return e }
|
||||
func (e ProxyConsumeError) Error() string {
|
||||
if len(e) == 0 {
|
||||
return "invalid proxy consume error"
|
||||
}
|
||||
|
||||
// first error is usually the most relevant one
|
||||
s := e[0].Error()
|
||||
if len(e) > 1 {
|
||||
s += "; " + strconv.Itoa(len(e)) + " additional proxy errors occurred after this point"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// cloneAsProxyErrors clones and truncates proxyErrors if it contains errors,
|
||||
// returning the cloned slice.
|
||||
func (ctx *Context) cloneAsProxyErrors() (proxyErrors ProxyConsumeError) {
|
||||
if len(ctx.proxyErrors) == 0 {
|
||||
return
|
||||
}
|
||||
proxyErrors = slices.Clone(ctx.proxyErrors)
|
||||
ctx.proxyErrors = ctx.proxyErrors[:0]
|
||||
return
|
||||
}
|
||||
|
||||
// cloneProxyErrors is like cloneAsProxyErrors, but returns nil if proxyErrors
|
||||
// does not contain errors.
|
||||
func (ctx *Context) cloneProxyErrors() (err error) {
|
||||
proxyErrors := ctx.cloneAsProxyErrors()
|
||||
if len(proxyErrors) > 0 {
|
||||
err = proxyErrors
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// roundtripSyncID is the id passed to Context.coreSync during a [Context.Roundtrip].
|
||||
const roundtripSyncID = 0
|
||||
|
||||
// Roundtrip sends all pending messages to the server and processes events until
|
||||
// the server has no more messages.
|
||||
//
|
||||
// For a non-nil error, if the error happens over the network, it has concrete type
|
||||
// [os.SyscallError].
|
||||
func (ctx *Context) Roundtrip() (err error) {
|
||||
err = ctx.roundtrip()
|
||||
if err == nil {
|
||||
err = ctx.cloneProxyErrors()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// roundtrip implements the Roundtrip method without checking proxyErrors.
|
||||
func (ctx *Context) roundtrip() (err error) {
|
||||
if err = ctx.sendmsg(ctx.buf, ctx.pendingFiles...); err != nil {
|
||||
return
|
||||
}
|
||||
ctx.buf = ctx.buf[:0]
|
||||
ctx.pendingFiles = ctx.pendingFiles[:0]
|
||||
ctx.headerFiles = 0
|
||||
|
||||
defer func() {
|
||||
var danglingFiles DanglingFilesError
|
||||
if len(ctx.receivedFiles) > 0 {
|
||||
// having multiple *os.File with the same fd causes serious problems
|
||||
slices.Sort(ctx.receivedFiles)
|
||||
ctx.receivedFiles = slices.Compact(ctx.receivedFiles)
|
||||
|
||||
danglingFiles = make(DanglingFilesError, 0, len(ctx.receivedFiles))
|
||||
for _, fd := range ctx.receivedFiles {
|
||||
// hold these as *os.File so they are closed if this error never reaches the caller,
|
||||
// or the caller discards or otherwise does not handle this error, to avoid leaking fds
|
||||
danglingFiles = append(danglingFiles, os.NewFile(uintptr(fd),
|
||||
"dangling fd "+strconv.Itoa(fd)+" received from PipeWire"))
|
||||
}
|
||||
ctx.receivedFiles = ctx.receivedFiles[:0]
|
||||
}
|
||||
|
||||
// populated early for finalizers, but does not overwrite existing errors
|
||||
if len(danglingFiles) > 0 && err == nil {
|
||||
ctx.closeReceivedFiles()
|
||||
err = &ProxyFatalError{Err: danglingFiles, ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
var remaining []byte
|
||||
for {
|
||||
remaining, err = ctx.consume(remaining)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// only returned by recvmsg
|
||||
if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK {
|
||||
if len(remaining) == 0 {
|
||||
err = nil
|
||||
} else if len(remaining) < SizeHeader {
|
||||
err = &ProxyFatalError{Err: ErrRoundtripEOFHeader, ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||
} else {
|
||||
err = &ProxyFatalError{Err: ErrRoundtripEOFBody, ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// currentSeq returns the current sequence number.
|
||||
// This must only be called immediately after queueing a message.
|
||||
func (ctx *Context) currentSeq() Int { return ctx.sequence - 1 }
|
||||
|
||||
// currentRemoteSeq returns the current remote sequence number.
|
||||
// This must only be called from eventProxy.consume.
|
||||
func (ctx *Context) currentRemoteSeq() Int { return ctx.remoteSequence - 1 }
|
||||
|
||||
// consume receives messages from the server and processes events.
|
||||
func (ctx *Context) consume(receiveRemaining []byte) (remaining []byte, err error) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
ctx.closeReceivedFiles()
|
||||
|
||||
recoveredErr, ok := r.(error)
|
||||
if !ok {
|
||||
panic(r)
|
||||
}
|
||||
if recoveredErr == nil {
|
||||
panic(&runtime.PanicNilError{})
|
||||
}
|
||||
|
||||
err = &ProxyFatalError{Err: recoveredErr, ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||
return
|
||||
}()
|
||||
|
||||
if remaining, err = ctx.recvmsg(receiveRemaining); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var header Header
|
||||
for len(remaining) > 0 {
|
||||
if len(remaining) < SizeHeader {
|
||||
return
|
||||
}
|
||||
|
||||
if err = header.UnmarshalBinary(remaining[:SizeHeader]); err != nil {
|
||||
return
|
||||
}
|
||||
if header.Sequence != ctx.remoteSequence {
|
||||
return remaining, UnexpectedSequenceError(header.Sequence)
|
||||
}
|
||||
|
||||
if len(remaining) < int(SizeHeader+header.Size) {
|
||||
return
|
||||
}
|
||||
ctx.remoteSequence++
|
||||
|
||||
proxy, ok := ctx.proxy[header.ID]
|
||||
if !ok {
|
||||
return remaining, &UnknownIdError{header.ID, string(remaining[:SizeHeader+header.Size])}
|
||||
}
|
||||
|
||||
fileCount := int(header.FileCount)
|
||||
if fileCount > len(ctx.receivedFiles) {
|
||||
return remaining, UnexpectedFilesError(fileCount)
|
||||
}
|
||||
files := ctx.receivedFiles[:fileCount]
|
||||
ctx.receivedFiles = ctx.receivedFiles[fileCount:]
|
||||
|
||||
remaining = remaining[SizeHeader:]
|
||||
proxyErr := proxy.consume(header.Opcode, files, func(v any) {
|
||||
if unmarshalErr := ctx.unmarshal(&header, remaining, v); unmarshalErr != nil {
|
||||
panic(unmarshalErr)
|
||||
}
|
||||
})
|
||||
remaining = remaining[header.Size:]
|
||||
if proxyErr != nil {
|
||||
ctx.proxyErrors = append(ctx.proxyErrors, proxyErr)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// An UnexpectedFileCountError is returned as part of a [ProxyFatalError] for an event
|
||||
// that received an unexpected number of files.
|
||||
type UnexpectedFileCountError [2]int
|
||||
|
||||
func (e *UnexpectedFileCountError) Error() string {
|
||||
return "received " + strconv.Itoa(e[1]) + " files instead of the expected " + strconv.Itoa(e[0])
|
||||
}
|
||||
|
||||
// closeReceivedFiles closes all received files and panics with [UnexpectedFileCountError]
|
||||
// if one or more files are passed. This is used with events that do not expect files.
|
||||
func closeReceivedFiles(fds ...int) {
|
||||
for _, fd := range fds {
|
||||
_ = syscall.Close(fd)
|
||||
}
|
||||
if len(fds) > 0 {
|
||||
panic(&UnexpectedFileCountError{0, len(fds)})
|
||||
}
|
||||
}
|
||||
|
||||
// doSyncComplete calls syncComplete functions and collects their errors alongside errors
|
||||
// cloned from proxyErrors. A panic is translated into ProxyFatalError.
|
||||
func (ctx *Context) doSyncComplete() (err error) {
|
||||
proxyErrors := ctx.cloneAsProxyErrors()
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
ctx.closeReceivedFiles()
|
||||
|
||||
recoveredErr, ok := r.(error)
|
||||
if !ok {
|
||||
panic(r)
|
||||
}
|
||||
if recoveredErr == nil {
|
||||
panic(&runtime.PanicNilError{})
|
||||
}
|
||||
|
||||
err = &ProxyFatalError{Err: recoveredErr, ProxyErrs: proxyErrors}
|
||||
return
|
||||
}()
|
||||
|
||||
for _, f := range ctx.syncComplete {
|
||||
if scErr := f(); scErr != nil {
|
||||
proxyErrors = append(proxyErrors, scErr)
|
||||
}
|
||||
}
|
||||
ctx.syncComplete = ctx.syncComplete[:0]
|
||||
|
||||
if len(proxyErrors) > 0 {
|
||||
err = proxyErrors
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Close frees the underlying buffer and closes the connection.
|
||||
func (ctx *Context) Close() (err error) {
|
||||
ctx.free()
|
||||
err = ctx.doSyncComplete()
|
||||
closeErr := ctx.conn.Close()
|
||||
|
||||
if closeErr != nil {
|
||||
if err == nil {
|
||||
return closeErr
|
||||
} else if proxyErrors, ok := err.(ProxyConsumeError); ok {
|
||||
return &ProxyFatalError{Err: err, ProxyErrs: proxyErrors}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// expectsCoreError returns a function that inspects an error value and
|
||||
// returns the address of a [CoreError] if it is the only error present
|
||||
// and targets the specified proxy and sequence.
|
||||
//
|
||||
// The behaviour of expectsCoreError is only correct for an empty buf
|
||||
// prior to calling. If buf is not empty, [Core.Sync] is called, with
|
||||
// its return value stored to the value pointed to by errP if not nil,
|
||||
// and the function is not populated.
|
||||
//
|
||||
// The caller must queue a message and call [Core.Sync] immediately
|
||||
// after calling expectsCoreError.
|
||||
func (ctx *Context) expectsCoreError(id Int, errP *error) (asCoreError func() (coreError *CoreError)) {
|
||||
if len(ctx.buf) > 0 {
|
||||
if err := ctx.GetCore().Sync(); err != nil {
|
||||
*errP = err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
sequence := ctx.sequence
|
||||
return func() (coreError *CoreError) {
|
||||
if proxyErrors, ok := (*errP).(ProxyConsumeError); !ok ||
|
||||
len(proxyErrors) != 1 ||
|
||||
!errors.As(proxyErrors[0], &coreError) ||
|
||||
coreError == nil ||
|
||||
coreError.ID != id ||
|
||||
coreError.Sequence != sequence {
|
||||
// do not return a non-matching CoreError
|
||||
coreError = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// A PermissionError describes an error emitted by the server when trying to
|
||||
// perform an operation that the client has no permission for.
|
||||
type PermissionError struct {
|
||||
// The id of the resource (proxy if emitted by the client) that is in error.
|
||||
ID Int `json:"id"`
|
||||
// An error message.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (*PermissionError) Unwrap() error { return syscall.EPERM }
|
||||
func (e *PermissionError) Error() string { return e.Message }
|
||||
|
||||
// Remote is the environment (sic) with the remote name.
|
||||
const Remote = "PIPEWIRE_REMOTE"
|
||||
|
||||
/* modules/module-protocol-native/local-socket.c */
|
||||
|
||||
const DEFAULT_SYSTEM_RUNTIME_DIR = "/run/pipewire"
|
||||
|
||||
// connectName connects to a PipeWire remote by name and returns the [net.UnixConn].
|
||||
func connectName(name string, manager bool) (conn *net.UnixConn, err error) {
|
||||
if manager && !strings.HasSuffix(name, "-manager") {
|
||||
return connectName(name+"-manager", false)
|
||||
}
|
||||
|
||||
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
|
||||
return net.DialUnix("unix", nil, &net.UnixAddr{Name: name, Net: "unix"})
|
||||
} else {
|
||||
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
runtimeDir, ok = os.LookupEnv("XDG_RUNTIME_DIR")
|
||||
}
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
// this is cargo culted from windows stuff and has no effect normally;
|
||||
// keeping it to maintain compatibility in case someone sets this
|
||||
runtimeDir, ok = os.LookupEnv("USERPROFILE")
|
||||
}
|
||||
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
|
||||
}
|
||||
return net.DialUnix("unix", nil, &net.UnixAddr{Name: path.Join(runtimeDir, name), Net: "unix"})
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectName connects to a PipeWire remote by name.
|
||||
func ConnectName(name string, manager bool, props SPADict) (ctx *Context, err error) {
|
||||
if manager {
|
||||
props = append(props, SPADictItem{Key: PW_KEY_REMOTE_INTENTION, Value: "manager"})
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
if v, ok := os.LookupEnv(Remote); !ok || v == "" {
|
||||
name = PW_DEFAULT_REMOTE
|
||||
} else {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
|
||||
var conn *net.UnixConn
|
||||
if conn, err = connectName(name, manager); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx, err = New(SyscallConn{conn}, props); err != nil {
|
||||
ctx = nil
|
||||
_ = conn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Connect connects to the PipeWire remote.
|
||||
func Connect(manager bool, props SPADict) (ctx *Context, err error) {
|
||||
return ConnectName("", manager, props)
|
||||
}
|
||||
886
internal/pipewire/pipewire_test.go
Normal file
886
internal/pipewire/pipewire_test.go
Normal file
@@ -0,0 +1,886 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
. "syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
// Underlying connection stub holding test data.
|
||||
conn = stubUnixConn{samples: []stubUnixConnSample{
|
||||
{SYS_SENDMSG, samplePWContainer00, MSG_DONTWAIT | MSG_NOSIGNAL, nil, 0},
|
||||
{SYS_RECVMSG, samplePWContainer01, MSG_DONTWAIT | MSG_CMSG_CLOEXEC, nil, 0},
|
||||
{SYS_RECVMSG, "", MSG_DONTWAIT | MSG_CMSG_CLOEXEC, nil, EAGAIN},
|
||||
{SYS_SENDMSG, samplePWContainer03, MSG_DONTWAIT | MSG_NOSIGNAL, nil, 0},
|
||||
{SYS_RECVMSG, samplePWContainer04, MSG_DONTWAIT | MSG_CMSG_CLOEXEC, nil, 0},
|
||||
{SYS_RECVMSG, "", MSG_DONTWAIT | MSG_CMSG_CLOEXEC, nil, EAGAIN},
|
||||
{SYS_SENDMSG, samplePWContainer06, MSG_DONTWAIT | MSG_NOSIGNAL, []int{20, 21}, 0},
|
||||
{SYS_RECVMSG, samplePWContainer07, MSG_DONTWAIT | MSG_CMSG_CLOEXEC, nil, 0},
|
||||
{SYS_RECVMSG, "", MSG_DONTWAIT | MSG_CMSG_CLOEXEC, nil, EAGAIN},
|
||||
}}
|
||||
|
||||
// Context instance under testing.
|
||||
ctx = pipewire.MustNew(&conn, pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
})
|
||||
)
|
||||
|
||||
var registry *pipewire.Registry
|
||||
const wantRegistryId = 2
|
||||
if r, err := ctx.GetRegistry(); err != nil {
|
||||
t.Fatalf("GetRegistry: error = %v", err)
|
||||
} else {
|
||||
if r.ID != wantRegistryId {
|
||||
t.Fatalf("GetRegistry: ID = %d, want %d", r.ID, wantRegistryId)
|
||||
}
|
||||
registry = r
|
||||
}
|
||||
if err := ctx.GetCore().Sync(); err != nil {
|
||||
t.Fatalf("Sync: error = %v", err)
|
||||
}
|
||||
|
||||
wantCoreInfo0 := pipewire.CoreInfo{
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Cookie: -2069267610,
|
||||
UserName: "alice",
|
||||
HostName: "nixos",
|
||||
Version: "1.4.7",
|
||||
Name: "pipewire-0",
|
||||
ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_CONFIG_NAME, Value: "pipewire.conf"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pipewire"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pipewire"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1446"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "link.max-buffers", Value: "16"},
|
||||
{Key: pipewire.PW_KEY_CORE_DAEMON, Value: "true"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
{Key: "default.clock.min-quantum", Value: "1024"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "log.level", Value: "2"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
|
||||
},
|
||||
}
|
||||
|
||||
wantClient0 := pipewire.Client{
|
||||
Info: &pipewire.ClientInfo{
|
||||
ID: 34,
|
||||
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-alice-1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_BINARY, Value: "pw-container"},
|
||||
{Key: pipewire.PW_KEY_APP_LANGUAGE, Value: "en_US.UTF-8"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_ID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_USER, Value: "alice"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_HOST, Value: "nixos"},
|
||||
{Key: pipewire.PW_KEY_APP_PROCESS_SESSION_ID, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_WINDOW_X11_DISPLAY, Value: ":0"},
|
||||
{Key: "cpu.vm.name", Value: "qemu"},
|
||||
{Key: "log.level", Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CPU_MAX_ALIGN, Value: "32"},
|
||||
{Key: "default.clock.rate", Value: "48000"},
|
||||
{Key: "default.clock.quantum", Value: "1024"},
|
||||
{Key: "default.clock.min-quantum", Value: "32"},
|
||||
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||
{Key: "default.video.width", Value: "640"},
|
||||
{Key: "default.video.height", Value: "480"},
|
||||
{Key: "default.video.rate.num", Value: "25"},
|
||||
{Key: "default.video.rate.denom", Value: "1"},
|
||||
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||
{Key: "link.max-buffers", Value: "64"},
|
||||
{Key: "mem.warn-mlock", Value: "false"},
|
||||
{Key: "mem.allow-mlock", Value: "true"},
|
||||
{Key: "settings.check-quantum", Value: "false"},
|
||||
{Key: "settings.check-rate", Value: "false"},
|
||||
{Key: pipewire.PW_KEY_CORE_VERSION, Value: "1.4.7"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
},
|
||||
},
|
||||
Properties: pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
},
|
||||
}
|
||||
|
||||
wantRegistry0 := pipewire.Registry{
|
||||
ID: wantRegistryId,
|
||||
Objects: map[pipewire.Int]pipewire.RegistryGlobal{
|
||||
pipewire.PW_ID_CORE: {
|
||||
ID: pipewire.PW_ID_CORE,
|
||||
Permissions: pipewire.PW_CORE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Core,
|
||||
Version: pipewire.PW_VERSION_CORE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
|
||||
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
|
||||
},
|
||||
},
|
||||
|
||||
1: {
|
||||
ID: 1,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "1"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"},
|
||||
},
|
||||
},
|
||||
|
||||
3: {
|
||||
ID: 3,
|
||||
Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
|
||||
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
|
||||
},
|
||||
},
|
||||
|
||||
2: {
|
||||
ID: 2,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"},
|
||||
},
|
||||
},
|
||||
|
||||
5: {
|
||||
ID: 5,
|
||||
Permissions: pipewire.PW_PROFILER_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Profiler,
|
||||
Version: pipewire.PW_VERSION_PROFILER,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "5"},
|
||||
},
|
||||
},
|
||||
|
||||
4: {
|
||||
ID: 4,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "4"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-profiler"},
|
||||
},
|
||||
},
|
||||
|
||||
6: {
|
||||
ID: 6,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "6"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-metadata"},
|
||||
},
|
||||
},
|
||||
|
||||
7: {
|
||||
ID: 7,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "7"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "6"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "metadata"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Metadata},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
},
|
||||
|
||||
8: {
|
||||
ID: 8,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "8"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-device-factory"},
|
||||
},
|
||||
},
|
||||
|
||||
9: {
|
||||
ID: 9,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "9"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "8"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "spa-device-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Device},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
},
|
||||
|
||||
10: {
|
||||
ID: 10,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "10"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-node-factory"},
|
||||
},
|
||||
},
|
||||
|
||||
11: {
|
||||
ID: 11,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "10"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "spa-node-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
},
|
||||
|
||||
12: {
|
||||
ID: 12,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "12"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-node"},
|
||||
},
|
||||
},
|
||||
|
||||
13: {
|
||||
ID: 13,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "13"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "12"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-node"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_ClientNode},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "6"},
|
||||
},
|
||||
},
|
||||
|
||||
14: {
|
||||
ID: 14,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "14"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-device"},
|
||||
},
|
||||
},
|
||||
|
||||
15: {
|
||||
ID: 15,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "15"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "14"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-device"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "Spa:Pointer:Interface:Device"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
16: {
|
||||
ID: 16,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "16"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-portal"},
|
||||
},
|
||||
},
|
||||
|
||||
17: {
|
||||
ID: 17,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "17"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-access"},
|
||||
},
|
||||
},
|
||||
|
||||
18: {
|
||||
ID: 18,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "18"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-adapter"},
|
||||
},
|
||||
},
|
||||
|
||||
19: {
|
||||
ID: 19,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "19"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "18"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "adapter"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
},
|
||||
|
||||
20: {
|
||||
ID: 20,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "20"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-link-factory"},
|
||||
},
|
||||
},
|
||||
|
||||
21: {
|
||||
ID: 21,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "21"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "20"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "link-factory"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: pipewire.PW_TYPE_INTERFACE_Link},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "3"},
|
||||
},
|
||||
},
|
||||
|
||||
22: {
|
||||
ID: 22,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-session-manager"},
|
||||
},
|
||||
},
|
||||
|
||||
23: {
|
||||
ID: 23,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "23"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:ClientEndpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
24: {
|
||||
ID: 24,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "24"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "client-session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:ClientSession"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
25: {
|
||||
ID: 25,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "25"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:Session"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
26: {
|
||||
ID: 26,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "26"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:Endpoint"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
27: {
|
||||
ID: 27,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "27"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint-stream"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:EndpointStream"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
28: {
|
||||
ID: 28,
|
||||
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "28"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "22"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_NAME, Value: "endpoint-link"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_NAME, Value: "PipeWire:Interface:EndpointLink"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_TYPE_VERSION, Value: "0"},
|
||||
},
|
||||
},
|
||||
|
||||
29: {
|
||||
ID: 29,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "29"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-x11-bell"},
|
||||
},
|
||||
},
|
||||
|
||||
30: {
|
||||
ID: 30,
|
||||
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||
Version: pipewire.PW_VERSION_MODULE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "30"},
|
||||
{Key: pipewire.PW_KEY_MODULE_NAME, Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-jackdbus-detect"},
|
||||
},
|
||||
},
|
||||
|
||||
31: {
|
||||
ID: 31,
|
||||
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||
Version: pipewire.PW_VERSION_NODE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "31"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_ID, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_PRIORITY_DRIVER, Value: "200000"},
|
||||
{Key: pipewire.PW_KEY_NODE_NAME, Value: "Dummy-Driver"},
|
||||
},
|
||||
},
|
||||
|
||||
32: {
|
||||
ID: 32,
|
||||
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||
Version: pipewire.PW_VERSION_NODE,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "32"},
|
||||
{Key: pipewire.PW_KEY_FACTORY_ID, Value: "11"},
|
||||
{Key: pipewire.PW_KEY_PRIORITY_DRIVER, Value: "190000"},
|
||||
{Key: pipewire.PW_KEY_NODE_NAME, Value: "Freewheel-Driver"},
|
||||
},
|
||||
},
|
||||
|
||||
33: {
|
||||
ID: 33,
|
||||
Permissions: pipewire.PW_METADATA_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Metadata,
|
||||
Version: pipewire.PW_VERSION_METADATA,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "33"},
|
||||
{Key: "metadata.name", Value: "settings"},
|
||||
},
|
||||
},
|
||||
|
||||
34: {
|
||||
ID: 34,
|
||||
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||
Version: pipewire.PW_VERSION_CLIENT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "34"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1443"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "pw-container"},
|
||||
},
|
||||
},
|
||||
|
||||
35: {
|
||||
ID: 35,
|
||||
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||
Version: pipewire.PW_VERSION_CLIENT,
|
||||
Properties: &pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "35"},
|
||||
{Key: pipewire.PW_KEY_MODULE_ID, Value: "2"},
|
||||
{Key: pipewire.PW_KEY_PROTOCOL, Value: "protocol-native"},
|
||||
{Key: pipewire.PW_KEY_SEC_PID, Value: "1447"},
|
||||
{Key: pipewire.PW_KEY_SEC_UID, Value: "1000"},
|
||||
{Key: pipewire.PW_KEY_SEC_GID, Value: "100"},
|
||||
{Key: pipewire.PW_KEY_SEC_SOCKET, Value: "pipewire-0-manager"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "unrestricted"},
|
||||
{Key: pipewire.PW_KEY_APP_NAME, Value: "WirePlumber"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if coreInfo := ctx.GetCore().Info; !reflect.DeepEqual(coreInfo, &wantCoreInfo0) {
|
||||
t.Fatalf("New: CoreInfo = %s, want %s", mustMarshalJSON(coreInfo), mustMarshalJSON(&wantCoreInfo0))
|
||||
}
|
||||
if client := ctx.GetClient(); !reflect.DeepEqual(client, &wantClient0) {
|
||||
t.Fatalf("New: Client = %s, want %s", mustMarshalJSON(client), mustMarshalJSON(&wantClient0))
|
||||
}
|
||||
if registry.ID != wantRegistry0.ID {
|
||||
t.Fatalf("GetRegistry: ID = %d, want %d", registry.ID, wantRegistry0.ID)
|
||||
}
|
||||
if !reflect.DeepEqual(registry.Objects, wantRegistry0.Objects) {
|
||||
t.Fatalf("GetRegistry: Objects = %s, want %s", mustMarshalJSON(registry.Objects), mustMarshalJSON(wantRegistry0.Objects))
|
||||
}
|
||||
|
||||
var securityContext *pipewire.SecurityContext
|
||||
const wantSecurityContextId = 3
|
||||
if c, err := registry.GetSecurityContext(); err != nil {
|
||||
t.Fatalf("GetSecurityContext: error = %v", err)
|
||||
} else {
|
||||
if c.ID != wantSecurityContextId {
|
||||
t.Fatalf("GetSecurityContext: ID = %d, want %d", c.ID, wantSecurityContextId)
|
||||
}
|
||||
securityContext = c
|
||||
}
|
||||
if err := ctx.Roundtrip(); err != nil {
|
||||
t.Fatalf("Roundtrip: error = %v", err)
|
||||
}
|
||||
|
||||
// none of these should change
|
||||
if coreInfo := ctx.GetCore().Info; !reflect.DeepEqual(coreInfo, &wantCoreInfo0) {
|
||||
t.Fatalf("Roundtrip: CoreInfo = %s, want %s", mustMarshalJSON(coreInfo), mustMarshalJSON(&wantCoreInfo0))
|
||||
}
|
||||
if client := ctx.GetClient(); !reflect.DeepEqual(client, &wantClient0) {
|
||||
t.Fatalf("Roundtrip: Client = %s, want %s", mustMarshalJSON(client), mustMarshalJSON(&wantClient0))
|
||||
}
|
||||
if registry.ID != wantRegistry0.ID {
|
||||
t.Fatalf("Roundtrip: ID = %d, want %d", registry.ID, wantRegistry0.ID)
|
||||
}
|
||||
if !reflect.DeepEqual(registry.Objects, wantRegistry0.Objects) {
|
||||
t.Fatalf("Roundtrip: Objects = %s, want %s", mustMarshalJSON(registry.Objects), mustMarshalJSON(wantRegistry0.Objects))
|
||||
}
|
||||
|
||||
if err := securityContext.Create(21, 20, pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_SEC_ENGINE, Value: "org.flatpak"},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "restricted"},
|
||||
}); err != nil {
|
||||
t.Fatalf("SecurityContext.Create: error = %v", err)
|
||||
}
|
||||
|
||||
// none of these should change
|
||||
if coreInfo := ctx.GetCore().Info; !reflect.DeepEqual(coreInfo, &wantCoreInfo0) {
|
||||
t.Fatalf("Roundtrip: CoreInfo = %s, want %s", mustMarshalJSON(coreInfo), mustMarshalJSON(&wantCoreInfo0))
|
||||
}
|
||||
if client := ctx.GetClient(); !reflect.DeepEqual(client, &wantClient0) {
|
||||
t.Fatalf("Roundtrip: Client = %s, want %s", mustMarshalJSON(client), mustMarshalJSON(&wantClient0))
|
||||
}
|
||||
if registry.ID != wantRegistry0.ID {
|
||||
t.Fatalf("Roundtrip: ID = %d, want %d", registry.ID, wantRegistry0.ID)
|
||||
}
|
||||
if !reflect.DeepEqual(registry.Objects, wantRegistry0.Objects) {
|
||||
t.Fatalf("Roundtrip: Objects = %s, want %s", mustMarshalJSON(registry.Objects), mustMarshalJSON(wantRegistry0.Objects))
|
||||
}
|
||||
|
||||
if err := ctx.Close(); err != nil {
|
||||
t.Fatalf("Close: error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// stubUnixConnSample is sample data held by stubUnixConn.
|
||||
type stubUnixConnSample struct {
|
||||
nr uintptr
|
||||
iovec string
|
||||
flags int
|
||||
files []int
|
||||
errno Errno
|
||||
}
|
||||
|
||||
// stubUnixConn implements [pipewire.Conn] and checks the behaviour of [pipewire.Context].
|
||||
type stubUnixConn struct {
|
||||
samples []stubUnixConnSample
|
||||
current int
|
||||
}
|
||||
|
||||
// nextSample returns the current sample and increments the counter.
|
||||
func (conn *stubUnixConn) nextSample(nr uintptr) (sample *stubUnixConnSample, wantOOB []byte, err error) {
|
||||
sample = &conn.samples[conn.current]
|
||||
conn.current++
|
||||
if sample.nr != nr {
|
||||
err = fmt.Errorf("unexpected syscall %d", SYS_SENDMSG)
|
||||
return
|
||||
}
|
||||
if len(sample.files) > 0 {
|
||||
wantOOB = UnixRights(sample.files...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *stubUnixConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) {
|
||||
var (
|
||||
sample *stubUnixConnSample
|
||||
wantOOB []byte
|
||||
)
|
||||
sample, wantOOB, err = conn.nextSample(SYS_RECVMSG)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n = copy(p, sample.iovec); n != len(sample.iovec) {
|
||||
err = fmt.Errorf("insufficient iovec size %d, want at least %d", len(p), len(sample.iovec))
|
||||
return
|
||||
}
|
||||
if oobn = copy(oob, wantOOB); oobn != len(wantOOB) {
|
||||
err = fmt.Errorf("insufficient oob size %d, want at least %d", len(oob), len(wantOOB))
|
||||
return
|
||||
}
|
||||
if flags != sample.flags {
|
||||
err = fmt.Errorf("flags = %#x, want %#x", flags, sample.flags)
|
||||
return
|
||||
}
|
||||
|
||||
recvflags = MSG_CMSG_CLOEXEC
|
||||
if sample.errno != 0 {
|
||||
err = sample.errno
|
||||
if n != 0 {
|
||||
panic("invalid recvmsg: n = " + strconv.Itoa(n))
|
||||
}
|
||||
n = -1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *stubUnixConn) Sendmsg(p, oob []byte, flags int) (n int, err error) {
|
||||
var (
|
||||
sample *stubUnixConnSample
|
||||
wantOOB []byte
|
||||
)
|
||||
sample, wantOOB, err = conn.nextSample(SYS_SENDMSG)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if string(p) != sample.iovec {
|
||||
err = fmt.Errorf("iovec: %#v, want %#v", p, []byte(sample.iovec))
|
||||
return
|
||||
}
|
||||
if string(oob[:len(wantOOB)]) != string(wantOOB) {
|
||||
err = fmt.Errorf("oob: %#v, want %#v", oob[:len(wantOOB)], wantOOB)
|
||||
return
|
||||
}
|
||||
if flags != sample.flags {
|
||||
err = fmt.Errorf("flags = %#x, want %#x", flags, sample.flags)
|
||||
return
|
||||
}
|
||||
|
||||
n = len(sample.iovec)
|
||||
if sample.errno != 0 {
|
||||
err = sample.errno
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *stubUnixConn) Close() error {
|
||||
if conn.current != len(conn.samples) {
|
||||
return fmt.Errorf("consumed %d samples, want %d", conn.current, len(conn.samples))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestContextErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"ProxyConsumeError invalid", pipewire.ProxyConsumeError{}, "invalid proxy consume error"},
|
||||
{"ProxyConsumeError single", pipewire.ProxyConsumeError{
|
||||
stub.UniqueError(0),
|
||||
}, "unique error 0 injected by the test suite"},
|
||||
{"ProxyConsumeError multiple", pipewire.ProxyConsumeError{
|
||||
stub.UniqueError(1),
|
||||
stub.UniqueError(2),
|
||||
stub.UniqueError(3),
|
||||
stub.UniqueError(4),
|
||||
stub.UniqueError(5),
|
||||
stub.UniqueError(6),
|
||||
stub.UniqueError(7),
|
||||
}, "unique error 1 injected by the test suite; 7 additional proxy errors occurred after this point"},
|
||||
|
||||
{"ProxyFatalError", &pipewire.ProxyFatalError{
|
||||
Err: stub.UniqueError(8),
|
||||
}, "unique error 8 injected by the test suite"},
|
||||
{"ProxyFatalError proxy errors", &pipewire.ProxyFatalError{
|
||||
Err: stub.UniqueError(9),
|
||||
ProxyErrs: make([]error, 1<<4),
|
||||
}, "unique error 9 injected by the test suite; 16 additional proxy errors occurred before this point"},
|
||||
|
||||
{"UnexpectedFileCountError", &pipewire.UnexpectedFileCountError{0, -1}, "received -1 files instead of the expected 0"},
|
||||
{"UnacknowledgedProxyError", make(pipewire.UnacknowledgedProxyError, 1<<4), "server did not acknowledge 16 proxies"},
|
||||
{"UnacknowledgedProxyDestructionError", make(pipewire.UnacknowledgedProxyDestructionError, 1<<4), "server did not acknowledge 16 proxy destructions"},
|
||||
{"DanglingFilesError", make(pipewire.DanglingFilesError, 1<<4), "received 16 dangling files"},
|
||||
{"UnexpectedFilesError", pipewire.UnexpectedFilesError(1 << 4), "server message headers claim to have sent more files than actually received"},
|
||||
{"UnexpectedSequenceError", pipewire.UnexpectedSequenceError(1 << 4), "unexpected seq 16"},
|
||||
{"UnsupportedFooterOpcodeError", pipewire.UnsupportedFooterOpcodeError(1 << 4), "unsupported footer opcode 16"},
|
||||
|
||||
{"RoundtripUnexpectedEOFError ErrRoundtripEOFHeader", pipewire.ErrRoundtripEOFHeader, "unexpected EOF decoding message header"},
|
||||
{"RoundtripUnexpectedEOFError ErrRoundtripEOFBody", pipewire.ErrRoundtripEOFBody, "unexpected EOF establishing message body bounds"},
|
||||
{"RoundtripUnexpectedEOFError ErrRoundtripEOFFooter", pipewire.ErrRoundtripEOFFooter, "unexpected EOF establishing message footer bounds"},
|
||||
{"RoundtripUnexpectedEOFError ErrRoundtripEOFFooterOpcode", pipewire.ErrRoundtripEOFFooterOpcode, "unexpected EOF decoding message footer opcode"},
|
||||
{"RoundtripUnexpectedEOFError invalid", pipewire.RoundtripUnexpectedEOFError(0xbad), "unexpected EOF"},
|
||||
{"InconsistentFilesError", &pipewire.InconsistentFilesError{0, 2}, "queued 0 files instead of the expected 2"},
|
||||
|
||||
{"UnsupportedOpcodeError", &pipewire.UnsupportedOpcodeError{
|
||||
Opcode: 0xff,
|
||||
Interface: pipewire.PW_TYPE_INFO_INTERFACE_BASE + "Invalid",
|
||||
}, "unsupported PipeWire:Interface:Invalid opcode 255"},
|
||||
|
||||
{"UnknownIdError", &pipewire.UnknownIdError{
|
||||
Id: -1,
|
||||
Data: "\x00",
|
||||
}, "unknown proxy id -1"},
|
||||
|
||||
{"InvalidPingError", &pipewire.InvalidPingError{
|
||||
ID: 0xbad,
|
||||
Sequence: 0xcafe,
|
||||
}, "received Core::Ping seq 51966 targeting unknown proxy id 2989"},
|
||||
|
||||
{"GlobalIDCollisionError", &pipewire.GlobalIDCollisionError{
|
||||
ID: 0xbad,
|
||||
Previous: &pipewire.RegistryGlobal{Type: "PipeWire:Interface:Invalid"},
|
||||
Current: &pipewire.RegistryGlobal{Type: "PipeWire:Interface:NewInvalid"},
|
||||
}, "new Registry::Global event for PipeWire:Interface:NewInvalid stepping on previous id 2989 for PipeWire:Interface:Invalid"},
|
||||
|
||||
{"UnknownGlobalIDRemoveError", pipewire.UnknownGlobalIDRemoveError(0xbad), "Registry::GlobalRemove event targets unknown id 2989"},
|
||||
|
||||
{"PermissionError", &pipewire.PermissionError{
|
||||
ID: 2,
|
||||
Message: "no permission to destroy 0",
|
||||
}, "no permission to destroy 0"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
641
internal/pipewire/pod.go
Normal file
641
internal/pipewire/pod.go
Normal file
@@ -0,0 +1,641 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
// A Word is a 32-bit unsigned integer.
|
||||
//
|
||||
// Values internal to a message appear to always be aligned to 32-bit boundary.
|
||||
Word = uint32
|
||||
|
||||
// A Bool is a boolean value representing SPA_TYPE_Bool.
|
||||
Bool = bool
|
||||
// An Id is an enumerated value representing SPA_TYPE_Id.
|
||||
Id = Word
|
||||
// An Int is a signed integer value representing SPA_TYPE_Int.
|
||||
Int = int32
|
||||
// A Long is a signed integer value representing SPA_TYPE_Long.
|
||||
Long = int64
|
||||
// A Float is a floating point value representing SPA_TYPE_Float.
|
||||
Float = float32
|
||||
// A Double is a floating point value representing SPA_TYPE_Double.
|
||||
Double = float64
|
||||
// A String is a string value representing SPA_TYPE_String.
|
||||
String = string
|
||||
// Bytes is a byte slice representing SPA_TYPE_Bytes.
|
||||
Bytes = []byte
|
||||
|
||||
// A Fd is a signed integer value representing SPA_TYPE_Fd.
|
||||
Fd Long
|
||||
)
|
||||
|
||||
const (
|
||||
// SizeAlign is the boundary which POD starts are always aligned to.
|
||||
SizeAlign = 8
|
||||
|
||||
// SizeSPrefix is the fixed, unpadded size of the fixed-size prefix encoding POD wire size.
|
||||
SizeSPrefix = 4
|
||||
// SizeTPrefix is the fixed, unpadded size of the fixed-size prefix encoding POD value type.
|
||||
SizeTPrefix = 4
|
||||
// SizePrefix is the fixed, unpadded size of the fixed-size POD prefix.
|
||||
SizePrefix = SizeSPrefix + SizeTPrefix
|
||||
|
||||
// SizeId is the fixed, unpadded size of a [SPA_TYPE_Id] value.
|
||||
SizeId Word = 4
|
||||
// SizeInt is the fixed, unpadded size of a [SPA_TYPE_Int] value.
|
||||
SizeInt Word = 4
|
||||
// SizeLong is the fixed, unpadded size of a [SPA_TYPE_Long] value.
|
||||
SizeLong Word = 8
|
||||
|
||||
// SizeFd is the fixed, unpadded size of a [SPA_TYPE_Fd] value.
|
||||
SizeFd = SizeLong
|
||||
)
|
||||
|
||||
// A KnownSize value has known POD encoded size before serialisation.
|
||||
type KnownSize interface {
|
||||
// Size returns the POD encoded size of the receiver.
|
||||
Size() Word
|
||||
}
|
||||
|
||||
// PaddingSize returns the padding size corresponding to a wire size.
|
||||
func PaddingSize[W Word | int](wireSize W) W { return (SizeAlign - (wireSize)%SizeAlign) % SizeAlign }
|
||||
|
||||
// PaddedSize returns the padded size corresponding to a wire size.
|
||||
func PaddedSize[W Word | int](wireSize W) W { return wireSize + PaddingSize(wireSize) }
|
||||
|
||||
// Size returns prefixed and padded size corresponding to a wire size.
|
||||
func Size[W Word | int](wireSize W) W { return SizePrefix + PaddedSize(wireSize) }
|
||||
|
||||
// SizeString returns prefixed and padded size corresponding to a string.
|
||||
func SizeString[W Word | int](s string) W { return Size(W(len(s)) + 1) }
|
||||
|
||||
// PODMarshaler is the interface implemented by an object that can
|
||||
// marshal itself into PipeWire POD encoding.
|
||||
type PODMarshaler interface {
|
||||
// MarshalPOD encodes the receiver into PipeWire POD encoding,
|
||||
// appends it to data, and returns the result.
|
||||
MarshalPOD(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// An UnsupportedTypeError is returned by [Marshal] when attempting
|
||||
// to encode an unsupported value type.
|
||||
type UnsupportedTypeError struct{ Type reflect.Type }
|
||||
|
||||
func (e *UnsupportedTypeError) Error() string { return "unsupported type " + e.Type.String() }
|
||||
|
||||
// An UnsupportedSizeError is returned by [Marshal] when attempting
|
||||
// to encode a value with its encoded size exceeding what could be
|
||||
// represented by the format.
|
||||
type UnsupportedSizeError int
|
||||
|
||||
func (e UnsupportedSizeError) Error() string { return "size " + strconv.Itoa(int(e)) + " out of range" }
|
||||
|
||||
// Marshal returns the PipeWire POD encoding of v.
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
var data []byte
|
||||
if s, ok := v.(KnownSize); ok {
|
||||
data = make([]byte, 0, s.Size())
|
||||
}
|
||||
return MarshalAppend(data, v)
|
||||
}
|
||||
|
||||
// MarshalAppend appends the PipeWire POD encoding of v to data.
|
||||
func MarshalAppend(data []byte, v any) ([]byte, error) {
|
||||
return marshalValueAppend(data, reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
// appendInner calls f and handles size prefix and padding around the appended data.
|
||||
// f must only append to data.
|
||||
func appendInner(data []byte, f func(data []byte) ([]byte, error)) ([]byte, error) {
|
||||
data = append(data, make([]byte, SizeSPrefix)...)
|
||||
|
||||
rData, err := f(data)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
size := len(rData) - len(data) + SizeSPrefix
|
||||
// compensated for size and type prefix
|
||||
wireSize := size - SizePrefix
|
||||
if wireSize > math.MaxUint32 {
|
||||
return data, UnsupportedSizeError(wireSize)
|
||||
}
|
||||
binary.NativeEndian.PutUint32(rData[len(data)-SizeSPrefix:len(data)], Word(wireSize))
|
||||
rData = append(rData, make([]byte, PaddingSize(size))...)
|
||||
|
||||
return rData, nil
|
||||
}
|
||||
|
||||
// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value].
|
||||
func marshalValueAppend(data []byte, v reflect.Value) ([]byte, error) {
|
||||
if v.CanInterface() && (v.Kind() != reflect.Pointer || !v.IsNil()) {
|
||||
if m, ok := v.Interface().(PODMarshaler); ok {
|
||||
var err error
|
||||
data, err = m.MarshalPOD(data)
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
return appendInner(data, func(data []byte) ([]byte, error) { return marshalValueAppendRaw(data, v) })
|
||||
}
|
||||
|
||||
// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value] without the size prefix.
|
||||
func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) {
|
||||
if v.CanInterface() {
|
||||
switch c := v.Interface().(type) {
|
||||
case Fd:
|
||||
data = SPA_TYPE_Fd.append(data)
|
||||
data = binary.NativeEndian.AppendUint64(data, uint64(c))
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Uint32:
|
||||
data = SPA_TYPE_Id.append(data)
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(v.Uint()))
|
||||
return data, nil
|
||||
|
||||
case reflect.Int32:
|
||||
data = SPA_TYPE_Int.append(data)
|
||||
data = binary.NativeEndian.AppendUint32(data, Word(v.Int()))
|
||||
return data, nil
|
||||
|
||||
case reflect.Int64:
|
||||
data = SPA_TYPE_Long.append(data)
|
||||
data = binary.NativeEndian.AppendUint64(data, uint64(v.Int()))
|
||||
return data, nil
|
||||
|
||||
case reflect.Struct:
|
||||
data = SPA_TYPE_Struct.append(data)
|
||||
var err error
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
data, err = marshalValueAppend(data, v.Field(i))
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
|
||||
case reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
data = SPA_TYPE_None.append(data)
|
||||
return data, nil
|
||||
}
|
||||
return marshalValueAppendRaw(data, v.Elem())
|
||||
|
||||
case reflect.String:
|
||||
data = SPA_TYPE_String.append(data)
|
||||
data = append(data, []byte(v.String())...)
|
||||
data = append(data, 0)
|
||||
return data, nil
|
||||
|
||||
default:
|
||||
return data, &UnsupportedTypeError{v.Type()}
|
||||
}
|
||||
}
|
||||
|
||||
// PODUnmarshaler is the interface implemented by an object that can
|
||||
// unmarshal a PipeWire POD encoding representation of itself.
|
||||
type PODUnmarshaler interface {
|
||||
// UnmarshalPOD must be able to decode the form generated by MarshalPOD.
|
||||
// UnmarshalPOD must copy the data if it wishes to retain the data
|
||||
// after returning.
|
||||
UnmarshalPOD(data []byte) (Word, error)
|
||||
}
|
||||
|
||||
// An InvalidUnmarshalError describes an invalid argument passed to [Unmarshal].
|
||||
// (The argument to [Unmarshal] must be a non-nil pointer.)
|
||||
type InvalidUnmarshalError struct{ Type reflect.Type }
|
||||
|
||||
func (e *InvalidUnmarshalError) Error() string {
|
||||
if e.Type == nil {
|
||||
return "attempting to unmarshal to nil"
|
||||
}
|
||||
|
||||
if e.Type.Kind() != reflect.Pointer {
|
||||
return "attempting to unmarshal to non-pointer type " + e.Type.String()
|
||||
}
|
||||
return "attempting to unmarshal to nil " + e.Type.String()
|
||||
}
|
||||
|
||||
// UnexpectedEOFError describes an unexpected EOF encountered in the middle of decoding POD data.
|
||||
type UnexpectedEOFError uintptr
|
||||
|
||||
const (
|
||||
// ErrEOFPrefix is returned when unexpectedly encountering EOF
|
||||
// decoding the fixed-size POD prefix.
|
||||
ErrEOFPrefix UnexpectedEOFError = iota
|
||||
// ErrEOFData is returned when unexpectedly encountering EOF
|
||||
// establishing POD data bounds.
|
||||
ErrEOFData
|
||||
// ErrEOFDataString is returned when unexpectedly encountering EOF
|
||||
// establishing POD [String] bounds.
|
||||
ErrEOFDataString
|
||||
)
|
||||
|
||||
func (UnexpectedEOFError) Unwrap() error { return io.ErrUnexpectedEOF }
|
||||
func (e UnexpectedEOFError) Error() string {
|
||||
var suffix string
|
||||
switch e {
|
||||
case ErrEOFPrefix:
|
||||
suffix = "decoding fixed-size POD prefix"
|
||||
case ErrEOFData:
|
||||
suffix = "establishing POD data bounds"
|
||||
case ErrEOFDataString:
|
||||
suffix = "establishing POD String bounds"
|
||||
|
||||
default:
|
||||
return "unexpected EOF"
|
||||
}
|
||||
|
||||
return "unexpected EOF " + suffix
|
||||
}
|
||||
|
||||
// Unmarshal parses the PipeWire POD encoded data and stores the result
|
||||
// in the value pointed to by v. If v is nil or not a pointer,
|
||||
// Unmarshal returns an [InvalidUnmarshalError].
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
if n, err := UnmarshalNext(data, v); err != nil {
|
||||
return err
|
||||
} else if len(data) > int(n) {
|
||||
return TrailingGarbageError(data[int(n):])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalNext implements [Unmarshal] but returns the size of message decoded
|
||||
// and skips the final trailing garbage check.
|
||||
func UnmarshalNext(data []byte, v any) (size Word, err error) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||
return 0, &InvalidUnmarshalError{reflect.TypeOf(v)}
|
||||
}
|
||||
err = unmarshalValue(data, rv.Elem(), &size)
|
||||
// prefix and padding size
|
||||
size = Size(size)
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalSetError describes a value that cannot be set during [Unmarshal].
|
||||
// This is likely an unexported struct field.
|
||||
type UnmarshalSetError struct{ Type reflect.Type }
|
||||
|
||||
func (u *UnmarshalSetError) Error() string { return "cannot set " + u.Type.String() }
|
||||
|
||||
// A TrailingGarbageError describes extra bytes after decoding
|
||||
// has completed during [Unmarshal].
|
||||
type TrailingGarbageError []byte
|
||||
|
||||
func (e TrailingGarbageError) Error() string {
|
||||
if len(e) < SizePrefix {
|
||||
return "got " + strconv.Itoa(len(e)) + " bytes of trailing garbage"
|
||||
}
|
||||
return "data has extra values starting with " + SPAKind(binary.NativeEndian.Uint32(e[SizeSPrefix:])).String()
|
||||
}
|
||||
|
||||
// A StringTerminationError describes an incorrectly terminated string
|
||||
// encountered during [Unmarshal].
|
||||
type StringTerminationError byte
|
||||
|
||||
func (e StringTerminationError) Error() string {
|
||||
return "got byte " + strconv.Itoa(int(e)) + " instead of NUL"
|
||||
}
|
||||
|
||||
// unmarshalValue implements [Unmarshal] on [reflect.Value] without compensating for prefix and padding size.
|
||||
func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
|
||||
if !v.CanSet() {
|
||||
return &UnmarshalSetError{v.Type()}
|
||||
}
|
||||
|
||||
if v.CanInterface() {
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
|
||||
if u, ok := v.Interface().(PODUnmarshaler); ok {
|
||||
var err error
|
||||
*wireSizeP, err = u.UnmarshalPOD(data)
|
||||
return err
|
||||
}
|
||||
|
||||
switch v.Interface().(type) {
|
||||
case Fd:
|
||||
*wireSizeP = SizeFd
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Fd, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetInt(int64(binary.NativeEndian.Uint64(data)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Uint32:
|
||||
*wireSizeP = SizeId
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Id, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetUint(uint64(binary.NativeEndian.Uint32(data)))
|
||||
return nil
|
||||
|
||||
case reflect.Int32:
|
||||
*wireSizeP = SizeInt
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Int, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetInt(int64(binary.NativeEndian.Uint32(data)))
|
||||
return nil
|
||||
|
||||
case reflect.Int64:
|
||||
*wireSizeP = SizeLong
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Long, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetInt(int64(binary.NativeEndian.Uint64(data)))
|
||||
return nil
|
||||
|
||||
case reflect.Struct:
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fieldWireSize Word
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil {
|
||||
return err
|
||||
}
|
||||
// bounds check completed in successful call to unmarshalValue
|
||||
data = data[Size(fieldWireSize):]
|
||||
}
|
||||
|
||||
if len(data) != 0 {
|
||||
return TrailingGarbageError(data)
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Pointer:
|
||||
if ok, err := unmarshalHandleNone(&data, wireSizeP); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
v.SetZero()
|
||||
return nil
|
||||
}
|
||||
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
return unmarshalValue(data, v.Elem(), wireSizeP)
|
||||
|
||||
case reflect.String:
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_String, wireSizeP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// string size, one extra NUL byte
|
||||
size := int(*wireSizeP)
|
||||
if len(data) < size {
|
||||
return ErrEOFDataString
|
||||
}
|
||||
|
||||
// the serialised strings still include NUL termination
|
||||
if data[size-1] != 0 {
|
||||
return StringTerminationError(data[size-1])
|
||||
}
|
||||
|
||||
v.SetString(string(data[:size-1]))
|
||||
return nil
|
||||
|
||||
default:
|
||||
return &UnsupportedTypeError{v.Type()}
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshalHandleNone establishes prefix bounds and, for a value of type [SPA_TYPE_None],
|
||||
// validates its size and skips the header. This is for unmarshalling values that can be nil.
|
||||
func unmarshalHandleNone(data *[]byte, wireSizeP *Word) (bool, error) {
|
||||
if len(*data) < SizePrefix {
|
||||
return false, ErrEOFPrefix
|
||||
}
|
||||
|
||||
if SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) != SPA_TYPE_None {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(data, SPA_TYPE_None, wireSizeP); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if len(*data) != 0 {
|
||||
return true, TrailingGarbageError(*data)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// An InconsistentSizeError describes an inconsistent size prefix encountered
|
||||
// in data passed to [Unmarshal].
|
||||
type InconsistentSizeError struct{ Prefix, Expect Word }
|
||||
|
||||
func (e InconsistentSizeError) Error() string {
|
||||
return "prefix claims size " + strconv.Itoa(int(e.Prefix)) +
|
||||
" for a " + strconv.Itoa(int(e.Expect)) + "-byte long segment"
|
||||
}
|
||||
|
||||
// An UnexpectedTypeError describes an unexpected type encountered
|
||||
// in data passed to [Unmarshal].
|
||||
type UnexpectedTypeError struct{ Type, Expect SPAKind }
|
||||
|
||||
func (e UnexpectedTypeError) Error() string {
|
||||
return "received " + e.Type.String() + " for a value of type " + e.Expect.String()
|
||||
}
|
||||
|
||||
// unmarshalCheckTypeBounds performs bounds checks on data and validates the type and size prefixes.
|
||||
// An expected size of zero skips further bounds checks.
|
||||
func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error {
|
||||
if len(*data) < SizePrefix {
|
||||
return ErrEOFPrefix
|
||||
}
|
||||
|
||||
wantSize := *sizeP
|
||||
gotSize := binary.NativeEndian.Uint32(*data)
|
||||
*sizeP = gotSize
|
||||
|
||||
if wantSize != 0 && gotSize != wantSize {
|
||||
return InconsistentSizeError{gotSize, wantSize}
|
||||
}
|
||||
if len(*data)-SizePrefix < int(gotSize) {
|
||||
return ErrEOFData
|
||||
}
|
||||
|
||||
gotType := SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:]))
|
||||
if gotType != t {
|
||||
return UnexpectedTypeError{gotType, t}
|
||||
}
|
||||
|
||||
*data = (*data)[SizePrefix : gotSize+SizePrefix]
|
||||
return nil
|
||||
}
|
||||
|
||||
// The Footer contains additional messages, not directed to
|
||||
// the destination object defined by the Id field.
|
||||
type Footer[P KnownSize] struct {
|
||||
// The footer opcode.
|
||||
Opcode Id `json:"opcode"`
|
||||
// The footer payload struct.
|
||||
Payload P `json:"payload"`
|
||||
}
|
||||
|
||||
// Size satisfies [KnownSize] with a usually compile-time known value.
|
||||
func (f *Footer[P]) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeId) +
|
||||
f.Payload.Size()
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (f *Footer[T]) MarshalBinary() ([]byte, error) { return Marshal(f) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (f *Footer[T]) UnmarshalBinary(data []byte) error { return Unmarshal(data, f) }
|
||||
|
||||
// SPADictItem represents spa_dict_item.
|
||||
type SPADictItem struct {
|
||||
// Dot-separated string.
|
||||
Key string `json:"key"`
|
||||
// Arbitrary string.
|
||||
//
|
||||
// Integer values are represented in base 10,
|
||||
// boolean values are represented as "true" or "false".
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// SPADict represents spa_dict.
|
||||
type SPADict []SPADictItem
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (d *SPADict) Size() Word {
|
||||
if d == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// struct prefix, NItems value
|
||||
size := SizePrefix + int(Size(SizeInt))
|
||||
for i := range *d {
|
||||
size += SizeString[int]((*d)[i].Key)
|
||||
size += SizeString[int]((*d)[i].Value)
|
||||
}
|
||||
return Word(size)
|
||||
}
|
||||
|
||||
// MarshalPOD satisfies [PODMarshaler] as [SPADict] violates the POD type system.
|
||||
func (d *SPADict) MarshalPOD(data []byte) ([]byte, error) {
|
||||
return appendInner(data, func(dataPrefix []byte) (data []byte, err error) {
|
||||
data = SPA_TYPE_Struct.append(dataPrefix)
|
||||
if data, err = MarshalAppend(data, Int(len(*d))); err != nil {
|
||||
return
|
||||
}
|
||||
for i := range *d {
|
||||
if data, err = MarshalAppend(data, (*d)[i].Key); err != nil {
|
||||
return
|
||||
}
|
||||
if data, err = MarshalAppend(data, (*d)[i].Value); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalPOD satisfies [PODUnmarshaler] as [SPADict] violates the POD type system.
|
||||
func (d *SPADict) UnmarshalPOD(data []byte) (Word, error) {
|
||||
var wireSize Word
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, &wireSize); err != nil {
|
||||
return wireSize, err
|
||||
}
|
||||
// bounds check completed in successful call to unmarshalCheckTypeBounds
|
||||
data = data[:wireSize]
|
||||
|
||||
var count Int
|
||||
if size, err := UnmarshalNext(data, &count); err != nil {
|
||||
return wireSize, err
|
||||
} else {
|
||||
// bounds check completed in successful call to Unmarshal
|
||||
data = data[size:]
|
||||
}
|
||||
|
||||
*d = make([]SPADictItem, count)
|
||||
for i := range *d {
|
||||
if size, err := UnmarshalNext(data, &(*d)[i].Key); err != nil {
|
||||
return wireSize, err
|
||||
} else {
|
||||
// bounds check completed in successful call to Unmarshal
|
||||
data = data[size:]
|
||||
}
|
||||
if size, err := UnmarshalNext(data, &(*d)[i].Value); err != nil {
|
||||
return wireSize, err
|
||||
} else {
|
||||
// bounds check completed in successful call to Unmarshal
|
||||
data = data[size:]
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) != 0 {
|
||||
return wireSize, TrailingGarbageError(data)
|
||||
}
|
||||
return wireSize, nil
|
||||
}
|
||||
|
||||
// A Message is a value that can be transmitted as a message over PipeWire protocol native.
|
||||
type Message interface {
|
||||
// Opcode returns the opcode of this message.
|
||||
Opcode() byte
|
||||
// FileCount returns the number of files associated with this message.
|
||||
FileCount() Int
|
||||
|
||||
KnownSize
|
||||
}
|
||||
|
||||
// A MessageEncoder provides methods for encoding a [Message].
|
||||
type MessageEncoder struct{ Message }
|
||||
|
||||
// SizeMessage returns the size of Message transmitted over protocol native.
|
||||
func (m MessageEncoder) SizeMessage(footer KnownSize) (size Word) {
|
||||
size = SizeHeader + m.Message.Size()
|
||||
if footer != nil {
|
||||
size += footer.Size()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AppendMessage appends the protocol native encoding of Message to dst and returns the appended slice.
|
||||
func (m MessageEncoder) AppendMessage(dst []byte, Id, sequence Int, footer KnownSize) (data []byte, err error) {
|
||||
size := m.SizeMessage(footer)
|
||||
if size&^SizeMax != 0 {
|
||||
return dst, ErrSizeRange
|
||||
}
|
||||
|
||||
data = slices.Grow(dst, int(size))
|
||||
data = (&Header{
|
||||
ID: Id,
|
||||
Opcode: m.Message.Opcode(),
|
||||
Size: size - SizeHeader,
|
||||
Sequence: sequence,
|
||||
FileCount: m.Message.FileCount(),
|
||||
}).append(data)
|
||||
data, err = MarshalAppend(data, m.Message)
|
||||
if err == nil && footer != nil {
|
||||
data, err = MarshalAppend(data, footer)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
226
internal/pipewire/pod_test.go
Normal file
226
internal/pipewire/pod_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package pipewire_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
type encodingTestCases[V any, S interface {
|
||||
encoding.BinaryMarshaler
|
||||
encoding.BinaryUnmarshaler
|
||||
|
||||
*V
|
||||
}] []struct {
|
||||
// Uninterpreted name of subtest.
|
||||
name string
|
||||
// Encoded data.
|
||||
wantData []byte
|
||||
// Value corresponding to wantData.
|
||||
value V
|
||||
// Expected decoding error. Skips encoding check if non-nil.
|
||||
wantErr error
|
||||
}
|
||||
|
||||
// run runs all test cases as subtests of [testing.T].
|
||||
func (testCases encodingTestCases[V, S]) run(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var value V
|
||||
if err := S(&value).UnmarshalBinary(tc.wantData); err != nil {
|
||||
t.Fatalf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(&value, &tc.value) {
|
||||
t.Fatalf("UnmarshalBinary:\n%s\nwant\n%s", mustMarshalJSON(value), mustMarshalJSON(tc.value))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if gotData, err := S(&tc.value).MarshalBinary(); err != nil {
|
||||
t.Fatalf("MarshalBinary: error = %v", err)
|
||||
} else if string(gotData) != string(tc.wantData) {
|
||||
t.Fatalf("MarshalBinary: %#v, want %#v", gotData, tc.wantData)
|
||||
}
|
||||
})
|
||||
|
||||
if s, ok := any(&tc.value).(pipewire.KnownSize); ok {
|
||||
t.Run("size", func(t *testing.T) {
|
||||
if got := int(s.Size()); got != len(tc.wantData) {
|
||||
t.Errorf("Size: %d, want %d", got, len(tc.wantData))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPODErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"UnsupportedTypeError", &pipewire.UnsupportedTypeError{
|
||||
Type: reflect.TypeFor[any](),
|
||||
}, "unsupported type interface {}"},
|
||||
|
||||
{"UnsupportedSizeError", pipewire.UnsupportedSizeError(pipewire.SizeMax + 1), "size 16777216 out of range"},
|
||||
|
||||
{"InvalidUnmarshalError untyped nil", new(pipewire.InvalidUnmarshalError), "attempting to unmarshal to nil"},
|
||||
{"InvalidUnmarshalError non-pointer", &pipewire.InvalidUnmarshalError{
|
||||
Type: reflect.TypeFor[uintptr](),
|
||||
}, "attempting to unmarshal to non-pointer type uintptr"},
|
||||
{"InvalidUnmarshalError nil", &pipewire.InvalidUnmarshalError{
|
||||
Type: reflect.TypeFor[*uintptr](),
|
||||
}, "attempting to unmarshal to nil *uintptr"},
|
||||
|
||||
{"UnexpectedEOFError ErrEOFPrefix", pipewire.ErrEOFPrefix, "unexpected EOF decoding fixed-size POD prefix"},
|
||||
{"UnexpectedEOFError ErrEOFData", pipewire.ErrEOFData, "unexpected EOF establishing POD data bounds"},
|
||||
{"UnexpectedEOFError ErrEOFDataString", pipewire.ErrEOFDataString, "unexpected EOF establishing POD String bounds"},
|
||||
{"UnexpectedEOFError invalid", pipewire.UnexpectedEOFError(0xbad), "unexpected EOF"},
|
||||
|
||||
{"UnmarshalSetError", &pipewire.UnmarshalSetError{
|
||||
Type: reflect.TypeFor[*uintptr](),
|
||||
}, "cannot set *uintptr"},
|
||||
|
||||
{"TrailingGarbageError short", make(pipewire.TrailingGarbageError, 1<<3-1), "got 7 bytes of trailing garbage"},
|
||||
{"TrailingGarbageError String", pipewire.TrailingGarbageError{
|
||||
/* size: */ 0, 0, 0, 0,
|
||||
/* type: */ byte(pipewire.SPA_TYPE_String), 0, 0, 0,
|
||||
}, "data has extra values starting with String"},
|
||||
{"TrailingGarbageError invalid", pipewire.TrailingGarbageError{
|
||||
/* size: */ 0, 0, 0, 0,
|
||||
/* type: */ 0xff, 0xff, 0xff, 0xff,
|
||||
/* garbage: */ 0,
|
||||
}, "data has extra values starting with invalid type field 0xffffffff"},
|
||||
|
||||
{"StringTerminationError", pipewire.StringTerminationError(0xff), "got byte 255 instead of NUL"},
|
||||
|
||||
{"InconsistentSizeError", pipewire.InconsistentSizeError{
|
||||
Prefix: 0xbad,
|
||||
Expect: 0xff,
|
||||
}, "prefix claims size 2989 for a 255-byte long segment"},
|
||||
|
||||
{"UnexpectedTypeError zero", pipewire.UnexpectedTypeError{}, "received invalid type field 0x0 for a value of type invalid type field 0x0"},
|
||||
{"UnexpectedTypeError", pipewire.UnexpectedTypeError{
|
||||
Type: pipewire.SPA_TYPE_String,
|
||||
Expect: pipewire.SPA_TYPE_Array,
|
||||
}, "received String for a value of type Array"},
|
||||
{"UnexpectedTypeError invalid", pipewire.UnexpectedTypeError{
|
||||
Type: 0xdeadbeef,
|
||||
Expect: pipewire.SPA_TYPE_Long,
|
||||
}, "received invalid type field 0xdeadbeef for a value of type Long"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var benchmarkSample = func() (sample pipewire.CoreInfo) {
|
||||
if err := sample.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
func BenchmarkMarshal(b *testing.B) {
|
||||
for b.Loop() {
|
||||
if _, err := benchmarkSample.MarshalBinary(); err != nil {
|
||||
b.Fatalf("MarshalBinary: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalJSON(b *testing.B) {
|
||||
for b.Loop() {
|
||||
if _, err := json.Marshal(benchmarkSample); err != nil {
|
||||
b.Fatalf("json.Marshal: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGobEncode(b *testing.B) {
|
||||
e := gob.NewEncoder(io.Discard)
|
||||
type sampleRaw pipewire.CoreInfo
|
||||
|
||||
for b.Loop() {
|
||||
if err := e.Encode((*sampleRaw)(&benchmarkSample)); err != nil {
|
||||
b.Fatalf("(*gob.Encoder).Encode: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshal(b *testing.B) {
|
||||
var got pipewire.CoreInfo
|
||||
|
||||
for b.Loop() {
|
||||
if err := got.UnmarshalBinary(samplePWContainer[1][0][1]); err != nil {
|
||||
b.Fatalf("UnmarshalBinary: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalJSON(b *testing.B) {
|
||||
var got pipewire.CoreInfo
|
||||
data, err := json.Marshal(benchmarkSample)
|
||||
if err != nil {
|
||||
b.Fatalf("json.Marshal: error = %v", err)
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
if err = json.Unmarshal(data, &got); err != nil {
|
||||
b.Fatalf("json.Unmarshal: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGobDecode(b *testing.B) {
|
||||
type sampleRaw pipewire.CoreInfo
|
||||
var buf bytes.Buffer
|
||||
e := gob.NewEncoder(&buf)
|
||||
d := gob.NewDecoder(&buf)
|
||||
|
||||
for b.Loop() {
|
||||
b.StopTimer()
|
||||
if err := e.Encode((*sampleRaw)(&benchmarkSample)); err != nil {
|
||||
b.Fatalf("(*gob.Encoder).Encode: error = %v", err)
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
if err := d.Decode(new(sampleRaw)); err != nil {
|
||||
b.Fatalf("(*gob.Encoder).Decode: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mustMarshalJSON calls [json.Marshal] and returns the result.
|
||||
func mustMarshalJSON(v any) string {
|
||||
if data, err := json.Marshal(v); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return string(data)
|
||||
}
|
||||
}
|
||||
247
internal/pipewire/securitycontext.go
Normal file
247
internal/pipewire/securitycontext.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package pipewire
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
/* pipewire/extensions/security-context.h */
|
||||
|
||||
const (
|
||||
PW_TYPE_INTERFACE_SecurityContext = PW_TYPE_INFO_INTERFACE_BASE + "SecurityContext"
|
||||
PW_SECURITY_CONTEXT_PERM_MASK = PW_PERM_RWX
|
||||
PW_VERSION_SECURITY_CONTEXT = 3
|
||||
|
||||
PW_EXTENSION_MODULE_SECURITY_CONTEXT = PIPEWIRE_MODULE_PREFIX + "module-security-context"
|
||||
)
|
||||
|
||||
const (
|
||||
PW_SECURITY_CONTEXT_EVENT_NUM = iota
|
||||
|
||||
PW_VERSION_SECURITY_CONTEXT_EVENTS = 0
|
||||
)
|
||||
|
||||
const (
|
||||
PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER = iota
|
||||
|
||||
PW_SECURITY_CONTEXT_METHOD_CREATE
|
||||
|
||||
PW_SECURITY_CONTEXT_METHOD_NUM
|
||||
PW_VERSION_SECURITY_CONTEXT_METHODS = 0
|
||||
)
|
||||
|
||||
// SecurityContextCreate is sent to create a new security context.
|
||||
//
|
||||
// Creates a new security context with a socket listening FD.
|
||||
// PipeWire will accept new client connections on listen_fd.
|
||||
//
|
||||
// listen_fd must be ready to accept new connections when this
|
||||
// request is sent by the client. In other words, the client must
|
||||
// call bind(2) and listen(2) before sending the FD.
|
||||
//
|
||||
// close_fd is a FD closed by the client when PipeWire should stop
|
||||
// accepting new connections on listen_fd.
|
||||
//
|
||||
// PipeWire must continue to accept connections on listen_fd when
|
||||
// the client which created the security context disconnects.
|
||||
//
|
||||
// After sending this request, closing listen_fd and close_fd
|
||||
// remains the only valid operation on them.
|
||||
type SecurityContextCreate struct {
|
||||
// The offset in the SCM_RIGHTS msg_control message to
|
||||
// the fd to listen on for new connections.
|
||||
ListenFd Fd
|
||||
// The offset in the SCM_RIGHTS msg_control message to
|
||||
// the fd used to stop listening.
|
||||
CloseFd Fd
|
||||
|
||||
// Extra properties. These will be copied on the client
|
||||
// that connects through this context.
|
||||
Properties *SPADict `json:"props"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *SecurityContextCreate) Opcode() byte { return PW_SECURITY_CONTEXT_METHOD_CREATE }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *SecurityContextCreate) FileCount() Int { return 2 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *SecurityContextCreate) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeFd) +
|
||||
Size(SizeFd) +
|
||||
c.Properties.Size()
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *SecurityContextCreate) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *SecurityContextCreate) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// SecurityContext holds state of [PW_TYPE_INTERFACE_SecurityContext].
|
||||
type SecurityContext struct {
|
||||
// Proxy id as tracked by [Context].
|
||||
ID Int `json:"proxy_id"`
|
||||
// Global id as tracked by [Registry].
|
||||
GlobalID Int `json:"id"`
|
||||
|
||||
ctx *Context
|
||||
|
||||
destructible
|
||||
}
|
||||
|
||||
// GetSecurityContext queues a [RegistryBind] message for the PipeWire server
|
||||
// and returns the address of the newly allocated [SecurityContext].
|
||||
func (registry *Registry) GetSecurityContext() (securityContext *SecurityContext, err error) {
|
||||
securityContext = &SecurityContext{ctx: registry.ctx}
|
||||
for globalId, object := range registry.Objects {
|
||||
if object.Type == securityContext.String() {
|
||||
securityContext.GlobalID = globalId
|
||||
securityContext.ID, err = registry.bind(securityContext, securityContext.GlobalID, PW_VERSION_SECURITY_CONTEXT)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, UnsupportedObjectTypeError(securityContext.String())
|
||||
}
|
||||
|
||||
// Create queues a [SecurityContextCreate] message for the PipeWire server.
|
||||
func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPADict) (err error) {
|
||||
if err = securityContext.checkDestroy(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
asCoreError := securityContext.ctx.expectsCoreError(securityContext.ID, &err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// queued in reverse based on upstream behaviour, unsure why
|
||||
offset := securityContext.ctx.queueFiles(closeFd, listenFd)
|
||||
if err = securityContext.ctx.writeMessage(
|
||||
securityContext.ID,
|
||||
&SecurityContextCreate{ListenFd: offset + 1, CloseFd: offset + 0, Properties: &props},
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if err = securityContext.ctx.GetCore().Sync(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if coreError := asCoreError(); coreError == nil {
|
||||
return
|
||||
} else {
|
||||
switch syscall.Errno(-coreError.Result) {
|
||||
case syscall.EPERM:
|
||||
return &PermissionError{securityContext.ID, coreError.Message}
|
||||
|
||||
default:
|
||||
return coreError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// securityContextCloser holds onto resources associated to the security context.
|
||||
type securityContextCloser struct {
|
||||
// Pipe with its write end passed to [SecurityContextCreate.CloseFd].
|
||||
closeFds [2]int
|
||||
// Pathname the socket was bound to.
|
||||
pathname string
|
||||
}
|
||||
|
||||
// Close closes both ends of the pipe.
|
||||
func (scc *securityContextCloser) Close() (err error) {
|
||||
err = errors.Join(
|
||||
syscall.Close(scc.closeFds[1]),
|
||||
syscall.Close(scc.closeFds[0]),
|
||||
// there is still technically a TOCTOU here but this is internal
|
||||
// and has access to the privileged pipewire socket, so it only
|
||||
// receives trusted input (e.g. from cmd/hakurei) anyway
|
||||
os.Remove(scc.pathname),
|
||||
)
|
||||
|
||||
// no need for a finalizer anymore
|
||||
runtime.SetFinalizer(scc, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// BindAndCreate binds a new socket to the specified pathname and pass it to Create.
|
||||
// It returns an [io.Closer] corresponding to [SecurityContextCreate.CloseFd].
|
||||
func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPADict) (io.Closer, error) {
|
||||
if err := securityContext.checkDestroy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scc securityContextCloser
|
||||
|
||||
// ensure pathname is available
|
||||
if f, err := os.Create(pathname); err != nil {
|
||||
return nil, err
|
||||
} else if err = f.Close(); err != nil {
|
||||
_ = os.Remove(pathname)
|
||||
return nil, err
|
||||
} else if err = os.Remove(pathname); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scc.pathname = pathname
|
||||
|
||||
var listenFd int
|
||||
if fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0); err != nil {
|
||||
return nil, os.NewSyscallError("socket", err)
|
||||
} else {
|
||||
securityContext.ctx.cleanup(func() error { return syscall.Close(fd) })
|
||||
listenFd = fd
|
||||
}
|
||||
if err := syscall.Bind(listenFd, &syscall.SockaddrUnix{Name: pathname}); err != nil {
|
||||
return nil, os.NewSyscallError("bind", err)
|
||||
} else if err = syscall.Listen(listenFd, 0); err != nil {
|
||||
_ = os.Remove(pathname)
|
||||
return nil, os.NewSyscallError("listen", err)
|
||||
}
|
||||
|
||||
if err := syscall.Pipe2(scc.closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||
_ = os.Remove(pathname)
|
||||
return nil, err
|
||||
}
|
||||
runtime.SetFinalizer(&scc, (*securityContextCloser).Close)
|
||||
|
||||
if err := securityContext.Create(listenFd, scc.closeFds[1], props); err != nil {
|
||||
_ = scc.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &scc, nil
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) consume(opcode byte, files []int, _ func(v any)) error {
|
||||
securityContext.mustCheckDestroy()
|
||||
closeReceivedFiles(files...)
|
||||
switch opcode {
|
||||
// SecurityContext does not receive any events
|
||||
|
||||
default:
|
||||
panic(&UnsupportedOpcodeError{opcode, securityContext.String()})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) error {
|
||||
securityContext.mustCheckDestroy()
|
||||
if securityContext.ID != event.ID {
|
||||
return &InconsistentIdError{Proxy: securityContext, ID: securityContext.ID, ServerID: event.ID}
|
||||
}
|
||||
if securityContext.GlobalID != event.GlobalID {
|
||||
return &InconsistentIdError{Global: true, Proxy: securityContext, ID: securityContext.GlobalID, ServerID: event.GlobalID}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy destroys this [SecurityContext] proxy.
|
||||
func (securityContext *SecurityContext) Destroy() error {
|
||||
return securityContext.destroy(securityContext.ctx, securityContext.ID)
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) String() string { return PW_TYPE_INTERFACE_SecurityContext }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user