aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/helper/proc
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-11-12 23:12:38 +0900
committerOphestra <cat@gensokyo.uk>2025-11-12 23:16:13 +0900
commitf347d44c2281f507e3786aa498f634bfa4c92d84 (patch)
treefe85b41d5f6a5e308b87585f46efd64f90221e2b /internal/helper/proc
parentb5630f6883c874d458d9ae3c666cfb7ae5e3121d (diff)
internal/helper: relocate from helper
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>
Diffstat (limited to 'internal/helper/proc')
-rw-r--r--internal/helper/proc/files.go162
-rw-r--r--internal/helper/proc/pipe.go110
2 files changed, 272 insertions, 0 deletions
diff --git a/internal/helper/proc/files.go b/internal/helper/proc/files.go
new file mode 100644
index 00000000..0612718e
--- /dev/null
+++ b/internal/helper/proc/files.go
@@ -0,0 +1,162 @@
+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
+}
diff --git a/internal/helper/proc/pipe.go b/internal/helper/proc/pipe.go
new file mode 100644
index 00000000..838c4ae4
--- /dev/null
+++ b/internal/helper/proc/pipe.go
@@ -0,0 +1,110 @@
+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
+}