104 Commits

Author SHA1 Message Date
ccc0d98bd7 release: 0.3.2
All checks were successful
Release / Create release (push) Successful in 48s
Test / Create distribution (push) Successful in 28s
Test / Sandbox (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 7m4s
Test / Hakurei (push) Successful in 4m2s
Test / Flake checks (push) Successful in 1m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 08:12:52 +09:00
a3fd05765e container: load initial process started before syscall
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m21s
Test / Hakurei (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 7m7s
Test / Flake checks (push) Successful in 1m39s
This avoids a race between returning from syscall and checking the state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 08:12:22 +09:00
c538df7daa internal/pipewire: expose connection props
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m29s
Test / Hakurei (push) Successful in 45s
Test / Flake checks (push) Successful in 1m37s
Unused in hakurei but could be useful when the package is moved out of internal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:51:12 +09:00
44e5aa1a36 internal/pipewire: include remaining size in recvmsg wrapper
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hpkg (push) Successful in 4m44s
Test / Hakurei (race detector) (push) Successful in 6m20s
Test / Hakurei (push) Successful in 3m35s
Test / Flake checks (push) Successful in 1m22s
This otherwise truncates the received data by len(remaining) bytes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:36:46 +09:00
cf0e7d8c27 internal/pipewire: reset per-roundtrip state once per call
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m38s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 5m1s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m22s
Test / Flake checks (push) Successful in 1m21s
This was left in consume when relocating per-roundtrip code out of the per-receive consume method.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:28:33 +09:00
130add21e5 internal/pipewire: increment remote sequence after establishing bounds
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 4m45s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m39s
This avoids incrementing it twice proceeding from a partial message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:21:41 +09:00
5ec4045e24 internal/pipewire: do not clobber error parsing SCMs
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m35s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 4m50s
Test / Hpkg (push) Successful in 4m54s
Test / Hakurei (race detector) (push) Successful in 6m29s
Test / Flake checks (push) Successful in 1m21s
The error is handled later, clobbering it here breaks error handling when SCMs are present.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:02:15 +09:00
be2075f169 Revert "internal/pipewire: work around remote sequence quirk"
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hpkg (push) Successful in 4m52s
Test / Hakurei (race detector) (push) Successful in 6m28s
Test / Flake checks (push) Successful in 1m21s
This reverts commit 564db6863b.
2025-12-09 05:25:41 +09:00
e9fb1d7be5 container/initdaemon: copy wstatus from wait4 loop
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 44s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 48s
Test / Hpkg (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 47s
Test / Flake checks (push) Successful in 1m37s
Due to the special nature of the init process, direct use of wait outside the wait4 loop is racy. This change copies the wstatus from wait4 loop state instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:58:42 +09:00
dafe9f8efc container: spin instead of block on wait4 ECHILD
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hpkg (push) Successful in 4m57s
Test / Hakurei (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m27s
Blocking prevents further wait4 processing causing ops to never receive their signals.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:56:13 +09:00
96dd7abd80 container: improve error message fallback
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m38s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hpkg (push) Successful in 4m52s
Test / Hakurei (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m28s
This now falls back to message.Error if no other concrete type is matched.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:45:54 +09:00
d5fb179012 cmd/hakurei: exec instead of fork/exec from shell
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m53s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m27s
There is no reason to keep the shell process around.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:29:41 +09:00
462863e290 container: friendlier error message for op timing out
All checks were successful
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 4m50s
Test / Sandbox (race detector) (push) Successful in 4m52s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m26s
This includes the string for the failing op which helps with troubleshooting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:19:03 +09:00
2786611b88 test/interactive: add app with bad daemon
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m28s
This is useful for testing daemon error handling behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:12:07 +09:00
791a1dfa55 container: make wait4 loop available to ops
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hpkg (push) Successful in 4m44s
Test / Hakurei (push) Successful in 4m55s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m32s
Due to the special nature of the init process, regular wait calls are unavailable. This change provides infrastructure to access wait4 loop state from Op.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 21:43:49 +09:00
564db6863b internal/pipewire: work around remote sequence quirk
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (push) Successful in 4m57s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m17s
Test / Flake checks (push) Successful in 1m26s
Remote sequence sometimes start with a non-zero value, and keeps the same value for a while before going back to zero. Conditions for reproducing this behaviour is not yet known. This change works around this behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 20:10:01 +09:00
87781c7658 treewide: include PipeWire op and enforce PulseAudio check
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Flake checks (push) Successful in 1m29s
This fully replaces PulseAudio with PipeWire and enforces the PulseAudio check and error message. The pipewire-pulse daemon is handled in the NixOS module.

Closes #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 08:53:04 +09:00
0c38fb7b6a hst: expose daemon as fs entry
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m48s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (push) Successful in 5m15s
Test / Hpkg (push) Successful in 5m18s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m25s
This is slightly counterintuitive, but it turned out well under this framework since the daemon backs its target file.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 07:38:47 +09:00
357cfcddee container: start daemons within container
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m32s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (push) Successful in 5m7s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m27s
This is useful for daemons internal to the container. The only current use case is pipewire-pulse.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 07:21:04 +09:00
6bf245cf1b container: pass context as setup state
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 4m52s
Test / Hakurei (push) Successful in 5m26s
Test / Hpkg (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 7m1s
Test / Flake checks (push) Successful in 1m32s
This is useful currently for daemon Op, but could be used for many other things.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 06:06:19 +09:00
c8eeb4a4d1 internal/outcome: integrate pipewire server
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (push) Successful in 4m47s
Test / Hpkg (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m31s
This is very simple and takes almost no inputs. This is not yet hooked up to anything to prevent breaking any existing behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 05:03:16 +09:00
5785714b64 container: call op method right before initial process
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 4m51s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m41s
This is at a point considered to be already "within" the container. Daemons internal to the container can be started here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 04:57:24 +09:00
422efcf258 hst: check for insecure PulseAudio enablement
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 5m39s
Test / Flake checks (push) Successful in 1m32s
This is currently still a noop, but required for #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 03:13:02 +09:00
104eeecf65 cmd/hakurei: add pipewire flag
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m38s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m30s
This is for "run" command, formerly permissive defaults behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 02:39:55 +09:00
bf856f06e5 internal/pipewire: constant for PIPEWIRE_REMOTE
All checks were successful
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 2m43s
Test / Sandbox (race detector) (push) Successful in 4m45s
Test / Hakurei (push) Successful in 5m2s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m30s
This is not defined anywhere in upstream PipeWire, no idea why.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 23:35:29 +09:00
1931b54600 hst: add pipewire flag
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 4m55s
Test / Hakurei (push) Successful in 4m59s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m31s
These are for #26. None of them are implemented yet. This fixes up test cases for the change to happen. Existing source code and JSON configuration continue to have the same effect. Existing flags get its EPulse bit replaced by EPipeWire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 22:34:40 +09:00
093e30c788 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>
2025-12-07 17:39:34 +09:00
1b17ccda91 internal/system: optional op check parallelism
All checks were successful
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m46s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hpkg (push) Successful in 5m17s
Test / Hakurei (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m47s
Test / Flake checks (push) Successful in 1m33s
The PipeWire Op check cannot be made parallel due to the OS interaction.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:31:10 +09:00
7c6fc1128b internal/pipewire: set finalizer on scc
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 2m38s
Test / Hpkg (push) Successful in 4m16s
Test / Hakurei (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 4m52s
Test / Flake checks (push) Successful in 1m38s
This prevents leaking the socket and pipe fds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:29:22 +09:00
8cdd659239 internal/pipewire: seq access method for consume
All checks were successful
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (push) Successful in 4m56s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m33s
This improves readability as the offset is not immediately obvious.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:07:30 +09:00
15c2839a09 internal/pipewire: respond to Core::Ping
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m17s
Test / Hakurei (push) Successful in 4m11s
Test / Flake checks (push) Successful in 1m39s
There is currently no known message that will get the PipeWire server to emit this event. It should be handled regardless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 14:55:56 +09:00
b9b9705b52 internal/pipewire: specify opcode and file count with message
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (push) Successful in 4m56s
Test / Hpkg (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m30s
This adds checking of FileCount while writing a message. Message encoding is relocated to an exported method to be used externally, probably for test stubbing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 13:54:11 +09:00
246e04214a internal/system: pass syscall error message
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m43s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m2s
Test / Hpkg (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m25s
Test / Flake checks (push) Successful in 1m32s
This makes wrapped syscall errors produce a cleaner error message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 02:08:05 +09:00
503bfc6468 internal/system: port connect by name
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m33s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (push) Successful in 5m13s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m31s
This behaviour is a bit messy and checks what appears to be a windows-specific environment variable for some reason. Keeping everything intact regardless to match upstream behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 02:00:19 +09:00
d837628b4c internal/system: remove ineffectual join reverting wayland
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m35s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hpkg (push) Successful in 4m59s
Test / Hakurei (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m33s
Removing the pathname socket used to be handled separately, now it is done during the Close call.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 00:43:50 +09:00
3cb58b4b72 internal/pipewire: high level SecurityContext helper
All checks were successful
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 5m10s
Test / Hakurei (push) Successful in 5m19s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m33s
This sets up close pipe and socket internally, and exposes the resulting pathname socket and close_fd cleanup as an io.Closer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 21:16:27 +09:00
bb1fc4c7bc internal/pipewire: check pending ids after done
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 4m49s
Test / Sandbox (race detector) (push) Successful in 4m49s
Test / Hpkg (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 47s
Test / Flake checks (push) Successful in 1m42s
This is not guaranteed to have completed after a roundtrip. This is leftover from when Roundtrip also sent and waited for sync.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 21:09:55 +09:00
f44923da29 internal/pipewire: post-sync cleanup functions
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 5m40s
Test / Hakurei (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 6m33s
Test / Flake checks (push) Successful in 1m43s
This makes it easier to handle resources who only needs to stay alive before the next sync.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 21:03:21 +09:00
5e7861bb00 internal/pipewire: handle dangling files in roundtrip
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m50s
Test / Flake checks (push) Successful in 1m32s
This should not be handled on every receive as it could cause valid (though impossible in current upstream implementation) messages to be rejected and raise a protocol error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 19:21:57 +09:00
7cb3308a53 internal/pipewire: store proxy errors in context
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (push) Successful in 4m50s
Test / Hpkg (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 4m39s
Test / Flake checks (push) Successful in 1m41s
This change fixes handling of non-fatal errors during a roundtrip as there can be multiple receive calls per roundtrip.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 19:13:46 +09:00
490093a659 internal/pipewire: set errno on an empty message
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 5m0s
Test / Hpkg (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m30s
This matches upstream behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 16:50:49 +09:00
2b22efcdf1 internal/pipewire: rename context consume method
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Flake checks (push) Successful in 1m37s
This name is more correct since it does not roundtrip, but receives messages. This is also more consistent with the method on event proxies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 16:44:39 +09:00
8a2f9edcf9 internal/pipewire: use sendmsg/recvmsg directly
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hakurei (push) Successful in 4m58s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m29s
The PipeWire protocol does not work with Go abstractions. This change makes relevant methods call sendmsg/recvmsg directly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 02:12:47 +09:00
0d3f332d45 internal/pipewire: do not send ancillary msg without files
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hakurei (push) Successful in 4m55s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m30s
This is unnecessary and does not match upstream behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-05 04:20:04 +09:00
d5509cc6e5 internal/pipewire: constants from pipewire/keys.h
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m22s
These are PipeWire spa_dict keys. Interestingly many keys in the sample are undefined and appear as magic strings in upstream source code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-05 03:27:30 +09:00
0d3ae6cb23 internal/pipewire: improve protocol error messages
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m45s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m27s
These are mostly small formatting changes, with the biggest change being to UnexpectedEOFError where its kind is now described as part of the error type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-04 03:15:58 +09:00
69b1131d66 internal/pipewire: use type name in error strings
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hakurei (push) Successful in 4m52s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m28s
Test / Flake checks (push) Successful in 1m33s
This provides more useful messages for protocol errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-04 02:29:27 +09:00
2c0b92771a internal/pipewire: relocate constants
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (push) Successful in 4m48s
Test / Hpkg (push) Successful in 4m50s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m21s
This should make things easier to navigate, and possible to fully automatically generate the constants in the future.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-03 03:52:17 +09:00
054c91879f internal/pipewire: finalizers for dangling files
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 43s
Test / Hpkg (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 3m47s
Test / Flake checks (push) Successful in 1m26s
This makes their handling much less error-prone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-03 03:36:02 +09:00
c34439fc5f internal/pipewire: collect non-protocol errors
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hpkg (push) Successful in 4m45s
Test / Hakurei (push) Successful in 4m59s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m30s
These errors are recoverable and should not terminate event handling. Only terminate event handling for protocol errors or inconsistent state that makes further event handling impossible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-03 01:35:43 +09:00
32fb137bb2 internal/pipewire: fail on unacknowledged proxies
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 5m0s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m28s
These proxies (with special cases documented in the implementation) are only safe for use after acknowledgement from the server.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 21:17:46 +09:00
e7a665e043 internal/pipewire: handle Core::Error
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m59s
Test / Hpkg (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m24s
Test / Flake checks (push) Successful in 1m28s
This event is not encountered in the pw-container sample, but already has existing sample from an excerpt.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 06:16:18 +09:00
af741f20a0 internal/pipewire: implement client context
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m35s
Test / Sandbox (race detector) (push) Successful in 4m45s
Test / Hakurei (push) Successful in 5m0s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m34s
This consumes the entire sample, is validated to send identical messages and correctly handle received messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 06:03:21 +09:00
39c6716fb0 internal/pipewire: use correct types in header
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hpkg (push) Successful in 4m54s
Test / Hakurei (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m32s
This was written when the protocol was still barely understood, so none of the types here are correct and match the rest of the protocol. This change corrects these types.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 03:32:10 +09:00
7bc73afadd internal/pipewire: wrap EOF error for deserialisation
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m34s
The io.ErrUnexpectedEOF error can be returned from multiple places. This change eases error handling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 03:19:37 +09:00
647aa9d02f internal/pipewire: preallocate for footer
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m11s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m31s
This is useful during serialisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 03:06:27 +09:00
91aaabaa1b internal/pipewire: benchmarks against Gob and JSON
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m33s
Performance does not matter for the use case of this library, but it is still interesting to know.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-30 12:13:46 +09:00
3d4c7cdd9e internal/pipewire: implement Core::Error
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m31s
Sample was captured from pw-cli.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-30 01:39:39 +09:00
4fd6d6c037 internal/pipewire: implement Core::Ping, Core::Pong
All checks were successful
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m33s
Test / Hpkg (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m34s
I could not get the server to produce these events, however I am confident enough with the implementation to do it by hand.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 23:25:29 +09:00
de3fc7ba38 internal/pipewire: implement SecurityContext::Create
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m27s
This is finally the thing we are after.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 16:46:39 +09:00
5a5c4705dd internal/pipewire: implement Registry::Bind
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m30s
This change also adds test cases for messages before this one.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 03:01:53 +09:00
f703aa20a5 internal/pipewire: implement client generation footer
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m29s
This corresponds with the core generation footer and seem to be the only other possible footer type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 02:55:30 +09:00
5c12425d48 internal/pipewire: implement Registry::Global
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m31s
Dealing with this event reawakened my burning hatred for OOP.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 02:32:45 +09:00
cbe86dc4f0 internal/pipewire: add json struct tags
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m30s
These match the names found in documentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 01:33:32 +09:00
d08a1081bd internal/pipewire: do not store spa_dict fields
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m21s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Flake checks (push) Successful in 1m30s
This is effectively a poor man's slice, it is entirely unnecessary here and can be handled internally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 01:25:18 +09:00
72a2601d74 internal/pipewire: store sample iovec continuously
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m22s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Hakurei (push) Successful in 2m26s
Test / Flake checks (push) Successful in 1m39s
This removes the need for manual splitting. The understanding of the format is robust enough to allow this to happen anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 00:35:10 +09:00
1dab87aaf0 internal/pipewire: add missing constants
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m24s
These did not appear useful at first since it was assumed to be filenames for loading modules.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 21:28:16 +09:00
2bafde99e3 internal/pipewire: shorten test data filenames
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m24s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m30s
These were getting very annoying to type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 20:06:01 +09:00
91efeb101a internal/pipewire: spa_dict size nil check
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m24s
This fixes serialisation of NULL spa_dict.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:51:36 +09:00
dcb22a61c0 internal/pipewire: require appending marshaler
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m24s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m23s
This eliminates all non-reflect allocations.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:33:19 +09:00
e028a61fc1 internal/pipewire: preallocate for known size
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m27s
This is still not efficient by any means, but it should eliminate most non-reflect allocation (all allocation if PODMarshaler is not used).

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:26:31 +09:00
73987be7d4 internal/pipewire: size without serialisation
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m28s
This is required to achieve zero allocation (other than reflect).

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:17:38 +09:00
563b5e66fc internal/pipewire: simplify spa_dict appends
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m31s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m30s
This change uses the (somewhat) newly exposed MarshalAppend which improves readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 01:52:13 +09:00
2edcfe1e68 internal/pipewire: define size constants
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m26s
This gets rid of magic numbers in marshal/unmarshal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 00:54:56 +09:00
2698ca00e8 internal/pipewire: implement Core::Done
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m20s
The message in the sample does not correspond to any known method call. The spec does not mention what to do with messages like this, but all existing usage code simply drops it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 19:02:21 +09:00
1d0143386d internal/pipewire: optional final trailing garbage check
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m20s
Omitting the check is only useful for custom unmarshaler.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 18:50:39 +09:00
a55c209099 internal/pipewire: additional Client::Info test case
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m20s
This appears to add *one single entry* compared to the message before it. The inefficiency of this protocol is beyond imagination.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 16:28:57 +09:00
10ff276da1 internal/pipewire: additional Client::Info test case
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m19s
This message follows the other Client::Info event before it. No idea why.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 16:17:38 +09:00
fd4d379b67 internal/pipewire: implement Client::Info
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m22s
Everything is already supported, as usual.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 16:05:46 +09:00
77f5b89a41 internal/pipewire: implement Core::BoundProps
All checks were successful
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 2m28s
Test / Hakurei (push) Successful in 3m26s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m20s
Very straightforward type, everything is already supported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 18:40:19 +09:00
14e33f17e5 internal/pipewire: check nil marshaler
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m26s
Test / Sandbox (race detector) (push) Successful in 2m19s
Test / Hakurei (push) Successful in 2m34s
Test / Hakurei (race detector) (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m22s
Test / Flake checks (push) Successful in 1m33s
NULL values have special case in the format. This check ensures correctness serialising nil pointers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 18:36:08 +09:00
cfeb7818eb internal/pipewire: implement Core::Info and generation footer
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m21s
These are not directly related but are first encountered on the same message in the capture.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:16:12 +09:00
05391da556 internal/pipewire: implement footer
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Flake checks (push) Successful in 1m39s
The POD itself is serialised without requiring a special case, however its presence is only indicated by the difference in size recorded in the header and payload.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:11:22 +09:00
463f8836e6 internal/pipewire: implement Long type
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 1m42s
Test / Hakurei (push) Successful in 2m30s
Test / Hpkg (push) Successful in 3m20s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m37s
Thankfully no special case here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:05:37 +09:00
2e465c94da internal/pipewire: implement Id type
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m22s
This is, in fact, just a glorified Int type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:01:58 +09:00
26009fd3f7 internal/pipewire: slice at POD boundary
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 47s
Test / Sandbox (race detector) (push) Successful in 2m16s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m18s
Test / Flake checks (push) Successful in 1m34s
This prevents incorrectly reading trailing data as part of the current POD.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 14:58:56 +09:00
2d7b896a8c internal/pipewire: bounds check against wire size
All checks were successful
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m25s
This covers cases where wire size is not known ahead of time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 13:42:31 +09:00
a0eb010aab internal/pipewire: spa_dict trailing garbage within POD
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m26s
Test / Hpkg (push) Successful in 3m20s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m24s
This performs the check within the bounds of the POD only. This was not caught since spa_dict was only used as the final struct field until now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 13:39:02 +09:00
b1b27ac1df internal/pipewire: zero size before validation
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m21s
Leftover values from previous invocations cause incorrect behaviour here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 12:21:37 +09:00
fc3d78fe01 internal/pipewire: implement Core::Sync
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 2m59s
Test / Flake checks (push) Successful in 1m31s
Once again, already entirely supported, the offset is not yet fully verified but makes intuitive sense. Will verify this on future occurrences of the message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 08:52:06 +09:00
591637264a internal/pipewire: implement Core::GetRegistry
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m28s
This struct is entirely supported, so this change is very straightforward.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 08:34:19 +09:00
e77652bf89 internal/pipewire: move test data to files
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m28s
These get very big later on, and would be painful to represent as the compound literal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 08:09:10 +09:00
88d3e46413 internal/pipewire: implement Client::UpdateProperties
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m29s
This is the second message on the captured sample.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:16:11 +09:00
e51e81bb22 internal/pipewire: implement spa_dict type
All checks were successful
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m18s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m26s
This is a terrible type that defies the type system. It is implemented on the concrete type to avoid special cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:08:52 +09:00
8f4a3bcf9f internal/pipewire: use custom marshaler when available
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 2m19s
Test / Hakurei (push) Successful in 2m27s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Hpkg (push) Successful in 3m31s
Test / Flake checks (push) Successful in 1m34s
This reduces special cases. This change also exposes unmarshalled message size on the wire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:05:22 +09:00
827dc9e1ba internal/pipewire: implement string type
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m39s
This is still NUL terminated strings, and an extra NUL character on an 8-byte string does cause an extra 7 bytes of padding.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:00:59 +09:00
d92de1c709 internal/pipewire: check for trailing garbage
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m28s
This is useful during development.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 01:59:29 +09:00
5bcafcf734 internal/pipewire: implement Core::Hello
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m28s
This implements enough types to correctly marshal and unmarshal Core::Hello.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 01:20:30 +09:00
9f7b0c2f46 internal/pipewire: add type constants
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Hakurei (push) Successful in 3m18s
Test / Sandbox (push) Successful in 1m29s
Test / Sandbox (race detector) (push) Successful in 2m27s
Test / Hpkg (push) Successful in 3m22s
Test / Flake checks (push) Successful in 1m28s
This change also centralises encoding testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-24 22:00:09 +09:00
3e87187c4c internal/pipewire: implement message header
All checks were successful
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m41s
Test / Hpkg (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hakurei (race detector) (push) Successful in 5m30s
Test / Flake checks (push) Successful in 1m25s
Test cases are from interactions between pw-container and PipeWire. Results are validated against corresponding body.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-23 16:20:35 +09:00
b651d95e77 workflows: do not duplicate on pulls
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m32s
This condition causes two runs to be created on a pull, as gitea does not check whether a run has already been created for the current commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-19 06:59:32 +09:00
aab92ce3c1 internal/wayland: clean up pathname socket
All checks were successful
Test / Hakurei (push) Successful in 10m33s
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 1m32s
Test / Hpkg (push) Successful in 3m24s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m36s
This is cleaner than cleaning up in internal/system as it covers the failure paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-19 06:37:04 +09:00
a495e09a8f internal/wayland: do not double close fd
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m29s
These are already closed during securityContextBindPipe on a non-nil error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-17 22:03:29 +09:00
3afca2bd5b internal/wayland: expose WAYLAND_VERSION
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m31s
This might be useful troubleshooting information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-17 01:46:01 +09:00
97 changed files with 7634 additions and 283 deletions

View File

@@ -2,7 +2,6 @@ name: Test
on:
- push
- pull_request
jobs:
hakurei:

View File

@@ -14,6 +14,7 @@ import (
_ "unsafe" // for go:linkname
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
@@ -91,7 +92,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 +147,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{
@@ -186,6 +187,14 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}})
}
// start pipewire-pulse: this most likely exists on host if PipeWire is available
if flagPulse {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSDaemon{
Target: fhs.AbsRunUser.Append(strconv.Itoa(container.OverflowUid(msg)), "pulse/native"),
Exec: shell, Args: []string{"-lc", "exec pipewire-pulse"},
}})
}
config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
@@ -297,8 +306,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")
}
{

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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;

View File

@@ -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"))

View File

@@ -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 }

View File

@@ -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)

View File

@@ -759,7 +759,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()

View File

@@ -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

View File

@@ -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: &params.Params, Msg: msg}
ctx, cancel := context.WithCancel(context.Background())
state := &setupState{process: make(map[int]WaitStatus), Params: &params.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) }()

View File

@@ -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{}, nil, 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,10 +2071,10 @@ 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("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock
@@ -2162,10 +2171,10 @@ 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("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock
@@ -2262,10 +2271,10 @@ 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("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
@@ -2353,10 +2362,10 @@ 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("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2448,10 +2457,10 @@ 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("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2586,10 +2595,10 @@ 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("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2728,10 +2737,10 @@ 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("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),

View File

@@ -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
View 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) }

View 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"`},
})
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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() &&

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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() &&

View File

@@ -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)

3
dist/comp/_hakurei vendored
View File

@@ -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]' \

View File

@@ -23,9 +23,20 @@ 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 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, this 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 +60,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 +109,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
}

View File

@@ -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,

View File

@@ -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:
@@ -62,10 +66,11 @@ type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct {
Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"`
DBus bool `json:"dbus,omitempty"`
Pulse bool `json:"pulse,omitempty"`
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"`
}
// Unwrap returns the underlying [Enablement].
@@ -81,10 +86,11 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{
Wayland: Enablement(*e)&EWayland != 0,
X11: Enablement(*e)&EX11 != 0,
DBus: Enablement(*e)&EDBus != 0,
Pulse: Enablement(*e)&EPulse != 0,
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
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
View 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
View 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`},
})
}

View File

@@ -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},

View File

@@ -105,7 +105,7 @@ func TestTemplate(t *testing.T) {
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
"pipewire": true
},
"session_bus": {
"see": null,

View File

@@ -11,10 +11,22 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/env"
"hakurei.app/internal/info"
"hakurei.app/internal/system"
"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
@@ -160,6 +172,8 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland 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,7 +187,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,
directWayland: config.DirectWayland, directPulse: config.DirectPulse,
extraPerms: config.ExtraPerms,
sessionBus: config.SessionBus, systemBus: config.SystemBus,
sys: sys, outcomeState: s,
}
@@ -280,6 +295,7 @@ func (state *outcomeStateSys) toSystem() error {
// optional via enablements
&spWaylandOp{},
&spX11Op{},
spPipeWireOp{},
&spPulseOp{},
&spDBusOp{},

View File

@@ -27,7 +27,7 @@ import (
"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,8 @@ 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")).
// spDBusOp
MustProxyDBus(
@@ -106,8 +96,7 @@ 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",
"PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
@@ -144,7 +133,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,9 +146,8 @@ 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)).
// spPipeWireOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"), m("/run/user/1971/pipewire-0"), 0).
// spDBusOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
@@ -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")).
@@ -298,7 +286,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 +335,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")).
MustProxyDBus(&hst.BusConfig{
Talk: []string{
"org.freedesktop.Notifications",
@@ -397,8 +382,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 +397,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).
@@ -440,7 +423,7 @@ func TestOutcomeMain(t *testing.T) {
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
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 +485,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")).
MustProxyDBus(&hst.BusConfig{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
@@ -544,8 +526,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 +540,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).

View File

@@ -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

View File

@@ -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)}
}

View File

@@ -0,0 +1,31 @@
package outcome
import (
"encoding/gob"
"hakurei.app/hst"
"hakurei.app/internal/pipewire"
)
func init() { gob.Register(spPipeWireOp{}) }
// spPipeWireOp exports the PipeWire server to the container via SecurityContext.
// Runs after spRuntimeOp.
type spPipeWireOp struct{}
func (s spPipeWireOp) toSystem(state *outcomeStateSys) error {
if state.et&hst.EPipeWire == 0 {
return errNotEnabled
}
state.sys.PipeWire(state.instance().Append("pipewire"))
return nil
}
func (s spPipeWireOp) toContainer(state *outcomeStateParams) error {
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
state.env[pipewire.Remote] = innerPath.String()
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
return nil
}

View File

@@ -0,0 +1,43 @@
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 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 spPipeWireOp{}
}, hst.Template, nil, []stub.Call{}, newI().
// state.instance
Ephemeral(system.Process, m(wantInstancePrefix), 0711).
// toSystem
PipeWire(
m(wantInstancePrefix + "/pipewire"),
), 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},
})
}

View File

@@ -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
}

View File

@@ -18,24 +18,40 @@ import (
func TestSpPulseOp(t *testing.T) {
t.Parallel()
config := hst.Template()
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),

View File

@@ -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)

View File

@@ -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",

127
internal/pipewire/client.go Normal file
View File

@@ -0,0 +1,127 @@
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"`
}
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:
return &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 }

View 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)
}

735
internal/pipewire/core.go Normal file
View File

@@ -0,0 +1,735 @@
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 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("attempted to store bound props on 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 { return ErrBadBoundProps }
// 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) }
// coreHello queues a [CoreHello] message for the PipeWire server.
// This method should not be called directly, the New function queues this message.
func (ctx *Context) coreHello() error {
return 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) }
// coreSync queues a [CoreSync] message for the PipeWire server.
// This is not safe to use directly, callers should use Sync instead.
func (ctx *Context) coreSync(id Int) error {
return ctx.writeMessage(
PW_ID_CORE,
&CoreSync{id, CoreSyncSequenceOffset + Int(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.ctx.coreSync(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()}
}
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},
)
}
// 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) }
// 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,
)
}
// 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) }
// 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
}
// ErrUnexpectedDone is a [CoreDone] event with unexpected values.
var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync")
// An UnknownBoundIdError describes the server claiming to have bound a proxy id that was never allocated.
type UnknownBoundIdError[E any] struct {
// Offending id decoded from Data.
Id Int
// Event received from the server.
Event E
}
func (e *UnknownBoundIdError[E]) Error() string {
return "unknown bound 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_BOUND_PROPS:
var boundProps CoreBoundProps
unmarshal(&boundProps)
delete(core.ctx.pendingIds, boundProps.ID)
proxy, ok := core.ctx.proxy[boundProps.ID]
if !ok {
return &UnknownBoundIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
}
return proxy.setBoundProps(&boundProps)
default:
return &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
}
// 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
}
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
default:
return &UnsupportedOpcodeError{opcode, registry.String()}
}
}
func (registry *Registry) String() string { return PW_TYPE_INTERFACE_Registry }

View File

@@ -0,0 +1,759 @@
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 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 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 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)
}

