184 lines
6.0 KiB
Go
184 lines
6.0 KiB
Go
// Package supervisor defines the narrow process-control seam shared by the
|
|
// client runtime and platform implementations. It intentionally contains no
|
|
// operating-system handles or process APIs.
|
|
package supervisor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
|
|
"github.com/rvbox/rvbox/internal/domain"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidStartSpec = errors.New("invalid supervisor start specification")
|
|
ErrUnsupported = errors.New("supervisor operation is unsupported")
|
|
)
|
|
|
|
// StartError carries durable context-selection evidence when a command is
|
|
// rejected before a process exists. The runtime records the evidence with
|
|
// the rejection event without treating the error as a second public protocol
|
|
// type. Unwrap keeps ordinary retry/error classification working.
|
|
type StartError struct {
|
|
Cause error
|
|
WindowsIdentity *rvboxv1.WindowsExecutionIdentity
|
|
}
|
|
|
|
func (err *StartError) Error() string {
|
|
if err == nil || err.Cause == nil {
|
|
return "supervisor start failed"
|
|
}
|
|
return err.Cause.Error()
|
|
}
|
|
|
|
func (err *StartError) Unwrap() error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return err.Cause
|
|
}
|
|
|
|
type SignalKind uint8
|
|
|
|
const (
|
|
SignalTerm SignalKind = iota + 1
|
|
SignalKill
|
|
)
|
|
|
|
// StartSpec is already validated at the protocol boundary. The supervisor
|
|
// still checks the identity/revision/source invariants because it is a second
|
|
// durability boundary and may be called after a restart.
|
|
type StartSpec struct {
|
|
IssueUUID domain.UUID
|
|
CommandRevision uint64
|
|
Execution *rvboxv1.ExecutionSpec
|
|
// ScriptBody is the verified, durable body for Execution.script. It is
|
|
// supplied by the client spool only after the declared digest/length have
|
|
// been checked; command_text requests leave it empty.
|
|
ScriptBody []byte
|
|
WorkingDirectory string
|
|
Environment map[string]string
|
|
ExecutionProfiles []string
|
|
}
|
|
|
|
func (spec StartSpec) Validate() error {
|
|
if _, err := domain.ParseUUIDv7(spec.IssueUUID.String()); err != nil || spec.CommandRevision == 0 || spec.Execution == nil || spec.Execution.Source == nil {
|
|
return ErrInvalidStartSpec
|
|
}
|
|
if spec.WorkingDirectory == "" {
|
|
return ErrInvalidStartSpec
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EffectiveIdentity is immutable process evidence captured before launch.
|
|
// Empty user/session fields mean a Session 0 service context.
|
|
type EffectiveIdentity struct {
|
|
Context string
|
|
SessionID uint32
|
|
SessionUserSID string
|
|
UserSID string
|
|
LogonSID string
|
|
Elevated bool
|
|
Integrity string
|
|
AttemptedContexts []string
|
|
SelectionDetail string
|
|
}
|
|
|
|
// WindowsIdentity converts the platform-neutral evidence to the public
|
|
// immutable status/event shape. Unknown contexts are deliberately omitted so
|
|
// the portable test supervisor never pretends to be a Windows launch.
|
|
func (identity EffectiveIdentity) WindowsIdentity() *rvboxv1.WindowsExecutionIdentity {
|
|
result := &rvboxv1.WindowsExecutionIdentity{SessionUserSid: identity.SessionUserSID, EffectiveUserSid: identity.UserSID, SelectionDetail: identity.SelectionDetail}
|
|
for _, contextName := range identity.AttemptedContexts {
|
|
if context, ok := windowsExecutionContext(contextName); ok {
|
|
result.AttemptedContexts = append(result.AttemptedContexts, context)
|
|
}
|
|
}
|
|
if context, ok := windowsExecutionContext(identity.Context); ok {
|
|
result.EffectiveContext = &context
|
|
if context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER || context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED || context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM {
|
|
session := identity.SessionID
|
|
result.SessionId = &session
|
|
}
|
|
}
|
|
if result.EffectiveContext == nil && len(result.AttemptedContexts) == 0 && result.SelectionDetail == "" && result.SessionUserSid == "" && result.EffectiveUserSid == "" {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func windowsExecutionContext(value string) (rvboxv1.WindowsExecutionContext, bool) {
|
|
switch value {
|
|
case "LOCAL_SERVICE":
|
|
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SERVICE, true
|
|
case "LOCAL_SYSTEM":
|
|
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SYSTEM, true
|
|
case "ACTIVE_USER":
|
|
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER, true
|
|
case "ACTIVE_USER_ELEVATED":
|
|
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED, true
|
|
case "ACTIVE_SYSTEM":
|
|
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM, true
|
|
default:
|
|
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_UNSPECIFIED, false
|
|
}
|
|
}
|
|
|
|
type Process interface {
|
|
IssueUUID() domain.UUID
|
|
Identity() EffectiveIdentity
|
|
// ReadOutput returns the next bounded stdout/stderr chunk. It continues
|
|
// until both child pipes reach EOF, so Wait never reports a terminal
|
|
// result before the captured output has drained.
|
|
ReadOutput(context.Context) (OutputChunk, error)
|
|
Wait(context.Context) (ExitStatus, error)
|
|
WriteStdin(context.Context, []byte, bool) error
|
|
CloseStdin(context.Context) error
|
|
}
|
|
|
|
// OutputChunk is intentionally raw. Compression, quota admission, local
|
|
// ordering, and wire sequencing belong to the client spool rather than the
|
|
// operating-system supervisor.
|
|
type OutputChunk struct {
|
|
Stream rvboxv1.StreamKind
|
|
Data []byte
|
|
}
|
|
|
|
type ExitStatus struct {
|
|
Code int32
|
|
Signaled bool
|
|
Signal SignalKind
|
|
StartedAt time.Time
|
|
FinishedAt time.Time
|
|
OutputDrained bool
|
|
Output bool
|
|
}
|
|
|
|
type SignalOutcome struct {
|
|
Delivered bool
|
|
Escalated bool
|
|
Detail string
|
|
ObservedAt time.Time
|
|
}
|
|
|
|
type ResourceSnapshot struct {
|
|
CPUTime time.Duration
|
|
ResidentBytes uint64
|
|
IOReadBytes uint64
|
|
IOWriteBytes uint64
|
|
ProcessCount uint64
|
|
ObservedAt time.Time
|
|
Complete bool
|
|
Detail string
|
|
}
|
|
|
|
type Supervisor interface {
|
|
Start(context.Context, StartSpec) (Process, error)
|
|
Signal(context.Context, Process, SignalKind) (SignalOutcome, error)
|
|
Snapshot(context.Context, Process) (ResourceSnapshot, error)
|
|
StopAll(context.Context) error
|
|
}
|