internal/helper: relocate from helper
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m23s
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m23s
This package is ugly and is pending removal only kept alive by xdg-dbus-proxy. Its exported symbols are made available until v0.4.0 where it will be removed for #24. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type argsWt [][]byte
|
||||
|
||||
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
|
||||
nt := 0
|
||||
for _, arg := range a {
|
||||
n, err := w.Write(arg)
|
||||
nt += n
|
||||
|
||||
if err != nil {
|
||||
return int64(nt), err
|
||||
}
|
||||
}
|
||||
|
||||
return int64(nt), nil
|
||||
}
|
||||
|
||||
func (a argsWt) String() string {
|
||||
return string(
|
||||
bytes.TrimSuffix(
|
||||
bytes.ReplaceAll(
|
||||
bytes.Join(a, nil),
|
||||
[]byte{0}, []byte{' '},
|
||||
),
|
||||
[]byte{' '},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||
func NewCheckedArgs(args ...string) (wt io.WriterTo, err error) {
|
||||
a := make(argsWt, len(args))
|
||||
for i, arg := range args {
|
||||
a[i], err = syscall.ByteSliceFromString(arg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
wt = a
|
||||
return
|
||||
}
|
||||
|
||||
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||
// If s contains a NUL byte this function panics instead of returning an error.
|
||||
func MustNewCheckedArgs(args ...string) io.WriterTo {
|
||||
a, err := NewCheckedArgs(args...)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package helper_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/helper"
|
||||
)
|
||||
|
||||
func TestArgsString(t *testing.T) {
|
||||
t.Parallel()
|
||||
wantString := strings.Join(wantArgs, " ")
|
||||
if got := argsWt.(fmt.Stringer).String(); got != wantString {
|
||||
t.Errorf("String: %q, want %q", got, wantString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCheckedArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
args := []string{"\x00"}
|
||||
if _, err := helper.NewCheckedArgs(args...); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("NewCheckedArgs: error = %v, wantErr %v", err, syscall.EINVAL)
|
||||
}
|
||||
|
||||
t.Run("must panic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
badPayload := []string{"\x00"}
|
||||
defer func() {
|
||||
wantPanic := "invalid argument"
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
helper.MustNewCheckedArgs(badPayload...)
|
||||
})
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/helper/proc"
|
||||
)
|
||||
|
||||
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
func NewDirect(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
cmdF func(cmd *exec.Cmd),
|
||||
extraFiles []*os.File,
|
||||
) Helper {
|
||||
d, args := newHelperCmd(ctx, name, wt, stat, argF, extraFiles)
|
||||
d.Args = append(d.Args, args...)
|
||||
if cmdF != nil {
|
||||
cmdF(d.Cmd)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func newHelperCmd(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
extraFiles []*os.File,
|
||||
) (cmd *helperCmd, args []string) {
|
||||
cmd = new(helperCmd)
|
||||
cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||
cmd.Cmd = exec.CommandContext(ctx, name)
|
||||
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||
cmd.WaitDelay = WaitDelay
|
||||
return
|
||||
}
|
||||
|
||||
// helperCmd provides a [exec.Cmd] wrapper around helper ipc.
|
||||
type helperCmd struct {
|
||||
mu sync.RWMutex
|
||||
*helperFiles
|
||||
*exec.Cmd
|
||||
}
|
||||
|
||||
func (h *helperCmd) Start() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Check for doubled Start calls before we defer failure cleanup. If the prior
|
||||
// call to Start succeeded, we don't want to spuriously close its pipes.
|
||||
if h.Cmd != nil && h.Cmd.Process != nil {
|
||||
return errors.New("helper: already started")
|
||||
}
|
||||
|
||||
h.Env = slices.Grow(h.Env, 2)
|
||||
if h.useArgsFd {
|
||||
h.Env = append(h.Env, HakureiHelper+"=1")
|
||||
} else {
|
||||
h.Env = append(h.Env, HakureiHelper+"=0")
|
||||
}
|
||||
if h.useStatFd {
|
||||
h.Env = append(h.Env, HakureiStatus+"=1")
|
||||
|
||||
// stat is populated on fulfill
|
||||
h.Cancel = func() error { return h.stat.Close() }
|
||||
} else {
|
||||
h.Env = append(h.Env, HakureiStatus+"=0")
|
||||
}
|
||||
|
||||
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package helper_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/helper"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
t.Run("start non-existent helper path", func(t *testing.T) {
|
||||
h := helper.NewDirect(t.Context(), container.Nonexistent, argsWt, false, argF, nil, nil)
|
||||
|
||||
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("Start: error = %v, wantErr %v",
|
||||
err, os.ErrNotExist)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||
if got := helper.NewDirect(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
|
||||
t.Errorf("NewDirect(%q, %q) got nil",
|
||||
argsWt, "hakurei")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("implementation compliance", func(t *testing.T) {
|
||||
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||
return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
|
||||
setOutput(&cmd.Stdout, &cmd.Stderr)
|
||||
}, nil)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/helper/proc"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// New initialises a Helper instance with wt as the null-terminated argument writer.
|
||||
func New(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
pathname *check.Absolute, name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
cmdF func(z *container.Container),
|
||||
extraFiles []*os.File,
|
||||
) Helper {
|
||||
var args []string
|
||||
h := new(helperContainer)
|
||||
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||
h.Container = container.NewCommand(ctx, msg, pathname, name, args...)
|
||||
h.WaitDelay = WaitDelay
|
||||
if cmdF != nil {
|
||||
cmdF(h.Container)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// helperContainer provides a [sandbox.Container] wrapper around helper ipc.
|
||||
type helperContainer struct {
|
||||
started bool
|
||||
|
||||
mu sync.Mutex
|
||||
*helperFiles
|
||||
*container.Container
|
||||
}
|
||||
|
||||
func (h *helperContainer) Start() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.started {
|
||||
return errors.New("helper: already started")
|
||||
}
|
||||
h.started = true
|
||||
|
||||
h.Env = slices.Grow(h.Env, 2)
|
||||
if h.useArgsFd {
|
||||
h.Env = append(h.Env, HakureiHelper+"=1")
|
||||
} else {
|
||||
h.Env = append(h.Env, HakureiHelper+"=0")
|
||||
}
|
||||
if h.useStatFd {
|
||||
h.Env = append(h.Env, HakureiStatus+"=1")
|
||||
|
||||
// stat is populated on fulfill
|
||||
h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
|
||||
} else {
|
||||
h.Env = append(h.Env, HakureiStatus+"=0")
|
||||
}
|
||||
|
||||
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {
|
||||
if err := h.Container.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.Container.Serve()
|
||||
}, h.files, h.extraFiles)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package helper_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/helper"
|
||||
)
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
t.Run("start invalid container", func(t *testing.T) {
|
||||
h := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
|
||||
|
||||
wantErr := "container: starting an invalid container"
|
||||
if err := h.Start(); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("Start: error = %v, wantErr %q",
|
||||
err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||
if got := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil {
|
||||
t.Errorf("New(%q, %q) got nil",
|
||||
argsWt, "hakurei")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("implementation compliance", func(t *testing.T) {
|
||||
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||
return helper.New(ctx, nil, check.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) {
|
||||
setOutput(&z.Stdout, &z.Stderr)
|
||||
z.
|
||||
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
|
||||
Proc(fhs.AbsProc).
|
||||
Dev(fhs.AbsDev, true)
|
||||
}, nil)
|
||||
})
|
||||
})
|
||||
}
|
||||
73
helper/deprecated.go
Normal file
73
helper/deprecated.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Package helper exposes the internal/helper package.
|
||||
//
|
||||
// Deprecated: This package will be removed in 0.4.
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
//go:linkname WaitDelay hakurei.app/internal/helper.WaitDelay
|
||||
var WaitDelay time.Duration
|
||||
|
||||
const (
|
||||
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||
HakureiHelper = helper.HakureiHelper
|
||||
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
|
||||
HakureiStatus = helper.HakureiStatus
|
||||
)
|
||||
|
||||
type Helper = helper.Helper
|
||||
|
||||
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||
//
|
||||
//go:linkname NewCheckedArgs hakurei.app/internal/helper.NewCheckedArgs
|
||||
func NewCheckedArgs(args ...string) (wt io.WriterTo, err error)
|
||||
|
||||
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||
// If s contains a NUL byte this function panics instead of returning an error.
|
||||
//
|
||||
//go:linkname MustNewCheckedArgs hakurei.app/internal/helper.MustNewCheckedArgs
|
||||
func MustNewCheckedArgs(args ...string) io.WriterTo
|
||||
|
||||
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
//
|
||||
//go:linkname NewDirect hakurei.app/internal/helper.NewDirect
|
||||
func NewDirect(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
cmdF func(cmd *exec.Cmd),
|
||||
extraFiles []*os.File,
|
||||
) Helper
|
||||
|
||||
// New initialises a Helper instance with wt as the null-terminated argument writer.
|
||||
//
|
||||
//go:linkname New hakurei.app/internal/helper.New
|
||||
func New(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
pathname *check.Absolute, name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
cmdF func(z *container.Container),
|
||||
extraFiles []*os.File,
|
||||
) Helper
|
||||
|
||||
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||
// it is part of the implementation of the helper stub.
|
||||
func InternalHelperStub() { helper.InternalHelperStub() }
|
||||
@@ -1,83 +0,0 @@
|
||||
// Package helper runs external helpers with optional sandboxing.
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"hakurei.app/helper/proc"
|
||||
)
|
||||
|
||||
var WaitDelay = 2 * time.Second
|
||||
|
||||
const (
|
||||
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||
HakureiHelper = "HAKUREI_HELPER"
|
||||
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
|
||||
HakureiStatus = "HAKUREI_STATUS"
|
||||
)
|
||||
|
||||
type Helper interface {
|
||||
// Start starts the helper process.
|
||||
Start() error
|
||||
// Wait blocks until Helper exits.
|
||||
Wait() error
|
||||
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
func newHelperFiles(
|
||||
ctx context.Context,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
extraFiles []*os.File,
|
||||
) (hl *helperFiles, args []string) {
|
||||
hl = new(helperFiles)
|
||||
hl.ctx = ctx
|
||||
hl.useArgsFd = wt != nil
|
||||
hl.useStatFd = stat
|
||||
|
||||
hl.extraFiles = new(proc.ExtraFilesPre)
|
||||
for _, f := range extraFiles {
|
||||
_, v := hl.extraFiles.Append()
|
||||
*v = f
|
||||
}
|
||||
|
||||
argsFd := -1
|
||||
if hl.useArgsFd {
|
||||
f := proc.NewWriterTo(wt)
|
||||
argsFd = int(proc.InitFile(f, hl.extraFiles))
|
||||
hl.files = append(hl.files, f)
|
||||
}
|
||||
|
||||
statFd := -1
|
||||
if hl.useStatFd {
|
||||
f := proc.NewStat(&hl.stat)
|
||||
statFd = int(proc.InitFile(f, hl.extraFiles))
|
||||
hl.files = append(hl.files, f)
|
||||
}
|
||||
|
||||
args = argF(argsFd, statFd)
|
||||
return
|
||||
}
|
||||
|
||||
// helperFiles provides a generic wrapper around helper ipc.
|
||||
type helperFiles struct {
|
||||
// whether argsFd is present
|
||||
useArgsFd bool
|
||||
// whether statFd is present
|
||||
useStatFd bool
|
||||
|
||||
// closes statFd
|
||||
stat io.Closer
|
||||
// deferred extraFiles fulfillment
|
||||
files []proc.File
|
||||
// passed through to [proc.Fulfill] and [proc.InitFile]
|
||||
extraFiles *proc.ExtraFilesPre
|
||||
|
||||
ctx context.Context
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package helper_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/helper"
|
||||
)
|
||||
|
||||
var (
|
||||
wantArgs = []string{
|
||||
"unix:path=/run/dbus/system_bus_socket",
|
||||
"/tmp/hakurei.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
|
||||
"--filter",
|
||||
"--talk=org.bluez",
|
||||
"--talk=org.freedesktop.Avahi",
|
||||
"--talk=org.freedesktop.UPower",
|
||||
}
|
||||
|
||||
wantPayload = strings.Join(wantArgs, "\x00") + "\x00"
|
||||
argsWt = helper.MustNewCheckedArgs(wantArgs...)
|
||||
)
|
||||
|
||||
func argF(argsFd, statFd int) []string {
|
||||
if argsFd == -1 {
|
||||
panic("invalid args fd")
|
||||
}
|
||||
|
||||
return argFChecked(argsFd, statFd)
|
||||
}
|
||||
|
||||
func argFChecked(argsFd, statFd int) (args []string) {
|
||||
args = make([]string, 0, 6)
|
||||
if argsFd > -1 {
|
||||
args = append(args, "--args", strconv.Itoa(argsFd))
|
||||
}
|
||||
if statFd > -1 {
|
||||
args = append(args, "--fd", strconv.Itoa(statFd))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
containerTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// this function tests an implementation of the helper.Helper interface
|
||||
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
|
||||
oldWaitDelay := helper.WaitDelay
|
||||
helper.WaitDelay = 16 * time.Second
|
||||
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||
|
||||
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
|
||||
stdout := new(strings.Builder)
|
||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
|
||||
|
||||
t.Run("wait not yet started helper", func(t *testing.T) {
|
||||
if err := h.Wait(); !reflect.DeepEqual(err, syscall.EINVAL) &&
|
||||
!reflect.DeepEqual(err, errors.New("exec: not started")) {
|
||||
t.Errorf("Wait: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Log("starting helper stub")
|
||||
if err := h.Start(); err != nil {
|
||||
t.Errorf("Start: error = %v", err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
t.Log("cancelling context")
|
||||
cancel()
|
||||
|
||||
t.Run("start already started helper", func(t *testing.T) {
|
||||
wantErr := "helper: already started"
|
||||
if err := h.Start(); err != nil && err.Error() != wantErr {
|
||||
t.Errorf("Start: error = %v, wantErr %v",
|
||||
err, wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Log("waiting on helper")
|
||||
if err := h.Wait(); !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("Wait: error = %v",
|
||||
err)
|
||||
}
|
||||
|
||||
t.Run("wait already finalised helper", func(t *testing.T) {
|
||||
wantErr := "exec: Wait was already called"
|
||||
if err := h.Wait(); err != nil && err.Error() != wantErr {
|
||||
t.Errorf("Wait: error = %v, wantErr %v",
|
||||
err, wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if got := trimStdout(stdout); got != wantPayload {
|
||||
t.Errorf("Start: stdout = %q, want %q",
|
||||
got, wantPayload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("start helper and wait", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
|
||||
defer cancel()
|
||||
stdout := new(strings.Builder)
|
||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
|
||||
|
||||
if err := h.Start(); err != nil {
|
||||
t.Errorf("Start: error = %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.Wait(); err != nil {
|
||||
t.Errorf("Wait: error = %v stdout = %q",
|
||||
err, stdout)
|
||||
}
|
||||
|
||||
if got := trimStdout(stdout); got != wantPayload {
|
||||
t.Errorf("Start: stdout = %q, want %q",
|
||||
got, wantPayload)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func trimStdout(stdout fmt.Stringer) string {
|
||||
return strings.TrimPrefix(stdout.String(), "=== RUN TestHelperInit\n")
|
||||
}
|
||||
63
helper/proc/deprecated.go
Normal file
63
helper/proc/deprecated.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Deprecated: This package will be removed in 0.4.
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/internal/helper/proc"
|
||||
)
|
||||
|
||||
//go:linkname FulfillmentTimeout hakurei.app/internal/helper/proc.FulfillmentTimeout
|
||||
var FulfillmentTimeout time.Duration
|
||||
|
||||
// A File is an extra file with deferred initialisation.
|
||||
type File = proc.File
|
||||
|
||||
// ExtraFilesPre is a linked list storing addresses of [os.File].
|
||||
type ExtraFilesPre = proc.ExtraFilesPre
|
||||
|
||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
||||
//
|
||||
//go:linkname Fulfill hakurei.app/internal/helper/proc.Fulfill
|
||||
func Fulfill(ctx context.Context,
|
||||
v *[]*os.File, start func() error,
|
||||
files []File, extraFiles *ExtraFilesPre,
|
||||
) (err error)
|
||||
|
||||
// InitFile initialises f as part of the slice extraFiles points to,
|
||||
// and returns its final fd value.
|
||||
//
|
||||
//go:linkname InitFile hakurei.app/internal/helper/proc.InitFile
|
||||
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr)
|
||||
|
||||
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
|
||||
type BaseFile = proc.BaseFile
|
||||
|
||||
//go:linkname ExtraFile hakurei.app/internal/helper/proc.ExtraFile
|
||||
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr)
|
||||
|
||||
//go:linkname ExtraFileSlice hakurei.app/internal/helper/proc.ExtraFileSlice
|
||||
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr)
|
||||
|
||||
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
|
||||
//
|
||||
//go:linkname NewWriterTo hakurei.app/internal/helper/proc.NewWriterTo
|
||||
func NewWriterTo(wt io.WriterTo) File
|
||||
|
||||
// NewStat returns a [File] implementing the behaviour
|
||||
// of the receiving end of xdg-dbus-proxy stat fd.
|
||||
//
|
||||
//go:linkname NewStat hakurei.app/internal/helper/proc.NewStat
|
||||
func NewStat(s *io.Closer) File
|
||||
|
||||
var (
|
||||
//go:linkname ErrStatFault hakurei.app/internal/helper/proc.ErrStatFault
|
||||
ErrStatFault error
|
||||
//go:linkname ErrStatRead hakurei.app/internal/helper/proc.ErrStatRead
|
||||
ErrStatRead error
|
||||
)
|
||||
@@ -1,162 +0,0 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var FulfillmentTimeout = 2 * time.Second
|
||||
|
||||
func init() {
|
||||
if testing.Testing() {
|
||||
FulfillmentTimeout *= 10
|
||||
}
|
||||
}
|
||||
|
||||
// A File is an extra file with deferred initialisation.
|
||||
type File interface {
|
||||
// Init initialises File state. Init must not be called more than once.
|
||||
Init(fd uintptr, v **os.File) uintptr
|
||||
// Fd returns the fd value set on initialisation.
|
||||
Fd() uintptr
|
||||
// ErrCount returns count of error values emitted during fulfillment.
|
||||
ErrCount() int
|
||||
// Fulfill is called prior to process creation and must populate its corresponding file address.
|
||||
// Calls to dispatchErr must match the return value of ErrCount.
|
||||
// Fulfill must not be called more than once.
|
||||
Fulfill(ctx context.Context, dispatchErr func(error)) error
|
||||
}
|
||||
|
||||
// ExtraFilesPre is a linked list storing addresses of [os.File].
|
||||
type ExtraFilesPre struct {
|
||||
n *ExtraFilesPre
|
||||
v *os.File
|
||||
}
|
||||
|
||||
// Append grows the list by one entry and returns an address of the address of [os.File] stored in the new entry.
|
||||
func (f *ExtraFilesPre) Append() (uintptr, **os.File) { return f.append(3) }
|
||||
|
||||
// Files returns a slice pointing to a continuous segment of memory containing all addresses stored in f in order.
|
||||
func (f *ExtraFilesPre) Files() []*os.File { return f.copy(make([]*os.File, 0, f.len())) }
|
||||
|
||||
func (f *ExtraFilesPre) append(i uintptr) (uintptr, **os.File) {
|
||||
if f.n == nil {
|
||||
f.n = new(ExtraFilesPre)
|
||||
return i, &f.v
|
||||
}
|
||||
return f.n.append(i + 1)
|
||||
}
|
||||
func (f *ExtraFilesPre) len() uintptr {
|
||||
if f == nil {
|
||||
return 0
|
||||
}
|
||||
return f.n.len() + 1
|
||||
}
|
||||
func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
|
||||
if f == nil {
|
||||
// the public methods ensure the first call is never nil;
|
||||
// the last element is unused, slice it off here
|
||||
return e[:len(e)-1]
|
||||
}
|
||||
return f.n.copy(append(e, f.v))
|
||||
}
|
||||
|
||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
||||
func Fulfill(ctx context.Context,
|
||||
v *[]*os.File, start func() error,
|
||||
files []File, extraFiles *ExtraFilesPre,
|
||||
) (err error) {
|
||||
var ecs int
|
||||
for _, o := range files {
|
||||
ecs += o.ErrCount()
|
||||
}
|
||||
ec := make(chan error, ecs)
|
||||
|
||||
c, cancel := context.WithTimeout(ctx, FulfillmentTimeout)
|
||||
defer cancel()
|
||||
|
||||
for _, f := range files {
|
||||
err = f.Fulfill(c, makeDispatchErr(f, ec))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*v = extraFiles.Files()
|
||||
if err = start(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for ecs > 0 {
|
||||
select {
|
||||
case err = <-ec:
|
||||
ecs--
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
case <-ctx.Done():
|
||||
err = syscall.ECANCELED
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// InitFile initialises f as part of the slice extraFiles points to,
|
||||
// and returns its final fd value.
|
||||
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr) { return f.Init(extraFiles.Append()) }
|
||||
|
||||
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
|
||||
type BaseFile struct {
|
||||
fd uintptr
|
||||
v **os.File
|
||||
}
|
||||
|
||||
func (f *BaseFile) Init(fd uintptr, v **os.File) uintptr {
|
||||
if v == nil || fd < 3 {
|
||||
panic("invalid extra file initial state")
|
||||
}
|
||||
if f.v != nil {
|
||||
panic("extra file initialised twice")
|
||||
}
|
||||
f.fd, f.v = fd, v
|
||||
return fd
|
||||
}
|
||||
|
||||
func (f *BaseFile) Fd() uintptr {
|
||||
if f.v == nil {
|
||||
panic("use of uninitialised extra file")
|
||||
}
|
||||
return f.fd
|
||||
}
|
||||
|
||||
func (f *BaseFile) Set(v *os.File) {
|
||||
*f.v = v // runtime guards against use before init
|
||||
}
|
||||
|
||||
func makeDispatchErr(f File, ec chan<- error) func(error) {
|
||||
c := new(atomic.Int32)
|
||||
c.Store(int32(f.ErrCount()))
|
||||
return func(err error) {
|
||||
if c.Add(-1) < 0 {
|
||||
panic("unexpected error dispatches")
|
||||
}
|
||||
ec <- err
|
||||
}
|
||||
}
|
||||
|
||||
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr) {
|
||||
return ExtraFileSlice(&cmd.ExtraFiles, f)
|
||||
}
|
||||
|
||||
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr) {
|
||||
// ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
|
||||
fd = uintptr(3 + len(*extraFiles))
|
||||
*extraFiles = append(*extraFiles, f)
|
||||
return
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
|
||||
func NewWriterTo(wt io.WriterTo) File { return &writeToFile{wt: wt} }
|
||||
|
||||
// writeToFile exports the read end of a pipe with data written by an [io.WriterTo].
|
||||
type writeToFile struct {
|
||||
wt io.WriterTo
|
||||
BaseFile
|
||||
}
|
||||
|
||||
func (f *writeToFile) ErrCount() int { return 3 }
|
||||
func (f *writeToFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Set(r)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, err = f.wt.WriteTo(w)
|
||||
dispatchErr(err)
|
||||
dispatchErr(w.Close())
|
||||
close(done)
|
||||
runtime.KeepAlive(r)
|
||||
}()
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
dispatchErr(nil)
|
||||
case <-ctx.Done():
|
||||
dispatchErr(w.Close()) // this aborts WriteTo with file already closed
|
||||
runtime.KeepAlive(r)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewStat returns a [File] implementing the behaviour
|
||||
// of the receiving end of xdg-dbus-proxy stat fd.
|
||||
func NewStat(s *io.Closer) File { return &statFile{s: s} }
|
||||
|
||||
var (
|
||||
ErrStatFault = errors.New("generic stat fd fault")
|
||||
ErrStatRead = errors.New("unexpected stat behaviour")
|
||||
)
|
||||
|
||||
// statFile implements xdg-dbus-proxy stat fd behaviour.
|
||||
type statFile struct {
|
||||
s *io.Closer
|
||||
BaseFile
|
||||
}
|
||||
|
||||
func (f *statFile) ErrCount() int { return 2 }
|
||||
func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Set(w)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
var n int
|
||||
|
||||
n, err = r.Read(make([]byte, 1))
|
||||
switch n {
|
||||
case -1:
|
||||
if err == nil {
|
||||
err = ErrStatFault
|
||||
}
|
||||
dispatchErr(err)
|
||||
case 0:
|
||||
if err == nil {
|
||||
err = ErrStatRead
|
||||
}
|
||||
dispatchErr(err)
|
||||
case 1:
|
||||
dispatchErr(err)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
runtime.KeepAlive(w)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
dispatchErr(nil)
|
||||
case <-ctx.Done():
|
||||
dispatchErr(r.Close()) // this aborts Read with file already closed
|
||||
runtime.KeepAlive(w)
|
||||
}
|
||||
}()
|
||||
|
||||
// this gets closed by the caller
|
||||
*f.s = r
|
||||
return nil
|
||||
}
|
||||
102
helper/stub.go
102
helper/stub.go
@@ -1,102 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||
// it is part of the implementation of the helper stub.
|
||||
func InternalHelperStub() {
|
||||
// this test mocks the helper process
|
||||
var ap, sp string
|
||||
if v, ok := os.LookupEnv(HakureiHelper); !ok {
|
||||
return
|
||||
} else {
|
||||
ap = v
|
||||
}
|
||||
if v, ok := os.LookupEnv(HakureiStatus); !ok {
|
||||
panic(HakureiStatus)
|
||||
} else {
|
||||
sp = v
|
||||
}
|
||||
|
||||
genericStub(flagRestoreFiles(1, ap, sp))
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func newFile(fd int, name, p string) *os.File {
|
||||
present := false
|
||||
switch p {
|
||||
case "0":
|
||||
case "1":
|
||||
present = true
|
||||
default:
|
||||
panic(fmt.Sprintf("%s fd has unexpected presence value %q", name, p))
|
||||
}
|
||||
|
||||
f := os.NewFile(uintptr(fd), name)
|
||||
if !present && f != nil {
|
||||
panic(fmt.Sprintf("%s fd set but not present", name))
|
||||
}
|
||||
if present && f == nil {
|
||||
panic(fmt.Sprintf("%s fd preset but unset", name))
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
|
||||
argsFd := flag.Int("args", -1, "")
|
||||
statFd := flag.Int("fd", -1, "")
|
||||
_ = flag.CommandLine.Parse(os.Args[offset:])
|
||||
argsFile = newFile(*argsFd, "args", ap)
|
||||
statFile = newFile(*statFd, "stat", sp)
|
||||
return
|
||||
}
|
||||
|
||||
func genericStub(argsFile, statFile *os.File) {
|
||||
if argsFile != nil {
|
||||
// this output is checked by parent
|
||||
if _, err := io.Copy(os.Stdout, argsFile); err != nil {
|
||||
panic("cannot read args: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if statFile != nil {
|
||||
// simulate status pipe behaviour
|
||||
var epoll int
|
||||
if fd, err := syscall.EpollCreate1(0); err != nil {
|
||||
panic("cannot open epoll fd: " + err.Error())
|
||||
} else {
|
||||
defer func() {
|
||||
if err = syscall.Close(fd); err != nil {
|
||||
panic("cannot close epoll fd: " + err.Error())
|
||||
}
|
||||
}()
|
||||
epoll = fd
|
||||
}
|
||||
if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(statFile.Fd()), &syscall.EpollEvent{}); err != nil {
|
||||
panic("cannot add status pipe to epoll: " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := statFile.Write([]byte{'x'}); err != nil {
|
||||
panic("cannot write to status pipe: " + err.Error())
|
||||
}
|
||||
|
||||
// wait for status pipe close
|
||||
events := make([]syscall.EpollEvent, 1)
|
||||
if _, err := syscall.EpollWait(epoll, events, -1); err != nil {
|
||||
panic("cannot poll status pipe: " + err.Error())
|
||||
}
|
||||
if events[0].Events != syscall.EPOLLERR {
|
||||
panic(strconv.Itoa(int(events[0].Events)))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package helper_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/helper"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
|
||||
Reference in New Issue
Block a user