mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-21 10:40:13 +00:00
test(agentcompat): add agent runtime harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user