Files
hakurei/internal/system/pipewire_test.go
Ophestra ce06b7b663
All checks were successful
Test / Create distribution (push) Successful in 30s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m29s
Test / Hpkg (push) Successful in 4m24s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m27s
Test / Flake checks (push) Successful in 1m40s
internal/pipewire: inform conn of blocking intent
The interface does not expose underlying kernel notification mechanisms. This change removes the need to poll in situations were the next call might block.

This is made cumbersome by the SyscallConn interface left over from a previous implementation, it will be replaced in a later commit as the current implementation does not make use of any net.Conn methods other than Close.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-19 00:00:33 +09:00

553 lines
16 KiB
Go

package system
import (
"fmt"
"os"
"path"
"syscall"
"testing"
"time"
"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")),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, []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,
0x40, 1, 0, 1,
5, 0, 0, 0,
2, 0, 0, 0,
// Struct
0x38, 1, 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
0x10, 1, 0, 0,
0xe, 0, 0, 0,
// Int: n_items = 4
4, 0, 0, 0,
4, 0, 0, 0,
4, 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.sec.app-id"
0x14, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x61, 0x70, 0x70,
0x2d, 0x69, 0x64, 0,
0, 0, 0, 0,
// String: value = "org.chromium.Chromium"
0x16, 0, 0, 0,
8, 0, 0, 0,
0x6f, 0x72, 0x67, 0x2e,
0x63, 0x68, 0x72, 0x6f,
0x6d, 0x69, 0x75, 0x6d,
0x2e, 0x43, 0x68, 0x72,
0x6f, 0x6d, 0x69, 0x75,
// String: key = "pipewire.sec.instance-id"
0x6d, 0, 0, 0,
0x19, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x69, 0x6e, 0x73,
0x74, 0x61, 0x6e, 0x63,
0x65, 0x2d, 0x69, 0x64,
0, 0, 0, 0,
0, 0, 0, 0,
// String: value = "ebf083d1b175911782d413369b64ce7c"
0x21, 0, 0, 0,
8, 0, 0, 0,
0x65, 0x62, 0x66, 0x30,
0x38, 0x33, 0x64, 0x31,
0x62, 0x31, 0x37, 0x35,
0x39, 0x31, 0x31, 0x37,
0x38, 0x32, 0x64, 0x34,
0x31, 0x33, 0x33, 0x36,
0x39, 0x62, 0x36, 0x34,
0x63, 0x65, 0x37, 0x63,
0, 0, 0, 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"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c")
}, []Op{&pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}}, stub.Expect{}},
})
checkOpIs(t, []opIsTestCase{
{"dst differs", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, false},
{"equals", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"sample", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, 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
}
func (conn *stubPipeWireConn) MightBlock(timeout time.Duration) {
if timeout != 5*time.Second {
panic("unexpected timeout " + timeout.String())
}
}
// 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
}