View 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
}

View 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)
}
})
}

View File

@@ -0,0 +1,851 @@
// 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"
"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
// Current server-side [Header.Sequence] value, incremented on every event processed.
remoteSequence 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
// Server side registry generation number.
generation Long
// Pending file descriptors to be sent with the next message.
pendingFiles []int
// File count already kept track of in [Header].
headerFiles 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
// 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
// Proxy for built-in core events.
core Core
// Proxy for built-in client events.
client Client
// 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))
if err := ctx.coreHello(); 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
// 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 [Context.Roundtrip].
type UnacknowledgedProxyError []Int
func (e UnacknowledgedProxyError) Error() string {
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxies"
}
// 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 from eventProxy.consume.
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
}
}
// 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)
}

View File

@@ -0,0 +1,875 @@
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)
}
if err := ctx.GetCore().Sync(); err != nil {
t.Fatalf("Sync: 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"},
{"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"},
}
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)
}
})
}
}

621
internal/pipewire/pod.go Normal file
View File

@@ -0,0 +1,621 @@
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 len(data) < SizePrefix {
return ErrEOFPrefix
}
switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) {
case SPA_TYPE_None:
v.SetZero()
return nil
default:
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()}
}
}
// 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
}

View 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)
}
}

View File

@@ -0,0 +1,208 @@
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
}
// 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) error {
// queued in reverse based on upstream behaviour, unsure why
offset := securityContext.ctx.queueFiles(closeFd, listenFd)
return securityContext.ctx.writeMessage(
securityContext.ID,
&SecurityContextCreate{ListenFd: offset + 1, CloseFd: offset + 0, Properties: &props},
)
}
// 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) {
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 {
closeReceivedFiles(files...)
switch opcode {
// SecurityContext does not receive any events
default:
return &UnsupportedOpcodeError{opcode, securityContext.String()}
}
}
func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) error {
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
}
func (securityContext *SecurityContext) String() string { return PW_TYPE_INTERFACE_SecurityContext }

