Files
2026-07-20 04:45:05 +00:00

171 lines
5.3 KiB
Go

//go:build linux
package agent
import (
"context"
"errors"
"fmt"
"strings"
"syscall"
"time"
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
)
type ProcessIdentity struct {
Generation uint64
PID int
ProcessGroupID int
}
type ProcessTransition struct {
Previous ProcessIdentity
Current ProcessIdentity
}
type processGeneration struct {
supervisor *processharness.Supervisor
identity ProcessIdentity
record processharness.CleanupRecord
}
func (agent *Agent) RuntimeIdentity() ProcessIdentity {
agent.processMu.Lock()
defer agent.processMu.Unlock()
if agent.currentProcess == nil {
return ProcessIdentity{}
}
return agent.currentProcess.identity
}
func (agent *Agent) StartProcess(ctx context.Context) (ProcessTransition, error) {
agent.processMu.Lock()
defer agent.processMu.Unlock()
if agent.closed {
return ProcessTransition{}, errors.New("agent is closed")
}
if agent.currentProcess != nil {
return ProcessTransition{}, errors.New("agent process is already running")
}
agent.generation++
logFile, err := agent.workspace.Log(fmt.Sprintf("agent-%s-generation-%d", strings.ReplaceAll(agent.uuid, "-", ""), agent.generation))
if err != nil {
return ProcessTransition{}, err
}
agent.logPath = logFile.Name()
newSupervisor := processharness.NewSupervisor
if agent.startConfig.newSupervisor != nil {
newSupervisor = agent.startConfig.newSupervisor
}
supervisor := newSupervisor(ctx, processharness.Spec{
Name: "agent", Path: agent.binaryPath, Args: []string{"-c", agent.configPath}, Env: agent.environment,
Stdout: logFile, Stderr: logFile, MaxLogBytes: agentMaxLogBytes,
TerminateTimeout: agentStopTimeout, KillTimeout: agentKillTimeout,
Credential: agent.startConfig.Credential,
})
if err := supervisor.Start(); err != nil {
return ProcessTransition{}, err
}
identity := ProcessIdentity{Generation: agent.generation, PID: supervisor.PID(), ProcessGroupID: supervisor.ProcessGroupID()}
generation := &processGeneration{supervisor: supervisor, identity: identity, record: supervisor.CleanupRecord()}
// Register the started generation before post-start setup so failures remain cleanup-owned.
agent.currentProcess = generation
agent.supervisor = supervisor
agent.processes = append(agent.processes, generation)
if err := agent.trackPID(identity.PID); err != nil {
return agent.rollbackStartedProcess(ctx, generation, err)
}
if err := agent.trackProcessGroup(identity.ProcessGroupID); err != nil {
return agent.rollbackStartedProcess(ctx, generation, err)
}
previous := ProcessIdentity{}
return ProcessTransition{Previous: previous, Current: identity}, nil
}
func (agent *Agent) rollbackStartedProcess(ctx context.Context, generation *processGeneration, trackingErr error) (ProcessTransition, error) {
agent.currentProcess = nil
agent.supervisor = nil
rollbackContext, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
defer cancel()
rollbackErr := generation.supervisor.Stop(rollbackContext)
generation.record = generation.supervisor.CleanupRecord()
return ProcessTransition{}, errors.Join(trackingErr, rollbackErr)
}
func (agent *Agent) StopProcess(ctx context.Context) (ProcessTransition, error) {
agent.processMu.Lock()
process := agent.currentProcess
if process == nil {
agent.processMu.Unlock()
return ProcessTransition{}, errors.New("agent process is not running")
}
agent.currentProcess = nil
agent.supervisor = nil
agent.processMu.Unlock()
if err := process.supervisor.Stop(ctx); err != nil {
return ProcessTransition{Previous: process.identity}, fmt.Errorf("stop agent process: %w", err)
}
process.record = process.supervisor.CleanupRecord()
return ProcessTransition{Previous: process.identity}, nil
}
func (agent *Agent) RestartProcess(ctx context.Context) (ProcessTransition, error) {
stopped, err := agent.StopProcess(ctx)
if err != nil {
return stopped, err
}
started, err := agent.StartProcess(ctx)
if err != nil {
return ProcessTransition{Previous: stopped.Previous}, err
}
return ProcessTransition{Previous: stopped.Previous, Current: started.Current}, nil
}
func (agent *Agent) Restart(ctx context.Context) error {
_, err := agent.RestartProcess(ctx)
return err
}
func (agent *Agent) Close(ctx context.Context) error {
agent.processMu.Lock()
agent.closed = true
agent.processMu.Unlock()
return agent.Stop(ctx)
}
func (agent *Agent) closeProcesses(ctx context.Context) error {
agent.processMu.Lock()
processes := append([]*processGeneration(nil), agent.processes...)
agent.processMu.Unlock()
var cleanupError error
for _, process := range processes {
stopContext, cancel := context.WithTimeout(ctx, 15*time.Second)
cleanupError = errors.Join(cleanupError, process.supervisor.Stop(stopContext))
cancel()
process.record = process.supervisor.CleanupRecord()
}
return cleanupError
}
func (agent *Agent) processesQuiescent() bool {
agent.processMu.Lock()
processes := append([]*processGeneration(nil), agent.processes...)
agent.processMu.Unlock()
for _, process := range processes {
select {
case <-process.supervisor.Exited():
default:
return false
}
err := syscall.Kill(-process.identity.ProcessGroupID, 0)
if err == nil || errors.Is(err, syscall.EPERM) {
return false
}
if !errors.Is(err, syscall.ESRCH) {
return false
}
}
return true
}