From d8417e292762eebbbe7203aa98eb34eb5c34434e Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 10 Dec 2025 00:59:01 +0900 Subject: [PATCH] internal/pipewire: implement Registry::GlobalRemove This is emitted by PipeWire when a global object disappears, because PipeWire insists that all clients that had called Core::GetRegistry must constantly sync its local registry state with the remote. Signed-off-by: Ophestra --- internal/pipewire/core.go | 43 ++++++++++++++++++++++++++++++ internal/pipewire/core_test.go | 17 ++++++++++++ internal/pipewire/pipewire_test.go | 8 ++++++ 3 files changed, 68 insertions(+) diff --git a/internal/pipewire/core.go b/internal/pipewire/core.go index a6d4725..fc3edd4 100644 --- a/internal/pipewire/core.go +++ b/internal/pipewire/core.go @@ -533,6 +533,30 @@ func (c *RegistryGlobal) MarshalBinary() ([]byte, error) { return Marshal(c) } // UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. func (c *RegistryGlobal) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } +// A RegistryGlobalRemove event is emitted when a global with id was removed. +type RegistryGlobalRemove struct { + // The global id that was removed. + ID Int `json:"id"` +} + +// Opcode satisfies [Message] with a constant value. +func (c *RegistryGlobalRemove) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL_REMOVE } + +// FileCount satisfies [Message] with a constant value. +func (c *RegistryGlobalRemove) FileCount() Int { return 0 } + +// Size satisfies [KnownSize] with a constant value. +func (c *RegistryGlobalRemove) Size() Word { + return SizePrefix + + Size(SizeInt) +} + +// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal]. +func (c *RegistryGlobalRemove) MarshalBinary() ([]byte, error) { return Marshal(c) } + +// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. +func (c *RegistryGlobalRemove) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } + // RegistryBind is sent when the client requests to bind to the // global object with id and use the client proxy with new_id as // the proxy. After this call, methods can be sent to the remote @@ -714,6 +738,14 @@ func (e *GlobalIDCollisionError) Error() string { " stepping on previous id " + strconv.Itoa(int(e.ID)) + " for " + e.Previous.Type } +// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event announcing the removal of +// a global id that is not yet known to [Registry] or was already deleted. +type UnknownGlobalIDRemoveError Int + +func (e UnknownGlobalIDRemoveError) Error() string { + return "Registry::GlobalRemove event targets unknown id " + strconv.Itoa(int(e)) +} + func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any)) error { closeReceivedFiles(files...) switch opcode { @@ -727,6 +759,17 @@ func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any registry.Objects[global.ID] = global return nil + case PW_REGISTRY_EVENT_GLOBAL_REMOVE: + var globalRemove RegistryGlobalRemove + unmarshal(&globalRemove) + l := len(registry.Objects) + delete(registry.Objects, globalRemove.ID) + if len(registry.Objects) != l-1 { + // this should never happen so is non-recoverable if it does + panic(UnknownGlobalIDRemoveError(globalRemove.ID)) + } + return nil + default: return &UnsupportedOpcodeError{opcode, registry.String()} } diff --git a/internal/pipewire/core_test.go b/internal/pipewire/core_test.go index a6b6729..42b18b9 100644 --- a/internal/pipewire/core_test.go +++ b/internal/pipewire/core_test.go @@ -745,6 +745,23 @@ func TestRegistryGlobal(t *testing.T) { }.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() diff --git a/internal/pipewire/pipewire_test.go b/internal/pipewire/pipewire_test.go index 535bac0..7c2cc33 100644 --- a/internal/pipewire/pipewire_test.go +++ b/internal/pipewire/pipewire_test.go @@ -862,6 +862,14 @@ func TestContextErrors(t *testing.T) { 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"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) {