View File

@@ -0,0 +1,21 @@
package pipewire_test
import (
"testing"
"hakurei.app/internal/pipewire"
)
func TestSecurityContextCreate(t *testing.T) {
t.Parallel()
encodingTestCases[pipewire.SecurityContextCreate, *pipewire.SecurityContextCreate]{
{"sample", samplePWContainer[6][0][1], pipewire.SecurityContextCreate{
ListenFd: 1 /* 21: duplicated from listen_fd */, CloseFd: 0, /* 20: duplicated from close_fd */
Properties: &pipewire.SPADict{
{Key: pipewire.PW_KEY_SEC_ENGINE, Value: "org.flatpak"},
{Key: pipewire.PW_KEY_ACCESS, Value: "restricted"},
},
}, nil},
}.run(t)
}

837
internal/pipewire/spa.go Normal file
View File

@@ -0,0 +1,837 @@
package pipewire
import (
"encoding/binary"
"fmt"
)
// A SPAKind describes the kind of data being encoded right after it.
//
// These do not always follow the same rules, and encoding/decoding
// is very much context-dependent. Callers should therefore not
// attempt to use these values directly and rely on [Marshal] and
// [Unmarshal] and their variants instead.
type SPAKind Word
/* Basic types */
const (
/* POD's can contain a number of basic SPA types: */
SPA_TYPE_START SPAKind = 0x00000 + iota
SPA_TYPE_None // No value or a NULL pointer.
SPA_TYPE_Bool // A boolean value.
SPA_TYPE_Id // An enumerated value.
SPA_TYPE_Int // An integer value, 32-bit.
SPA_TYPE_Long // An integer value, 64-bit.
SPA_TYPE_Float // A floating point value, 32-bit.
SPA_TYPE_Double // A floating point value, 64-bit.
SPA_TYPE_String // A string.
SPA_TYPE_Bytes // A byte array.
SPA_TYPE_Rectangle // A rectangle with width and height.
SPA_TYPE_Fraction // A fraction with numerator and denominator.
SPA_TYPE_Bitmap // An array of bits.
/* POD's can be grouped together in these container types: */
SPA_TYPE_Array // An array of equal sized objects.
SPA_TYPE_Struct // A collection of types and objects.
SPA_TYPE_Object // An object with properties.
SPA_TYPE_Sequence // A timed sequence of POD's.
/* POD's can also contain some extra types: */
SPA_TYPE_Pointer // A typed pointer in memory.
SPA_TYPE_Fd // A file descriptor.
SPA_TYPE_Choice // A choice of values.
SPA_TYPE_Pod // A generic type for the POD itself.
_SPA_TYPE_LAST // not part of ABI
)
// append appends the representation of [SPAKind] to data and returns the appended slice.
func (kind SPAKind) append(data []byte) []byte {
return binary.NativeEndian.AppendUint32(data, Word(kind))
}
// String returns the name of the [SPAKind] for basic types.
func (kind SPAKind) String() string {
switch kind {
case SPA_TYPE_None:
return "None"
case SPA_TYPE_Bool:
return "Bool"
case SPA_TYPE_Id:
return "Id"
case SPA_TYPE_Int:
return "Int"
case SPA_TYPE_Long:
return "Long"
case SPA_TYPE_Float:
return "Float"
case SPA_TYPE_Double:
return "Double"
case SPA_TYPE_String:
return "String"
case SPA_TYPE_Bytes:
return "Bytes"
case SPA_TYPE_Rectangle:
return "Rectangle"
case SPA_TYPE_Fraction:
return "Fraction"
case SPA_TYPE_Bitmap:
return "Bitmap"
case SPA_TYPE_Array:
return "Array"
case SPA_TYPE_Struct:
return "Struct"
case SPA_TYPE_Object:
return "Object"
case SPA_TYPE_Sequence:
return "Sequence"
case SPA_TYPE_Pointer:
return "Pointer"
case SPA_TYPE_Fd:
return "Fd"
case SPA_TYPE_Choice:
return "Choice"
case SPA_TYPE_Pod:
return "Pod"
default:
return fmt.Sprintf("invalid type field %#x", Word(kind))
}
}
/* Pointers */
const (
SPA_TYPE_POINTER_START = 0x10000 + iota
SPA_TYPE_POINTER_Buffer
SPA_TYPE_POINTER_Meta
SPA_TYPE_POINTER_Dict
_SPA_TYPE_POINTER_LAST // not part of ABI
)
/* Events */
const (
SPA_TYPE_EVENT_START = 0x20000 + iota
SPA_TYPE_EVENT_Device
SPA_TYPE_EVENT_Node
_SPA_TYPE_EVENT_LAST // not part of ABI
)
/* Commands */
const (
SPA_TYPE_COMMAND_START = 0x30000 + iota
SPA_TYPE_COMMAND_Device
SPA_TYPE_COMMAND_Node
_SPA_TYPE_COMMAND_LAST // not part of ABI
)
/* Objects */
const (
SPA_TYPE_OBJECT_START = 0x40000 + iota
SPA_TYPE_OBJECT_PropInfo
SPA_TYPE_OBJECT_Props
SPA_TYPE_OBJECT_Format
SPA_TYPE_OBJECT_ParamBuffers
SPA_TYPE_OBJECT_ParamMeta
SPA_TYPE_OBJECT_ParamIO
SPA_TYPE_OBJECT_ParamProfile
SPA_TYPE_OBJECT_ParamPortConfig
SPA_TYPE_OBJECT_ParamRoute
SPA_TYPE_OBJECT_Profiler
SPA_TYPE_OBJECT_ParamLatency
SPA_TYPE_OBJECT_ParamProcessLatency
SPA_TYPE_OBJECT_ParamTag
_SPA_TYPE_OBJECT_LAST // not part of ABI
)
/* vendor extensions */
const (
SPA_TYPE_VENDOR_PipeWire = 0x02000000
SPA_TYPE_VENDOR_Other = 0x7f000000
)
/* spa/include/spa/utils/type.h */
const (
SPA_TYPE_INFO_BASE = "Spa:"
SPA_TYPE_INFO_Flags = SPA_TYPE_INFO_BASE + "Flags"
SPA_TYPE_INFO_FLAGS_BASE = SPA_TYPE_INFO_Flags + ":"
SPA_TYPE_INFO_Enum = SPA_TYPE_INFO_BASE + "Enum"
SPA_TYPE_INFO_ENUM_BASE = SPA_TYPE_INFO_Enum + ":"
SPA_TYPE_INFO_Pod = SPA_TYPE_INFO_BASE + "Pod"
SPA_TYPE_INFO_POD_BASE = SPA_TYPE_INFO_Pod + ":"
SPA_TYPE_INFO_Struct = SPA_TYPE_INFO_POD_BASE + "Struct"
SPA_TYPE_INFO_STRUCT_BASE = SPA_TYPE_INFO_Struct + ":"
SPA_TYPE_INFO_Object = SPA_TYPE_INFO_POD_BASE + "Object"
SPA_TYPE_INFO_OBJECT_BASE = SPA_TYPE_INFO_Object + ":"
SPA_TYPE_INFO_Pointer = SPA_TYPE_INFO_BASE + "Pointer"
SPA_TYPE_INFO_POINTER_BASE = SPA_TYPE_INFO_Pointer + ":"
SPA_TYPE_INFO_Interface = SPA_TYPE_INFO_POINTER_BASE + "Interface"
SPA_TYPE_INFO_INTERFACE_BASE = SPA_TYPE_INFO_Interface + ":"
SPA_TYPE_INFO_Event = SPA_TYPE_INFO_OBJECT_BASE + "Event"
SPA_TYPE_INFO_EVENT_BASE = SPA_TYPE_INFO_Event + ":"
SPA_TYPE_INFO_Command = SPA_TYPE_INFO_OBJECT_BASE + "Command"
SPA_TYPE_INFO_COMMAND_BASE = SPA_TYPE_INFO_Command + ":"
)
/* pipewire/device.h */
const (
PW_TYPE_INTERFACE_Device = PW_TYPE_INFO_INTERFACE_BASE + "Device"
PW_DEVICE_PERM_MASK = PW_PERM_RWXM
PW_VERSION_DEVICE = 3
)
const (
PW_DEVICE_CHANGE_MASK_PROPS = 1 << iota
PW_DEVICE_CHANGE_MASK_PARAMS
PW_DEVICE_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_DEVICE_EVENT_INFO = iota
PW_DEVICE_EVENT_PARAM
PW_DEVICE_EVENT_NUM
PW_VERSION_DEVICE_EVENTS = 0
)
const (
PW_DEVICE_METHOD_ADD_LISTENER = iota
PW_DEVICE_METHOD_SUBSCRIBE_PARAMS
PW_DEVICE_METHOD_ENUM_PARAMS
PW_DEVICE_METHOD_SET_PARAM
PW_DEVICE_METHOD_NUM
PW_VERSION_DEVICE_METHODS = 0
)
/* pipewire/factory.h */
const (
PW_TYPE_INTERFACE_Factory = PW_TYPE_INFO_INTERFACE_BASE + "Factory"
PW_FACTORY_PERM_MASK = PW_PERM_R | PW_PERM_M
PW_VERSION_FACTORY = 3
)
const (
PW_FACTORY_CHANGE_MASK_PROPS = 1 << iota
PW_FACTORY_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_FACTORY_EVENT_INFO = iota
PW_FACTORY_EVENT_NUM
PW_VERSION_FACTORY_EVENTS = 0
)
const (
PW_FACTORY_METHOD_ADD_LISTENER = iota
PW_FACTORY_METHOD_NUM
PW_VERSION_FACTORY_METHODS = 0
)
/* pipewire/link.h */
const (
PW_TYPE_INTERFACE_Link = PW_TYPE_INFO_INTERFACE_BASE + "Link"
PW_LINK_PERM_MASK = PW_PERM_R | PW_PERM_X
PW_VERSION_LINK = 3
)
const (
PW_LINK_STATE_ERROR = iota - 2 // the link is in error
PW_LINK_STATE_UNLINKED // the link is unlinked
PW_LINK_STATE_INIT // the link is initialized
PW_LINK_STATE_NEGOTIATING // the link is negotiating formats
PW_LINK_STATE_ALLOCATING // the link is allocating buffers
PW_LINK_STATE_PAUSED // the link is paused
PW_LINK_STATE_ACTIVE // the link is active
)
const (
PW_LINK_CHANGE_MASK_STATE = (1 << iota)
PW_LINK_CHANGE_MASK_FORMAT
PW_LINK_CHANGE_MASK_PROPS
PW_LINK_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_LINK_EVENT_INFO = iota
PW_LINK_EVENT_NUM
PW_VERSION_LINK_EVENTS = 0
)
const (
PW_LINK_METHOD_ADD_LISTENER = iota
PW_LINK_METHOD_NUM
PW_VERSION_LINK_METHODS = 0
)
/* pipewire/module.h */
const (
PW_TYPE_INTERFACE_Module = PW_TYPE_INFO_INTERFACE_BASE + "Module"
PW_MODULE_PERM_MASK = PW_PERM_R | PW_PERM_M
PW_VERSION_MODULE = 3
)
const (
PW_MODULE_CHANGE_MASK_PROPS = 1 << iota
PW_MODULE_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_MODULE_EVENT_INFO = iota
PW_MODULE_EVENT_NUM
PW_VERSION_MODULE_EVENTS = 0
)
const (
PW_MODULE_METHOD_ADD_LISTENER = iota
PW_MODULE_METHOD_NUM
PW_VERSION_MODULE_METHODS = 0
)
/* pipewire/impl-module.h */
const (
PIPEWIRE_SYMBOL_MODULE_INIT = "pipewire__module_init"
PIPEWIRE_MODULE_PREFIX = "libpipewire-"
PW_VERSION_IMPL_MODULE_EVENTS = 0
)
/* pipewire/node.h */
const (
PW_TYPE_INTERFACE_Node = PW_TYPE_INFO_INTERFACE_BASE + "Node"
PW_NODE_PERM_MASK = PW_PERM_RWXML
PW_VERSION_NODE = 3
)
const (
PW_NODE_STATE_ERROR = iota - 1 // error state
PW_NODE_STATE_CREATING // the node is being created
PW_NODE_STATE_SUSPENDED // the node is suspended, the device might be closed
PW_NODE_STATE_IDLE // the node is running but there is no active port
PW_NODE_STATE_RUNNING // the node is running
)
const (
PW_NODE_CHANGE_MASK_INPUT_PORTS = 1 << iota
PW_NODE_CHANGE_MASK_OUTPUT_PORTS
PW_NODE_CHANGE_MASK_STATE
PW_NODE_CHANGE_MASK_PROPS
PW_NODE_CHANGE_MASK_PARAMS
PW_NODE_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_NODE_EVENT_INFO = iota
PW_NODE_EVENT_PARAM
PW_NODE_EVENT_NUM
PW_VERSION_NODE_EVENTS = 0
)
const (
PW_NODE_METHOD_ADD_LISTENER = iota
PW_NODE_METHOD_SUBSCRIBE_PARAMS
PW_NODE_METHOD_ENUM_PARAMS
PW_NODE_METHOD_SET_PARAM
PW_NODE_METHOD_SEND_COMMAND
PW_NODE_METHOD_NUM
PW_VERSION_NODE_METHODS = 0
)
/* pipewire/permission.h */
const (
PW_PERM_R = 0400 // object can be seen and events can be received
PW_PERM_W = 0200 // methods can be called that modify the object
PW_PERM_X = 0100 // methods can be called on the object. The W flag must be present in order to call methods that modify the object.
PW_PERM_M = 0010 // metadata can be set on object, Since 0.3.9
PW_PERM_L = 0020 // a link can be made between a node that doesn't have permission to see the other node, Since 0.3.77
PW_PERM_RW = PW_PERM_R | PW_PERM_W
PW_PERM_RWX = PW_PERM_RW | PW_PERM_X
PW_PERM_RWXM = PW_PERM_RWX | PW_PERM_M
PW_PERM_RWXML = PW_PERM_RWXM | PW_PERM_L
PW_PERM_ALL = PW_PERM_RWXM
PW_PERM_INVALID Word = 0xffffffff
)
/* pipewire/port.h */
const (
PW_TYPE_INTERFACE_Port = PW_TYPE_INFO_INTERFACE_BASE + "Port"
PW_PORT_PERM_MASK = PW_PERM_R | PW_PERM_X | PW_PERM_M
PW_VERSION_PORT = 3
)
const (
PW_PORT_CHANGE_MASK_PROPS = 1 << iota
PW_PORT_CHANGE_MASK_PARAMS
PW_PORT_CHANGE_MASK_ALL = 1<<iota - 1
)
const (
PW_PORT_EVENT_INFO = iota
PW_PORT_EVENT_PARAM
PW_PORT_EVENT_NUM
PW_VERSION_PORT_EVENTS = 0
)
const (
PW_PORT_METHOD_ADD_LISTENER = iota
PW_PORT_METHOD_SUBSCRIBE_PARAMS
PW_PORT_METHOD_ENUM_PARAMS
PW_PORT_METHOD_NUM
PW_VERSION_PORT_METHODS = 0
)
/* pipewire/extensions/client-node.h */
const (
PW_TYPE_INTERFACE_ClientNode = PW_TYPE_INFO_INTERFACE_BASE + "ClientNode"
PW_VERSION_CLIENT_NODE = 6
PW_EXTENSION_MODULE_CLIENT_NODE = PIPEWIRE_MODULE_PREFIX + "module-client-node"
)
const (
PW_CLIENT_NODE_EVENT_TRANSPORT = iota
PW_CLIENT_NODE_EVENT_SET_PARAM
PW_CLIENT_NODE_EVENT_SET_IO
PW_CLIENT_NODE_EVENT_EVENT
PW_CLIENT_NODE_EVENT_COMMAND
PW_CLIENT_NODE_EVENT_ADD_PORT
PW_CLIENT_NODE_EVENT_REMOVE_PORT
PW_CLIENT_NODE_EVENT_PORT_SET_PARAM
PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS
PW_CLIENT_NODE_EVENT_PORT_SET_IO
PW_CLIENT_NODE_EVENT_SET_ACTIVATION
PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO
PW_CLIENT_NODE_EVENT_NUM
PW_VERSION_CLIENT_NODE_EVENTS = 1
)
const (
PW_CLIENT_NODE_METHOD_ADD_LISTENER = iota
PW_CLIENT_NODE_METHOD_GET_NODE
PW_CLIENT_NODE_METHOD_UPDATE
PW_CLIENT_NODE_METHOD_PORT_UPDATE
PW_CLIENT_NODE_METHOD_SET_ACTIVE
PW_CLIENT_NODE_METHOD_EVENT
PW_CLIENT_NODE_METHOD_PORT_BUFFERS
PW_CLIENT_NODE_METHOD_NUM
PW_VERSION_CLIENT_NODE_METHODS = 0
)
const (
PW_CLIENT_NODE_UPDATE_PARAMS = 1 << iota
PW_CLIENT_NODE_UPDATE_INFO
)
const (
PW_CLIENT_NODE_PORT_UPDATE_PARAMS = 1 << iota
PW_CLIENT_NODE_PORT_UPDATE_INFO
)
/* pipewire/extensions/metadata.h */
const (
PW_TYPE_INTERFACE_Metadata = PW_TYPE_INFO_INTERFACE_BASE + "Metadata"
PW_METADATA_PERM_MASK = PW_PERM_RWX
PW_VERSION_METADATA = 3
PW_EXTENSION_MODULE_METADATA = PIPEWIRE_MODULE_PREFIX + "module-metadata"
)
const (
PW_METADATA_EVENT_PROPERTY = iota
PW_METADATA_EVENT_NUM
PW_VERSION_METADATA_EVENTS = 0
)
const (
PW_METADATA_METHOD_ADD_LISTENER = iota
PW_METADATA_METHOD_SET_PROPERTY
PW_METADATA_METHOD_CLEAR
PW_METADATA_METHOD_NUM
PW_VERSION_METADATA_METHODS = 0
)
const (
PW_KEY_METADATA_NAME = "metadata.name"
PW_KEY_METADATA_VALUES = "metadata.values"
)
/* pipewire/extensions/profiler.h */
const (
PW_TYPE_INTERFACE_Profiler = PW_TYPE_INFO_INTERFACE_BASE + "Profiler"
PW_VERSION_PROFILER = 3
PW_PROFILER_PERM_MASK = PW_PERM_R
PW_EXTENSION_MODULE_PROFILER = PIPEWIRE_MODULE_PREFIX + "module-profiler"
)
const (
PW_PROFILER_EVENT_PROFILE = iota
PW_PROFILER_EVENT_NUM
PW_VERSION_PROFILER_EVENTS = 0
)
const (
PW_PROFILER_METHOD_ADD_LISTENER = iota
PW_PROFILER_METHOD_NUM
PW_VERSION_PROFILER_METHODS = 0
)
const (
PW_KEY_PROFILER_NAME = "profiler.name"
)
/* pipewire/type.h */
const (
PW_TYPE_INFO_BASE = "PipeWire:"
PW_TYPE_INFO_Object = PW_TYPE_INFO_BASE + "Object"
PW_TYPE_INFO_OBJECT_BASE = PW_TYPE_INFO_Object + ":"
PW_TYPE_INFO_Interface = PW_TYPE_INFO_BASE + "Interface"
PW_TYPE_INFO_INTERFACE_BASE = PW_TYPE_INFO_Interface + ":"
)
/* pipewire/keys.h */
/**
* Key Names
*
* A collection of keys that are used to add extra information on objects.
*
* Keys that start with "pipewire." are in general set-once and then
* read-only. They are usually used for security sensitive information that
* needs to be fixed.
*
* Properties from other objects can also appear. This usually suggests some
* sort of parent/child or owner/owned relationship.
*
*/
const (
PW_KEY_PROTOCOL = "pipewire.protocol" /* protocol used for connection */
PW_KEY_ACCESS = "pipewire.access" /* how the client access is controlled */
PW_KEY_CLIENT_ACCESS = "pipewire.client.access" /* how the client wants to be access controlled */
/** Various keys related to the identity of a client process and its security.
* Must be obtained from trusted sources by the protocol and placed as
* read-only properties. */
PW_KEY_SEC_PID = "pipewire.sec.pid" /* Client pid, set by protocol */
PW_KEY_SEC_UID = "pipewire.sec.uid" /* Client uid, set by protocol*/
PW_KEY_SEC_GID = "pipewire.sec.gid" /* client gid, set by protocol*/
PW_KEY_SEC_LABEL = "pipewire.sec.label" /* client security label, set by protocol*/
PW_KEY_SEC_SOCKET = "pipewire.sec.socket" /* client socket name, set by protocol */
PW_KEY_SEC_ENGINE = "pipewire.sec.engine" /* client secure context engine, set by protocol. This can also be set by a client when making a new security context. */
PW_KEY_SEC_APP_ID = "pipewire.sec.app-id" /* client secure application id */
PW_KEY_SEC_INSTANCE_ID = "pipewire.sec.instance-id" /* client secure instance id */
PW_KEY_LIBRARY_NAME_SYSTEM = "library.name.system" /* name of the system library to use */
PW_KEY_LIBRARY_NAME_LOOP = "library.name.loop" /* name of the loop library to use */
PW_KEY_LIBRARY_NAME_DBUS = "library.name.dbus" /* name of the dbus library to use */
/** object properties */
PW_KEY_OBJECT_PATH = "object.path" /* unique path to construct the object */
PW_KEY_OBJECT_ID = "object.id" /* a global object id */
PW_KEY_OBJECT_SERIAL = "object.serial" /* a 64 bit object serial number. This is a number incremented for each object that is created. The lower 32 bits are guaranteed to never be SPA_ID_INVALID. */
PW_KEY_OBJECT_LINGER = "object.linger" /* the object lives on even after the client that created it has been destroyed */
PW_KEY_OBJECT_REGISTER = "object.register" /* If the object should be registered. */
PW_KEY_OBJECT_EXPORT = "object.export" /* If the object should be exported, since 0.3.72 */
/* config */
PW_KEY_CONFIG_PREFIX = "config.prefix" /* a config prefix directory */
PW_KEY_CONFIG_NAME = "config.name" /* a config file name */
PW_KEY_CONFIG_OVERRIDE_PREFIX = "config.override.prefix" /* a config override prefix directory */
PW_KEY_CONFIG_OVERRIDE_NAME = "config.override.name" /* a config override file name */
/* loop */
PW_KEY_LOOP_NAME = "loop.name" /* the name of a loop */
PW_KEY_LOOP_CLASS = "loop.class" /* the classes this loop handles, array of strings */
PW_KEY_LOOP_RT_PRIO = "loop.rt-prio" /* realtime priority of the loop */
PW_KEY_LOOP_CANCEL = "loop.cancel" /* if the loop can be canceled */
/* context */
PW_KEY_CONTEXT_PROFILE_MODULES = "context.profile.modules" /* a context profile for modules, deprecated */
PW_KEY_USER_NAME = "context.user-name" /* The user name that runs pipewire */
PW_KEY_HOST_NAME = "context.host-name" /* The host name of the machine */
/* core */
PW_KEY_CORE_NAME = "core.name" /* The name of the core. Default is `pipewire-<username>-<pid>`, overwritten by env(PIPEWIRE_CORE) */
PW_KEY_CORE_VERSION = "core.version" /* The version of the core. */
PW_KEY_CORE_DAEMON = "core.daemon" /* If the core is listening for connections. */
PW_KEY_CORE_ID = "core.id" /* the core id */
PW_KEY_CORE_MONITORS = "core.monitors" /* the apis monitored by core. */
/* cpu */
PW_KEY_CPU_MAX_ALIGN = "cpu.max-align" /* maximum alignment needed to support all CPU optimizations */
PW_KEY_CPU_CORES = "cpu.cores" /* number of cores */
/* priorities */
PW_KEY_PRIORITY_SESSION = "priority.session" /* priority in session manager */
PW_KEY_PRIORITY_DRIVER = "priority.driver" /* priority to be a driver */
/* remote keys */
PW_KEY_REMOTE_NAME = "remote.name" /* The name of the remote to connect to, default pipewire-0, overwritten by env(PIPEWIRE_REMOTE). May also be a SPA-JSON array of sockets, to be tried in order. The "internal" remote name and "generic" intention connects to the local PipeWire instance. */
PW_KEY_REMOTE_INTENTION = "remote.intention" /* The intention of the remote connection, "generic", "screencast", "manager" */
/** application keys */
PW_KEY_APP_NAME = "application.name" /* application name. Ex: "Totem Music Player" */
PW_KEY_APP_ID = "application.id" /* a textual id for identifying an application logically. Ex: "org.gnome.Totem" */
PW_KEY_APP_VERSION = "application.version" /* application version. Ex: "1.2.0" */
PW_KEY_APP_ICON = "application.icon" /* aa base64 blob with PNG image data */
PW_KEY_APP_ICON_NAME = "application.icon-name" /* an XDG icon name for the application. Ex: "totem" */
PW_KEY_APP_LANGUAGE = "application.language" /* application language if applicable, in standard POSIX format. Ex: "en_GB" */
PW_KEY_APP_PROCESS_ID = "application.process.id" /* process id (pid)*/
PW_KEY_APP_PROCESS_BINARY = "application.process.binary" /* binary name */
PW_KEY_APP_PROCESS_USER = "application.process.user" /* user name */
PW_KEY_APP_PROCESS_HOST = "application.process.host" /* host name */
PW_KEY_APP_PROCESS_MACHINE_ID = "application.process.machine-id" /* the D-Bus host id the application runs on */
PW_KEY_APP_PROCESS_SESSION_ID = "application.process.session-id" /* login session of the application, on Unix the value of $XDG_SESSION_ID. */
/** window system */
PW_KEY_WINDOW_X11_DISPLAY = "window.x11.display" /* the X11 display string. Ex. ":0.0" */
/** Client properties */
PW_KEY_CLIENT_ID = "client.id" /* a client id */
PW_KEY_CLIENT_NAME = "client.name" /* the client name */
PW_KEY_CLIENT_API = "client.api" /* the client api used to access PipeWire */
/** Node keys */
PW_KEY_NODE_ID = "node.id" /* node id */
PW_KEY_NODE_NAME = "node.name" /* node name */
PW_KEY_NODE_NICK = "node.nick" /* short node name */
PW_KEY_NODE_DESCRIPTION = "node.description" /* localized human readable node one-line description. Ex. "Foobar USB Headset" */
PW_KEY_NODE_PLUGGED = "node.plugged" /* when the node was created. As a uint64 in nanoseconds. */
PW_KEY_NODE_SESSION = "node.session" /* the session id this node is part of */
PW_KEY_NODE_GROUP = "node.group" /* the group id this node is part of. Nodes in the same group are always scheduled with the same driver. Can be an array of group names. */
PW_KEY_NODE_SYNC_GROUP = "node.sync-group" /* the sync group this node is part of. Nodes in the same sync group are always scheduled together with the same driver when the sync is active. Can be an array of sync names. */
PW_KEY_NODE_SYNC = "node.sync" /* if the sync-group is active or not */
PW_KEY_NODE_TRANSPORT = "node.transport" /* if the transport is active or not */
PW_KEY_NODE_EXCLUSIVE = "node.exclusive" /* node wants exclusive access to resources */
PW_KEY_NODE_AUTOCONNECT = "node.autoconnect" /* node wants to be automatically connected to a compatible node */
PW_KEY_NODE_LATENCY = "node.latency" /* the requested latency of the node as a fraction. Ex: 128/48000 */
PW_KEY_NODE_MAX_LATENCY = "node.max-latency" /* the maximum supported latency of the node as a fraction. Ex: 1024/48000 */
PW_KEY_NODE_LOCK_QUANTUM = "node.lock-quantum" /* don't change quantum when this node is active */
PW_KEY_NODE_FORCE_QUANTUM = "node.force-quantum" /* force a quantum while the node is active */
PW_KEY_NODE_RATE = "node.rate" /* the requested rate of the graph as a fraction. Ex: 1/48000 */
PW_KEY_NODE_LOCK_RATE = "node.lock-rate" /* don't change rate when this node is active */
PW_KEY_NODE_FORCE_RATE = "node.force-rate" /* force a rate while the node is active. A value of 0 takes the denominator of node.rate */
PW_KEY_NODE_DONT_RECONNECT = "node.dont-reconnect" /* don't reconnect this node. The node is initially linked to target.object or the default node. If the target is removed, the node is destroyed */
PW_KEY_NODE_ALWAYS_PROCESS = "node.always-process" /* process even when unlinked */
PW_KEY_NODE_WANT_DRIVER = "node.want-driver" /* the node wants to be grouped with a driver node in order to schedule the graph. */
PW_KEY_NODE_PAUSE_ON_IDLE = "node.pause-on-idle" /* pause the node when idle */
PW_KEY_NODE_SUSPEND_ON_IDLE = "node.suspend-on-idle" /* suspend the node when idle */
PW_KEY_NODE_CACHE_PARAMS = "node.cache-params" /* cache the node params */
PW_KEY_NODE_TRANSPORT_SYNC = "node.transport.sync" /* the node handles transport sync */
PW_KEY_NODE_DRIVER = "node.driver" /* node can drive the graph. When the node is selected as the driver, it needs to start the graph periodically. */
PW_KEY_NODE_SUPPORTS_LAZY = "node.supports-lazy" /* the node can be a lazy driver. It will listen to RequestProcess commands and take them into account when deciding to start the graph. A value of 0 disables support, a value of > 0 enables with increasing preference. */
PW_KEY_NODE_SUPPORTS_REQUEST = "node.supports-request" /* The node supports emiting RequestProcess events when it wants the graph to be scheduled. A value of 0 disables support, a value of > 0 enables with increasing preference. */
PW_KEY_NODE_DRIVER_ID = "node.driver-id" /* the node id of the node assigned as driver for this node */
PW_KEY_NODE_ASYNC = "node.async" /* the node wants async scheduling */
PW_KEY_NODE_LOOP_NAME = "node.loop.name" /* the loop name fnmatch pattern to run in */
PW_KEY_NODE_LOOP_CLASS = "node.loop.class" /* the loop class fnmatch pattern to run in */
PW_KEY_NODE_STREAM = "node.stream" /* node is a stream, the server side should add a converter */
PW_KEY_NODE_VIRTUAL = "node.virtual" /* the node is some sort of virtual object */
PW_KEY_NODE_PASSIVE = "node.passive" /* indicate that a node wants passive links on output/input/all ports when the value is "out"/"in"/"true" respectively */
PW_KEY_NODE_LINK_GROUP = "node.link-group" /* the node is internally linked to nodes with the same link-group. Can be an array of group names. */
PW_KEY_NODE_NETWORK = "node.network" /* the node is on a network */
PW_KEY_NODE_TRIGGER = "node.trigger" /* the node is not scheduled automatically based on the dependencies in the graph but it will be triggered explicitly. */
PW_KEY_NODE_CHANNELNAMES = "node.channel-names" /* names of node's channels (unrelated to positions) */
PW_KEY_NODE_DEVICE_PORT_NAME_PREFIX = "node.device-port-name-prefix" /* override port name prefix for device ports, like capture and playback or disable the prefix completely if an empty string is provided */
PW_KEY_NODE_PHYSICAL = "node.physical" /* ports from the node are physical */
PW_KEY_NODE_TERMINAL = "node.terminal" /* ports from the node are terminal */
PW_KEY_NODE_RELIABLE = "node.reliable" /* node uses reliable transport 1.6.0 */
/** Port keys */
PW_KEY_PORT_ID = "port.id" /* port id */
PW_KEY_PORT_NAME = "port.name" /* port name */
PW_KEY_PORT_DIRECTION = "port.direction" /* the port direction, one of "in" or "out" or "control" and "notify" for control ports */
PW_KEY_PORT_ALIAS = "port.alias" /* port alias */
PW_KEY_PORT_PHYSICAL = "port.physical" /* if this is a physical port */
PW_KEY_PORT_TERMINAL = "port.terminal" /* if this port consumes the data */
PW_KEY_PORT_CONTROL = "port.control" /* if this port is a control port */
PW_KEY_PORT_MONITOR = "port.monitor" /* if this port is a monitor port */
PW_KEY_PORT_CACHE_PARAMS = "port.cache-params" /* cache the node port params */
PW_KEY_PORT_EXTRA = "port.extra" /* api specific extra port info, API name should be prefixed. "jack:flags:56" */
PW_KEY_PORT_PASSIVE = "port.passive" /* the ports wants passive links, since 0.3.67 */
PW_KEY_PORT_IGNORE_LATENCY = "port.ignore-latency" /* latency ignored by peers, since 0.3.71 */
PW_KEY_PORT_GROUP = "port.group" /* the port group of the port 1.2.0 */
PW_KEY_PORT_EXCLUSIVE = "port.exclusive" /* link port only once 1.6.0 */
PW_KEY_PORT_RELIABLE = "port.reliable" /* port uses reliable transport 1.6.0 */
/** link properties */
PW_KEY_LINK_ID = "link.id" /* a link id */
PW_KEY_LINK_INPUT_NODE = "link.input.node" /* input node id of a link */
PW_KEY_LINK_INPUT_PORT = "link.input.port" /* input port id of a link */
PW_KEY_LINK_OUTPUT_NODE = "link.output.node" /* output node id of a link */
PW_KEY_LINK_OUTPUT_PORT = "link.output.port" /* output port id of a link */
PW_KEY_LINK_PASSIVE = "link.passive" /* indicate that a link is passive and does not cause the graph to be runnable. */
PW_KEY_LINK_FEEDBACK = "link.feedback" /* indicate that a link is a feedback link and the target will receive data in the next cycle */
PW_KEY_LINK_ASYNC = "link.async" /* the link is using async io */
/** device properties */
PW_KEY_DEVICE_ID = "device.id" /* device id */
PW_KEY_DEVICE_NAME = "device.name" /* device name */
PW_KEY_DEVICE_PLUGGED = "device.plugged" /* when the device was created. As a uint64 in nanoseconds. */
PW_KEY_DEVICE_NICK = "device.nick" /* a short device nickname */
PW_KEY_DEVICE_STRING = "device.string" /* device string in the underlying layer's format. Ex. "surround51:0" */
PW_KEY_DEVICE_API = "device.api" /* API this device is accessed with. Ex. "alsa", "v4l2" */
PW_KEY_DEVICE_DESCRIPTION = "device.description" /* localized human readable device one-line description. Ex. "Foobar USB Headset" */
PW_KEY_DEVICE_BUS_PATH = "device.bus-path" /* bus path to the device in the OS' format. Ex. "pci-0000:00:14.0-usb-0:3.2:1.0" */
PW_KEY_DEVICE_SERIAL = "device.serial" /* Serial number if applicable */
PW_KEY_DEVICE_VENDOR_ID = "device.vendor.id" /* vendor ID if applicable */
PW_KEY_DEVICE_VENDOR_NAME = "device.vendor.name" /* vendor name if applicable */
PW_KEY_DEVICE_PRODUCT_ID = "device.product.id" /* product ID if applicable */
PW_KEY_DEVICE_PRODUCT_NAME = "device.product.name" /* product name if applicable */
PW_KEY_DEVICE_CLASS = "device.class" /* device class */
PW_KEY_DEVICE_FORM_FACTOR = "device.form-factor" /* form factor if applicable. One of "internal", "speaker", "handset", "tv", "webcam", "microphone", "headset", "headphone", "hands-free", "car", "hifi", "computer", "portable" */
PW_KEY_DEVICE_BUS = "device.bus" /* bus of the device if applicable. One of "isa", "pci", "usb", "firewire", "bluetooth" */
PW_KEY_DEVICE_SUBSYSTEM = "device.subsystem" /* device subsystem */
PW_KEY_DEVICE_SYSFS_PATH = "device.sysfs.path" /* device sysfs path */
PW_KEY_DEVICE_ICON = "device.icon" /* icon for the device. A base64 blob containing PNG image data */
PW_KEY_DEVICE_ICON_NAME = "device.icon-name" /* an XDG icon name for the device. Ex. "sound-card-speakers-usb" */
PW_KEY_DEVICE_INTENDED_ROLES = "device.intended-roles" /* intended use. A space separated list of roles (see PW_KEY_MEDIA_ROLE) this device is particularly well suited for, due to latency, quality or form factor. */
PW_KEY_DEVICE_CACHE_PARAMS = "device.cache-params" /* cache the device spa params */
/** module properties */
PW_KEY_MODULE_ID = "module.id" /* the module id */
PW_KEY_MODULE_NAME = "module.name" /* the name of the module */
PW_KEY_MODULE_AUTHOR = "module.author" /* the author's name */
PW_KEY_MODULE_DESCRIPTION = "module.description" /* a human readable one-line description of the module's purpose.*/
PW_KEY_MODULE_USAGE = "module.usage" /* a human readable usage description of the module's arguments. */
PW_KEY_MODULE_VERSION = "module.version" /* a version string for the module. */
PW_KEY_MODULE_DEPRECATED = "module.deprecated" /* the module is deprecated with this message */
/** Factory properties */
PW_KEY_FACTORY_ID = "factory.id" /* the factory id */
PW_KEY_FACTORY_NAME = "factory.name" /* the name of the factory */
PW_KEY_FACTORY_USAGE = "factory.usage" /* the usage of the factory */
PW_KEY_FACTORY_TYPE_NAME = "factory.type.name" /* the name of the type created by a factory */
PW_KEY_FACTORY_TYPE_VERSION = "factory.type.version" /* the version of the type created by a factory */
/** Stream properties */
PW_KEY_STREAM_IS_LIVE = "stream.is-live" /* Indicates that the stream is live. */
PW_KEY_STREAM_LATENCY_MIN = "stream.latency.min" /* The minimum latency of the stream. */
PW_KEY_STREAM_LATENCY_MAX = "stream.latency.max" /* The maximum latency of the stream */
PW_KEY_STREAM_MONITOR = "stream.monitor" /* Indicates that the stream is monitoring and might select a less accurate but faster conversion algorithm. Monitor streams are also ignored when calculating the latency of their peer ports (since 0.3.71).
*/
PW_KEY_STREAM_DONT_REMIX = "stream.dont-remix" /* don't remix channels */
PW_KEY_STREAM_CAPTURE_SINK = "stream.capture.sink" /* Try to capture the sink output instead of source output */
/** Media */
PW_KEY_MEDIA_TYPE = "media.type" /* Media type, one of Audio, Video, Midi */
PW_KEY_MEDIA_CATEGORY = "media.category" /* Media Category: Playback, Capture, Duplex, Monitor, Manager */
PW_KEY_MEDIA_ROLE = "media.role" /* Role: Movie, Music, Camera, Screen, Communication, Game, Notification, DSP, Production, Accessibility, Test */
PW_KEY_MEDIA_CLASS = "media.class" /* class Ex: "Video/Source" */
PW_KEY_MEDIA_NAME = "media.name" /* media name. Ex: "Pink Floyd: Time" */
PW_KEY_MEDIA_TITLE = "media.title" /* title. Ex: "Time" */
PW_KEY_MEDIA_ARTIST = "media.artist" /* artist. Ex: "Pink Floyd" */
PW_KEY_MEDIA_ALBUM = "media.album" /* album. Ex: "Dark Side of the Moon" */
PW_KEY_MEDIA_COPYRIGHT = "media.copyright" /* copyright string */
PW_KEY_MEDIA_SOFTWARE = "media.software" /* generator software */
PW_KEY_MEDIA_LANGUAGE = "media.language" /* language in POSIX format. Ex: en_GB */
PW_KEY_MEDIA_FILENAME = "media.filename" /* filename */
PW_KEY_MEDIA_ICON = "media.icon" /* icon for the media, a base64 blob with PNG image data */
PW_KEY_MEDIA_ICON_NAME = "media.icon-name" /* an XDG icon name for the media. Ex: "audio-x-mp3" */
PW_KEY_MEDIA_COMMENT = "media.comment" /* extra comment */
PW_KEY_MEDIA_DATE = "media.date" /* date of the media */
PW_KEY_MEDIA_FORMAT = "media.format" /* format of the media */
/** format related properties */
PW_KEY_FORMAT_DSP = "format.dsp" /* a dsp format. Ex: "32 bit float mono audio" */
/** audio related properties */
PW_KEY_AUDIO_CHANNEL = "audio.channel" /* an audio channel. Ex: "FL" */
PW_KEY_AUDIO_RATE = "audio.rate" /* an audio samplerate */
PW_KEY_AUDIO_CHANNELS = "audio.channels" /* number of audio channels */
PW_KEY_AUDIO_FORMAT = "audio.format" /* an audio format. Ex: "S16LE" */
PW_KEY_AUDIO_ALLOWED_RATES = "audio.allowed-rates" /* a list of allowed samplerates ex. "[ 44100 48000 ]" */
/** video related properties */
PW_KEY_VIDEO_RATE = "video.framerate" /* a video framerate */
PW_KEY_VIDEO_FORMAT = "video.format" /* a video format */
PW_KEY_VIDEO_SIZE = "video.size" /* a video size as "<width>x<height" */
PW_KEY_TARGET_OBJECT = "target.object" /* a target object to link to. This can be and object name or object.serial */
)

