mirror of
https://github.com/cirruslabs/orchard.git
synced 2026-09-29 19:41:15 +02:00
154 lines
4.0 KiB
Go
154 lines
4.0 KiB
Go
//nolint:err113,mnd // Preserve the original host-process errors and retry policy.
|
|
package hostprocess
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"maps"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/avast/retry-go/v4"
|
|
"github.com/cirruslabs/orchard/internal/worker/socketalias"
|
|
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
|
)
|
|
|
|
type Process struct {
|
|
cancel context.CancelFunc
|
|
socketPath string
|
|
done chan struct{}
|
|
}
|
|
|
|
func NewProcess(
|
|
spec v1.HostProcess,
|
|
workerName string,
|
|
vmName string,
|
|
controlSocket string,
|
|
) (*Process, error) {
|
|
// Create a directory where the host process will create its Unix socket
|
|
//
|
|
// To keep the Unix socket path short enough and fit the platform limit,
|
|
// we're specifically requesting "/var/tmp" instead of "/var/folders/.../T/".
|
|
//
|
|
// And unlike "/tmp", "/var/tmp" is not subject to macOS's nightly cleanup.
|
|
runtimeDir, err := os.MkdirTemp("/var/tmp", "orchard-hp-")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use a fixed socket name within the process-specific runtime directory
|
|
socketPath := filepath.Join(runtimeDir, "process.sock")
|
|
|
|
// Route the Tart control socket through the runtime directory as well
|
|
// to work around macOS's 104-byte Unix-domain socket limit
|
|
controlSocket, err = socketalias.Create(runtimeDir, controlSocket)
|
|
if err != nil {
|
|
return nil, errors.Join(err, os.RemoveAll(runtimeDir))
|
|
}
|
|
|
|
// Give the process an independently cancellable lifetime
|
|
cmdCtx, cmdCtxCancel := context.WithCancel(context.Background())
|
|
|
|
//nolint:gosec // Executing the configured host process is the purpose of this API
|
|
command := exec.CommandContext(
|
|
cmdCtx,
|
|
spec.Program,
|
|
expandArgs(spec.Args, workerName, vmName, controlSocket, socketPath)...,
|
|
)
|
|
command.Dir = runtimeDir
|
|
|
|
// Ensure that we terminate the host process and any children it spawns
|
|
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
command.Cancel = func() error {
|
|
return syscall.Kill(-command.Process.Pid, syscall.SIGKILL)
|
|
}
|
|
|
|
// Craft the environment starting from the host process specification
|
|
env := make(map[string]string)
|
|
|
|
maps.Copy(env, spec.Env)
|
|
|
|
// Preserve only PATH from the worker and make Orchard-managed variables authoritative
|
|
env["PATH"] = os.Getenv("PATH")
|
|
env["ORCHARD_WORKER_NAME"] = workerName
|
|
env["ORCHARD_VM_NAME"] = vmName
|
|
env["ORCHARD_VM_CONTROL_SOCKET"] = controlSocket
|
|
env["ORCHARD_PROCESS_SOCKET"] = socketPath
|
|
|
|
// Convert the environment to the format expected by exec.Cmd
|
|
for name, value := range env {
|
|
command.Env = append(command.Env, name+"="+value)
|
|
}
|
|
|
|
if err := command.Start(); err != nil {
|
|
cmdCtxCancel()
|
|
|
|
return nil, errors.Join(err, os.RemoveAll(runtimeDir))
|
|
}
|
|
|
|
process := &Process{
|
|
cancel: cmdCtxCancel,
|
|
socketPath: socketPath,
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
go func() {
|
|
defer close(process.done)
|
|
|
|
// Reap the process; callers observe termination through done regardless of exit status
|
|
_ = command.Wait()
|
|
_ = command.Cancel()
|
|
_ = os.RemoveAll(runtimeDir)
|
|
}()
|
|
|
|
return process, nil
|
|
}
|
|
|
|
func (process *Process) Dial(ctx context.Context) (net.Conn, error) {
|
|
var dialer net.Dialer
|
|
|
|
return retry.DoWithData(func() (net.Conn, error) {
|
|
select {
|
|
case <-process.done:
|
|
return nil, retry.Unrecoverable(errors.New("process exited before accepting connections"))
|
|
default:
|
|
// Continue
|
|
}
|
|
|
|
return dialer.DialContext(ctx, "unix", process.socketPath)
|
|
},
|
|
retry.Context(ctx),
|
|
retry.Attempts(200),
|
|
retry.Delay(50*time.Millisecond),
|
|
retry.DelayType(retry.FixedDelay),
|
|
retry.LastErrorOnly(true),
|
|
)
|
|
}
|
|
|
|
func (process *Process) Close() {
|
|
process.cancel()
|
|
<-process.done
|
|
}
|
|
|
|
func expandArgs(args []string, workerName string, vmName string, controlSocket string, socketPath string) []string {
|
|
replacer := strings.NewReplacer(
|
|
"${ORCHARD_WORKER_NAME}", workerName,
|
|
"${ORCHARD_VM_NAME}", vmName,
|
|
"${ORCHARD_VM_CONTROL_SOCKET}", controlSocket,
|
|
"${ORCHARD_PROCESS_SOCKET}", socketPath,
|
|
)
|
|
|
|
var expanded []string
|
|
|
|
for _, arg := range args {
|
|
expanded = append(expanded, replacer.Replace(arg))
|
|
}
|
|
|
|
return expanded
|
|
}
|