aidp 0.39.7 → 0.39.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eeb4172f8395dae1cecfd4019377e8ec5435746c39e785d8a74e60f0ffdfd202
4
- data.tar.gz: 60b3ee0ca0796fbbd96014487689313c1c360558df80b22404c4a8457c9f27df
3
+ metadata.gz: 48e9a4d5c62e99992a0b61d3547b20b2a7598c13dbc6ba8b23a4f8126cd74c35
4
+ data.tar.gz: 9427e78702251d548a938d5b93f466b9514686128162c4be5bb52eb8375013a0
5
5
  SHA512:
6
- metadata.gz: 3f4e93eed39bb3b8dc88c21f65e64504689c6c53192994dbe4d8e6b6798aef1912d620a1d3cbd1b01602b9b27715b820a4f49e812f20756e06a61a8ca7ca44d9
7
- data.tar.gz: 5e881d0b36e7d7d746c2eddf4d353c091fe51265369841ae8e58a35f1322265a64672afb857c9010e487b60b1f1b97d080608d281898be5b972a836763fdded6
6
+ metadata.gz: eb8f0d861f8b85f8df71034a2e6cbef875be34dcf28eacec009aaa65c8589527b904fa060598b43c9854e63c9b354ca0299bb70b96eb5da80e36c19b6ea0a033
7
+ data.tar.gz: a46a2734b9094b966c242a284b1673cd956df4281294557b813925d19f8204bdf470ad93d4a1c01e27a2846de2b6544258d062f0f36dacd29607827a7bc3049c
@@ -128,6 +128,8 @@ module Aidp
128
128
  class TtyCommandExecutor
129
129
  include CommandExecutorInterface
130
130
 
131
+ TERMINATION_GRACE_PERIOD = 0.1
132
+
131
133
  # @param logger [LoggerInterface] optional logger for debug output
132
134
  # @param component_name [String] component name for logging
133
135
  def initialize(logger: nil, component_name: "command_executor")
@@ -144,20 +146,14 @@ module Aidp
144
146
  # @param options [Hash] additional options passed to TTY::Command#run
145
147
  # @return [CommandResult] the result of the command execution
146
148
  def execute(command, args: [], input: nil, timeout: nil, **options)
147
- require "tty-command"
149
+ require "open3"
148
150
 
149
151
  log_debug("executing_command", command: command, args: args, timeout: timeout)
150
152
 
151
153
  start_time = Time.now
152
154
 
153
155
  begin
154
- cmd_obj = TTY::Command.new(printer: :null)
155
-
156
- # Prepare input data
157
- input_data = resolve_input(input)
158
-
159
- # Execute command - use run! to get result even on non-zero exit
160
- result = cmd_obj.run!(command, *args, input: input_data, timeout: timeout, **options)
156
+ result = execute_command(command, args, resolve_input(input), timeout, options)
161
157
 
162
158
  duration = Time.now - start_time
163
159
  log_debug("command_completed",
@@ -170,7 +166,7 @@ module Aidp
170
166
  stderr: result.err,
171
167
  exit_status: result.exit_status
172
168
  )
173
- rescue TTY::Command::TimeoutExceeded
169
+ rescue CommandTimeoutError
174
170
  duration = Time.now - start_time