View File

@@ -0,0 +1,81 @@
package pipewire_test
import (
_ "embed"
"encoding/binary"
"testing"
"hakurei.app/internal/pipewire"
)
func TestSPAKind(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
v pipewire.SPAKind
want string
}{
{"invalid", 0xdeadbeef, "invalid type field 0xdeadbeef"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.v.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}
// splitMessages splits concatenated messages into groups of
// header, payload, footer of each individual message.
// splitMessages panics on any decoding error.
func splitMessages(iovec string) (messages [][3][]byte) {
data := []byte(iovec)
messages = make([][3][]byte, 0, 1<<7)
var header pipewire.Header
for len(data) != 0 {
if err := header.UnmarshalBinary(data[:pipewire.SizeHeader]); err != nil {
panic(err)
}
size := pipewire.SizePrefix + binary.NativeEndian.Uint32(data[pipewire.SizeHeader:])
messages = append(messages, [3][]byte{
data[:pipewire.SizeHeader],
data[pipewire.SizeHeader : pipewire.SizeHeader+size],
data[pipewire.SizeHeader+size : pipewire.SizeHeader+header.Size],
})
data = data[pipewire.SizeHeader+header.Size:]
}
return
}
var (
//go:embed testdata/pw-container-00-sendmsg
samplePWContainer00 string
//go:embed testdata/pw-container-01-recvmsg
samplePWContainer01 string
//go:embed testdata/pw-container-03-sendmsg
samplePWContainer03 string
//go:embed testdata/pw-container-04-recvmsg
samplePWContainer04 string
//go:embed testdata/pw-container-06-sendmsg
samplePWContainer06 string
//go:embed testdata/pw-container-07-recvmsg
samplePWContainer07 string
// samplePWContainer is a collection of messages from the pw-container sample.
samplePWContainer = [...][][3][]byte{
splitMessages(samplePWContainer00),
splitMessages(samplePWContainer01),
nil,
splitMessages(samplePWContainer03),
splitMessages(samplePWContainer04),
nil,
splitMessages(samplePWContainer06),
splitMessages(samplePWContainer07),
nil,
}
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -34,7 +34,7 @@ func TestEntryHeader(t *testing.T) {
{"success high", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
0xff, 0x00}, 0xff, nil},
{"success", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
0x09, 0xf6}, hst.EWayland | hst.EPulse, nil},
0x09, 0xf6}, hst.EWayland | hst.EPipeWire, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

View File

@@ -13,7 +13,7 @@ import (
func TestACLUpdateOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"apply aclUpdate", 0xbeef, 0xff,
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),

View File

@@ -18,7 +18,7 @@ import (
func TestDBusProxyOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"dbusProxyStart", 0xdead, 0xff, &dbusProxyOp{
final: dbusNewFinalSample(4),
out: new(linePrefixWriter), // panics on write

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,7 +48,11 @@ type syscallDispatcher interface {
// aclUpdate provides [acl.Update].
aclUpdate(name string, uid int, perms ...acl.Perm) error
waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (*wayland.SecurityContext, 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
@@ -80,10 +85,12 @@ func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error {
return acl.Update(name, uid, perms...)
}
func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (*wayland.SecurityContext, error) {
func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) {
return wayland.New(displayPath, bindPath, appID, instanceID)
}
func (k direct) pipewireConnect() (*pipewire.Context, error) { return pipewire.Connect(true, nil) }
func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
return xcb.ChangeHosts(mode, family, address)
}

View File

@@ -1,6 +1,7 @@
package system
import (
"io"
"log"
"os"
"reflect"
@@ -13,7 +14,7 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/wayland"
"hakurei.app/internal/pipewire"
"hakurei.app/internal/xcb"
)
@@ -36,17 +37,26 @@ type opBehaviourTestCase struct {
wantErrRevert error
}
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
const (
// checkNoParallel causes checkOpBehaviour to skip setting tests as parallel.
checkNoParallel = 1 << iota
)
func checkOpBehaviour(t *testing.T, flags int, testCases []opBehaviourTestCase) {
t.Helper()
t.Run("behaviour", func(t *testing.T) {
t.Helper()
t.Parallel()
if flags&checkNoParallel == 0 {
t.Parallel()
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
if flags&checkNoParallel == 0 {
t.Parallel()
}
var ec *Criteria
if tc.ec != 0xff {
@@ -236,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))
}
@@ -264,21 +282,35 @@ 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))
}
func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (*wayland.SecurityContext, error) {
func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) {
k.Helper()
return nil, k.Expects("waylandNew").Error(
return io.NopCloser(nil), k.Expects("waylandNew").Error(
stub.CheckArgReflect(k.Stub, "displayPath", displayPath, 0),
stub.CheckArgReflect(k.Stub, "bindPath", bindPath, 1),
stub.CheckArg(k.Stub, "appID", appID, 2),
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(
@@ -378,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

@@ -10,7 +10,7 @@ import (
func TestHardlinkOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"link", 0xbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(1)),

View File

@@ -10,7 +10,7 @@ import (
func TestMkdirOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"mkdir", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(2)),

View File

@@ -26,6 +26,7 @@ func (e *OpError) Error() string {
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.LinkError)),
errors.As(e.Err, new(*os.SyscallError)),
errors.As(e.Err, new(*net.OpError)),
errors.As(e.Err, new(*container.StartError)):
return e.Err.Error()

View File

@@ -44,6 +44,11 @@ func TestOpError(t *testing.T) {
syscall.EISDIR, syscall.ENOTDIR,
"cannot stat /run/dbus: is a directory"},
{"syscall", newOpError("pipewire", os.NewSyscallError("pipe2", syscall.ENOTRECOVERABLE), false),
"pipe2: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ENOTDIR,
"cannot pipe2: state not recoverable"},
{"net", newOpError("wayland", &net.OpError{Op: "dial", Net: "unix", Addr: &net.UnixAddr{Name: "/run/user/1000/wayland-1", Net: "unix"}, Err: syscall.ENOENT}, false),
"dial unix /run/user/1000/wayland-1: no such file or directory",
syscall.ENOENT, syscall.EPERM,

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
}

View File

@@ -3,12 +3,11 @@ package system
import (
"errors"
"fmt"
"os"
"io"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/wayland"
)
// Wayland maintains a wayland socket with security-context-v1 attached via [wayland].
@@ -22,7 +21,7 @@ func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I {
// waylandOp implements [I.Wayland].
type waylandOp struct {
ctx *wayland.SecurityContext
ctx io.Closer
dst, src *check.Absolute
appID, instanceID string
}
@@ -54,20 +53,11 @@ func (w *waylandOp) apply(sys *I) (err error) {
}
func (w *waylandOp) revert(sys *I, _ *Criteria) error {
var (
hangupErr error
removeErr error
)
sys.msg.Verbosef("hanging up wayland socket on %q", w.dst)
if w.ctx != nil {
hangupErr = w.ctx.Close()
sys.msg.Verbosef("hanging up wayland socket on %q", w.dst)
return newOpError("wayland", w.ctx.Close(), true)
}
if err := sys.remove(w.dst.String()); err != nil && !errors.Is(err, os.ErrNotExist) {
removeErr = err
}
return newOpError("wayland", errors.Join(hangupErr, removeErr), true)
return nil
}
func (w *waylandOp) Is(o Op) bool {

View File

@@ -1,7 +1,6 @@
package system
import (
"errors"
"os"
"testing"
@@ -12,7 +11,7 @@ import (
func TestWaylandOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"chmod", 0xbeef, 0xff, &waylandOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
m("/run/user/1971/wayland-0"),
@@ -22,7 +21,7 @@ func TestWaylandOp(t *testing.T) {
call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil),
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, stub.UniqueError(3)),
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(3), os.ErrInvalid)}, nil, nil},
}, &OpError{Op: "wayland", Err: stub.UniqueError(3)}, nil, nil},
{"aclUpdate", 0xbeef, 0xff, &waylandOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
@@ -34,22 +33,7 @@ func TestWaylandOp(t *testing.T) {
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(2), os.ErrInvalid)}, nil, nil},
{"remove", 0xbeef, 0xff, &waylandOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
m("/run/user/1971/wayland-0"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, []stub.Call{
call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil),
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, stub.UniqueError(1)),
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(1)), Revert: true}},
}, &OpError{Op: "wayland", Err: stub.UniqueError(2)}, nil, nil},
{"success", 0xbeef, 0xff, &waylandOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
@@ -63,7 +47,6 @@ func TestWaylandOp(t *testing.T) {
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil),
}, nil},
})

