taski 0.8.2 → 0.9.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +65 -50
  4. data/docs/GUIDE.md +41 -56
  5. data/examples/README.md +10 -29
  6. data/examples/clean_demo.rb +25 -65
  7. data/examples/large_tree_demo.rb +356 -0
  8. data/examples/message_demo.rb +0 -1
  9. data/examples/progress_demo.rb +13 -24
  10. data/examples/reexecution_demo.rb +8 -44
  11. data/lib/taski/execution/execution_facade.rb +150 -0
  12. data/lib/taski/execution/executor.rb +156 -357
  13. data/lib/taski/execution/registry.rb +15 -19
  14. data/lib/taski/execution/scheduler.rb +161 -140
  15. data/lib/taski/execution/task_observer.rb +41 -0
  16. data/lib/taski/execution/task_output_router.rb +41 -58
  17. data/lib/taski/execution/task_wrapper.rb +123 -219
  18. data/lib/taski/execution/worker_pool.rb +238 -64
  19. data/lib/taski/logging.rb +105 -0
  20. data/lib/taski/progress/layout/base.rb +600 -0
  21. data/lib/taski/progress/layout/filters.rb +126 -0
  22. data/lib/taski/progress/layout/log.rb +27 -0
  23. data/lib/taski/progress/layout/simple.rb +166 -0
  24. data/lib/taski/progress/layout/tags.rb +76 -0
  25. data/lib/taski/progress/layout/theme_drop.rb +84 -0
  26. data/lib/taski/progress/layout/tree.rb +300 -0
  27. data/lib/taski/progress/theme/base.rb +224 -0
  28. data/lib/taski/progress/theme/compact.rb +58 -0
  29. data/lib/taski/progress/theme/default.rb +25 -0
  30. data/lib/taski/progress/theme/detail.rb +48 -0
  31. data/lib/taski/progress/theme/plain.rb +40 -0
  32. data/lib/taski/static_analysis/analyzer.rb +5 -17
  33. data/lib/taski/static_analysis/dependency_graph.rb +19 -1
  34. data/lib/taski/static_analysis/visitor.rb +1 -39
  35. data/lib/taski/task.rb +44 -58
  36. data/lib/taski/test_helper/errors.rb +1 -1
  37. data/lib/taski/test_helper.rb +21 -35
  38. data/lib/taski/version.rb +1 -1
  39. data/lib/taski.rb +60 -61
  40. data/sig/taski.rbs +194 -203
  41. metadata +31 -8
  42. data/examples/section_demo.rb +0 -195
  43. data/lib/taski/execution/base_progress_display.rb +0 -364
  44. data/lib/taski/execution/execution_context.rb +0 -390
  45. data/lib/taski/execution/plain_progress_display.rb +0 -76
  46. data/lib/taski/execution/simple_progress_display.rb +0 -206
  47. data/lib/taski/execution/tree_progress_display.rb +0 -643
  48. data/lib/taski/section.rb +0 -74
@@ -22,10 +22,10 @@ module Taski
22
22
  READ_BUFFER_SIZE = 4096
23
23
  MAX_RECENT_LINES = 30 # Maximum number of recent lines to keep per task
24
24
 
25
- def initialize(original_stdout, execution_context = nil)
25
+ def initialize(original_stdout, execution_facade = nil)
26
26
  super()
27
27
  @original = original_stdout
28
- @execution_context = execution_context
28
+ @execution_facade = execution_facade
29
29
  @pipes = {} # task_class => TaskOutputPipe
30
30
  @thread_map = {} # Thread => task_class
31
31
  @recent_lines = {} # task_class => Array<String>
@@ -50,27 +50,22 @@ module Taski
50
50
  end
51
51
  end
52
52
 
53
- # Stop the background polling thread
54
53
  def stop_polling
55
54
  synchronize { @polling = false }
56
55
  @poll_thread&.join(0.5)
57
56
  @poll_thread = nil
58
57
  end
59
58
 
60
- # Start capturing output for the current thread
61
- # Creates a new pipe for the task and registers the thread mapping
62
- # @param task_class [Class] The task class being executed
63
59
  def start_capture(task_class)
64
60
  synchronize do
65
61
  pipe = TaskOutputPipe.new(task_class)
66
62
  @pipes[task_class] = pipe
67
63
  @thread_map[Thread.current] = task_class
68
- debug_log("Started capture for #{task_class} on thread #{Thread.current.object_id}")
64
+ Taski::Logging.debug(Taski::Logging::Events::OUTPUT_ROUTER_START_CAPTURE, task: task_class.name)
69
65
  end
70
66
  end
71
67
 
72
- # Stop capturing output for the current thread
73
- # Closes the write end of the pipe and drains remaining data
68
+ # Closes the write end and drains remaining data.
74
69
  def stop_capture
75
70
  task_class = nil
76
71
  pipe = nil
@@ -78,41 +73,20 @@ module Taski
78
73
  synchronize do
79
74
  task_class = @thread_map.delete(Thread.current)
80
75
  unless task_class
81
- debug_log("Warning: stop_capture called for unregistered thread #{Thread.current.object_id}")
76
+ Taski::Logging.debug(Taski::Logging::Events::OUTPUT_ROUTER_STOP_CAPTURE_UNREGISTERED)
82
77
  return
83
78
  end
84
79
 
85
80
  pipe = @pipes[task_class]
86
81
  pipe&.close_write
87
- debug_log("Stopped capture for #{task_class} on thread #{Thread.current.object_id}")
82
+ Taski::Logging.debug(Taski::Logging::Events::OUTPUT_ROUTER_STOP_CAPTURE, task: task_class.name)
88
83
  end
89
84
 
90
85
  # Drain any remaining data from the pipe after closing write end
91
86
  drain_pipe(pipe) if pipe
92
87
  end
93
88
 
94
- # Drain all remaining data from a pipe
95
- # Called after close_write to ensure all output is captured
96
- def drain_pipe(pipe)
97
- return if pipe.read_closed?
98
-
99
- loop do
100
- data = pipe.read_io.read_nonblock(READ_BUFFER_SIZE)
101
- debug_log("drain_pipe read #{data.bytesize} bytes for #{pipe.task_class}")
102
- store_output_lines(pipe.task_class, data)
103
- rescue IO::WaitReadable
104
- # Check if there's more data with a very short timeout
105
- ready, = IO.select([pipe.read_io], nil, nil, 0.001)
106
- break unless ready
107
- rescue IOError
108
- # All data has been read (EOFError) or pipe was closed by another thread
109
- synchronize { pipe.close_read }
110
- break
111
- end
112
- end
113
-
114
- # Poll all open pipes for available data
115
- # Should be called periodically from the display thread
89
+ # Called periodically from the display thread.
116
90
  def poll
117
91
  readable_pipes = synchronize do
118
92
  @pipes.values.reject { |p| p.read_closed? }.map(&:read_io)
@@ -129,25 +103,21 @@ module Taski
129
103
 
130
104
  read_from_pipe(pipe)
131
105
  end
132
- rescue IOError
106
+ rescue IOError, Errno::EBADF
133
107
  # Pipe was closed by another thread (drain_pipe), ignore
134
108
  end
135
109
 
136
- # Get the last output line for a task
137
- # @param task_class [Class] The task class
138
- # @return [String, nil] The last output line
139
110
  def last_line_for(task_class)
140
111
  synchronize { @recent_lines[task_class]&.last }
141
112
  end
142
113
 
143
- # Get recent output lines for a task (up to MAX_RECENT_LINES)
144
- # @param task_class [Class] The task class
145
- # @return [Array<String>] Recent output lines
146
- def recent_lines_for(task_class)
147
- synchronize { (@recent_lines[task_class] || []).dup }
114
+ def read(task_class, limit: nil)
115
+ synchronize do
116
+ lines = (@recent_lines[task_class] || []).dup
117
+ limit ? lines.last(limit) : lines
118
+ end
148
119
  end
149
120
 
150
- # Close all pipes and clean up
151
121
  def close_all
