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,344 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tentacle
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (session *Session) run() {
|
||||
var cleanupError error
|
||||
starting := true
|
||||
ready := func() {
|
||||
if starting {
|
||||
starting = false
|
||||
<-session.manager.starting
|
||||
close(session.ready)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
session.cancel()
|
||||
session.mutex.Lock()
|
||||
if cleanupError != nil {
|
||||
session.fail_locked(cleanupError)
|
||||
// Even a requested disable must report incomplete containment.
|
||||
session.state, session.failure = Failed, cleanupError
|
||||
} else if session.state == Stopping {
|
||||
session.state = Stopped
|
||||
} else if session.state != Failed {
|
||||
session.fail_locked(ErrExited)
|
||||
}
|
||||
session.mutex.Unlock()
|
||||
session.callbacks.Wait()
|
||||
// A retained status/session handle must not retain unsent argument text.
|
||||
for len(session.outbound) != 0 {
|
||||
<-session.outbound
|
||||
}
|
||||
ready()
|
||||
session.manager.mutex.Lock()
|
||||
if cleanupError != nil {
|
||||
// Keep the admission slot and reject further starts. Do not claim
|
||||
// capacity was reclaimed when owned processes may remain alive.
|
||||
session.manager.cleanupError = cleanupError
|
||||
} else {
|
||||
delete(session.manager.sessions, session.spec.ID)
|
||||
}
|
||||
close(session.done)
|
||||
session.manager.mutex.Unlock()
|
||||
}()
|
||||
select {
|
||||
case <-session.stop:
|
||||
return
|
||||
case <-session.ctx.Done():
|
||||
session.fail(session.ctx.Err())
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Use owned files instead of exec's copier goroutines. Closing our ends
|
||||
// unblocks workers even when a descendant retains an inherited pipe.
|
||||
var pipes []*os.File
|
||||
defer func() {
|
||||
for _, pipe := range pipes {
|
||||
pipe.Close()
|
||||
}
|
||||
}()
|
||||
newPipe := func() (*os.File, *os.File, error) {
|
||||
reader, writer, err := os.Pipe()
|
||||
if err == nil {
|
||||
pipes = append(pipes, reader, writer)
|
||||
}
|
||||
return reader, writer, err
|
||||
}
|
||||
childInput, input, err := newPipe()
|
||||
if err != nil {
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
output, childOutput, err := newPipe()
|
||||
if err != nil {
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
diagnostics, childDiagnostics, err := newPipe()
|
||||
if err != nil {
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
command := exec.Command(session.spec.Program[0], session.spec.Program[1:]...)
|
||||
supervised := len(session.manager.supervisorProgram) != 0
|
||||
var lifetime, childLifetime, configuration, childConfiguration *os.File
|
||||
if supervised {
|
||||
childLifetime, lifetime, err = newPipe()
|
||||
if err != nil {
|
||||
session.fail(ErrSupervision)
|
||||
return
|
||||
}
|
||||
childConfiguration, configuration, err = newPipe()
|
||||
if err != nil {
|
||||
session.fail(ErrSupervision)
|
||||
return
|
||||
}
|
||||
program := session.manager.supervisorProgram
|
||||
command = exec.Command(program[0], program[1:]...)
|
||||
command.ExtraFiles = []*os.File{childLifetime, childConfiguration}
|
||||
} else {
|
||||
command.Dir = session.spec.Directory
|
||||
}
|
||||
command.Stdin, command.Stdout, command.Stderr = childInput, childOutput, childDiagnostics
|
||||
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
var group *cgroup
|
||||
if containment := session.manager.containment; containment != nil {
|
||||
group, err = create_cgroup(filepath.Join(containment.group.path, session.identity), containment.limits.PerSession, 0)
|
||||
if err != nil {
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
defer func() { cleanupError = group.kill_and_remove(containment.limits.CleanupTimeout) }()
|
||||
// clone3(CLONE_INTO_CGROUP) applies limits before the child executes.
|
||||
// Moving an already-running PID would leave a fork/allocation race.
|
||||
command.SysProcAttr.UseCgroupFD = true
|
||||
command.SysProcAttr.CgroupFD = int(group.file.Fd())
|
||||
}
|
||||
var waitError error
|
||||
exited := make(chan struct{})
|
||||
err = command.Start()
|
||||
if err == nil {
|
||||
go func() { waitError = command.Wait(); close(exited) }()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Do not retain the command line, environment or child-supplied text.
|
||||
if group != nil {
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) {
|
||||
session.fail(fmt.Errorf("%w: child placement/exec: %w", ErrContainment, errno))
|
||||
} else {
|
||||
session.fail(ErrContainment)
|
||||
}
|
||||
} else {
|
||||
session.fail(ErrExited)
|
||||
}
|
||||
return
|
||||
}
|
||||
childInput.Close()
|
||||
childOutput.Close()
|
||||
childDiagnostics.Close()
|
||||
var workers sync.WaitGroup
|
||||
if supervised {
|
||||
childLifetime.Close()
|
||||
childConfiguration.Close()
|
||||
workers.Add(1)
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
_ = json.NewEncoder(configuration).Encode(session.spec)
|
||||
configuration.Close()
|
||||
}()
|
||||
}
|
||||
workers.Add(3)
|
||||
go func() { defer workers.Done(); session.write_requests(input) }()
|
||||
go func() { defer workers.Done(); session.read_replies(output) }()
|
||||
go func() { defer workers.Done(); session.drain_diagnostics(diagnostics) }()
|
||||
defer func() {
|
||||
session.cancel()
|
||||
if supervised {
|
||||
lifetime.Close()
|
||||
configuration.Close()
|
||||
}
|
||||
if group != nil {
|
||||
_ = write_control(group.path, "cgroup.kill", "1")
|
||||
if command.Process != nil {
|
||||
_ = command.Process.Kill()
|
||||
}
|
||||
} else if !supervised {
|
||||
// Explicit trusted-fixture mode only; detached descendants escape.
|
||||
_ = syscall.Kill(-command.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
input.Close()
|
||||
output.Close()
|
||||
diagnostics.Close()
|
||||
if supervised && group == nil {
|
||||
select {
|
||||
case <-exited:
|
||||
if waitError != nil {
|
||||
cleanupError = ErrSupervision
|
||||
}
|
||||
case <-time.After(DefaultResourceLimits().CleanupTimeout + time.Second):
|
||||
cleanupError = ErrSupervision
|
||||
_ = command.Process.Kill()
|
||||
<-exited
|
||||
}
|
||||
} else {
|
||||
<-exited
|
||||
}
|
||||
workers.Wait()
|
||||
}()
|
||||
|
||||
session.mutex.Lock()
|
||||
var initialized <-chan Result
|
||||
if session.state == Starting && session.ctx.Err() == nil {
|
||||
apis := []string{"commands"}
|
||||
if session.spec.Presentation {
|
||||
apis = append(apis, "presentation")
|
||||
}
|
||||
if session.spec.Interaction {
|
||||
apis = append(apis, "interaction")
|
||||
}
|
||||
if session.spec.ReaderSettings {
|
||||
apis = append(apis, "reader-settings")
|
||||
}
|
||||
initialized, err = session.request_locked(session.ctx, "initialize", Initialization{
|
||||
Protocol: Version{Major: 1, Minor: 0}, Session: session.identity,
|
||||
APIs: apis, Capabilities: apis,
|
||||
}, session.manager.limits.StartupTimeout)
|
||||
} else {
|
||||
err = ErrUnavailable
|
||||
}
|
||||
session.mutex.Unlock()
|
||||
if err != nil {
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case result := <-initialized:
|
||||
if result.Err != nil {
|
||||
session.fail(result.Err)
|
||||
return
|
||||
}
|
||||
var registration Registration
|
||||
if decode_strict(result.Value, ®istration) != nil || validate_registration(registration, session.identity) != nil {
|
||||
session.fail(ErrProtocol)
|
||||
return
|
||||
}
|
||||
for _, command := range registration.Commands {
|
||||
if command.Reader && !session.spec.ReaderSettings || command.Presentation && !session.spec.Presentation || command.Interaction && (!command.Presentation || !session.spec.Interaction) {
|
||||
session.fail(ErrProtocol)
|
||||
return
|
||||
}
|
||||
}
|
||||
if registration.Extensions != nil && !session.spec.ReaderSettings {
|
||||
session.fail(ErrProtocol)
|
||||
return
|
||||
}
|
||||
session.mutex.Lock()
|
||||
if session.state == Starting && session.ctx.Err() == nil {
|
||||
session.commands = registration.Commands
|
||||
if registration.Extensions != nil {
|
||||
session.extensions = *registration.Extensions
|
||||
}
|
||||
session.state = Ready
|
||||
}
|
||||
session.mutex.Unlock()
|
||||
ready()
|
||||
case <-session.ctx.Done():
|
||||
session.fail(session.ctx.Err())
|
||||
return
|
||||
case <-exited:
|
||||
session.fail(ErrExited)
|
||||
return
|
||||
case <-session.stop:
|
||||
session.wait_shutdown(exited)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-session.ctx.Done():
|
||||
session.fail(session.ctx.Err())
|
||||
case <-exited:
|
||||
session.fail(ErrExited)
|
||||
case <-session.stop:
|
||||
session.wait_shutdown(exited)
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) wait_shutdown(exited <-chan struct{}) {
|
||||
timer := time.NewTimer(session.manager.limits.ShutdownTimeout)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-exited:
|
||||
case <-timer.C:
|
||||
case <-session.ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) write_requests(writer io.Writer) {
|
||||
for {
|
||||
select {
|
||||
case <-session.ctx.Done():
|
||||
return
|
||||
case item := <-session.outbound:
|
||||
if item.id != "" {
|
||||
session.mutex.Lock()
|
||||
call := session.pending[item.id]
|
||||
eligible := call != nil && call.ctx.Err() == nil
|
||||
session.mutex.Unlock()
|
||||
if !eligible {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, err := writer.Write(item.frame); err != nil {
|
||||
session.fail(ErrExited)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) read_replies(reader io.Reader) {
|
||||
buffer := bufio.NewReaderSize(reader, 4096)
|
||||
for session.ctx.Err() == nil {
|
||||
frame, err := read_frame(buffer)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = ErrExited
|
||||
}
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
reply, err := decode_response(frame)
|
||||
if err != nil {
|
||||
session.fail(err)
|
||||
return
|
||||
}
|
||||
session.accept_reply(reply)
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) drain_diagnostics(reader io.Reader) {
|
||||
count, err := io.Copy(io.Discard, io.LimitReader(reader, MaxDiagnosticBytes+1))
|
||||
if count > MaxDiagnosticBytes {
|
||||
session.fail(ErrLimit)
|
||||
} else if err != nil && session.ctx.Err() == nil {
|
||||
session.fail(ErrExited)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user