View File

@@ -11,7 +11,7 @@ import (
func TestXHostOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
checkOpBehaviour(t, 0, []opBehaviourTestCase{
{"xcbChangeHosts revert", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)),

View File

@@ -12,18 +12,32 @@ import (
type SecurityContext struct {
// Pipe with its write end passed to security-context-v1.
closeFds [2]int
// Absolute pathname the socket was bound to.
bindPath *check.Absolute
}
// Close releases any resources held by [SecurityContext], and prevents further
// connections to its associated socket.
//
// A non-nil error has the concrete type [Error].
func (sc *SecurityContext) Close() error {
if sc == nil {
if sc == nil || sc.bindPath == nil {
return os.ErrInvalid
}
return errors.Join(
e := Error{RCleanup, sc.bindPath.String(), "", errors.Join(
syscall.Close(sc.closeFds[1]),
syscall.Close(sc.closeFds[0]),
)
// there is still technically a TOCTOU here but this is internal
// and has access to the privileged wayland socket, so it only
// receives trusted input (e.g. from cmd/hakurei) anyway
os.Remove(sc.bindPath.String()),
)}
if e.Errno != nil {
return &e
}
return nil
}
// New creates a new security context on the Wayland display at displayPath
@@ -38,6 +52,7 @@ func New(displayPath, bindPath *check.Absolute, appID, instanceID string) (*Secu
if f, err := os.Create(bindPath.String()); err != nil {
return nil, &Error{RCreate, bindPath.String(), displayPath.String(), err}
} else if err = f.Close(); err != nil {
_ = os.Remove(bindPath.String())
return nil, &Error{RCreate, bindPath.String(), displayPath.String(), err}
} else if err = os.Remove(bindPath.String()); err != nil {
return nil, &Error{RCreate, bindPath.String(), displayPath.String(), err}
@@ -51,14 +66,19 @@ func New(displayPath, bindPath *check.Absolute, appID, instanceID string) (*Secu
} else {
closeFds, bindErr := securityContextBindPipe(fd, bindPath, appID, instanceID)
if bindErr != nil {
// do not leak the pipe and socket
// securityContextBindPipe does not try to remove the socket during cleanup
closeErr := os.Remove(bindPath.String())
if closeErr != nil && errors.Is(closeErr, os.ErrNotExist) {
closeErr = nil
}
err = errors.Join(bindErr, // already wrapped
syscall.Close(closeFds[1]),
syscall.Close(closeFds[0]),
closeErr,
// do not leak the socket
syscall.Close(fd),
)
}
return &SecurityContext{closeFds}, err
return &SecurityContext{closeFds, bindPath}, err
}
}

View File

@@ -3,6 +3,7 @@ package wayland
import (
"errors"
"os"
"path"
"reflect"
"syscall"
"testing"
@@ -11,13 +12,18 @@ import (
)
func TestSecurityContextClose(t *testing.T) {
t.Parallel()
// do not parallel: fd test not thread safe
if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) {
t.Fatalf("Close: error = %v", err)
}
var ctx SecurityContext
if f, err := os.Create(path.Join(t.TempDir(), "remove")); err != nil {
t.Fatal(err)
} else {
ctx.bindPath = check.MustAbs(f.Name())
}
if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil {
t.Fatalf("Pipe: error = %v", err)
}
@@ -25,9 +31,15 @@ func TestSecurityContextClose(t *testing.T) {
if err := ctx.Close(); err != nil {
t.Fatalf("Close: error = %v", err)
} else if _, err = os.Stat(ctx.bindPath.String()); err == nil || !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Did not remove %q", ctx.bindPath)
}
wantErr := errors.Join(syscall.EBADF, syscall.EBADF)
wantErr := &Error{Cause: RCleanup, Path: ctx.bindPath.String(), Errno: errors.Join(syscall.EBADF, syscall.EBADF, &os.PathError{
Op: "remove",
Path: ctx.bindPath.String(),
Err: syscall.ENOENT,
})}
if err := ctx.Close(); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("Close: error = %#v, want %#v", err, wantErr)
}

View File

@@ -24,6 +24,8 @@ typedef enum {
HAKUREI_WAYLAND_HOST_SOCKET,
/* connect for host server failed, implemented in conn.go */
HAKUREI_WAYLAND_HOST_CONNECT,
/* cleanup failed, implemented in conn.go */
HAKUREI_WAYLAND_CLEANUP,
} hakurei_wayland_res;
hakurei_wayland_res hakurei_security_context_bind(

View File

@@ -9,14 +9,20 @@ package wayland
#cgo freebsd openbsd LDFLAGS: -lwayland-client
#include "wayland-client-helper.h"
#include <wayland-version.h>
*/
import "C"
import (
"errors"
"os"
"strings"
"syscall"
)
const (
// Version is the value of WAYLAND_VERSION.
Version = C.WAYLAND_VERSION
// Display contains the name of the server socket
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147)
// which is concatenated with XDG_RUNTIME_DIR
@@ -79,6 +85,9 @@ const (
RHostSocket Res = C.HAKUREI_WAYLAND_HOST_SOCKET
// RHostConnect is returned if connect failed for host server. Returned by [New].
RHostConnect Res = C.HAKUREI_WAYLAND_HOST_CONNECT
// RCleanup is returned if cleanup fails. Returned by [SecurityContext.Close].
RCleanup Res = C.HAKUREI_WAYLAND_CLEANUP
)
func (e *Error) Unwrap() error { return e.Errno }
@@ -120,6 +129,19 @@ func (e *Error) Error() string {
case RHostConnect:
return e.withPrefix("cannot connect to " + e.Host)
case RCleanup:
var pathError *os.PathError
if errors.As(e.Errno, &pathError) && pathError != nil {
return pathError.Error()
}
var errno syscall.Errno
if errors.As(e.Errno, &errno) && errno != 0 {
return "cannot close wayland close_fd pipe: " + errno.Error()
}
return e.withPrefix("cannot hang up wayland security_context")
default:
return e.withPrefix("impossible outcome") /* not reached */
}

View File

@@ -58,15 +58,15 @@ func TestError(t *testing.T) {
{"bind", Error{
Cause: RBind,
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Errno: stub.UniqueError(5),
}, "cannot bind /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 5 injected by the test suite"},
}, "cannot bind /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 5 injected by the test suite"},
{"listen", Error{
Cause: RListen,
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Errno: stub.UniqueError(6),
}, "cannot listen on /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 6 injected by the test suite"},
}, "cannot listen on /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 6 injected by the test suite"},
{"socket invalid", Error{
Cause: RSocket,
@@ -92,6 +92,27 @@ func TestError(t *testing.T) {
Errno: stub.UniqueError(8),
}, "cannot connect to /run/user/1971/wayland-1: unique error 8 injected by the test suite"},
{"cleanup", Error{
Cause: RCleanup,
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
}, "cannot hang up wayland security_context"},
{"cleanup PathError", Error{
Cause: RCleanup,
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Errno: errors.Join(syscall.EINVAL, &os.PathError{
Op: "remove",
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Err: stub.UniqueError(9),
}),
}, "remove /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 9 injected by the test suite"},
{"cleanup errno", Error{
Cause: RCleanup,
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
Errno: errors.Join(syscall.EINVAL),
}, "cannot close wayland close_fd pipe: invalid argument"},
{"invalid", Error{
Cause: 0xbad,
}, "impossible outcome"},

View File

@@ -196,6 +196,15 @@ in
}
]
)
++ optional (app.enablements.pipewire && app.pulse) {
type = "daemon";
dst = if app.mapRealUid then "/run/user/${toString config.users.users.${username}.uid}/pulse/native" else "/run/user/65534/pulse/native";
path = cfg.shell;
args = [
"-lc"
"exec pipewire-pulse"
];
}
++ [
{
type = "bind";

View File

@@ -218,7 +218,7 @@ in
type = nullOr bool;
default = true;
description = ''
Whether to share the Wayland socket.
Whether to share the Wayland server via security-context-v1.
'';
};
@@ -238,15 +238,23 @@ in
'';
};
pulse = mkOption {
pipewire = mkOption {
type = nullOr bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
Whether to share the PipeWire server via SecurityContext.
'';
};
};
pulse = mkOption {
type = nullOr bool;
default = true;
description = ''
Whether to run the PulseAudio compatibility daemon.
'';
};
share = mkOption {
type = nullOr package;
default = null;

View File

@@ -32,7 +32,7 @@
buildGoModule rec {
pname = "hakurei";
version = "0.3.1";
version = "0.3.2";
srcFiltered = builtins.path {
name = "${pname}-src";

View File

@@ -133,7 +133,7 @@
wait_delay = 1;
enablements = {
wayland = false;
pulse = false;
pipewire = false;
};
};
@@ -152,7 +152,7 @@
command = "foot";
enablements = {
dbus = false;
pulse = false;
pipewire = false;
};
};
@@ -167,7 +167,7 @@
command = "foot";
enablements = {
dbus = false;
pulse = false;
pipewire = false;
};
};
@@ -199,7 +199,7 @@
wayland = false;
x11 = true;
dbus = false;
pulse = false;
pipewire = false;
};
};
@@ -218,7 +218,7 @@
command = "foot";
enablements = {
dbus = false;
pulse = false;
pipewire = false;
};
};
@@ -232,7 +232,7 @@
wayland = false;
x11 = false;
dbus = false;
pulse = false;
pipewire = false;
};
};
};