152
122
  synchronize do
153
123
  @pipes.each_value(&:close)
@@ -156,8 +126,6 @@ module Taski
156
126
  end
157
127
  end
158
128
 
159
- # Check if there are any active (not fully closed) pipes
160
- # @return [Boolean] true if there are active pipes
161
129
  def active?
162
130
  synchronize do
163
131
  @pipes.values.any? { |p| !p.read_closed? }
@@ -219,9 +187,7 @@ module Taski
219
187
  @original.winsize
220
188
  end
221
189
 
222
- # Get the write IO for the current thread's pipe
223
- # Used by Task#system to redirect subprocess output directly to the pipe
224
- # @return [IO, nil] The write IO or nil if not capturing
190
+ # Used by Task#system to redirect subprocess output to the pipe.
225
191
  def current_write_io
226
192
  synchronize do
227
193
  task_class = @thread_map[Thread.current]
@@ -232,7 +198,6 @@ module Taski
232
198
  end
233
199
  end
234
200
 
235
- # Delegate unknown methods to original stdout
236
201
  def method_missing(method, ...)
237
202
  @original.send(method, ...)
238
203
  end
@@ -243,6 +208,24 @@ module Taski
243
208
 
244
209
  private
245
210
 
211
+ def drain_pipe(pipe)
212
+ return if pipe.read_closed?
213
+
214
+ loop do
215
+ data = pipe.read_io.read_nonblock(READ_BUFFER_SIZE)
216
+ Taski::Logging.debug(Taski::Logging::Events::OUTPUT_ROUTER_DRAIN_PIPE, task: pipe.task_class.name, bytes: data.bytesize)
217
+ store_output_lines(pipe.task_class, data)
218
+ rescue IO::WaitReadable
219
+ # Check if there's more data with a very short timeout
220
+ ready, = IO.select([pipe.read_io], nil, nil, 0.001)
221
+ break unless ready
222
+ rescue IOError, Errno::EBADF
223
+ # All data has been read (EOFError) or pipe was closed by another thread
224
+ synchronize { pipe.close_read }
225
+ break
226
+ end
227
+ end
228
+
246
229
  def current_thread_pipe
247
230
  synchronize do
248
231
  task_class = @thread_map[Thread.current]
@@ -256,7 +239,7 @@ module Taski
256
239
  store_output_lines(pipe.task_class, data)
257
240
  rescue IO::WaitReadable
258
241
  # No data available yet
259
- rescue IOError
242
+ rescue IOError, Errno::EBADF
260
243
  # Pipe closed by writer (EOFError) or by another thread, close read end
261
244
  synchronize { pipe.close_read }
262
245
  end
@@ -269,20 +252,20 @@ module Taski
269
252
  @recent_lines[task_class] ||= []
270
253
  lines.each do |line|
271
254
  stripped = line.chomp
272
- @recent_lines[task_class] << stripped unless stripped.strip.empty?
255
+ next if stripped.strip.empty?
256
+ @recent_lines[task_class] << stripped
257
+ Taski::Logging.debug(
258
+ Taski::Logging::Events::TASK_OUTPUT,
259
+ task: task_class.name,
260
+ line: stripped
261
+ )
273
262
  end
274
- # Keep only the last MAX_RECENT_LINES
275
263
  if @recent_lines[task_class].size > MAX_RECENT_LINES
276
264
  @recent_lines[task_class] = @recent_lines[task_class].last(MAX_RECENT_LINES)
277
265
  end
278
- debug_log("store_output_lines: #{task_class} now has #{@recent_lines[task_class].size} lines")
266
+ Taski::Logging.debug(Taski::Logging::Events::OUTPUT_ROUTER_STORE_LINES, task: task_class.name, line_count: @recent_lines[task_class].size)
279
267
  end
280
268
  end
281
-
282
- def debug_log(message)
283
- return unless ENV["TASKI_DEBUG"]
284
- warn "[TaskOutputRouter] #{message}"
285
- end
286
269
  end
287
270
  end
288
271
  end