mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-31 04:09:45 +02:00
- Add replayDBOperations to reinitialize databases from imported workspaces, with proper parameter resolution and pre-condition skipping - Implement ISO 8601 timestamp format for snapshots (2026-02-13T18-20-34Z) instead of Unix epoch - Add --include-heavy flag to include large fields (raw_response, screenshot, blob_content) in database queries - Fix asset table default columns (url, status_code, content_length, title) for better UX - Skip heavy initialization for lightweight commands (help, version, completion) to avoid ~50MB overhead - Optimize database function execution via lazy config initialization - Fix binary installation via Nix to copy already-installed binaries to binaries folder
149 lines
3.5 KiB
Go
149 lines
3.5 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
)
|
|
|
|
// HostRunner executes commands on the local machine
|
|
type HostRunner struct {
|
|
binariesPath string
|
|
onPIDStart PIDCallback
|
|
onPIDEnd PIDCallback
|
|
}
|
|
|
|
// NewHostRunner creates a new host runner
|
|
func NewHostRunner(binariesPath string) *HostRunner {
|
|
return &HostRunner{binariesPath: binariesPath}
|
|
}
|
|
|
|
// Execute runs a command on the local machine
|
|
func (r *HostRunner) Execute(ctx context.Context, command string) (*CommandResult, error) {
|
|
// @NOTE: This is intentional - the workflow engine is designed to execute arbitrary
|
|
// commands from YAML workflow definitions. The command input comes from trusted
|
|
// workflow files, not from untrusted user input.
|
|
cmd := exec.Command("sh", "-c", command)
|
|
|
|
// Create new process group so we can kill all children on interrupt
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
|
|
// Prepend binaries path to PATH if configured
|
|
if r.binariesPath != "" {
|
|
env := os.Environ()
|
|
pathUpdated := false
|
|
for i, e := range env {
|
|
if strings.HasPrefix(e, "PATH=") {
|
|
env[i] = "PATH=" + r.binariesPath + ":" + strings.TrimPrefix(e, "PATH=")
|
|
pathUpdated = true
|
|
break
|
|
}
|
|
}
|
|
if !pathUpdated {
|
|
env = append(env, "PATH="+r.binariesPath)
|
|
}
|
|
cmd.Env = env
|
|
}
|
|
|
|
stdout := NewLimitedBuffer(MaxOutputSize)
|
|
stderr := NewLimitedBuffer(MaxStderrSize)
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
|
|
// Start the command
|
|
if err := cmd.Start(); err != nil {
|
|
return &CommandResult{
|
|
Output: "",
|
|
ExitCode: -1,
|
|
Error: err,
|
|
}, nil
|
|
}
|
|
|
|
// Track PID for cancellation support
|
|
pid := cmd.Process.Pid
|
|
if r.onPIDStart != nil {
|
|
r.onPIDStart(pid)
|
|
}
|
|
|
|
// Wait for command completion or context cancellation
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- cmd.Wait()
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
// Context cancelled - kill entire process group
|
|
if cmd.Process != nil {
|
|
// Kill process group (negative PID)
|
|
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
|
}
|
|
<-done // Wait for process to exit
|
|
|
|
// Notify PID ended
|
|
if r.onPIDEnd != nil {
|
|
r.onPIDEnd(pid)
|
|
}
|
|
|
|
return &CommandResult{
|
|
Output: combineOutput(stdout, stderr),
|
|
ExitCode: -1,
|
|
Error: ctx.Err(),
|
|
}, nil
|
|
|
|
case err := <-done:
|
|
// Normal completion - notify PID ended
|
|
if r.onPIDEnd != nil {
|
|
r.onPIDEnd(pid)
|
|
}
|
|
|
|
exitCode := 0
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
exitCode = exitErr.ExitCode()
|
|
}
|
|
}
|
|
return &CommandResult{
|
|
Output: combineOutput(stdout, stderr),
|
|
ExitCode: exitCode,
|
|
Error: err,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// SetPIDCallbacks sets callbacks for process lifecycle events
|
|
func (r *HostRunner) SetPIDCallbacks(onStart, onEnd PIDCallback) {
|
|
r.onPIDStart = onStart
|
|
r.onPIDEnd = onEnd
|
|
}
|
|
|
|
// Setup is a no-op for host runner
|
|
func (r *HostRunner) Setup(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is a no-op for host runner
|
|
func (r *HostRunner) Cleanup(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// CopyFromRemote is not supported for host runner (local execution doesn't need file copy)
|
|
func (r *HostRunner) CopyFromRemote(ctx context.Context, remotePath, localPath string) error {
|
|
return fmt.Errorf("CopyFromRemote not supported for host runner")
|
|
}
|
|
|
|
// Type returns the runner type
|
|
func (r *HostRunner) Type() core.RunnerType {
|
|
return core.RunnerTypeHost
|
|
}
|
|
|
|
// IsRemote returns false since host runner runs locally
|
|
func (r *HostRunner) IsRemote() bool {
|
|
return false
|
|
}
|