mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
171 lines
5.3 KiB
Go
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
|
|
}
|