View File

@@ -15,9 +15,30 @@
command = "foot";
enablements = {
dbus = false;
pulse = false;
pipewire = false;
};
};
"cat.gensokyo.extern.foot.badDaemon" = {
name = "bd-foot";
identity = 1;
shareUid = true;
verbose = true;
share = pkgs.foot;
packages = [ pkgs.foot ];
command = "foot";
enablements = {
dbus = false;
};
extraPaths = [
{
type = "daemon";
dst = "/proc/nonexistent";
path = "/usr/bin/env";
args = [ "false" ];
}
];
};
};
extraHomeConfig.home.stateVersion = "23.05";

View File

@@ -41,7 +41,7 @@ in
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"DISPLAY=unix:/tmp/.X11-unix/X0"
"HOME=/var/lib/hakurei/u0/a4"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a4"
@@ -137,8 +137,12 @@ in
user = fs "800001ed" {
"65534" = fs "800001c0" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -220,13 +224,13 @@ in
(ent "/" ignore ignore ignore ignore ignore)
(ent "/" ignore ignore ignore ignore ignore)
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10004,gid=10004")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10004,gid=10004")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10004,gid=10004")
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@@ -49,7 +49,7 @@ in
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
"HOME=/var/lib/hakurei/u0/a3"
"PULSE_SERVER=unix:/run/user/1000/pulse/native"
"PIPEWIRE_REMOTE=/run/user/1000/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a3"
@@ -162,8 +162,12 @@ in
user = fs "800001ed" {
"1000" = fs "800001f8" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -247,13 +251,13 @@ in
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10003,gid=10003")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10003,gid=10003")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10003,gid=10003")
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@@ -181,7 +181,7 @@
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10000,gid=10000")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10000,gid=10000")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10000,gid=10000")
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")

