//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 }