219 lines
5.2 KiB
Go
219 lines
5.2 KiB
Go
|
|
package coreexec
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"syscall"
|
||
|
|
|
||
|
|
core "dappco.re/go/core"
|
||
|
|
)
|
||
|
|
|
||
|
|
var localFS = (&core.Fs{}).NewUnrestricted()
|
||
|
|
|
||
|
|
const executeAccess = 1
|
||
|
|
|
||
|
|
// Result captures process output and exit status.
|
||
|
|
// Usage: result, err := coreexec.Run(ctx, "git", "status", "--short")
|
||
|
|
type Result struct {
|
||
|
|
Stdout string
|
||
|
|
Stderr string
|
||
|
|
ExitCode int
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookPath resolves an executable name against PATH.
|
||
|
|
// Usage: path, err := coreexec.LookPath("gh")
|
||
|
|
func LookPath(name string) (string, error) {
|
||
|
|
if name == "" {
|
||
|
|
return "", core.E("coreexec.LookPath", "empty executable name", nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
ds := core.Env("DS")
|
||
|
|
if core.PathIsAbs(name) || core.Contains(name, ds) {
|
||
|
|
if isExecutable(name) {
|
||
|
|
return name, nil
|
||
|
|
}
|
||
|
|
return "", core.E("coreexec.LookPath", core.Concat("executable not found: ", name), nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, dir := range core.Split(core.Env("PATH"), core.Env("PS")) {
|
||
|
|
if dir == "" {
|
||
|
|
dir = core.Env("DIR_CWD")
|
||
|
|
} else if !core.PathIsAbs(dir) {
|
||
|
|
dir = core.Path(core.Env("DIR_CWD"), dir)
|
||
|
|
}
|
||
|
|
candidate := core.Path(dir, name)
|
||
|
|
if isExecutable(candidate) {
|
||
|
|
return candidate, nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return "", core.E("coreexec.LookPath", core.Concat("executable not found in PATH: ", name), nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run executes a command and captures stdout, stderr, and exit status.
|
||
|
|
// Usage: result, err := coreexec.Run(ctx, "gh", "api", "repos/org/repo")
|
||
|
|
func Run(ctx context.Context, name string, args ...string) (Result, error) {
|
||
|
|
path, err := LookPath(name)
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
tempDir := localFS.TempDir("coreexec-")
|
||
|
|
if tempDir == "" {
|
||
|
|
return Result{}, core.E("coreexec.Run", "create capture directory", nil)
|
||
|
|
}
|
||
|
|
defer func() { _ = coreResultErr(localFS.DeleteAll(tempDir), "coreexec.Run") }()
|
||
|
|
|
||
|
|
stdoutPath := core.Path(tempDir, "stdout")
|
||
|
|
stderrPath := core.Path(tempDir, "stderr")
|
||
|
|
|
||
|
|
stdoutFile, err := createFile(stdoutPath)
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, err
|
||
|
|
}
|
||
|
|
defer func() { _ = stdoutFile.Close() }()
|
||
|
|
|
||
|
|
stderrFile, err := createFile(stderrPath)
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, err
|
||
|
|
}
|
||
|
|
defer func() { _ = stderrFile.Close() }()
|
||
|
|
|
||
|
|
pid, err := syscall.ForkExec(path, append([]string{name}, args...), &syscall.ProcAttr{
|
||
|
|
Dir: core.Env("DIR_CWD"),
|
||
|
|
Env: syscall.Environ(),
|
||
|
|
Files: []uintptr{
|
||
|
|
0,
|
||
|
|
stdoutFile.Fd(),
|
||
|
|
stderrFile.Fd(),
|
||
|
|
},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, core.E("coreexec.Run", core.Concat("start ", name), err)
|
||
|
|
}
|
||
|
|
|
||
|
|
status, err := waitForPID(ctx, pid, name)
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
stdout, err := readFile(stdoutPath)
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
stderr, err := readFile(stderrPath)
|
||
|
|
if err != nil {
|
||
|
|
return Result{}, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return Result{
|
||
|
|
Stdout: stdout,
|
||
|
|
Stderr: stderr,
|
||
|
|
ExitCode: exitCode(status),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Exec replaces the current process with the named executable.
|
||
|
|
// Usage: return coreexec.Exec("ssh", "-i", keyPath, host)
|
||
|
|
func Exec(name string, args ...string) error {
|
||
|
|
path, err := LookPath(name)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := syscall.Exec(path, append([]string{name}, args...), syscall.Environ()); err != nil {
|
||
|
|
return core.E("coreexec.Exec", core.Concat("exec ", name), err)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type captureFile interface {
|
||
|
|
Close() error
|
||
|
|
Fd() uintptr
|
||
|
|
}
|
||
|
|
|
||
|
|
type waitResult struct {
|
||
|
|
status syscall.WaitStatus
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
|
||
|
|
func isExecutable(path string) bool {
|
||
|
|
if !localFS.IsFile(path) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return syscall.Access(path, executeAccess) == nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func createFile(path string) (captureFile, error) {
|
||
|
|
created := localFS.Create(path)
|
||
|
|
if !created.OK {
|
||
|
|
return nil, core.E("coreexec.Run", core.Concat("create ", path), coreResultErr(created, "coreexec.Run"))
|
||
|
|
}
|
||
|
|
|
||
|
|
file, ok := created.Value.(captureFile)
|
||
|
|
if !ok {
|
||
|
|
return nil, core.E("coreexec.Run", core.Concat("capture handle type for ", path), nil)
|
||
|
|
}
|
||
|
|
return file, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func readFile(path string) (string, error) {
|
||
|
|
read := localFS.Read(path)
|
||
|
|
if !read.OK {
|
||
|
|
return "", core.E("coreexec.Run", core.Concat("read ", path), coreResultErr(read, "coreexec.Run"))
|
||
|
|
}
|
||
|
|
|
||
|
|
content, ok := read.Value.(string)
|
||
|
|
if !ok {
|
||
|
|
return "", core.E("coreexec.Run", core.Concat("unexpected content type for ", path), nil)
|
||
|
|
}
|
||
|
|
return content, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func waitForPID(ctx context.Context, pid int, name string) (syscall.WaitStatus, error) {
|
||
|
|
done := make(chan waitResult, 1)
|
||
|
|
go func() {
|
||
|
|
var status syscall.WaitStatus
|
||
|
|
_, err := syscall.Wait4(pid, &status, 0, nil)
|
||
|
|
done <- waitResult{status: status, err: err}
|
||
|
|
}()
|
||
|
|
|
||
|
|
select {
|
||
|
|
case result := <-done:
|
||
|
|
if result.err != nil {
|
||
|
|
return 0, core.E("coreexec.Run", core.Concat("wait ", name), result.err)
|
||
|
|
}
|
||
|
|
return result.status, nil
|
||
|
|
case <-ctx.Done():
|
||
|
|
_ = syscall.Kill(pid, syscall.SIGKILL)
|
||
|
|
result := <-done
|
||
|
|
if result.err != nil {
|
||
|
|
return 0, core.E("coreexec.Run", core.Concat("wait ", name), result.err)
|
||
|
|
}
|
||
|
|
return 0, core.E("coreexec.Run", core.Concat("command cancelled: ", name), ctx.Err())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func exitCode(status syscall.WaitStatus) int {
|
||
|
|
if status.Exited() {
|
||
|
|
return status.ExitStatus()
|
||
|
|
}
|
||
|
|
if status.Signaled() {
|
||
|
|
return 128 + int(status.Signal())
|
||
|
|
}
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
func coreResultErr(r core.Result, op string) error {
|
||
|
|
if r.OK {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if err, ok := r.Value.(error); ok && err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if r.Value == nil {
|
||
|
|
return core.E(op, "unexpected empty core result", nil)
|
||
|
|
}
|
||
|
|
return core.E(op, core.Sprint(r.Value), nil)
|
||
|
|
}
|