View File

@@ -49,7 +49,7 @@ in
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/hakurei/u0/a5"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a5"
@@ -160,8 +160,12 @@ in
user = fs "800001ed" {
"65534" = fs "800001f8" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -245,13 +249,13 @@ in
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10005,gid=10005")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10005,gid=10005")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10005,gid=10005")
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@@ -49,7 +49,7 @@ in
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/hakurei/u0/a1"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a1"
@@ -159,8 +159,12 @@ in
user = fs "800001ed" {
"65534" = fs "800001c0" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -243,12 +247,12 @@ in
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10001,gid=10001")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10001,gid=10001")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10001,gid=10001")
(ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10001,gid=10001")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@@ -50,7 +50,7 @@ in
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"DISPLAY=:0"
"HOME=/var/lib/hakurei/u0/a2"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a2"
@@ -164,8 +164,12 @@ in
user = fs "800001ed" {
"65534" = fs "800001f8" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -252,14 +256,14 @@ in
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10002,gid=10002")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10002,gid=10002")
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")

View File

@@ -83,4 +83,4 @@ swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
print(machine.succeed("find /run/user/1000/hakurei"))
print(machine.fail("ls /run/user/1000/hakurei"))

View File

@@ -160,17 +160,17 @@ machine.succeed("pkill -9 mako")
# Check revert type selection:
hakurei("-v run --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok")
wait_for_window("p0@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000"))
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
hakurei("-v run --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok")
wait_for_window("p1@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000"))
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
machine.send_chars("exit\n")
machine.wait_for_file("/tmp/p1-exit-ok", timeout=15)
# Verify acl is kept alive:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000"))
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
machine.send_chars("exit\n")
machine.wait_for_file("/tmp/p0-exit-ok", timeout=15)
machine.fail("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000")
machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")
# Check invalid identifier fd behaviour:
machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v app --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd')
@@ -219,15 +219,22 @@ machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}", timeout=5)
# Test PulseAudio (hakurei does not support PipeWire yet):
# Test pipewire-pulse:
swaymsg("exec pa-foot")
wait_for_window(f"u0_a{hakurei_identity(1)}@machine")
machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n")
machine.wait_for_file("/var/tmp/pulse-ok", timeout=15)
collect_state_ui("pulse_wayland")
check_state("pa-foot", {"wayland": True, "pulse": True})
check_state("pa-foot", {"wayland": True, "pipewire": True})
# Test PipeWire:
machine.send_chars("clear; pw-cli i 0 && touch /var/tmp/pw-ok\n")
machine.wait_for_file("/var/tmp/pw-ok", timeout=15)
collect_state_ui("pipewire_wayland")
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5)
# Test PipeWire SecurityContext:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl info")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
# Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty")