Files
hakurei/internal/pipewire/core.go
Ophestra b0f2ab6fff
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m28s
Test / Hakurei (push) Successful in 3m25s
Test / Hpkg (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m43s
internal/pipewire: implement Core::Destroy
This change also implements pending destructible check on Sync. Destruction method should always be implemented as a wrapper of destructible.destroy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:20:58 +09:00

1055 lines
35 KiB
Go

package pipewire
import (
"errors"
"fmt"
"maps"
"slices"
"strconv"
"syscall"
"time"
)
/* pipewire/core.h */
const (
PW_TYPE_INTERFACE_Core = PW_TYPE_INFO_INTERFACE_BASE + "Core"
PW_TYPE_INTERFACE_Registry = PW_TYPE_INFO_INTERFACE_BASE + "Registry"
PW_CORE_PERM_MASK = PW_PERM_R | PW_PERM_X | PW_PERM_M
PW_VERSION_CORE = 4
PW_VERSION_REGISTRY = 3
PW_DEFAULT_REMOTE = "pipewire-0"
PW_ID_CORE = 0
PW_ID_ANY = Word(0xffffffff)
)
const (
PW_CORE_CHANGE_MASK_PROPS = 1 << iota
PW_CORE_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_CORE_EVENT_INFO = iota
PW_CORE_EVENT_DONE
PW_CORE_EVENT_PING
PW_CORE_EVENT_ERROR
PW_CORE_EVENT_REMOVE_ID
PW_CORE_EVENT_BOUND_ID
PW_CORE_EVENT_ADD_MEM
PW_CORE_EVENT_REMOVE_MEM
PW_CORE_EVENT_BOUND_PROPS
PW_CORE_EVENT_NUM
PW_VERSION_CORE_EVENTS = 1
)
const (
PW_CORE_METHOD_ADD_LISTENER = iota
PW_CORE_METHOD_HELLO
PW_CORE_METHOD_SYNC
PW_CORE_METHOD_PONG
PW_CORE_METHOD_ERROR
PW_CORE_METHOD_GET_REGISTRY
PW_CORE_METHOD_CREATE_OBJECT
PW_CORE_METHOD_DESTROY
PW_CORE_METHOD_NUM
PW_VERSION_CORE_METHODS = 0
)
const (
PW_REGISTRY_EVENT_GLOBAL = iota
PW_REGISTRY_EVENT_GLOBAL_REMOVE
PW_REGISTRY_EVENT_NUM
PW_VERSION_REGISTRY_EVENTS = 0
)
const (
PW_REGISTRY_METHOD_ADD_LISTENER = iota
PW_REGISTRY_METHOD_BIND
PW_REGISTRY_METHOD_DESTROY
PW_REGISTRY_METHOD_NUM
PW_VERSION_REGISTRY_METHODS = 0
)
const (
FOOTER_CORE_OPCODE_GENERATION = iota
FOOTER_CORE_OPCODE_LAST
)
// The FooterCoreGeneration indicates to the client what is the current
// registry generation number of the Context on the server side.
//
// The server shall include this footer in the next message it sends that
// follows the increment of the registry generation number.
type FooterCoreGeneration struct {
RegistryGeneration Long `json:"registry_generation"`
}
// Size satisfies [KnownSize] with a constant value.
func (fcg FooterCoreGeneration) Size() Word {
return SizePrefix +
Size(SizeLong)
}
// The FooterClientGeneration indicates to the server what is the last
// registry generation number the client has processed.
//
// The client shall include this footer in the next message it sends,
// after it has processed an incoming message whose footer includes a
// registry generation update.
type FooterClientGeneration struct {
ClientGeneration Long `json:"client_generation"`
}
// Size satisfies [KnownSize] with a constant value.
func (fcg FooterClientGeneration) Size() Word {
return SizePrefix +
Size(SizeLong)
}
// A CoreInfo event is emitted by the server upon connection
// with the more information about the server.
type CoreInfo struct {
// The id of the server (PW_ID_CORE).
ID Int `json:"id"`
// A unique cookie for this server.
Cookie Int `json:"cookie"`
// The name of the user running the server.
UserName String `json:"user_name"`
// The name of the host running the server.
HostName String `json:"host_name"`
// A version string of the server.
Version String `json:"version"`
// The name of the server.
Name String `json:"name"`
// A set of bits with changes to the info.
ChangeMask Long `json:"change_mask"`
// Optional key/value properties, valid when change_mask has PW_CORE_CHANGE_MASK_PROPS.
Properties *SPADict `json:"props"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreInfo) Opcode() byte { return PW_CORE_EVENT_INFO }
// FileCount satisfies [Message] with a constant value.
func (c *CoreInfo) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreInfo) Size() Word {
return SizePrefix +
Size(SizeInt) +
Size(SizeInt) +
SizeString[Word](c.UserName) +
SizeString[Word](c.HostName) +
SizeString[Word](c.Version) +
SizeString[Word](c.Name) +
Size(SizeLong) +
c.Properties.Size()
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// The CoreDone event is emitted as a result of a client Sync method.
type CoreDone struct {
// Passed from [CoreSync.ID].
ID Int `json:"id"`
// Passed from [CoreSync.Sequence].
Sequence Int `json:"seq"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreDone) Opcode() byte { return PW_CORE_EVENT_DONE }
// FileCount satisfies [Message] with a constant value.
func (c *CoreDone) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreDone) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreDone) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreDone) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// The CorePing event is emitted by the server when it wants to check if a client is
// alive or ensure that it has processed the previous events.
type CorePing struct {
// The object id to ping.
ID Int `json:"id"`
// Usually automatically generated.
// The client should pass this in the Pong method reply.
Sequence Int `json:"seq"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CorePing) Opcode() byte { return PW_CORE_EVENT_PING }
// FileCount satisfies [Message] with a constant value.
func (c *CorePing) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CorePing) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CorePing) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CorePing) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// The CoreError can be emitted by both the client and the server.
//
// When emitted by the server, the error event is sent out when a fatal
// (non-recoverable) error has occurred. The id argument is the proxy
// object where the error occurred, most often in response to a request
// to that object. The message is a brief description of the error, for
// (debugging) convenience.
//
// When emitted by the client, it indicates an error occurred in an
// object on the client.
type CoreError struct {
// The id of the resource (proxy if emitted by the client) that is in error.
ID Int `json:"id"`
// A seq number from the failing request (if any).
Sequence Int `json:"seq"`
// A negative errno style error code.
Result Int `json:"res"`
// An error message.
Message String `json:"message"`
}
// FileCount satisfies [Message] with a constant value.
func (c *CoreError) FileCount() Int { return 0 }
func (c *CoreError) Error() string {
return "received Core::Error on" +
" id " + strconv.Itoa(int(c.ID)) +
" seq " + strconv.Itoa(int(c.Sequence)) +
" res " + strconv.Itoa(int(c.Result)) +
": " + c.Message
}
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreError) Size() Word {
return SizePrefix +
Size(SizeInt) +
Size(SizeInt) +
Size(SizeInt) +
SizeString[Word](c.Message)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreError) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreError) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// CoreErrorMethod is [CoreError] as a method [Message].
type CoreErrorMethod struct{ CoreError }
// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorMethod) Opcode() byte { return PW_CORE_METHOD_ERROR }
// CoreErrorEvent is [CoreError] as an event [Message].
type CoreErrorEvent struct{ CoreError }
// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorEvent) Opcode() byte { return PW_CORE_EVENT_ERROR }
// The CoreRemoveId event is used internally by the object ID management logic.
//
// When a client deletes an object, the server will send this event to acknowledge
// that it has seen the delete request. When the client receives this event, it
// will know that it can safely reuse the object ID.
type CoreRemoveId struct {
// A proxy id that was removed.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreRemoveId) Opcode() byte { return PW_CORE_EVENT_REMOVE_ID }
// FileCount satisfies [Message] with a constant value.
func (c *CoreRemoveId) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreRemoveId) Size() Word { return SizePrefix + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreRemoveId) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreRemoveId) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// The CoreBoundProps event is emitted when a local object ID is bound to a global ID.
// It is emitted before the global becomes visible in the registry.
type CoreBoundProps struct {
// A proxy id.
ID Int `json:"id"`
// The global_id as it will appear in the registry.
GlobalID Int `json:"global_id"`
// The properties of the global.
Properties *SPADict `json:"props"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreBoundProps) Opcode() byte { return PW_CORE_EVENT_BOUND_PROPS }
// FileCount satisfies [Message] with a constant value.
func (c *CoreBoundProps) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreBoundProps) Size() Word {
return SizePrefix +
Size(SizeInt) +
Size(SizeInt) +
c.Properties.Size()
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreBoundProps) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreBoundProps) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// ErrBadBoundProps is returned when a [CoreBoundProps] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadBoundProps = errors.New("attempting to store bound props on a proxy that should never be targeted")
// noAck is embedded by proxies that are never targeted by [CoreBoundProps].
type noAck struct{}
// setBoundProps should never be called as this proxy should never be targeted by [CoreBoundProps].
func (noAck) setBoundProps(*CoreBoundProps) error { panic(ErrBadBoundProps) }
// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadRemove = errors.New("attempting to remove a proxy that should never be targeted")
// noRemove is embedded by proxies that are never targeted by [CoreRemoveId].
type noRemove struct{}
// remove should never be called as this proxy should never be targeted by [CoreRemoveId].
func (noRemove) remove() error { panic(ErrBadRemove) }
// ErrInvalidRemove is returned when a proxy is somehow removed twice. This is only reached for
// an implementation error as the proxy struct should no longer be reachable after the first call.
var ErrInvalidRemove = errors.New("attempting to remove an already freed proxy")
// removable is embedded by proxies that can be targeted by [CoreRemoveId] and requires no cleanup.
type removable bool
// remove checks against removal of a freed proxy and marks the proxy as removed.
func (s *removable) remove() error {
if *s {
panic(ErrInvalidRemove)
}
*s = true
return nil
}
// ErrProxyDestroyed is returned when attempting to use a proxy method when the underlying
// proxy has already been targeted by a [CoreRemoveId] event.
var ErrProxyDestroyed = errors.New("underlying proxy has been removed")
// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been destroyed.
// Must be called at the beginning of any exported method of a proxy embedding removable.
func (s *removable) checkDestroy() error {
if *s {
// not fatal: the caller is allowed to recover from this and allocate a new proxy
return ErrProxyDestroyed
}
return nil
}
// mustCheckDestroy calls checkDestroy and panics if a non-nil error is returned.
// This is useful for non-exported methods as they should become unreachable.
func (s *removable) mustCheckDestroy() {
if err := s.checkDestroy(); err != nil {
panic(err)
}
}
// destructible is embedded by proxies that can be targeted by the [CoreRemoveId] event and the
// [CoreDestroy] method and requires no cleanup. destructible purposefully does not override
// removable.mustCheckDestroy because it is used by unexported methods called during event handling
// and are exempt from the destruction check.
type destructible struct {
destroyed bool
removable
}
// checkDestroy overrides removable.checkDestroy to also check the destroyed field.
func (s *destructible) checkDestroy() error {
if s.destroyed {
return ErrProxyDestroyed
}
if err := s.removable.checkDestroy(); err != nil {
return err
}
return nil
}
// destroy calls removable.checkDestroy then queues a [CoreDestroy] event if it succeeds.
func (s *destructible) destroy(ctx *Context, id Int) error {
if err := s.checkDestroy(); err != nil {
return err
}
l := len(ctx.pendingDestruction)
ctx.pendingDestruction[id] = struct{}{}
if len(ctx.pendingDestruction) != l+1 {
return ErrProxyDestroyed
}
s.destroyed = true
return ctx.GetCore().destroy(id)
}
// An InconsistentIdError describes an inconsistent state where the server claims an impossible
// proxy or global id. This is only generated by the [CoreBoundProps] event.
type InconsistentIdError struct {
// Whether the inconsistent id is the global resource id.
Global bool
// Targeted proxy instance.
Proxy fmt.Stringer
// Differing ids.
ID, ServerID Int
}
func (e *InconsistentIdError) Error() string {
name := "proxy"
if e.Global {
name = "global"
}
return name + " id " + strconv.Itoa(int(e.ID)) + " targeting " + e.Proxy.String() +
" inconsistent with " + strconv.Itoa(int(e.ServerID)) + " claimed by the server"
}
// CoreHello is the first message sent by a client.
type CoreHello struct {
// The version number of the client, usually PW_VERSION_CORE.
Version Int `json:"version"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreHello) Opcode() byte { return PW_CORE_METHOD_HELLO }
// FileCount satisfies [Message] with a constant value.
func (c *CoreHello) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreHello) Size() Word { return SizePrefix + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// hello queues a [CoreHello] message for the PipeWire server.
// This method should not be called directly, the [New] function queues this message.
func (core *Core) hello() error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreHello{PW_VERSION_CORE},
)
}
const (
// CoreSyncSequenceOffset is the offset to [Header.Sequence] to produce [CoreSync.Sequence].
CoreSyncSequenceOffset = 0x40000000
)
// The CoreSync message will result in a Done event from the server.
// When the Done event is received, the client can be sure that all
// operations before the Sync method have been completed.
type CoreSync struct {
// The id will be returned in the Done event.
ID Int `json:"id"`
// Usually generated automatically and will be returned in the Done event.
Sequence Int `json:"seq"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreSync) Opcode() byte { return PW_CORE_METHOD_SYNC }
// FileCount satisfies [Message] with a constant value.
func (c *CoreSync) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreSync) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// sync queues a [CoreSync] message for the PipeWire server.
// This is not safe to use directly, callers should use Sync instead.
func (core *Core) sync(id Int) error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreSync{id, CoreSyncSequenceOffset + Int(core.ctx.sequence)},
)
}
// ErrNotDone is returned if [Core.Sync] returns from its [Context.Roundtrip] without
// receiving a [CoreDone] event targeting the [CoreSync] event it delivered.
var ErrNotDone = errors.New("did not receive a Core::Done event targeting previously delivered Core::Sync")
const (
// syncTimeout is the maximum duration [Core.Sync] is allowed to take before
// receiving [CoreDone] or failing.
syncTimeout = 5 * time.Second
)
// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
func (core *Core) Sync() error {
core.done = false
if err := core.sync(roundtripSyncID); err != nil {
return err
}
deadline := time.Now().Add(syncTimeout)
for !core.done {
if time.Now().After(deadline) {
return ErrNotDone
}
if err := core.ctx.roundtrip(); err != nil {
return err
}
}
if len(core.ctx.pendingIds) != 0 {
core.ctx.closeReceivedFiles()
return &ProxyFatalError{Err: UnacknowledgedProxyError(slices.Collect(maps.Keys(core.ctx.pendingIds))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
}
if len(core.ctx.pendingDestruction) != 0 {
core.ctx.closeReceivedFiles()
return &ProxyFatalError{Err: UnacknowledgedProxyDestructionError(slices.Collect(maps.Keys(core.ctx.pendingDestruction))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
}
return core.ctx.doSyncComplete()
}
// The CorePong message is sent from the client to the server when the server emits the Ping event.
type CorePong struct {
// Copied from [CorePing.ID].
ID Int `json:"id"`
// Copied from [CorePing.Sequence]
Sequence Int `json:"seq"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CorePong) Opcode() byte { return PW_CORE_METHOD_PONG }
// FileCount satisfies [Message] with a constant value.
func (c *CorePong) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CorePong) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CorePong) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CorePong) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// CoreGetRegistry is sent when a client requests to bind to the
// registry object and list the available objects on the server.
//
// Like with all bindings, first the client allocates a new proxy
// id and puts this as the new_id field. Methods and Events can
// then be sent and received on the new_id (in the message Id field).
type CoreGetRegistry struct {
// The version of the registry interface used on the client,
// usually PW_VERSION_REGISTRY.
Version Int `json:"version"`
// The id of the new proxy with the registry interface,
// ends up as [Header.ID] in future messages.
NewID Int `json:"new_id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreGetRegistry) Opcode() byte { return PW_CORE_METHOD_GET_REGISTRY }
// FileCount satisfies [Message] with a constant value.
func (c *CoreGetRegistry) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreGetRegistry) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreGetRegistry) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreGetRegistry) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// GetRegistry queues a [CoreGetRegistry] message for the PipeWire server
// and returns the address of the newly allocated [Registry].
func (ctx *Context) GetRegistry() (*Registry, error) {
registry := Registry{Objects: make(map[Int]RegistryGlobal), ctx: ctx}
newId := ctx.newProxyId(&registry, false)
registry.ID = newId
return &registry, ctx.writeMessage(
PW_ID_CORE,
&CoreGetRegistry{PW_VERSION_REGISTRY, newId},
)
}
// CoreCreateObject is sent when the client requests to create a
// new object from a factory of a certain type.
//
// The client allocates a new_id for the proxy. The server will
// allocate a new resource with the same new_id and from then on,
// Methods and Events will be exchanged between the new object of
// the given type.
type CoreCreateObject struct {
// The name of a server factory object to use.
FactoryName String `json:"factory_name"`
// The type of the object to create, this is also the type of
// the interface of the new_id proxy.
Type String `json:"type"`
// Undocumented, assumed to be the local version of the proxy.
Version Int `json:"version"`
// Extra properties to create the object.
Properties *SPADict `json:"props"`
// The proxy id of the new object.
NewID Int `json:"new_id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreCreateObject) Opcode() byte { return PW_CORE_METHOD_CREATE_OBJECT }
// FileCount satisfies [Message] with a constant value.
func (c *CoreCreateObject) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreCreateObject) Size() Word {
return SizePrefix +
SizeString[Word](c.FactoryName) +
SizeString[Word](c.Type) +
Size(SizeInt) +
c.Properties.Size() +
Size(SizeInt)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreCreateObject) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreCreateObject) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// createObject queues a [CoreCreateObject] message for the PipeWire server.
// This is not safe to use directly, callers should use typed wrapper methods on [Registry] instead.
func (core *Core) createObject(factoryName, typeName String, version Int, props SPADict, newId Int) error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreCreateObject{factoryName, typeName, version, &props, newId},
)
}
// CoreDestroy is sent when the client requests to destroy an object.
type CoreDestroy struct {
// The proxy id of the object to destroy.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreDestroy) Opcode() byte { return PW_CORE_METHOD_DESTROY }
// FileCount satisfies [Message] with a constant value.
func (c *CoreDestroy) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreDestroy) Size() Word { return SizePrefix + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// destroy queues a [CoreDestroy] message for the PipeWire server.
// This is not safe to use directly, callers should use the exported method
// on the proxy implementation instead.
func (core *Core) destroy(id Int) error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreDestroy{id},
)
}
// A RegistryGlobal event is emitted to notify a client about a new global object.
type RegistryGlobal struct {
// The global id.
ID Int `json:"id"`
// Permission bits.
Permissions Int `json:"permissions"`
// The type of object.
Type String `json:"type"`
// The server version of the object.
Version Int `json:"version"`
// Extra global properties.
Properties *SPADict `json:"props"`
}
// Opcode satisfies [Message] with a constant value.
func (c *RegistryGlobal) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL }
// FileCount satisfies [Message] with a constant value.
func (c *RegistryGlobal) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *RegistryGlobal) Size() Word {
return SizePrefix +
Size(SizeInt) +
Size(SizeInt) +
SizeString[Word](c.Type) +
Size(SizeInt) +
c.Properties.Size()
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
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
// global object and events can be received.
type RegistryBind struct {
// The [RegistryGlobal.ID] to bind to.
ID Int `json:"id"`
// the [RegistryGlobal.Type] of the global id.
Type String `json:"type"`
// The client version of the interface for type.
Version Int `json:"version"`
// The client proxy id for the global object.
NewID Int `json:"new_id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *RegistryBind) Opcode() byte { return PW_REGISTRY_METHOD_BIND }
// FileCount satisfies [Message] with a constant value.
func (c *RegistryBind) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *RegistryBind) Size() Word {
return SizePrefix +
Size(SizeInt) +
SizeString[Word](c.Type) +
Size(SizeInt) +
Size(SizeInt)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryBind) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryBind) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// bind queues a [RegistryBind] message for the PipeWire server
// and returns the newly allocated proxy id.
func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) {
bind := RegistryBind{
ID: id,
Type: proxy.String(),
Version: version,
NewID: registry.ctx.newProxyId(proxy, true),
}
return bind.NewID, registry.ctx.writeMessage(
registry.ID,
&bind,
)
}
// RegistryDestroy is sent to try to destroy the global object with id.
// This might fail when the client does not have permission.
type RegistryDestroy struct {
// The global id to destroy.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *RegistryDestroy) Opcode() byte { return PW_REGISTRY_METHOD_DESTROY }
// FileCount satisfies [Message] with a constant value.
func (c *RegistryDestroy) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *RegistryDestroy) Size() Word {
return SizePrefix +
Size(SizeInt)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// destroy queues a [RegistryDestroy] message for the PipeWire server.
func (registry *Registry) destroy(id Int) error {
return registry.ctx.writeMessage(
registry.ID,
&RegistryDestroy{id},
)
}
// Destroy tries to destroy the global object with id.
func (registry *Registry) Destroy(id Int) (err error) {
asCoreError := registry.ctx.expectsCoreError(registry.ID, &err)
if err != nil {
return
}
if err = registry.destroy(id); err != nil {
return err
}
if err = registry.ctx.GetCore().Sync(); err == nil {
return nil
}
if coreError := asCoreError(); coreError == nil {
return
} else {
switch syscall.Errno(-coreError.Result) {
case syscall.EPERM:
return &PermissionError{registry.ID, coreError.Message}
default:
return coreError
}
}
}
// An UnsupportedObjectTypeError is the name of a type not known by the server [Registry].
type UnsupportedObjectTypeError string
func (e UnsupportedObjectTypeError) Error() string { return "unsupported object type " + string(e) }
func (e UnsupportedObjectTypeError) Message() string { return e.Error() }
// Core holds state of [PW_TYPE_INTERFACE_Core].
type Core struct {
// Additional information from the server, populated or updated during [Context.Roundtrip].
Info *CoreInfo `json:"info"`
// Whether a [CoreDone] event was received during Sync.
done bool
ctx *Context
noAck
noRemove
}
// ErrUnexpectedDone is a [CoreDone] event with unexpected values.
var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync")
// An UnknownProxyIdError describes an event targeting a proxy id that was never allocated.
type UnknownProxyIdError[E any] struct {
// Offending id decoded from Data.
Id Int
// Event received from the server.
Event E
}
func (e *UnknownProxyIdError[E]) Error() string {
return "unknown proxy id " + strconv.Itoa(int(e.Id))
}
// An InvalidPingError is a [CorePing] event targeting a proxy id that was never allocated.
type InvalidPingError CorePing
func (e *InvalidPingError) Error() string {
return "received Core::Ping seq " + strconv.Itoa(int(e.Sequence)) +
" targeting unknown proxy id " + strconv.Itoa(int(e.ID))
}
func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error {
closeReceivedFiles(files...)
switch opcode {
case PW_CORE_EVENT_INFO:
unmarshal(&core.Info)
return nil
case PW_CORE_EVENT_DONE:
var done CoreDone
unmarshal(&done)
if done.ID == roundtripSyncID && done.Sequence == CoreSyncSequenceOffset+core.ctx.currentSeq() {
if core.done {
return ErrUnexpectedDone
}
core.done = true
}
// silently ignore non-matching events because the server sends out
// an event with id -1 seq 0 that does not appear to correspond to
// anything, and this behaviour is never mentioned in documentation
return nil
case PW_CORE_EVENT_PING:
var ping CorePing
unmarshal(&ping)
if _, ok := core.ctx.proxy[ping.ID]; ok {
core.ctx.mustWriteMessage(PW_ID_CORE, (*CorePong)(&ping))
return nil
} else {
invalidPingError := InvalidPingError(ping)
core.ctx.mustWriteMessage(PW_ID_CORE, &CoreErrorMethod{CoreError{
ID: PW_ID_CORE,
Sequence: core.ctx.currentRemoteSeq(),
Result: -Int(syscall.EINVAL),
Message: invalidPingError.Error(),
}})
return &invalidPingError
}
case PW_CORE_EVENT_ERROR:
var coreError CoreError
unmarshal(&coreError)
return &coreError
case PW_CORE_EVENT_REMOVE_ID:
var coreRemoveId CoreRemoveId
unmarshal(&coreRemoveId)
if proxy, ok := core.ctx.proxy[coreRemoveId.ID]; !ok {
// this should never happen so is non-recoverable if it does
panic(&UnknownProxyIdError[*CoreRemoveId]{Id: coreRemoveId.ID, Event: &coreRemoveId})
} else {
delete(core.ctx.proxy, coreRemoveId.ID)
// not always populated so this is not checked
delete(core.ctx.pendingDestruction, coreRemoveId.ID)
return proxy.remove()
}
case PW_CORE_EVENT_BOUND_PROPS:
var boundProps CoreBoundProps
unmarshal(&boundProps)
delete(core.ctx.pendingIds, boundProps.ID)
proxy, ok := core.ctx.proxy[boundProps.ID]
if !ok {
return &UnknownProxyIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
}
return proxy.setBoundProps(&boundProps)
default:
panic(&UnsupportedOpcodeError{opcode, core.String()})
}
}
func (core *Core) String() string { return PW_TYPE_INTERFACE_Core }
// Registry holds state of [PW_TYPE_INTERFACE_Registry].
type Registry struct {
// Proxy id as tracked by [Context].
ID Int `json:"proxy_id"`
// Global objects received via the [RegistryGlobal] event.
//
// This requires more processing before it can be used, but is not implemented
// as it is not used by Hakurei.
Objects map[Int]RegistryGlobal `json:"objects"`
ctx *Context
noAck
noRemove
}
// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a previous instance of itself.
type GlobalIDCollisionError struct {
// The colliding id.
ID Int
// Involved events.
Previous, Current *RegistryGlobal
}
func (e *GlobalIDCollisionError) Error() string {
return "new Registry::Global event for " + e.Current.Type +
" 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 {
case PW_REGISTRY_EVENT_GLOBAL:
var global RegistryGlobal
unmarshal(&global)
if object, ok := registry.Objects[global.ID]; ok {
// this should never happen so is non-recoverable if it does
panic(&GlobalIDCollisionError{global.ID, &object, &global})
}
registry.Objects[global.ID] = global
return nil
case PW_REGISTRY_EVENT_GLOBAL_REMOVE:
var globalRemove RegistryGlobalRemove
unmarshal(&globalRemove)
// server emits PW_CORE_EVENT_REMOVE_ID events targeting
// affected proxies so they do not need to be handled here
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:
panic(&UnsupportedOpcodeError{opcode, registry.String()})
}
}
func (registry *Registry) String() string { return PW_TYPE_INTERFACE_Registry }