internal/system: integrate PipeWire SecurityContext
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 43s
Test / Flake checks (push) Successful in 1m32s

Tests for this Op happens to be the best out of everything due to the robust infrastructure offered by internal/pipewire.

This is now ready to use in internal/outcome for implementing #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-12-07 17:39:34 +09:00
parent 1b17ccda91
commit 093e30c788
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 632 additions and 5 deletions

View File

@ -10,6 +10,7 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/pipewire"
"hakurei.app/internal/wayland"
"hakurei.app/internal/xcb"
)
@ -47,8 +48,12 @@ type syscallDispatcher interface {
// aclUpdate provides [acl.Update].
aclUpdate(name string, uid int, perms ...acl.Perm) error
// waylandNew provides [wayland.New].
waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error)
// pipewireConnect provides [pipewire.Connect].
pipewireConnect() (*pipewire.Context, error)
// xcbChangeHosts provides [xcb.ChangeHosts].
xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error
@ -84,6 +89,8 @@ func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanc
return wayland.New(displayPath, bindPath, appID, instanceID)
}
func (k direct) pipewireConnect() (*pipewire.Context, error) { return pipewire.Connect(true) }
func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
return xcb.ChangeHosts(mode, family, address)
}

View File

@ -14,6 +14,7 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/pipewire"
"hakurei.app/internal/xcb"
)
@ -245,8 +246,16 @@ func (k *kstub) mkdir(name string, perm os.FileMode) error {
func (k *kstub) chmod(name string, mode os.FileMode) error {
k.Helper()
return k.Expects("chmod").Error(
stub.CheckArg(k.Stub, "name", name, 0),
expect := k.Expects("chmod")
// translate ignored name
nameVal := any(name)
if _, ok := expect.Args[0].(ignoreValue); ok {
nameVal = ignoreValue{}
}
return expect.Error(
stub.CheckArgReflect(k.Stub, "name", nameVal, 0),
stub.CheckArg(k.Stub, "mode", mode, 1))
}
@ -273,8 +282,16 @@ func (k *kstub) println(v ...any) {
func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
k.Helper()
return k.Expects("aclUpdate").Error(
stub.CheckArg(k.Stub, "name", name, 0),
expect := k.Expects("aclUpdate")
// translate ignored name
nameVal := any(name)
if _, ok := expect.Args[0].(ignoreValue); ok {
nameVal = ignoreValue{}
}
return expect.Error(
stub.CheckArgReflect(k.Stub, "name", nameVal, 0),
stub.CheckArg(k.Stub, "uid", uid, 1),
stub.CheckArgReflect(k.Stub, "perms", perms, 2))
}
@ -288,6 +305,12 @@ func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanc
stub.CheckArg(k.Stub, "instanceID", instanceID, 3))
}
func (k *kstub) pipewireConnect() (*pipewire.Context, error) {
k.Helper()
expect := k.Expects("pipewireConnect")
return expect.Ret.(func() *pipewire.Context)(), expect.Error()
}
func (k *kstub) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
k.Helper()
return k.Expects("xcbChangeHosts").Error(
@ -387,7 +410,18 @@ func (k *kstub) Verbose(v ...any) {
func (k *kstub) Verbosef(format string, v ...any) {
k.Helper()
if k.Expects("verbosef").Error(
expect := k.Expects("verbosef")
// translate ignores in v
if want, ok := expect.Args[1].([]any); ok && len(v) == len(want) {
for i, a := range want {
if _, ok = a.(ignoreValue); ok {
v[i] = ignoreValue{}
}
}
}
if expect.Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()

View File

@ -0,0 +1,99 @@
package system
import (
"errors"
"fmt"
"io"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/pipewire"
)
// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire].
// The socket stops accepting connections once the pipe referred to by sync is closed.
// The socket is pathname only and is destroyed on revert.
func (sys *I) PipeWire(dst *check.Absolute) *I {
sys.ops = append(sys.ops, &pipewireOp{nil, dst})
return sys
}
// pipewireOp implements [I.PipeWire].
type pipewireOp struct {
scc io.Closer
dst *check.Absolute
}
func (p *pipewireOp) Type() hst.Enablement { return Process }
func (p *pipewireOp) apply(sys *I) (err error) {
var ctx *pipewire.Context
if ctx, err = sys.pipewireConnect(); err != nil {
return newOpError("pipewire", err, false)
}
defer func() {
if closeErr := ctx.Close(); closeErr != nil && err == nil {
err = newOpError("pipewire", closeErr, false)
}
}()
sys.msg.Verbosef("pipewire pathname socket on %q", p.dst)
var registry *pipewire.Registry
if registry, err = ctx.GetRegistry(); err != nil {
return newOpError("pipewire", err, false)
} else if err = ctx.GetCore().Sync(); err != nil {
return newOpError("pipewire", err, false)
}
var securityContext *pipewire.SecurityContext
if securityContext, err = registry.GetSecurityContext(); err != nil {
return newOpError("pipewire", err, false)
} else if err = ctx.Roundtrip(); err != nil {
return newOpError("pipewire", err, false)
}
if p.scc, err = securityContext.BindAndCreate(p.dst.String(), pipewire.SPADict{
{Key: pipewire.PW_KEY_SEC_ENGINE, Value: "app.hakurei"},
{Key: pipewire.PW_KEY_ACCESS, Value: "restricted"},
}); err != nil {
return newOpError("pipewire", err, false)
} else if err = ctx.GetCore().Sync(); err != nil {
_ = p.scc.Close()
return newOpError("pipewire", err, false)
}
if err = sys.chmod(p.dst.String(), 0); err != nil {
if closeErr := p.scc.Close(); closeErr != nil {
return newOpError("pipewire", errors.Join(err, closeErr), false)
}
return newOpError("pipewire", err, false)
}
if err = sys.aclUpdate(p.dst.String(), sys.uid, acl.Read, acl.Write, acl.Execute); err != nil {
if closeErr := p.scc.Close(); closeErr != nil {
return newOpError("pipewire", errors.Join(err, closeErr), false)
}
return newOpError("pipewire", err, false)
}
return nil
}
func (p *pipewireOp) revert(sys *I, _ *Criteria) error {
if p.scc != nil {
sys.msg.Verbosef("hanging up pipewire socket on %q", p.dst)
return newOpError("pipewire", p.scc.Close(), true)
}
return nil
}
func (p *pipewireOp) Is(o Op) bool {
target, ok := o.(*pipewireOp)
return ok && p != nil && target != nil &&
p.dst.Is(target.dst)
}
func (p *pipewireOp) Path() string { return p.dst.String() }
func (p *pipewireOp) String() string { return fmt.Sprintf("pipewire socket at %q", p.dst) }

View File

@ -0,0 +1,487 @@
package system
import (
"fmt"
"os"
"path"
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/internal/acl"
"hakurei.app/internal/pipewire"
)
func TestPipeWireOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{
{"success", 0xbeef, 0xff, &pipewireOp{nil,
m(path.Join(t.TempDir(), "pipewire")),
}, []stub.Call{
call("pipewireConnect", stub.ExpectArgs{}, func() *pipewire.Context {
if ctx, err := pipewire.New(&stubPipeWireConn{sendmsg: []string{
/* roundtrip 0 */
string([]byte{
// header: Core::Hello
0, 0, 0, 0,
0x18, 0, 0, 1,
0, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x10, 0, 0, 0,
0xe, 0, 0, 0,
// Int: version = 4
4, 0, 0, 0,
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
// header: Client::UpdateProperties
1, 0, 0, 0,
0x50, 0, 0, 2,
1, 0, 0, 0,
0, 0, 0, 0,
// Struct: spa_dict
0x48, 0, 0, 0,
0xe, 0, 0, 0,
// Struct: spa_dict_item
0x40, 0, 0, 0,
0xe, 0, 0, 0,
// Int: n_items
4, 0, 0, 0,
4, 0, 0, 0,
1, 0, 0, 0,
0, 0, 0, 0,
// String: key = "remote.intention"
0x11, 0, 0, 0,
8, 0, 0, 0,
0x72, 0x65, 0x6d, 0x6f,
0x74, 0x65, 0x2e, 0x69,
0x6e, 0x74, 0x65, 0x6e,
0x74, 0x69, 0x6f, 0x6e,
0, 0, 0, 0,
0, 0, 0, 0,
// String: value = "manager"
8, 0, 0, 0,
8, 0, 0, 0,
0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x72, 0,
// header: Core::GetRegistry
0, 0, 0, 0,
0x28, 0, 0, 5,
2, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x20, 0, 0, 0,
0xe, 0, 0, 0,
// Int: version = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
// Int: new_id = 2
4, 0, 0, 0,
4, 0, 0, 0,
2, 0, 0, 0,
0, 0, 0, 0,
// header: Core::Sync
0, 0, 0, 0,
0x28, 0, 0, 2,
3, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x20, 0, 0, 0,
0xe, 0, 0, 0,
// Int: id = 0
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
// Int: seq = 0x40000003
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0x40,
0, 0, 0, 0,
}),
/* roundtrip 1 */
string([]byte{
// header: Registry::Bind
2, 0, 0, 0,
0x68, 0, 0, 1,
4, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x60, 0, 0, 0,
0xe, 0, 0, 0,
// Int: id = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
// String: type = "PipeWire:Interface:SecurityContext"
0x23, 0, 0, 0,
8, 0, 0, 0,
0x50, 0x69, 0x70, 0x65,
0x57, 0x69, 0x72, 0x65,
0x3a, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x66, 0x61,
0x63, 0x65, 0x3a, 0x53,
0x65, 0x63, 0x75, 0x72,
0x69, 0x74, 0x79, 0x43,
0x6f, 0x6e, 0x74, 0x65,
0x78, 0x74, 0, 0,
0, 0, 0, 0,
// Int: version = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
// Int: new_id = 3
4, 0, 0, 0,
4, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
}),
/* roundtrip 2 */
string([]byte{
// header: SecurityContext::Create
3, 0, 0, 0,
0xa8, 0, 0, 1,
5, 0, 0, 0,
2, 0, 0, 0,
// Struct
0xa0, 0, 0, 0,
0xe, 0, 0, 0,
// Fd: listen_fd = 1
8, 0, 0, 0,
0x12, 0, 0, 0,
1, 0, 0, 0,
0, 0, 0, 0,
// Fd: close_fd = 0
8, 0, 0, 0,
0x12, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
// Struct: spa_dict
0x78, 0, 0, 0,
0xe, 0, 0, 0,
// Int: n_items = 2
4, 0, 0, 0,
4, 0, 0, 0,
2, 0, 0, 0,
0, 0, 0, 0,
// String: key = "pipewire.sec.engine"
0x14, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x65, 0x6e, 0x67,
0x69, 0x6e, 0x65, 0,
0, 0, 0, 0,
// String: value = "app.hakurei"
0xc, 0, 0, 0,
8, 0, 0, 0,
0x61, 0x70, 0x70, 0x2e,
0x68, 0x61, 0x6b, 0x75,
0x72, 0x65, 0x69, 0,
0, 0, 0, 0,
// String: key = "pipewire.access"
0x10, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x61, 0x63, 0x63,
0x65, 0x73, 0x73, 0,
// String: value = "restricted"
0xb, 0, 0, 0,
8, 0, 0, 0,
0x72, 0x65, 0x73, 0x74,
0x72, 0x69, 0x63, 0x74,
0x65, 0x64, 0, 0,
0, 0, 0, 0,
// header: Core::Sync
0, 0, 0, 0,
0x28, 0, 0, 2,
6, 0, 0, 0,
0, 0, 0, 0,
// Struct
0x20, 0, 0, 0,
0xe, 0, 0, 0,
// Int: id = 0
4, 0, 0, 0,
4, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
// Int: seq = 0x40000006
4, 0, 0, 0,
4, 0, 0, 0,
6, 0, 0, 0x40,
0, 0, 0, 0,
}),
}, recvmsg: []*stubMessage{
/* roundtrip 0 */
{pipewire.PW_ID_CORE, nil, &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_CORE_DAEMON, Value: "true"},
{Key: pipewire.PW_KEY_CORE_NAME, Value: "pipewire-0"},
{Key: pipewire.PW_KEY_OBJECT_ID, Value: "0"},
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "0"},
},
}},
{pipewire.PW_ID_CORE, nil, &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"},
},
}},
{pipewire.PW_ID_CLIENT, nil, &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_ACCESS, Value: "unrestricted"},
},
}},
{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
ID: -1,
Sequence: 0,
}},
{2, nil, &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"},
},
}},
{2, nil, &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"},
},
}},
{2, nil, &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"},
},
}},
{2, nil, &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"},
},
}},
{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
ID: 0,
Sequence: pipewire.CoreSyncSequenceOffset + 3,
}},
{2, nil, &pipewire.RegistryGlobal{
ID: 4,
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: "4"},
{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,
/* roundtrip 1 */
{pipewire.PW_ID_CORE, nil, &pipewire.CoreBoundProps{
ID: 3,
GlobalID: 3,
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_OBJECT_SERIAL, Value: "3"},
},
}},
nil,
/* roundtrip 2 */
{pipewire.PW_ID_CORE, nil, &pipewire.CoreDone{
ID: 0,
Sequence: pipewire.CoreSyncSequenceOffset + 6,
}},
nil,
}}, pipewire.SPADict{
{Key: pipewire.PW_KEY_REMOTE_INTENTION, Value: "manager"},
}); err != nil {
panic(err)
} else {
return ctx
}
}, nil),
call("verbosef", stub.ExpectArgs{"pipewire pathname socket on %q", []any{ignoreValue{}}}, nil, nil),
call("chmod", stub.ExpectArgs{ignoreValue{}, os.FileMode(0)}, nil, nil),
call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"hanging up pipewire socket on %q", []any{ignoreValue{}}}, nil, nil),
}, nil},
})
checkOpsBuilder(t, "PipeWire", []opsBuilderTestCase{
{"sample", 0xcafe, func(_ *testing.T, sys *I) {
sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"))
}, []Op{&pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
}}, stub.Expect{}},
})
checkOpIs(t, []opIsTestCase{
{"dst differs", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"),
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
}, false},
{"equals", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"sample", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
}, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire",
`pipewire socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"`},
})
}
// stubMessage is a [pipewire.Message] prepared ahead of time.
type stubMessage struct {
// Proxy Id included in the [pipewire.Header].
id pipewire.Int
// Footer optionally appended after the message body.
footer pipewire.KnownSize
// Known-good message prepared ahead of time.
m pipewire.Message
}
// stubPipeWireConn implements [pipewire.Conn] and checks the behaviour of [pipewire.Context].
type stubPipeWireConn struct {
// Marshaled and sent for Recvmsg.
recvmsg []*stubMessage
// Current position in recvmsg.
curRecvmsg int
// Current server seq number.
sequence pipewire.Int
// Compared against calls to Sendmsg.
sendmsg []string
// Current position in sendmsg.
curSendmsg int
}
// Recvmsg marshals and copies a stubMessage prepared ahead of time.
func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) {
defer func() { conn.curRecvmsg++ }()
recvflags = syscall.MSG_CMSG_CLOEXEC
if conn.recvmsg[conn.curRecvmsg] == nil {
err = syscall.EAGAIN
return
}
defer func() { conn.sequence++ }()
if data, marshalErr := (pipewire.MessageEncoder{Message: conn.recvmsg[conn.curRecvmsg].m}).AppendMessage(nil,
conn.recvmsg[conn.curRecvmsg].id,
conn.sequence,
conn.recvmsg[conn.curRecvmsg].footer,
); marshalErr != nil {
panic(marshalErr)
} else {
n = copy(p, data)
}
return
}
// Sendmsg checks a client message against a known-good sample.
func (conn *stubPipeWireConn) Sendmsg(p, _ []byte, _ int) (n int, err error) {
defer func() { conn.curSendmsg++ }()
n = len(p)
if string(p) != conn.sendmsg[conn.curSendmsg] {
err = fmt.Errorf("%#v, want %#v", p, []byte(conn.sendmsg[conn.curSendmsg]))
}
return
}
// Close checks whether Recvmsg and Sendmsg has depleted all samples.
func (conn *stubPipeWireConn) Close() error {
if conn.curRecvmsg != len(conn.recvmsg) {
return fmt.Errorf("consumed %d recvmsg samples, want %d", conn.curRecvmsg, len(conn.recvmsg))
}
if conn.curSendmsg != len(conn.sendmsg) {
return fmt.Errorf("consumed %d sendmsg samples, want %d", conn.curSendmsg, len(conn.sendmsg))
}
return nil
}