Module: Ukiryu::Executor

Defined in:
lib/ukiryu/executor.rb

Overview

Command execution with platform-specific methods

Handles execution of external commands with:

  • Shell-specific command line building

  • Environment variable management

  • Timeout handling

  • Error detection and reporting

Class Method Summary collapse

Class Method Details

.build_command(executable, args, shell_class) ⇒ String

Build a command line for the given shell

Parameters:

  • executable (String)

    the executable path

  • args (Array<String>)

    the arguments

  • shell_class (Class)

    the shell implementation class

Returns:

  • (String)

    the complete command line



161
162
163
164
165
166
167
168
169
# File 'lib/ukiryu/executor.rb', line 161

def build_command(executable, args, shell_class)
  shell_instance = shell_class.new

  # Format executable path if needed
  exe = shell_instance.format_path(executable)

  # Join executable and arguments
  shell_instance.join(exe, *args)
end

.execute(executable, args = [], options = {}) ⇒ Execution::Result

Execute a command with the given options

The user MUST explicitly specify both:

  • env: The Environment to use (inherited, derived, custom, or empty)

  • shell: The Shell to interpret the command (bash, zsh, powershell, cmd, etc.)

  • timeout: Maximum execution time in seconds

Parameters:

  • executable (String)

    the executable path

  • args (Array<String>) (defaults to: [])

    the command arguments

  • options (Hash) (defaults to: {})

    execution options

Options Hash (options):

  • :env (Environment)

    REQUIRED - The environment to use

  • :shell (Class, Symbol)

    REQUIRED - Shell class or shell name (:bash, :zsh, :powershell, :cmd)

  • :timeout (Integer)

    REQUIRED - maximum execution time in seconds

  • :cwd (String)

    working directory

  • :stdin (String, IO)

    stdin input data (string or IO object)

  • :tool_name (String)

    tool name for exit code lookups

  • :command_name (String)

    command name for exit code lookups

  • :allow_failure (Boolean)

    allow non-zero exit codes (default: false)

Returns:

Raises:

  • (ArgumentError)

    if shell or timeout is not specified

  • (TimeoutError)

    if command times out

  • (ExecutionError)

    if command fails



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ukiryu/executor.rb', line 42

def execute(executable, args = [], options = {})
  # Get shell - must be explicitly specified
  shell_arg = options[:shell]
  raise ArgumentError, 'shell is required - specify :shell option (e.g., shell: :bash)' unless shell_arg

  # Get timeout - must be explicitly specified
  timeout = options[:timeout]
  raise ArgumentError, 'timeout is required - specify :timeout option (e.g., timeout: 90)' unless timeout

  # Convert shell to shell class
  shell_class = if shell_arg.is_a?(Class)
                  shell_arg
                else
                  Ukiryu::Shell.class_for(shell_arg.to_sym)
                end

  shell_instance = shell_class.new

  # Debug logging for Ruby 4.0 CI
  if ENV['UKIRYU_DEBUG_EXECUTABLE']
    warn "[UKIRYU DEBUG Executor#execute] executable: #{executable.inspect}"
    warn "[UKIRYU DEBUG Executor#execute] args: #{args.inspect}"
    warn "[UKIRYU DEBUG Executor#execute] args.class: #{args.class}"
    warn "[UKIRYU DEBUG Executor#execute] shell_class: #{shell_class}"
  end

  # Prepare environment (requires Environment or Hash)
  env = prepare_environment(options[:env] || {}, shell_class)
  cwd = options[:cwd]
  stdin = options[:stdin]

  # Suppress thread warnings from Open3 (cosmetic IOError from stream closure)
  # Open3's internal threads may raise IOError when streams close early
  original_setting = Thread.report_on_exception
  Thread.report_on_exception = false

  started_at = Time.now
  begin
    result = if stdin
               shell_instance.execute_command_with_stdin(executable, args, env, timeout, cwd, stdin)
             else
               shell_instance.execute_command(executable, args, env, timeout, cwd)
             end
  rescue Timeout::Error
    Time.now
    raise Ukiryu::Errors::TimeoutError, "Command timed out after #{timeout} seconds: #{executable}"
  ensure
    Thread.report_on_exception = original_setting
  end
  finished_at = Time.now

  # Build command string for display (for error messages and debugging)
  command = shell_instance.join(executable, *args)

  # Create OOP result components using Execution namespace
  command_info = Execution::CommandInfo.new(
    executable: executable,
    arguments: args,
    full_command: command,
    shell: shell_instance.name,
    tool_name: options[:tool_name],
    command_name: options[:command_name]
  )

  output = Execution::Output.new(
    stdout: result[:stdout],
    stderr: result[:stderr],
    exit_status: result[:status]
  )

   = Execution::ExecutionMetadata.new(
    started_at: started_at,
    finished_at: finished_at,
    timeout: timeout
  )

  # Check exit status
  if result[:status] != 0 && !options[:allow_failure]
    raise Ukiryu::Errors::ExecutionError,
          format_error(executable, command, result)
  end

  Execution::Result.new(
    command_info: command_info,
    output: output,
    metadata: 
  )
end

.extract_status(status) ⇒ Integer

Extract exit status from Process::Status

Parameters:

  • status (Process::Status)

    the process status

Returns:

  • (Integer)

    exit status (128 + signal if terminated by signal)



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/ukiryu/executor.rb', line 175

def extract_status(status)
  if status.exited?
    status.exitstatus
  elsif status.signaled?
    # Process terminated by signal - return 128 + signal number
    # This matches how shells report terminated processes
    128 + status.termsig
  elsif status.stopped?
    # Process was stopped - return 128 + stop signal
    128 + status.stopsig
  else
    # Unknown status - return failure code
    1
  end
end

.find_executable(command, options = {}) ⇒ String?

Find an executable in the system PATH

Parameters:

  • command (String)

    the command or executable name

  • options (Hash) (defaults to: {})

    search options

Options Hash (options):

  • :additional_paths (Array<String>)

    additional search paths

Returns:

  • (String, nil)

    the full path to the executable, or nil if not found



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/ukiryu/executor.rb', line 137

def find_executable(command, options = {})
  # Try with PATHEXT extensions (Windows executables)
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']

  search_paths = Platform.executable_search_paths
  search_paths.concat(options[:additional_paths]) if options[:additional_paths]
  search_paths.uniq!

  search_paths.each do |dir|
    exts.each do |ext|
      exe = File.join(dir, "#{command}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    end
  end

  nil
end