internal/outcome: rename from app
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m27s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m1s
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m27s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m1s
This is less ambiguous, and more accurately describes the purpose of the package. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
210
internal/outcome/sppulse.go
Normal file
210
internal/outcome/sppulse.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
const pulseCookieSizeMax = 1 << 8
|
||||
|
||||
func init() { gob.Register(new(spPulseOp)) }
|
||||
|
||||
// spPulseOp exports the PulseAudio server to the container.
|
||||
// Runs after spRuntimeOp.
|
||||
type spPulseOp struct {
|
||||
// PulseAudio cookie data, populated during toSystem if a cookie is present.
|
||||
Cookie *[pulseCookieSizeMax]byte
|
||||
// PulseAudio cookie size, populated during toSystem if a cookie is present.
|
||||
CookieSize int
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EPulse == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
pulseRuntimeDir, pulseSocket := s.commonPaths(state.outcomeState)
|
||||
|
||||
if _, err := state.k.stat(pulseRuntimeDir.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
|
||||
}
|
||||
return newWithMessageError(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir), err)
|
||||
}
|
||||
|
||||
if fi, err := state.k.stat(pulseSocket.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
|
||||
}
|
||||
return newWithMessageError(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir), err)
|
||||
} else {
|
||||
if m := fi.Mode(); m&0o006 != 0o006 {
|
||||
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
|
||||
}
|
||||
}
|
||||
|
||||
// pulse socket is world writable and its parent directory DAC permissions prevents access;
|
||||
// hard link to target-executable share directory to grant access
|
||||
state.sys.Link(pulseSocket, state.runtime().Append("pulse"))
|
||||
|
||||
// load up to pulseCookieSizeMax bytes of pulse cookie for transmission to shim
|
||||
if a, err := discoverPulseCookie(state.k); err != nil {
|
||||
return err
|
||||
} else if a != nil {
|
||||
s.Cookie = new([pulseCookieSizeMax]byte)
|
||||
if s.CookieSize, err = loadFile(state.msg, state.k, "PulseAudio cookie", a.String(), s.Cookie[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
state.msg.Verbose("cannot locate PulseAudio cookie (tried " +
|
||||
"$PULSE_COOKIE, " +
|
||||
"$XDG_CONFIG_HOME/pulse/cookie, " +
|
||||
"$HOME/.pulse-cookie)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toContainer(state *outcomeStateParams) error {
|
||||
innerPulseSocket := state.runtimeDir.Append("pulse", "native")
|
||||
state.params.Bind(state.runtimePath().Append("pulse"), innerPulseSocket, 0)
|
||||
state.env["PULSE_SERVER"] = "unix:" + innerPulseSocket.String()
|
||||
|
||||
if s.Cookie != nil {
|
||||
innerDst := hst.AbsPrivateTmp.Append("/pulse-cookie")
|
||||
|
||||
if s.CookieSize < 0 || s.CookieSize > pulseCookieSizeMax {
|
||||
return newWithMessage("unexpected PulseAudio cookie size")
|
||||
}
|
||||
state.env["PULSE_COOKIE"] = innerDst.String()
|
||||
state.params.Place(innerDst, s.Cookie[:s.CookieSize])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSocket *check.Absolute) {
|
||||
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
|
||||
pulseRuntimeDir = state.sc.RuntimePath.Append("pulse")
|
||||
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
||||
pulseSocket = pulseRuntimeDir.Append("native")
|
||||
return
|
||||
}
|
||||
|
||||
// discoverPulseCookie attempts to discover the pathname of the PulseAudio cookie of the current user.
|
||||
// If both returned pathname and error are nil, the cookie is likely unavailable and can be silently skipped.
|
||||
func discoverPulseCookie(k syscallDispatcher) (*check.Absolute, error) {
|
||||
const paLocateStep = "locate PulseAudio cookie"
|
||||
|
||||
// from environment
|
||||
if p, ok := k.lookupEnv("PULSE_COOKIE"); ok {
|
||||
if a, err := check.NewAbs(p); err != nil {
|
||||
return nil, &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
// this takes precedence, do not verify whether the file is accessible
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
// $HOME/.pulse-cookie
|
||||
if p, ok := k.lookupEnv("HOME"); ok {
|
||||
var pulseCookiePath *check.Absolute
|
||||
if a, err := check.NewAbs(p); err != nil {
|
||||
return nil, &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
pulseCookiePath = a.Append(".pulse-cookie")
|
||||
}
|
||||
|
||||
if fi, err := k.stat(pulseCookiePath.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if fi.IsDir() {
|
||||
// fallthrough
|
||||
} else {
|
||||
return pulseCookiePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/pulse/cookie
|
||||
if p, ok := k.lookupEnv("XDG_CONFIG_HOME"); ok {
|
||||
var pulseCookiePath *check.Absolute
|
||||
if a, err := check.NewAbs(p); err != nil {
|
||||
return nil, &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
pulseCookiePath = a.Append("pulse", "cookie")
|
||||
}
|
||||
|
||||
if fi, err := k.stat(pulseCookiePath.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if fi.IsDir() {
|
||||
// fallthrough
|
||||
} else {
|
||||
return pulseCookiePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// cookie not present
|
||||
// not fatal: authentication is disabled
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// loadFile reads up to len(buf) bytes from the file at pathname.
|
||||
func loadFile(
|
||||
msg message.Msg, k syscallDispatcher,
|
||||
description, pathname string, buf []byte,
|
||||
) (int, error) {
|
||||
n := len(buf)
|
||||
if n == 0 {
|
||||
return -1, errors.New("invalid buffer")
|
||||
}
|
||||
|
||||
if fi, err := k.stat(pathname); err != nil {
|
||||
return -1, &hst.AppError{Step: "access " + description, Err: err}
|
||||
} else {
|
||||
if fi.IsDir() {
|
||||
return -1, &hst.AppError{Step: "read " + description,
|
||||
Err: &os.PathError{Op: "stat", Path: pathname, Err: syscall.EISDIR}}
|
||||
}
|
||||
if s := fi.Size(); s > int64(n) {
|
||||
return -1, newWithMessageError(
|
||||
description+" at "+strconv.Quote(pathname)+" exceeds expected size",
|
||||
&os.PathError{Op: "stat", Path: pathname, Err: syscall.ENOMEM},
|
||||
)
|
||||
} else if s < int64(n) {
|
||||
msg.Verbosef("%s at %q is %d bytes shorter than expected", description, pathname, int64(n)-s)
|
||||
} else {
|
||||
msg.Verbosef("loading %d bytes from %q", n, pathname)
|
||||
}
|
||||
}
|
||||
|
||||
if f, err := k.open(pathname); err != nil {
|
||||
return -1, &hst.AppError{Step: "open " + description, Err: err}
|
||||
} else {
|
||||
if n, err = f.Read(buf); err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
_ = f.Close()
|
||||
return n, &hst.AppError{Step: "read " + description, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
if err = f.Close(); err != nil {
|
||||
return n, &hst.AppError{Step: "close " + description, Err: err}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user