Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tentacle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrSupervision = errors.New("tentacle process supervision failed")
|
||||
|
||||
// RunSupervisor is a private per-session helper. Descriptors 0..2 carry only
|
||||
// the Tentacle protocol and diagnostics; 3 is a reader-owned lifetime pipe and
|
||||
// 4 is a bounded Spec. The child never inherits either private descriptor.
|
||||
// A separate subreaper keeps ownership of double-forked/setsid descendants
|
||||
// without making the reader a subreaper for unrelated desktop helpers.
|
||||
func RunSupervisor(ctx context.Context) error {
|
||||
// ExtraFiles arrive in blocking mode. Register the private pipes with Go's
|
||||
// poller so Close interrupts their readers on cancellation and child exit.
|
||||
for _, descriptor := range []int{3, 4} {
|
||||
syscall.CloseOnExec(descriptor)
|
||||
if err := syscall.SetNonblock(descriptor, true); err != nil {
|
||||
return fmt.Errorf("%w: private pipe: %w", ErrSupervision, err)
|
||||
}
|
||||
}
|
||||
lifetime := os.NewFile(3, "tentacle-lifetime")
|
||||
configuration := os.NewFile(4, "tentacle-spec")
|
||||
if lifetime == nil || configuration == nil {
|
||||
return ErrSupervision
|
||||
}
|
||||
defer configuration.Close()
|
||||
// PR_SET_CHILD_SUBREAPER is a Linux facility independent of the init system.
|
||||
if _, _, errno := syscall.Syscall6(syscall.SYS_PRCTL, 36, 1, 0, 0, 0, 0); errno != 0 {
|
||||
lifetime.Close()
|
||||
return fmt.Errorf("%w: subreaper: %w", ErrSupervision, errno)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
readerDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readerDone)
|
||||
_, _ = io.Copy(io.Discard, lifetime)
|
||||
cancel()
|
||||
}()
|
||||
defer func() { lifetime.Close(); <-readerDone }()
|
||||
// Closing this descriptor also interrupts a partial startup on termination.
|
||||
stop := context.AfterFunc(ctx, func() { configuration.Close() })
|
||||
defer stop()
|
||||
data, err := io.ReadAll(io.LimitReader(configuration, MaxFrameBytes+1))
|
||||
if err != nil || len(data) > MaxFrameBytes {
|
||||
// No child exists yet. A partial configuration can result from startup
|
||||
// cancellation. Protocol EOF fails registration, while a zero helper
|
||||
// exit certifies cleanup and lets the host release its admission slot.
|
||||
return nil
|
||||
}
|
||||
configuration.Close()
|
||||
var spec Spec
|
||||
if decode_strict(data, &spec) != nil || validate_spec(spec) != nil {
|
||||
return nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
// Pdeathsig follows the creating thread; retain it for the child lifetime.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
command := exec.Command(spec.Program[0], spec.Program[1:]...)
|
||||
command.Dir = spec.Directory
|
||||
command.Stdin, command.Stdout, command.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL}
|
||||
childSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(childSignals, syscall.SIGCHLD)
|
||||
defer signal.Stop(childSignals)
|
||||
if err := command.Start(); err != nil {
|
||||
// No process was started. Protocol EOF reports the startup failure;
|
||||
// successful cleanup keeps the manager available for a later retry.
|
||||
return nil
|
||||
}
|
||||
exited := make(chan struct{})
|
||||
go func() { _ = command.Wait(); close(exited) }()
|
||||
var supervisionError error
|
||||
running:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break running
|
||||
case <-exited:
|
||||
break running
|
||||
case <-childSignals:
|
||||
// The subreaper owns orphaned descendants for the entire session,
|
||||
// not just teardown. Only command.Wait may reap the main child.
|
||||
supervisionError = reap_exited_supervised_children(command.Process.Pid)
|
||||
if supervisionError != nil {
|
||||
break running
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = command.Process.Kill()
|
||||
<-exited
|
||||
return errors.Join(supervisionError, reap_supervised_children(DefaultResourceLimits().CleanupTimeout))
|
||||
}
|
||||
|
||||
func supervised_children() ([]int, error) {
|
||||
paths, err := filepath.Glob("/proc/self/task/*/children")
|
||||
if err != nil || len(paths) == 0 {
|
||||
return nil, ErrSupervision
|
||||
}
|
||||
var children []int
|
||||
for _, path := range paths {
|
||||
data, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) || errors.Is(err, syscall.ESRCH) { // A runtime thread exited during the scan.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ErrSupervision
|
||||
}
|
||||
for _, value := range strings.Fields(string(data)) {
|
||||
pid, err := strconv.Atoi(value)
|
||||
if err != nil || pid <= 1 {
|
||||
return nil, ErrSupervision
|
||||
}
|
||||
children = append(children, pid)
|
||||
}
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
|
||||
func reap_exited_supervised_children(mainPID int) error {
|
||||
children, err := supervised_children()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, childPID := range children {
|
||||
if childPID == mainPID {
|
||||
continue
|
||||
}
|
||||
for {
|
||||
_, err := syscall.Wait4(childPID, nil, syscall.WNOHANG, nil)
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
if err != nil && err != syscall.ECHILD {
|
||||
return fmt.Errorf("%w: reap exited descendant: %w", ErrSupervision, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only this helper's children are killed. A child PID cannot be reused until
|
||||
// it has been reaped, and no other goroutine calls Wait after command.Wait joins.
|
||||
// Killing direct children adopts the next generation, including detached ones.
|
||||
func reap_supervised_children(timeout time.Duration) error {
|
||||
deadline := time.NewTimer(timeout)
|
||||
defer deadline.Stop()
|
||||
tick := time.NewTicker(5 * time.Millisecond)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
children, err := supervised_children()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pid := range children {
|
||||
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && err != syscall.ESRCH {
|
||||
return fmt.Errorf("%w: terminate descendant: %w", ErrSupervision, err)
|
||||
}
|
||||
}
|
||||
for {
|
||||
var status syscall.WaitStatus
|
||||
pid, err := syscall.Wait4(-1, &status, syscall.WNOHANG, nil)
|
||||
if err == syscall.ECHILD {
|
||||
return nil
|
||||
}
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: reap descendant: %w", ErrSupervision, err)
|
||||
}
|
||||
if pid == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-deadline.C:
|
||||
return fmt.Errorf("%w: descendant cleanup deadline expired", ErrSupervision)
|
||||
case <-tick.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user