Files
2026-07-20 17:02:37 +00:00

126 lines
3.6 KiB
Go

//go:build linux
package agent
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/nezhahq/nezha/integration/agentcompat/internal/workspace"
)
type PreparedBinaryUsageError struct {
Operation string
Reason string
}
func (err *PreparedBinaryUsageError) Error() string {
return fmt.Sprintf("prepared agent binary %s: %s", err.Operation, err.Reason)
}
// PreparedBinary owns a build-only workspace. Each successful Start lease keeps
// that workspace alive; Agent workspaces own their own config, logs, and process tracking.
type PreparedBinary struct {
workspace *workspace.Workspace
binaryPath string
mu sync.Mutex
consumers int
closed bool
}
func PrepareBinary(ctx context.Context, sourceDir string) (*PreparedBinary, error) {
if err := validateSourceDir(sourceDir); err != nil {
return nil, &PreparedBinaryUsageError{Operation: "prepare", Reason: err.Error()}
}
workspaceRoot, err := workspace.New(context.WithoutCancel(ctx))
if err != nil {
return nil, fmt.Errorf("create prepared agent workspace: %w", err)
}
binaryPath, err := workspaceRoot.Build(ctx, agentBuildSpec(sourceDir))
if err != nil {
return nil, closePreparedWorkspace(workspaceRoot, err)
}
if err := exposePreparedBinary(workspaceRoot.Root(), binaryPath); err != nil {
return nil, closePreparedWorkspace(workspaceRoot, err)
}
return &PreparedBinary{workspace: workspaceRoot, binaryPath: binaryPath}, nil
}
func closePreparedWorkspace(workspaceRoot *workspace.Workspace, cause error) error {
return fmt.Errorf("prepare agent binary: %w", closeError(cause, workspaceRoot.Close()))
}
func exposePreparedBinary(root, binaryPath string) error {
for _, path := range []string{root, filepath.Dir(binaryPath)} {
if err := os.Chmod(path, 0o711); err != nil { // #nosec G302 -- Directories need search permission for the credentialed test agent without granting directory reads; payloads and logs remain private.
return fmt.Errorf("make prepared agent binary executable: %w", err)
}
}
return nil
}
func (prepared *PreparedBinary) BinaryPath() string {
if prepared == nil {
return ""
}
prepared.mu.Lock()
defer prepared.mu.Unlock()
return prepared.binaryPath
}
func (prepared *PreparedBinary) WorkspaceRoot() string {
if prepared == nil {
return ""
}
prepared.mu.Lock()
defer prepared.mu.Unlock()
return prepared.workspace.Root()
}
func (prepared *PreparedBinary) acquire() (string, func(), error) {
if prepared == nil {
return "", nil, &PreparedBinaryUsageError{Operation: "start", Reason: "is nil"}
}
prepared.mu.Lock()
defer prepared.mu.Unlock()
if prepared.workspace == nil || prepared.binaryPath == "" {
return "", nil, &PreparedBinaryUsageError{Operation: "start", Reason: "is uninitialized"}
}
if prepared.closed {
return "", nil, &PreparedBinaryUsageError{Operation: "start", Reason: "is closed"}
}
prepared.consumers++
return prepared.binaryPath, prepared.release, nil
}
func (prepared *PreparedBinary) release() {
prepared.mu.Lock()
defer prepared.mu.Unlock()
prepared.consumers--
}
func (prepared *PreparedBinary) Close() error {
if prepared == nil {
return &PreparedBinaryUsageError{Operation: "close", Reason: "is nil"}
}
prepared.mu.Lock()
defer prepared.mu.Unlock()
if prepared.workspace == nil || prepared.binaryPath == "" {
return &PreparedBinaryUsageError{Operation: "close", Reason: "is uninitialized"}
}
if prepared.closed {
return nil
}
if prepared.consumers != 0 {
return &PreparedBinaryUsageError{Operation: "close", Reason: "has active consumers"}
}
if err := prepared.workspace.Close(); err != nil {
return fmt.Errorf("close prepared agent workspace: %w", err)
}
prepared.closed = true
return nil
}