Files
kandev/apps/backend/internal/backendapp/agentctl.go
T
2026-08-21 19:28:21 +01:00

86 lines
3.0 KiB
Go

package backendapp
import (
"context"
"time"
agentctlclient "github.com/kandev/kandev/internal/agent/runtime/agentctl"
"github.com/kandev/kandev/internal/agent/runtime/agentctl/launcher"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/common/logger"
"go.uber.org/zap"
)
// agentctlLauncherResult holds the outputs of provideAgentctlLauncher.
type agentctlLauncherResult struct {
cleanup func() error
binaryPath string
}
// provideAgentctlLauncher starts the agentctl launcher for standalone runtime.
// agentctl is a core service that always runs - it's used by the Standalone runtime
// for agent execution on the host machine.
// If the configured port is unavailable, the launcher may fall back to an OS-assigned
// port. In that case, cfg.Agent.StandalonePort is updated to reflect the actual port.
func provideAgentctlLauncher(
ctx context.Context,
cfg *config.Config,
log *logger.Logger,
availability *agentctlclient.Availability,
) (*agentctlLauncherResult, error) {
l, cleanup, err := launcher.Provide(ctx, launcher.Config{
Host: cfg.Agent.StandaloneHost,
Port: cfg.Agent.StandalonePort,
StartupConfig: cfg.ManagedAgentctlStartupConfig(),
OnUnexpectedExit: availability.MarkUnavailable,
}, log)
if err != nil {
return nil, err
}
availability.MarkAvailable()
// Update config with the actual port (may differ if fallback was used)
if actualPort := l.Port(); actualPort != cfg.Agent.StandalonePort {
log.Info("agentctl port changed from configured value",
zap.Int("configured_port", cfg.Agent.StandalonePort),
zap.Int("actual_port", actualPort))
cfg.Agent.StandalonePort = actualPort
}
// Store the per-launch auth token so downstream clients can authenticate
cfg.Agent.StandaloneAuthToken = l.AuthToken()
// Store the agentctl control-server PID so local/standalone executor rows can
// carry a real host-local liveness handle (executors_running.local_pid).
cfg.Agent.StandalonePID = l.Pid()
return &agentctlLauncherResult{
cleanup: cleanup,
binaryPath: l.BinaryPath(),
}, nil
}
// waitForAgentctlControlHealthy waits for the agentctl control server to be healthy.
// This is called during startup to ensure agentctl is ready before accepting requests.
func waitForAgentctlControlHealthy(ctx context.Context, cfg *config.Config, log *logger.Logger) {
client := agentctlclient.NewControlClient(cfg.Agent.StandaloneHost, cfg.Agent.StandalonePort, log)
healthCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
var lastErr error
for {
attemptCtx, attemptCancel := context.WithTimeout(healthCtx, 1*time.Second)
err := client.Health(attemptCtx)
attemptCancel()
if err == nil {
log.Info("agentctl control server is healthy")
return
}
lastErr = err
if healthCtx.Err() != nil {
log.Warn("agentctl control server not ready; skipping resume wait", zap.Error(lastErr))
return
}
<-ticker.C
}
}