taski 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +139 -9
- data/docs/GUIDE.md +54 -0
- data/examples/README.md +3 -3
- data/examples/args_demo.rb +21 -20
- data/examples/data_pipeline_demo.rb +1 -1
- data/examples/large_tree_demo.rb +519 -0
- data/examples/simple_progress_demo.rb +80 -0
- data/lib/taski/args.rb +2 -8
- data/lib/taski/env.rb +17 -0
- data/lib/taski/execution/base_progress_display.rb +348 -0
- data/lib/taski/execution/execution_context.rb +4 -0
- data/lib/taski/execution/executor.rb +111 -131
- data/lib/taski/execution/plain_progress_display.rb +76 -0
- data/lib/taski/execution/simple_progress_display.rb +173 -0
- data/lib/taski/execution/task_output_router.rb +91 -20
- data/lib/taski/execution/task_wrapper.rb +34 -31
- data/lib/taski/execution/tree_progress_display.rb +121 -271
- data/lib/taski/static_analysis/visitor.rb +3 -0
- data/lib/taski/task.rb +42 -30
- data/lib/taski/test_helper/errors.rb +13 -0
- data/lib/taski/test_helper/minitest.rb +38 -0
- data/lib/taski/test_helper/mock_registry.rb +53 -0
- data/lib/taski/test_helper/mock_wrapper.rb +46 -0
- data/lib/taski/test_helper/rspec.rb +38 -0
- data/lib/taski/test_helper.rb +246 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +119 -8
- metadata +14 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_progress_display"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Execution
|
|
7
|
+
# PlainProgressDisplay provides plain text output without terminal escape codes.
|
|
8
|
+
# Designed for non-TTY environments (CI, log files, piped output).
|
|
9
|
+
#
|
|
10
|
+
# Output format:
|
|
11
|
+
# [START] TaskName
|
|
12
|
+
# [DONE] TaskName (123.4ms)
|
|
13
|
+
# [FAIL] TaskName: Error message
|
|
14
|
+
#
|
|
15
|
+
# Enable with: TASKI_PROGRESS_MODE=plain
|
|
16
|
+
class PlainProgressDisplay < BaseProgressDisplay
|
|
17
|
+
def initialize(output: $stderr)
|
|
18
|
+
super
|
|
19
|
+
@output.sync = true if @output.respond_to?(:sync=)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
# Template method: Called when a section impl is registered
|
|
25
|
+
def on_section_impl_registered(_section_class, impl_class)
|
|
26
|
+
@tasks[impl_class] ||= TaskProgress.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Template method: Called when a task state is updated
|
|
30
|
+
def on_task_updated(task_class, state, duration, error)
|
|
31
|
+
case state
|
|
32
|
+
when :running
|
|
33
|
+
@output.puts "[START] #{short_name(task_class)}"
|
|
34
|
+
when :completed
|
|
35
|
+
duration_str = duration ? " (#{format_duration(duration)})" : ""
|
|
36
|
+
@output.puts "[DONE] #{short_name(task_class)}#{duration_str}"
|
|
37
|
+
when :failed
|
|
38
|
+
error_msg = error ? ": #{error.message}" : ""
|
|
39
|
+
@output.puts "[FAIL] #{short_name(task_class)}#{error_msg}"
|
|
40
|
+
when :cleaning
|
|
41
|
+
@output.puts "[CLEAN] #{short_name(task_class)}"
|
|
42
|
+
when :clean_completed
|
|
43
|
+
duration_str = duration ? " (#{format_duration(duration)})" : ""
|
|
44
|
+
@output.puts "[CLEAN DONE] #{short_name(task_class)}#{duration_str}"
|
|
45
|
+
when :clean_failed
|
|
46
|
+
error_msg = error ? ": #{error.message}" : ""
|
|
47
|
+
@output.puts "[CLEAN FAIL] #{short_name(task_class)}#{error_msg}"
|
|
48
|
+
end
|
|
49
|
+
@output.flush
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Template method: Called when display starts
|
|
53
|
+
def on_start
|
|
54
|
+
if @root_task_class
|
|
55
|
+
@output.puts "[TASKI] Starting #{short_name(@root_task_class)}"
|
|
56
|
+
@output.flush
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Template method: Called when display stops
|
|
61
|
+
def on_stop
|
|
62
|
+
total_duration = @start_time ? ((Time.now - @start_time) * 1000).to_i : 0
|
|
63
|
+
completed = @tasks.values.count { |t| t.run_state == :completed }
|
|
64
|
+
failed = @tasks.values.count { |t| t.run_state == :failed }
|
|
65
|
+
total = @tasks.size
|
|
66
|
+
|
|
67
|
+
if failed > 0
|
|
68
|
+
@output.puts "[TASKI] Failed: #{failed}/#{total} tasks (#{total_duration}ms)"
|
|
69
|
+
else
|
|
70
|
+
@output.puts "[TASKI] Completed: #{completed}/#{total} tasks (#{total_duration}ms)"
|
|
71
|
+
end
|
|
72
|
+
@output.flush
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_progress_display"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Execution
|
|
7
|
+
# SimpleProgressDisplay provides a minimalist single-line progress display
|
|
8
|
+
# that shows task execution status in a compact format:
|
|
9
|
+
#
|
|
10
|
+
# ⠹ [3/5] DeployTask | Uploading files...
|
|
11
|
+
#
|
|
12
|
+
# This is an alternative to TreeProgressDisplay for users who prefer
|
|
13
|
+
# less verbose output.
|
|
14
|
+
class SimpleProgressDisplay < BaseProgressDisplay
|
|
15
|
+
SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
16
|
+
RENDER_INTERVAL = 0.1
|
|
17
|
+
|
|
18
|
+
ICONS = {
|
|
19
|
+
success: "✓",
|
|
20
|
+
failure: "✗",
|
|
21
|
+
pending: "○"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
COLORS = {
|
|
25
|
+
green: "\e[32m",
|
|
26
|
+
red: "\e[31m",
|
|
27
|
+
yellow: "\e[33m",
|
|
28
|
+
dim: "\e[2m",
|
|
29
|
+
reset: "\e[0m"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def initialize(output: $stdout)
|
|
33
|
+
super
|
|
34
|
+
@spinner_index = 0
|
|
35
|
+
@renderer_thread = nil
|
|
36
|
+
@running = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# Template method: Called when root task is set
|
|
42
|
+
def on_root_task_set
|
|
43
|
+
build_tree_structure
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Template method: Called when a section impl is registered
|
|
47
|
+
def on_section_impl_registered(_section_class, impl_class)
|
|
48
|
+
@tasks[impl_class] ||= TaskProgress.new
|
|
49
|
+
@tasks[impl_class].is_impl_candidate = false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Template method: Determine if display should activate
|
|
53
|
+
def should_activate?
|
|
54
|
+
tty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Template method: Called when display starts
|
|
58
|
+
def on_start
|
|
59
|
+
@running = true
|
|
60
|
+
@output.print "\e[?25l" # Hide cursor
|
|
61
|
+
@renderer_thread = Thread.new do
|
|
62
|
+
loop do
|
|
63
|
+
break unless @running
|
|
64
|
+
render_live
|
|
65
|
+
sleep RENDER_INTERVAL
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Template method: Called when display stops
|
|
71
|
+
def on_stop
|
|
72
|
+
@running = false
|
|
73
|
+
@renderer_thread&.join
|
|
74
|
+
@output.print "\e[?25h" # Show cursor
|
|
75
|
+
render_final
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def build_tree_structure
|
|
81
|
+
return unless @root_task_class
|
|
82
|
+
|
|
83
|
+
# Use TreeProgressDisplay's static method for tree building
|
|
84
|
+
tree = TreeProgressDisplay.build_tree_node(@root_task_class)
|
|
85
|
+
register_tasks_from_tree(tree)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def render_live
|
|
89
|
+
@monitor.synchronize do
|
|
90
|
+
@spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.size
|
|
91
|
+
line = build_status_line
|
|
92
|
+
# Clear line and write new content
|
|
93
|
+
@output.print "\r\e[K#{line}"
|
|
94
|
+
@output.flush
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def render_final
|
|
99
|
+
@monitor.synchronize do
|
|
100
|
+
total_duration = @start_time ? ((Time.now - @start_time) * 1000).to_i : 0
|
|
101
|
+
completed = @tasks.values.count { |p| p.run_state == :completed }
|
|
102
|
+
failed = @tasks.values.count { |p| p.run_state == :failed }
|
|
103
|
+
total = @tasks.size
|
|
104
|
+
|
|
105
|
+
line = if failed > 0
|
|
106
|
+
failed_tasks = @tasks.select { |_, p| p.run_state == :failed }
|
|
107
|
+
first_error = failed_tasks.values.first&.run_error
|
|
108
|
+
error_msg = first_error ? ": #{first_error.message}" : ""
|
|
109
|
+
"#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]} [#{completed}/#{total}] " \
|
|
110
|
+
"#{failed_tasks.keys.first} failed#{error_msg}"
|
|
111
|
+
else
|
|
112
|
+
"#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]} [#{completed}/#{total}] " \
|
|
113
|
+
"All tasks completed (#{total_duration}ms)"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@output.print "\r\e[K#{line}\n"
|
|
117
|
+
@output.flush
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_status_line
|
|
122
|
+
running_tasks = @tasks.select { |_, p| p.run_state == :running }
|
|
123
|
+
cleaning_tasks = @tasks.select { |_, p| p.clean_state == :cleaning }
|
|
124
|
+
completed = @tasks.values.count { |p| p.run_state == :completed }
|
|
125
|
+
failed = @tasks.values.count { |p| p.run_state == :failed }
|
|
126
|
+
total = @tasks.size
|
|
127
|
+
|
|
128
|
+
spinner = SPINNER_FRAMES[@spinner_index]
|
|
129
|
+
status_icon = if failed > 0
|
|
130
|
+
"#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]}"
|
|
131
|
+
elsif running_tasks.any? || cleaning_tasks.any?
|
|
132
|
+
"#{COLORS[:yellow]}#{spinner}#{COLORS[:reset]}"
|
|
133
|
+
else
|
|
134
|
+
"#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get current task names
|
|
138
|
+
current_tasks = if cleaning_tasks.any?
|
|
139
|
+
cleaning_tasks.keys.map { |t| short_name(t) }
|
|
140
|
+
else
|
|
141
|
+
running_tasks.keys.map { |t| short_name(t) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
task_names = current_tasks.first(3).join(", ")
|
|
145
|
+
task_names += "..." if current_tasks.size > 3
|
|
146
|
+
|
|
147
|
+
# Get last output message if available
|
|
148
|
+
output_suffix = build_output_suffix(running_tasks.keys.first || cleaning_tasks.keys.first)
|
|
149
|
+
|
|
150
|
+
parts = ["#{status_icon} [#{completed}/#{total}]"]
|
|
151
|
+
parts << task_names if task_names && !task_names.empty?
|
|
152
|
+
parts << "|" << output_suffix if output_suffix
|
|
153
|
+
|
|
154
|
+
parts.join(" ")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def build_output_suffix(task_class)
|
|
158
|
+
return nil unless @output_capture && task_class
|
|
159
|
+
|
|
160
|
+
last_line = @output_capture.last_line_for(task_class)
|
|
161
|
+
return nil unless last_line && !last_line.strip.empty?
|
|
162
|
+
|
|
163
|
+
# Truncate if too long
|
|
164
|
+
max_length = 40
|
|
165
|
+
if last_line.length > max_length
|
|
166
|
+
last_line[0, max_length - 3] + "..."
|
|
167
|
+
else
|
|
168
|
+
last_line
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -18,14 +18,42 @@ module Taski
|
|
|
18
18
|
include MonitorMixin
|
|
19
19
|
|
|
20
20
|
POLL_TIMEOUT = 0.05 # 50ms timeout for IO.select
|
|
21
|
+
POLL_INTERVAL = 0.1 # 100ms between polls (matches TreeProgressDisplay)
|
|
21
22
|
READ_BUFFER_SIZE = 4096
|
|
23
|
+
MAX_RECENT_LINES = 30 # Maximum number of recent lines to keep per task
|
|
22
24
|
|
|
23
25
|
def initialize(original_stdout)
|
|
24
26
|
super()
|
|
25
27
|
@original = original_stdout
|
|
26
|
-
@pipes = {}
|
|
27
|
-
@thread_map = {}
|
|
28
|
-
@
|
|
28
|
+
@pipes = {} # task_class => TaskOutputPipe
|
|
29
|
+
@thread_map = {} # Thread => task_class
|
|
30
|
+
@recent_lines = {} # task_class => Array<String>
|
|
31
|
+
@poll_thread = nil
|
|
32
|
+
@polling = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Start the background polling thread
|
|
36
|
+
# This ensures pipes are drained even when display doesn't poll
|
|
37
|
+
def start_polling
|
|
38
|
+
synchronize do
|
|
39
|
+
return if @polling
|
|
40
|
+
@polling = true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@poll_thread = Thread.new do
|
|
44
|
+
loop do
|
|
45
|
+
break unless @polling
|
|
46
|
+
poll
|
|
47
|
+
sleep POLL_INTERVAL
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Stop the background polling thread
|
|
53
|
+
def stop_polling
|
|
54
|
+
synchronize { @polling = false }
|
|
55
|
+
@poll_thread&.join(0.5)
|
|
56
|
+
@poll_thread = nil
|
|
29
57
|
end
|
|
30
58
|
|
|
31
59
|
# Start capturing output for the current thread
|
|
@@ -41,8 +69,11 @@ module Taski
|
|
|
41
69
|
end
|
|
42
70
|
|
|
43
71
|
# Stop capturing output for the current thread
|
|
44
|
-
# Closes the write end of the pipe
|
|
72
|
+
# Closes the write end of the pipe and drains remaining data
|
|
45
73
|
def stop_capture
|
|
74
|
+
task_class = nil
|
|
75
|
+
pipe = nil
|
|
76
|
+
|
|
46
77
|
synchronize do
|
|
47
78
|
task_class = @thread_map.delete(Thread.current)
|
|
48
79
|
unless task_class
|
|
@@ -54,6 +85,29 @@ module Taski
|
|
|
54
85
|
pipe&.close_write
|
|
55
86
|
debug_log("Stopped capture for #{task_class} on thread #{Thread.current.object_id}")
|
|
56
87
|
end
|
|
88
|
+
|
|
89
|
+
# Drain any remaining data from the pipe after closing write end
|
|
90
|
+
drain_pipe(pipe) if pipe
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Drain all remaining data from a pipe
|
|
94
|
+
# Called after close_write to ensure all output is captured
|
|
95
|
+
def drain_pipe(pipe)
|
|
96
|
+
return if pipe.read_closed?
|
|
97
|
+
|
|
98
|
+
loop do
|
|
99
|
+
data = pipe.read_io.read_nonblock(READ_BUFFER_SIZE)
|
|
100
|
+
debug_log("drain_pipe read #{data.bytesize} bytes for #{pipe.task_class}")
|
|
101
|
+
store_output_lines(pipe.task_class, data)
|
|
102
|
+
rescue IO::WaitReadable
|
|
103
|
+
# Check if there's more data with a very short timeout
|
|
104
|
+
ready, = IO.select([pipe.read_io], nil, nil, 0.001)
|
|
105
|
+
break unless ready
|
|
106
|
+
rescue IOError
|
|
107
|
+
# All data has been read (EOFError) or pipe was closed by another thread
|
|
108
|
+
synchronize { pipe.close_read }
|
|
109
|
+
break
|
|
110
|
+
end
|
|
57
111
|
end
|
|
58
112
|
|
|
59
113
|
# Poll all open pipes for available data
|
|
@@ -64,7 +118,8 @@ module Taski
|
|
|
64
118
|
end
|
|
65
119
|
return if readable_pipes.empty?
|
|
66
120
|
|
|
67
|
-
|
|
121
|
+
# Handle race condition: pipe may be closed between check and select
|
|
122
|
+
ready, = IO.select(readable_pipes, nil, nil, POLL_TIMEOUT)
|
|
68
123
|
return unless ready
|
|
69
124
|
|
|
70
125
|
ready.each do |read_io|
|
|
@@ -73,13 +128,22 @@ module Taski
|
|
|
73
128
|
|
|
74
129
|
read_from_pipe(pipe)
|
|
75
130
|
end
|
|
131
|
+
rescue IOError
|
|
132
|
+
# Pipe was closed by another thread (drain_pipe), ignore
|
|
76
133
|
end
|
|
77
134
|
|
|
78
135
|
# Get the last output line for a task
|
|
79
136
|
# @param task_class [Class] The task class
|
|
80
137
|
# @return [String, nil] The last output line
|
|
81
138
|
def last_line_for(task_class)
|
|
82
|
-
synchronize { @
|
|
139
|
+
synchronize { @recent_lines[task_class]&.last }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get recent output lines for a task (up to MAX_RECENT_LINES)
|
|
143
|
+
# @param task_class [Class] The task class
|
|
144
|
+
# @return [Array<String>] Recent output lines
|
|
145
|
+
def recent_lines_for(task_class)
|
|
146
|
+
synchronize { (@recent_lines[task_class] || []).dup }
|
|
83
147
|
end
|
|
84
148
|
|
|
85
149
|
# Close all pipes and clean up
|
|
@@ -104,14 +168,13 @@ module Taski
|
|
|
104
168
|
def write(str)
|
|
105
169
|
pipe = current_thread_pipe
|
|
106
170
|
if pipe && !pipe.write_closed?
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
else
|
|
113
|
-
debug_log("Output not captured: thread #{Thread.current.object_id} not mapped")
|
|
171
|
+
begin
|
|
172
|
+
pipe.write_io.write(str)
|
|
173
|
+
rescue IOError
|
|
174
|
+
# Pipe was closed by another thread (e.g., stop_capture), fall back to original
|
|
175
|
+
@original.write(str)
|
|
114
176
|
end
|
|
177
|
+
else
|
|
115
178
|
@original.write(str)
|
|
116
179
|
end
|
|
117
180
|
end
|
|
@@ -189,27 +252,35 @@ module Taski
|
|
|
189
252
|
|
|
190
253
|
def read_from_pipe(pipe)
|
|
191
254
|
data = pipe.read_io.read_nonblock(READ_BUFFER_SIZE)
|
|
192
|
-
|
|
255
|
+
store_output_lines(pipe.task_class, data)
|
|
193
256
|
rescue IO::WaitReadable
|
|
194
257
|
# No data available yet
|
|
195
|
-
rescue
|
|
196
|
-
# Pipe closed by writer, close read end
|
|
258
|
+
rescue IOError
|
|
259
|
+
# Pipe closed by writer (EOFError) or by another thread, close read end
|
|
197
260
|
synchronize { pipe.close_read }
|
|
198
261
|
end
|
|
199
262
|
|
|
200
|
-
def
|
|
263
|
+
def store_output_lines(task_class, data)
|
|
201
264
|
return if data.nil? || data.empty?
|
|
202
265
|
|
|
203
266
|
lines = data.lines
|
|
204
|
-
last_non_empty = lines.reverse.find { |l| !l.strip.empty? }
|
|
205
267
|
synchronize do
|
|
206
|
-
@
|
|
268
|
+
@recent_lines[task_class] ||= []
|
|
269
|
+
lines.each do |line|
|
|
270
|
+
stripped = line.chomp
|
|
271
|
+
@recent_lines[task_class] << stripped unless stripped.strip.empty?
|
|
272
|
+
end
|
|
273
|
+
# Keep only the last MAX_RECENT_LINES
|
|
274
|
+
if @recent_lines[task_class].size > MAX_RECENT_LINES
|
|
275
|
+
@recent_lines[task_class] = @recent_lines[task_class].last(MAX_RECENT_LINES)
|
|
276
|
+
end
|
|
277
|
+
debug_log("store_output_lines: #{task_class} now has #{@recent_lines[task_class].size} lines")
|
|
207
278
|
end
|
|
208
279
|
end
|
|
209
280
|
|
|
210
281
|
def debug_log(message)
|
|
211
282
|
return unless ENV["TASKI_DEBUG"]
|
|
212
|
-
|
|
283
|
+
warn "[TaskOutputRouter] #{message}"
|
|
213
284
|
end
|
|
214
285
|
end
|
|
215
286
|
end
|
|
@@ -38,10 +38,12 @@ module Taski
|
|
|
38
38
|
# @param [Object] task - The task instance being wrapped.
|
|
39
39
|
# @param [Object] registry - The registry used to query abort status and coordinate execution.
|
|
40
40
|
# @param [Object, nil] execution_context - Optional execution context used to trigger and report execution and cleanup.
|
|
41
|
-
|
|
41
|
+
# @param [Hash, nil] args - User-defined arguments for Task.new usage.
|
|
42
|
+
def initialize(task, registry:, execution_context: nil, args: nil)
|
|
42
43
|
@task = task
|
|
43
44
|
@registry = registry
|
|
44
45
|
@execution_context = execution_context
|
|
46
|
+
@args = args
|
|
45
47
|
@result = nil
|
|
46
48
|
@clean_result = nil
|
|
47
49
|
@error = nil
|
|
@@ -261,14 +263,18 @@ module Taski
|
|
|
261
263
|
##
|
|
262
264
|
# Ensures args are set during block execution, then resets if they weren't set before.
|
|
263
265
|
# This allows Task.new.run usage without requiring explicit args setup.
|
|
266
|
+
# If args are already set (e.g., from Task.run class method), just yields the block.
|
|
267
|
+
# Uses stored @args if set (from Task.new), otherwise uses empty hash.
|
|
264
268
|
# @yield The block to execute with args lifecycle management
|
|
265
269
|
# @return [Object] The result of the block
|
|
266
|
-
def with_args_lifecycle
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
Taski.
|
|
270
|
+
def with_args_lifecycle(&block)
|
|
271
|
+
# If args are already set, just execute the block
|
|
272
|
+
return yield if Taski.args
|
|
273
|
+
|
|
274
|
+
options = @args || {}
|
|
275
|
+
Taski.send(:with_env, root_task: @task.class) do
|
|
276
|
+
Taski.send(:with_args, options: options, &block)
|
|
277
|
+
end
|
|
272
278
|
end
|
|
273
279
|
|
|
274
280
|
##
|
|
@@ -276,26 +282,11 @@ module Taski
|
|
|
276
282
|
# If the task is pending, triggers execution (via the configured ExecutionContext when present, otherwise via Executor) outside the monitor; if the task is running, waits until it becomes completed; if already completed, returns immediately.
|
|
277
283
|
# @raise [Taski::TaskAbortException] If the registry requested an abort before execution begins.
|
|
278
284
|
def trigger_execution_and_wait
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
should_execute = true
|
|
285
|
-
when STATE_RUNNING
|
|
286
|
-
@condition.wait_until { @state == STATE_COMPLETED }
|
|
287
|
-
when STATE_COMPLETED
|
|
288
|
-
# Already done
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
if should_execute
|
|
293
|
-
# Execute outside the lock to avoid deadlock
|
|
294
|
-
# Use ensure_execution_context to create a shared context if not set
|
|
295
|
-
context = ensure_execution_context
|
|
296
|
-
context.trigger_execution(@task.class, registry: @registry)
|
|
297
|
-
# After execution returns, the task is completed
|
|
298
|
-
end
|
|
285
|
+
trigger_and_wait(
|
|
286
|
+
state_accessor: -> { @state },
|
|
287
|
+
condition: @condition,
|
|
288
|
+
trigger: ->(ctx) { ctx.trigger_execution(@task.class, registry: @registry) }
|
|
289
|
+
)
|
|
299
290
|
end
|
|
300
291
|
|
|
301
292
|
##
|
|
@@ -304,14 +295,27 @@ module Taski
|
|
|
304
295
|
# If an ExecutionContext is configured the cleanup is invoked through it; otherwise a fallback executor is used.
|
|
305
296
|
# @raise [Taski::TaskAbortException] if the registry has requested an abort.
|
|
306
297
|
def trigger_clean_and_wait
|
|
298
|
+
trigger_and_wait(
|
|
299
|
+
state_accessor: -> { @clean_state },
|
|
300
|
+
condition: @clean_condition,
|
|
301
|
+
trigger: ->(ctx) { ctx.trigger_clean(@task.class, registry: @registry) }
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Generic trigger-and-wait implementation for both run and clean phases.
|
|
306
|
+
# @param state_accessor [Proc] Lambda returning the current state
|
|
307
|
+
# @param condition [MonitorMixin::ConditionVariable] Condition to wait on
|
|
308
|
+
# @param trigger [Proc] Lambda receiving context to trigger execution
|
|
309
|
+
# @raise [Taski::TaskAbortException] If the registry requested an abort
|
|
310
|
+
def trigger_and_wait(state_accessor:, condition:, trigger:)
|
|
307
311
|
should_execute = false
|
|
308
312
|
@monitor.synchronize do
|
|
309
|
-
case
|
|
313
|
+
case state_accessor.call
|
|
310
314
|
when STATE_PENDING
|
|
311
315
|
check_abort!
|
|
312
316
|
should_execute = true
|
|
313
317
|
when STATE_RUNNING
|
|
314
|
-
|
|
318
|
+
condition.wait_until { state_accessor.call == STATE_COMPLETED }
|
|
315
319
|
when STATE_COMPLETED
|
|
316
320
|
# Already done
|
|
317
321
|
end
|
|
@@ -319,9 +323,8 @@ module Taski
|
|
|
319
323
|
|
|
320
324
|
if should_execute
|
|
321
325
|
# Execute outside the lock to avoid deadlock
|
|
322
|
-
# Use ensure_execution_context to reuse the context from run phase
|
|
323
326
|
context = ensure_execution_context
|
|
324
|
-
|
|
327
|
+
trigger.call(context)
|
|
325
328
|
# After execution returns, the task is completed
|
|
326
329
|
end
|
|
327
330
|
end
|