mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-20 06:35:48 +02:00
- Add worker eval command for distributed function execution with Redis hooks registration - Add worker set command to update worker fields (alias, public-ip, ssh-enabled, ssh-keys-path) - Enhance worker status with JSON output, search filtering, and column selection (--columns, --exclude-columns, --search) - Add --keep-setting flag to install base/validate commands to preserve osm-settings.yaml after base installation - Fix binary installation in Nix: replace CopyInstalledBinaryToFolder with SymlinkInstalledBinaryToFolder - Add --clean-ws flag to db clean command for removing workspace data - Add HooksEnabled field to Run records when creating runs from CLI and API - Add comprehensive test coverage for hook execution (pre/post hooks, execution order, failure handling) - Add test coverage for worker commands (eval, set, status with JSON) and db clean operations - Improve usage documentation for worker subcommands and db operations
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
package executor
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// exitCodeError wraps an error with its numeric exit code so callers
|
|
// can check for specific codes (e.g., 127 = command not found).
|
|
type exitCodeError struct {
|
|
code int
|
|
msg string
|
|
}
|
|
|
|
func (e *exitCodeError) Error() string {
|
|
return e.msg
|
|
}
|
|
|
|
// newExitCodeError creates an exitCodeError with the given code and message.
|
|
func newExitCodeError(code int, msg string) *exitCodeError {
|
|
return &exitCodeError{code: code, msg: msg}
|
|
}
|
|
|
|
// newExitCodeErrorf creates an exitCodeError with formatted message.
|
|
func newExitCodeErrorf(code int, format string, args ...any) *exitCodeError {
|
|
return &exitCodeError{code: code, msg: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// stripTimeoutResult holds the result of parsing a timeout prefix.
|
|
type stripTimeoutResult struct {
|
|
command string // remaining command after stripping prefix
|
|
duration time.Duration // parsed duration from the timeout prefix (0 if unparseable)
|
|
stripped bool // true if a timeout prefix was found and stripped
|
|
}
|
|
|
|
// stripTimeoutPrefix removes a "timeout" command prefix from a command string.
|
|
// It handles various flag forms: -k VAL, --kill-after=VAL, -s SIG, --signal=SIG,
|
|
// --foreground, --preserve-status, -v, --verbose.
|
|
// Returns a stripTimeoutResult with the remaining command, parsed duration, and whether stripping occurred.
|
|
func stripTimeoutPrefix(command string) stripTimeoutResult {
|
|
fields := strings.Fields(command)
|
|
if len(fields) == 0 {
|
|
return stripTimeoutResult{command: command}
|
|
}
|
|
|
|
// First token must be "timeout" (or a path ending in /timeout)
|
|
base := fields[0]
|
|
if idx := strings.LastIndex(base, "/"); idx >= 0 {
|
|
base = base[idx+1:]
|
|
}
|
|
if base != "timeout" {
|
|
return stripTimeoutResult{command: command}
|
|
}
|
|
|
|
i := 1 // skip "timeout"
|
|
|
|
// Parse optional flags before the DURATION argument
|
|
for i < len(fields) {
|
|
arg := fields[i]
|
|
|
|
// Flags that take a separate value: -k VAL, -s SIG
|
|
if arg == "-k" || arg == "--kill-after" || arg == "-s" || arg == "--signal" {
|
|
i += 2 // skip flag + value
|
|
continue
|
|
}
|
|
|
|
// Flags with = form: --kill-after=VAL, --signal=SIG
|
|
if strings.HasPrefix(arg, "--kill-after=") || strings.HasPrefix(arg, "--signal=") {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
// Short form -k5s (value attached)
|
|
if len(arg) > 2 && arg[0] == '-' && arg[1] == 'k' {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
// Boolean flags
|
|
if arg == "--foreground" || arg == "--preserve-status" || arg == "-v" || arg == "--verbose" {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
// Not a recognized flag — this should be the DURATION
|
|
break
|
|
}
|
|
|
|
// Skip the DURATION argument
|
|
if i >= len(fields) {
|
|
// No duration found — malformed, don't strip
|
|
return stripTimeoutResult{command: command}
|
|
}
|
|
durationStr := fields[i]
|
|
parsedDuration := parseTimeoutDuration(durationStr)
|
|
i++ // skip duration
|
|
|
|
// Everything after duration is the actual command
|
|
if i >= len(fields) {
|
|
// Nothing after duration — no command to run
|
|
return stripTimeoutResult{command: "", duration: parsedDuration, stripped: true}
|
|
}
|
|
|
|
return stripTimeoutResult{
|
|
command: strings.Join(fields[i:], " "),
|
|
duration: parsedDuration,
|
|
stripped: true,
|
|
}
|
|
}
|
|
|
|
// parseTimeoutDuration parses a GNU coreutils timeout duration string.
|
|
// Supports formats: plain number (seconds), or number with suffix s/m/h/d.
|
|
// Returns 0 if the string cannot be parsed.
|
|
func parseTimeoutDuration(s string) time.Duration {
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
|
|
// Check for suffix
|
|
last := s[len(s)-1]
|
|
switch last {
|
|
case 's':
|
|
return parseDurationNumber(s[:len(s)-1], time.Second)
|
|
case 'm':
|
|
return parseDurationNumber(s[:len(s)-1], time.Minute)
|
|
case 'h':
|
|
return parseDurationNumber(s[:len(s)-1], time.Hour)
|
|
case 'd':
|
|
return parseDurationNumber(s[:len(s)-1], 24*time.Hour)
|
|
default:
|
|
// No suffix — default is seconds
|
|
return parseDurationNumber(s, time.Second)
|
|
}
|
|
}
|
|
|
|
// parseDurationNumber parses a numeric string and multiplies by the given unit.
|
|
// Supports both integer and floating-point values. Returns 0 on parse error.
|
|
func parseDurationNumber(s string, unit time.Duration) time.Duration {
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
f, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return time.Duration(f * float64(unit))
|
|
}
|
|
|
|
// prependBinariesPath prepends binariesPath to the first token (binary name)
|
|
// of a command if it doesn't already contain a path separator.
|
|
// Returns the modified command and true if the path was prepended.
|
|
func prependBinariesPath(command, binariesPath string) (string, bool) {
|
|
if binariesPath == "" || command == "" {
|
|
return command, false
|
|
}
|
|
|
|
fields := strings.Fields(command)
|
|
if len(fields) == 0 {
|
|
return command, false
|
|
}
|
|
|
|
binary := fields[0]
|
|
|
|
// Don't prepend if binary already has a path
|
|
if strings.Contains(binary, "/") {
|
|
return command, false
|
|
}
|
|
|
|
// Ensure binariesPath doesn't have trailing slash
|
|
binariesPath = strings.TrimRight(binariesPath, "/")
|
|
|
|
fields[0] = binariesPath + "/" + binary
|
|
return strings.Join(fields, " "), true
|
|
}
|