175
171
  log_debug("command_timeout",
176
172
  command: command,
@@ -197,6 +193,57 @@ module Aidp
197
193
 
198
194
  private
199
195
 
196
+ def execute_command(command, args, input_data, timeout, options)
197
+ env = options.delete(:env) || {}
198
+ Open3.popen3(env, command.to_s, *args.map(&:to_s), **options) do |stdin, stdout, stderr, wait_thread|
199
+ write_input(stdin, input_data)
200
+ out_reader = Thread.new { read_stream(stdout) }
201
+ err_reader = Thread.new { read_stream(stderr) }
202
+ status = wait_for_process(wait_thread, command, timeout)
203
+ CommandResult.new(
204
+ stdout: out_reader.value,
205
+ stderr: err_reader.value,
206
+ exit_status: status.exitstatus
207
+ )
208
+ end
209
+ end
210
+
211
+ def write_input(stdin, input_data)
212
+ return stdin.close unless input_data
213
+
214
+ stdin.write(input_data)
215
+ stdin.close
216
+ end
217
+
218
+ def wait_for_process(wait_thread, command, timeout)
219
+ return wait_thread.value unless timeout
220
+
221
+ return wait_thread.value if wait_thread.join(timeout)
222
+
223
+ terminate_process(wait_thread)
224
+ raise CommandTimeoutError.new(command: command, timeout: timeout)
225
+ end
226
+
227
+ def terminate_process(wait_thread)
228
+ signal_process(wait_thread.pid, "TERM")
229
+ return if wait_thread.join(TERMINATION_GRACE_PERIOD)
230
+
231
+ signal_process(wait_thread.pid, "KILL")
232
+ wait_thread.join
233
+ end
234
+
235
+ def signal_process(pid, signal)
236
+ Process.kill(signal, pid)
237
+ rescue Errno::ESRCH
238
+ nil
239
+ end
240
+
241
+ def read_stream(stream)
242
+ stream.read
243
+ rescue IOError
244
+ ""
245
+ end
246
+
200
247
  def resolve_input(input)
201
248
  return nil unless input
202
249
 
@@ -13,6 +13,8 @@ module Aidp
13
13
  # Manages background execution of work loops
14
14
  # Runs harness in daemon process and tracks job metadata
15
15
  class BackgroundRunner
16
+ StartError = Class.new(StandardError)
17
+
16
18
  include Aidp::MessageDisplay
17
19
  include Aidp::RescueLogging
18
20
  include Aidp::SafeDirectory
@@ -34,9 +36,11 @@ module Aidp
34
36
  # Start a background job
35
37
  # Returns job_id
36
38
  def start(mode, options = {})
39
+ ensure_jobs_directory
37
40
  job_id = generate_job_id
38
- log_file = File.join(@jobs_dir, job_id, "output.log")
39
- pid_file = File.join(@jobs_dir, job_id, "job.pid")
41
+ log_file = job_file_path(job_id, "output.log")
42
+ pid_file = job_file_path(job_id, "job.pid")
43
+ raise_start_error unless log_file && pid_file
40
44
 
41
45
  # Create job directory
42
46
  FileUtils.mkdir_p(File.dirname(log_file))
@@ -109,6 +113,9 @@ module Aidp
109
113
  metadata = load_job_metadata(job_id)
110
114
  return nil unless metadata
111
115
 
116
+ log_file = job_log_path(job_id)
117
+ return nil unless log_file
118
+
112
119
  # Check if process is still running
113
120
  pid = metadata[:pid]
114
121
  running = pid && process_running?(pid)
@@ -125,7 +132,7 @@ module Aidp
125
132
  started_at: metadata[:started_at],
126
133
  completed_at: metadata[:completed_at],
127
134
  checkpoint: checkpoint,
128
- log_file: File.join(@jobs_dir, job_id, "output.log")
135
+ log_file: log_file
129
136
  }
130
137
  end
131
138
 
@@ -169,12 +176,12 @@ module Aidp
169
176
 
170
177
  # Get job logs
171
178
  def job_logs(job_id, options = {})
172
- log_file = File.join(@jobs_dir, job_id, "output.log")
179
+ log_file = job_log_path(job_id)
180
+ return nil unless log_file
173
181
  return nil unless File.exist?(log_file)
174
182
 
175
183
  if options[:tail]
176
- lines = options[:lines] || 50
177
- `tail -n #{lines} #{log_file}`
184
+ tail_job_logs(log_file, options[:lines])
178
185
  else
179
186
  File.read(log_file)
180
187
  end
@@ -182,7 +189,8 @@ module Aidp
182
189
 
183
190
  # Follow job logs in real-time
184
191
  def follow_job_logs(job_id)
185
- log_file = File.join(@jobs_dir, job_id, "output.log")
192
+ log_file = job_log_path(job_id)
193
+ return unless log_file
186
194
  return unless File.exist?(log_file)
187
195
 
188
196
  # Use tail -f to follow logs
@@ -195,6 +203,10 @@ module Aidp
195
203
  @jobs_dir = safe_mkdir_p(@jobs_dir, component_name: "BackgroundRunner")
196
204
  end
197
205
 
206
+ def raise_start_error
207
+ raise StartError, "Unable to create or access background job directory: #{@jobs_dir}"
208
+ end
209
+
198
210
  def generate_job_id
199
211
  timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
200
212
  random = SecureRandom.hex(4)
@@ -202,7 +214,8 @@ module Aidp
202
214
  end
203
215
 
204
216
  def save_job_metadata(job_id, pid, mode, options)
205
- metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
217
+ metadata_file = job_metadata_path(job_id)
218
+ return unless metadata_file
206
219
 
207
220
  metadata = {
208
221
  job_id: job_id,
@@ -217,7 +230,8 @@ module Aidp
217
230
  end
218
231
 
219
232
  def load_job_metadata(job_id)
220
- metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
233
+ metadata_file = job_metadata_path(job_id)
234
+ return nil unless metadata_file
221
235
  return nil unless File.exist?(metadata_file)
222
236
 
223
237
  # Return raw metadata with times as ISO8601 strings to avoid unsafe class loading
@@ -231,7 +245,8 @@ module Aidp
231
245
  return unless metadata
232
246
 
233
247
  metadata.merge!(updates)
234
- metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
248
+ metadata_file = job_metadata_path(job_id)
249
+ return unless metadata_file
235
250
  File.write(metadata_file, metadata.to_yaml)
236
251
  end
237
252
 
@@ -298,6 +313,57 @@ module Aidp
298
313
  metadata[:status] || "completed"
299
314
  end
300
315
  end
316
+
317
+ def job_log_path(job_id)
318
+ job_file_path(job_id, "output.log")
319
+ end
320
+
321
+ def job_metadata_path(job_id)
322
+ job_file_path(job_id, "metadata.yml")
323
+ end
324
+
325
+ def job_file_path(job_id, file_name)
326
+ return unless Dir.exist?(@jobs_dir)
327
+
328
+ jobs_dir = File.realpath(@jobs_dir)
329
+ resolved_job_dir = resolved_job_dir_path(jobs_dir, job_id)
330
+ return unless resolved_job_dir
331
+ return unless resolved_job_dir.start_with?("#{jobs_dir}/")
332
+
333
+ resolved_job_file_path(resolved_job_dir, file_name)
334
+ end
335
+
336
+ def tail_job_logs(log_file, requested_lines)
337
+ lines = Integer(requested_lines || 50, exception: false)
338
+ lines = 50 unless lines&.positive?
339
+
340
+ IO.popen(["tail", "-n", lines.to_s, log_file], &:read)
341
+ end
342
+
343
+ def resolved_job_dir_path(jobs_dir, job_id)
344
+ job_dir = File.join(@jobs_dir, job_id.to_s)
345
+
346
+ if File.exist?(job_dir)
347
+ File.realpath(job_dir)
348
+ else
349
+ File.join(jobs_dir, job_id.to_s)
350
+ end
351
+ rescue Errno::ENOENT, Errno::EACCES
352
+ nil
353
+ end
354
+
355
+ def resolved_job_file_path(job_dir, file_name)
356
+ candidate = File.join(job_dir, file_name)
357
+ return candidate unless File.exist?(candidate) || File.symlink?(candidate)
358
+ return if File.symlink?(candidate)
359
+
360
+ resolved_file = File.realpath(candidate)
361
+ return unless resolved_file.start_with?("#{job_dir}/")
362
+
363
+ resolved_file
364
+ rescue Errno::ENOENT, Errno::EACCES
365
+ nil
366
+ end
301
367
  end
302
368
  end
303
369
  end
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.39.7"
4
+ VERSION = "0.39.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.39.7
4
+ version: 0.39.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan