2026-03-18 15:07:03 +00:00
|
|
|
package process
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os/exec"
|
|
|
|
|
|
2026-04-04 01:19:59 +00:00
|
|
|
core "dappco.re/go/core"
|
fix(go-process): update stale coreerr alias to dappco.re/go/log (AX-6)
Updated `coreerr "dappco.re/go/core/log"` → `coreerr "dappco.re/go/log"`
across 12 files (actions.go, daemon.go, errors.go, exec/exec.go,
health.go, pkg/api/provider.go, process.go, process_global.go,
program.go, registry.go, runner.go, service.go). No stale path
remains in .go.
Pre-existing blocker (out of ticket scope): `dappco.re/go/io@v0.4.2`
is 404 from module proxy — affects `go test ./...` but is unrelated
to this import rename.
Closes tasks.lthn.sh/view.php?id=718
Co-authored-by: Codex <noreply@openai.com>
2026-04-24 21:45:29 +01:00
|
|
|
coreerr "dappco.re/go/log"
|
2026-03-18 15:07:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ErrProgramNotFound is returned when Find cannot locate the binary on PATH.
|
|
|
|
|
// Callers may use errors.Is to detect this condition.
|
|
|
|
|
var ErrProgramNotFound = coreerr.E("", "program: binary not found in PATH", nil)
|
|
|
|
|
|
2026-04-04 00:18:14 +00:00
|
|
|
// ErrProgramContextRequired is returned when Run or RunDir is called without a context.
|
|
|
|
|
var ErrProgramContextRequired = coreerr.E("", "program: command context is required", nil)
|
|
|
|
|
|
|
|
|
|
// ErrProgramNameRequired is returned when Run or RunDir is called without a program name.
|
|
|
|
|
var ErrProgramNameRequired = coreerr.E("", "program: program name is empty", nil)
|
|
|
|
|
|
2026-03-18 15:07:03 +00:00
|
|
|
// Program represents a named executable located on the system PATH.
|
2026-04-04 00:39:27 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// git := &process.Program{Name: "git"}
|
|
|
|
|
// if err := git.Find(); err != nil { return err }
|
|
|
|
|
// out, err := git.Run(ctx, "status")
|
2026-03-18 15:07:03 +00:00
|
|
|
type Program struct {
|
|
|
|
|
// Name is the binary name (e.g. "go", "node", "git").
|
|
|
|
|
Name string
|
|
|
|
|
// Path is the absolute path resolved by Find.
|
2026-04-04 00:39:27 +00:00
|
|
|
// Example: "/usr/bin/git"
|
2026-03-18 15:07:03 +00:00
|
|
|
// If empty, Run and RunDir fall back to Name for OS PATH resolution.
|
|
|
|
|
Path string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find resolves the program's absolute path using exec.LookPath.
|
|
|
|
|
// Returns ErrProgramNotFound (wrapped) if the binary is not on PATH.
|
2026-04-04 00:39:27 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// if err := p.Find(); err != nil { return err }
|
2026-03-18 15:07:03 +00:00
|
|
|
func (p *Program) Find() error {
|
2026-04-04 03:10:39 +00:00
|
|
|
target := p.Path
|
2026-04-04 03:07:13 +00:00
|
|
|
if target == "" {
|
2026-04-04 03:10:39 +00:00
|
|
|
target = p.Name
|
2026-04-04 03:07:13 +00:00
|
|
|
}
|
|
|
|
|
if target == "" {
|
2026-03-18 15:07:03 +00:00
|
|
|
return coreerr.E("Program.Find", "program name is empty", nil)
|
|
|
|
|
}
|
2026-04-04 03:07:13 +00:00
|
|
|
path, err := exec.LookPath(target)
|
2026-03-18 15:07:03 +00:00
|
|
|
if err != nil {
|
2026-04-04 03:07:13 +00:00
|
|
|
return coreerr.E("Program.Find", core.Sprintf("%q: not found in PATH", target), ErrProgramNotFound)
|
2026-03-18 15:07:03 +00:00
|
|
|
}
|
|
|
|
|
p.Path = path
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run executes the program with args in the current working directory.
|
|
|
|
|
// Returns trimmed combined stdout+stderr output and any error.
|
2026-04-04 00:39:27 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// out, err := p.Run(ctx, "hello")
|
2026-03-18 15:07:03 +00:00
|
|
|
func (p *Program) Run(ctx context.Context, args ...string) (string, error) {
|
|
|
|
|
return p.RunDir(ctx, "", args...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RunDir executes the program with args in dir.
|
|
|
|
|
// Returns trimmed combined stdout+stderr output and any error.
|
|
|
|
|
// If dir is empty, the process inherits the caller's working directory.
|
2026-04-04 00:39:27 +00:00
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// out, err := p.RunDir(ctx, "/tmp", "pwd")
|
2026-03-18 15:07:03 +00:00
|
|
|
func (p *Program) RunDir(ctx context.Context, dir string, args ...string) (string, error) {
|
2026-04-04 00:18:14 +00:00
|
|
|
if ctx == nil {
|
|
|
|
|
return "", coreerr.E("Program.RunDir", "program: command context is required", ErrProgramContextRequired)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 15:07:03 +00:00
|
|
|
binary := p.Path
|
|
|
|
|
if binary == "" {
|
|
|
|
|
binary = p.Name
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 00:18:14 +00:00
|
|
|
if binary == "" {
|
|
|
|
|
return "", coreerr.E("Program.RunDir", "program name is empty", ErrProgramNameRequired)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 08:57:42 +01:00
|
|
|
out := core.NewBuffer()
|
2026-03-18 15:07:03 +00:00
|
|
|
cmd := exec.CommandContext(ctx, binary, args...)
|
2026-04-25 08:57:42 +01:00
|
|
|
cmd.Stdout = out
|
|
|
|
|
cmd.Stderr = out
|
2026-03-18 15:07:03 +00:00
|
|
|
if dir != "" {
|
|
|
|
|
cmd.Dir = dir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
2026-04-25 08:57:42 +01:00
|
|
|
return trimRightSpace(out.String()), coreerr.E("Program.RunDir", core.Sprintf("%q: command failed", p.Name), err)
|
2026-03-18 15:07:03 +00:00
|
|
|
}
|
2026-04-25 08:57:42 +01:00
|
|
|
return trimRightSpace(out.String()), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func trimRightSpace(s string) string {
|
|
|
|
|
trimFrom := len(s)
|
|
|
|
|
for i, r := range s {
|
|
|
|
|
if core.IsSpace(r) {
|
|
|
|
|
if trimFrom == len(s) {
|
|
|
|
|
trimFrom = i
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
trimFrom = len(s)
|
|
|
|
|
}
|
|
|
|
|
return s[:trimFrom]
|
2026-03-18 15:07:03 +00:00
|
|
|
}
|