pindo 5.13.12 → 5.13.13

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/funlog.rb +62 -5
  3. data/lib/pindo/base/git_handler.rb +83 -22
  4. data/lib/pindo/base/output_sink.rb +69 -0
  5. data/lib/pindo/command/android/autobuild.rb +57 -8
  6. data/lib/pindo/command/appstore/autobuild.rb +10 -1
  7. data/lib/pindo/command/ios/autobuild.rb +59 -7
  8. data/lib/pindo/command/jps/media.rb +164 -13
  9. data/lib/pindo/command/jps/upload.rb +14 -9
  10. data/lib/pindo/command/unity/autobuild.rb +64 -10
  11. data/lib/pindo/command/utils/tag.rb +9 -1
  12. data/lib/pindo/command/web/autobuild.rb +59 -10
  13. data/lib/pindo/module/android/android_build_helper.rb +6 -7
  14. data/lib/pindo/module/build/git_repo_helper.rb +29 -25
  15. data/lib/pindo/module/pgyer/pgyerhelper.rb +174 -77
  16. data/lib/pindo/module/task/core/concurrent_execution_strategy.rb +237 -0
  17. data/lib/pindo/module/task/core/dependency_checker.rb +123 -0
  18. data/lib/pindo/module/task/core/execution_strategy.rb +61 -0
  19. data/lib/pindo/module/task/core/resource_lock_manager.rb +190 -0
  20. data/lib/pindo/module/task/core/serial_execution_strategy.rb +60 -0
  21. data/lib/pindo/module/task/core/task_executor.rb +131 -0
  22. data/lib/pindo/module/task/core/task_queue.rb +221 -0
  23. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +1 -1
  24. data/lib/pindo/module/task/model/build/android_build_task.rb +6 -2
  25. data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +2 -3
  26. data/lib/pindo/module/task/model/build/ios_build_task.rb +6 -0
  27. data/lib/pindo/module/task/model/build_task.rb +22 -0
  28. data/lib/pindo/module/task/model/git/git_commit_task.rb +11 -2
  29. data/lib/pindo/module/task/model/git_task.rb +6 -0
  30. data/lib/pindo/module/task/model/jps/jps_message_task.rb +9 -11
  31. data/lib/pindo/module/task/model/jps/jps_upload_media_task.rb +204 -103
  32. data/lib/pindo/module/task/model/jps_task.rb +0 -1
  33. data/lib/pindo/module/task/model/unity_task.rb +38 -2
  34. data/lib/pindo/module/task/output/multi_line_output_manager.rb +380 -0
  35. data/lib/pindo/module/task/output/multi_line_task_display.rb +185 -0
  36. data/lib/pindo/module/task/output/stdout_redirector.rb +95 -0
  37. data/lib/pindo/module/task/pindo_task.rb +133 -9
  38. data/lib/pindo/module/task/task_manager.rb +98 -268
  39. data/lib/pindo/module/task/task_reporter.rb +135 -0
  40. data/lib/pindo/module/task/task_resources/resource_instance.rb +90 -0
  41. data/lib/pindo/module/task/task_resources/resource_registry.rb +105 -0
  42. data/lib/pindo/module/task/task_resources/resource_type.rb +59 -0
  43. data/lib/pindo/module/task/task_resources/types/directory_based_resource.rb +63 -0
  44. data/lib/pindo/module/task/task_resources/types/global_exclusive_resource.rb +33 -0
  45. data/lib/pindo/module/task/task_resources/types/global_shared_resource.rb +34 -0
  46. data/lib/pindo/module/xcode/xcode_build_helper.rb +26 -8
  47. data/lib/pindo/options/groups/jps_options.rb +10 -0
  48. data/lib/pindo/options/groups/task_options.rb +39 -0
  49. data/lib/pindo/version.rb +3 -2
  50. metadata +20 -1
@@ -0,0 +1,380 @@
1
+ require 'thread'
2
+ require 'fileutils'
3
+ require_relative 'multi_line_task_display'
4
+ require_relative '../../../base/output_sink'
5
+
6
+ module Pindo
7
+ module TaskSystem
8
+ # 多行输出管理器
9
+ # 管理所有任务的输出(终端显示 + 日志文件)
10
+ # 实现 OutputSink 接口
11
+ class MultiLineOutputManager < OutputSink
12
+ # 初始化输出管理器
13
+ # @param log_dir [String] 日志目录路径
14
+ # @param max_lines_per_task [Integer] 每个任务显示的最大行数
15
+ # @param max_recent_completed [Integer] 显示的最近完成任务数
16
+ # @param auto_adjust [Boolean] 是否自动适应终端大小
17
+ def initialize(
18
+ log_dir: './pindo_logs',
19
+ max_lines_per_task: 5,
20
+ max_recent_completed: 3,
21
+ auto_adjust: true
22
+ )
23
+ @mutex = Mutex.new
24
+ @log_dir = log_dir
25
+ @max_lines_per_task = max_lines_per_task
26
+ @max_recent_completed = max_recent_completed
27
+ @auto_adjust = auto_adjust
28
+
29
+ # 任务显示器映射
30
+ @task_displays = {} # task_id => MultiLineTaskDisplay
31
+
32
+ # 日志文件映射
33
+ @task_loggers = {} # task_id => File
34
+
35
+ # 终端状态
36
+ @terminal_lines = 0 # 当前占用的终端行数
37
+ @last_render_time = Time.now
38
+ @min_render_interval = 0.1 # 最小刷新间隔(秒)
39
+
40
+ # 创建日志目录
41
+ FileUtils.mkdir_p(@log_dir)
42
+
43
+ # 隐藏光标(更流畅的动画)
44
+ STDOUT.print "\e[?25l" if supports_ansi?
45
+
46
+ # 注册退出时恢复光标
47
+ at_exit { STDOUT.print "\e[?25h" if supports_ansi? }
48
+ end
49
+
50
+ # ============================================
51
+ # 任务注册
52
+ # ============================================
53
+
54
+ # 注册任务到输出管理器
55
+ # @param task [Object] 任务对象(需要有 id, name, task_key 属性)
56
+ def register_task(task)
57
+ @mutex.synchronize do
58
+ task_id = task.id
59
+
60
+ # 创建多行显示器
61
+ display = MultiLineTaskDisplay.new(
62
+ task_id,
63
+ task.name,
64
+ max_lines: @max_lines_per_task
65
+ )
66
+ @task_displays[task_id] = display
67
+
68
+ # 创建日志文件
69
+ log_file_name = "#{task.task_key}_#{task_id}.log"
70
+ log_file_path = File.join(@log_dir, log_file_name)
71
+ @task_loggers[task_id] = File.open(log_file_path, 'w')
72
+
73
+ # 渲染终端
74
+ render_terminal
75
+ end
76
+ end
77
+
78
+ # ============================================
79
+ # 任务生命周期
80
+ # ============================================
81
+
82
+ # 任务开始
83
+ # @param task_id [String] 任务 ID
84
+ def start_task(task_id)
85
+ @mutex.synchronize do
86
+ display = @task_displays[task_id]
87
+ display&.add_line("任务开始")
88
+
89
+ write_log(task_id, "=" * 60)
90
+ write_log(task_id, "任务开始")
91
+ write_log(task_id, "=" * 60)
92
+
93
+ render_terminal
94
+ end
95
+ end
96
+
97
+ # 任务成功
98
+ # @param task_id [String] 任务 ID
99
+ # @param message [String, nil] 成功消息
100
+ # @param execution_time [Float, nil] 执行时间(秒)
101
+ def success_task(task_id, message = nil, execution_time: nil)
102
+ @mutex.synchronize do
103
+ display = @task_displays[task_id]
104
+
105
+ msg = if execution_time
106
+ message ? "#{message} (耗时 #{execution_time.round(2)}s)" : "完成 (耗时 #{execution_time.round(2)}s)"
107
+ else
108
+ message || "完成"
109
+ end
110
+
111
+ display&.mark_success(msg)
112
+
113
+ write_log(task_id, "=" * 60)
114
+ write_log(task_id, "任务成功: #{msg}")
115
+ write_log(task_id, "=" * 60)
116
+
117
+ close_log(task_id)
118
+ render_terminal
119
+ end
120
+ end
121
+
122
+ # 任务失败
123
+ # @param task_id [String] 任务 ID
124
+ # @param error_message [String] 错误消息
125
+ def error_task(task_id, error_message)
126
+ @mutex.synchronize do
127
+ display = @task_displays[task_id]
128
+ display&.mark_error(error_message)
129
+
130
+ write_log(task_id, "=" * 60)
131
+ write_log(task_id, "任务失败: #{error_message}")
132
+ write_log(task_id, "=" * 60)
133
+
134
+ close_log(task_id)
135
+ render_terminal
136
+ end
137
+ end
138
+
139
+ # ============================================
140
+ # 实现 OutputSink 接口
141
+ # ============================================
142
+
143
+ # 更新进度(终端可见 + 日志记录)
144
+ # @param task_id [String] 任务 ID
145
+ # @param message [String] 进度消息
146
+ def update_progress(task_id, message)
147
+ @mutex.synchronize do
148
+ display = @task_displays[task_id]
149
+ display&.add_line(message)
150
+
151
+ write_log(task_id, "[PROGRESS] #{message}")
152
+ render_terminal
153
+ end
154
+ end
155
+
156
+ # 更新当前行(不滚动)
157
+ # @param task_id [String] 任务 ID
158
+ # @param message [String] 更新消息
159
+ def update_current_line(task_id, message)
160
+ @mutex.synchronize do
161
+ display = @task_displays[task_id]
162
+ display&.update_last_line(message)
163
+
164
+ write_log(task_id, "[UPDATE] #{message}")
165
+ render_terminal
166
+ end
167
+ end
168
+
169
+ # 记录日志(更新终端 + 写文件)
170
+ # @param task_id [String] 任务 ID
171
+ # @param message [String] 日志消息
172
+ def log_info(task_id, message)
173
+ @mutex.synchronize do
174
+ display = @task_displays[task_id]
175
+ display&.add_line(message)
176
+
177
+ write_log(task_id, "[INFO] #{message}")
178
+ render_terminal
179
+ end
180
+ end
181
+
182
+ # 记录警告
183
+ # @param task_id [String] 任务 ID
184
+ # @param message [String] 警告消息
185
+ def log_warning(task_id, message)
186
+ @mutex.synchronize do
187
+ display = @task_displays[task_id]
188
+ display&.add_line("\e[33m#{message}\e[0m") # 黄色高亮
189
+
190
+ write_log(task_id, "[WARN] #{message}")
191
+ render_terminal
192
+ end
193
+ end
194
+
195
+ # 记录错误
196
+ # @param task_id [String] 任务 ID
197
+ # @param message [String] 错误消息
198
+ def log_error(task_id, message)
199
+ @mutex.synchronize do
200
+ display = @task_displays[task_id]
201
+ display&.add_line("\e[31m#{message}\e[0m") # 红色高亮
202
+
203
+ write_log(task_id, "[ERROR] #{message}")
204
+ render_terminal
205
+ end
206
+ end
207
+
208
+ # 记录详细日志
209
+ # @param task_id [String] 任务 ID
210
+ # @param message [String] 详细日志
211
+ def log_detail(task_id, message)
212
+ @mutex.synchronize do
213
+ # 只有非空消息才更新显示
214
+ unless message.strip.empty?
215
+ display = @task_displays[task_id]
216
+ # 为了避免刷屏,普通日志使用 update_last_line 还是 add_line?
217
+ # 考虑到 log_detail 可能是一行行输出,使用 add_line 更像滚动的日志
218
+ display&.add_line(message)
219
+ end
220
+
221
+ write_log(task_id, message)
222
+ render_terminal
223
+ end
224
+ end
225
+
226
+ # ============================================
227
+ # 终端渲染(核心显示逻辑)
228
+ # ============================================
229
+
230
+ private
231
+
232
+ # 渲染终端显示
233
+ def render_terminal
234
+ # 限制刷新频率
235
+ now = Time.now
236
+ return if now - @last_render_time < @min_render_interval
237
+
238
+ # 如果不支持 ANSI,跳过渲染
239
+ # 注意:这里使用 STDOUT 检查,因为 $stdout 可能已被重定向
240
+ return unless supports_ansi?
241
+
242
+ # 获取终端尺寸
243
+ terminal_height = get_terminal_height
244
+ terminal_width = get_terminal_width
245
+ available_lines = terminal_height - 5
246
+
247
+ # 清除旧内容
248
+ clear_terminal
249
+
250
+ # 分类任务
251
+ running_tasks = @task_displays.select { |_, d| d.status == MultiLineTaskDisplay::STATUS_RUNNING }
252
+ completed_tasks = @task_displays.select { |_, d|
253
+ d.status == MultiLineTaskDisplay::STATUS_SUCCESS || d.status == MultiLineTaskDisplay::STATUS_ERROR
254
+ }
255
+ pending_tasks = @task_displays.select { |_, d| d.status == MultiLineTaskDisplay::STATUS_PENDING }
256
+
257
+ # 标题
258
+ total_tasks = @task_displays.size
259
+ completed_count = completed_tasks.size
260
+ STDOUT.puts "\e[1;36m⣾ 执行任务中... (#{running_tasks.size}/#{total_tasks} 运行中, #{completed_count}/#{total_tasks} 已完成)\e[0m"
261
+ used_lines = 1
262
+
263
+ # 1. 正在运行的任务(优先级最高)
264
+ if running_tasks.any?
265
+ STDOUT.puts ""
266
+ STDOUT.puts "\e[2m━━━━━━━━━━━━━━━━ 正在运行 ━━━━━━━━━━━━━━━━\e[0m"
267
+ used_lines += 2
268
+
269
+ running_tasks.each do |task_id, display|
270
+ # 自动适应终端大小
271
+ lines_for_this_task = if @auto_adjust && (used_lines + display.line_count > available_lines)
272
+ [available_lines - used_lines, 2].max
273
+ else
274
+ @max_lines_per_task
275
+ end
276
+
277
+ STDOUT.puts ""
278
+ # 传递 max_width 防止换行
279
+ STDOUT.puts display.render(max_lines: lines_for_this_task, max_width: terminal_width)
280
+ used_lines += display.line_count + 1
281
+
282
+ break if used_lines >= available_lines
283
+ end
284
+ end
285
+
286
+ # 2. 最近完成的任务(如果空间允许)
287
+ if completed_tasks.any? && (used_lines + 10 < available_lines)
288
+ STDOUT.puts ""
289
+ STDOUT.puts "\e[2m━━━━━━━━━━━━━━━━ 最近完成 ━━━━━━━━━━━━━━━━\e[0m"
290
+ used_lines += 2
291
+
292
+ recent_completed = completed_tasks.values.last(@max_recent_completed)
293
+ recent_completed.each do |display|
294
+ STDOUT.puts ""
295
+ # 完成的任务只显示 1 行,同样需要防止换行
296
+ STDOUT.puts display.render(max_lines: 1, max_width: terminal_width)
297
+ used_lines += 2
298
+
299
+ break if used_lines >= available_lines
300
+ end
301
+ end
302
+
303
+ # 3. 其他任务(折叠显示)
304
+ if pending_tasks.any? || completed_tasks.size > @max_recent_completed
305
+ STDOUT.puts ""
306
+ STDOUT.puts "\e[2m━━━━━━━━━━━━━━━━ 其他 ━━━━━━━━━━━━━━━━\e[0m"
307
+
308
+ other_completed = [completed_tasks.size - @max_recent_completed, 0].max
309
+ summary = " 已完成: #{other_completed} 等待中: #{pending_tasks.size}"
310
+ STDOUT.puts summary
311
+ used_lines += 3
312
+ end
313
+
314
+ @terminal_lines = used_lines
315
+ @last_render_time = now
316
+ end
317
+
318
+ # 清除终端内容
319
+ def clear_terminal
320
+ if @terminal_lines > 0
321
+ STDOUT.print "\e[#{@terminal_lines}A" # 光标上移
322
+ STDOUT.print "\e[J" # 清除到屏幕底部
323
+ end
324
+ end
325
+
326
+ # 获取终端高度
327
+ # @return [Integer] 终端行数
328
+ def get_terminal_height
329
+ require 'io/console'
330
+ $stdout.winsize[0]
331
+ rescue
332
+ 40 # 默认 40 行
333
+ end
334
+
335
+ # 获取终端宽度
336
+ # @return [Integer] 终端列数
337
+ def get_terminal_width
338
+ require 'io/console'
339
+ $stdout.winsize[1]
340
+ rescue
341
+ 80 # 默认 80 列
342
+ end
343
+
344
+ # 检查是否支持 ANSI 转义序列
345
+ # @return [Boolean] 是否支持 ANSI
346
+ def supports_ansi?
347
+ return false unless $stdout.tty?
348
+ return false if ENV['TERM'] == 'dumb'
349
+ return false if ENV['PINDO_NO_SPINNER'] == '1'
350
+ true
351
+ end
352
+
353
+ # ============================================
354
+ # 日志文件操作
355
+ # ============================================
356
+
357
+ # 写入日志
358
+ # @param task_id [String] 任务 ID
359
+ # @param message [String] 日志消息
360
+ def write_log(task_id, message)
361
+ logger = @task_loggers[task_id]
362
+ return unless logger
363
+
364
+ timestamp = Time.now.strftime('%H:%M:%S')
365
+ logger.puts("[#{timestamp}] #{message}")
366
+ logger.flush
367
+ end
368
+
369
+ # 关闭日志文件
370
+ # @param task_id [String] 任务 ID
371
+ def close_log(task_id)
372
+ logger = @task_loggers[task_id]
373
+ return unless logger
374
+
375
+ logger.close
376
+ @task_loggers.delete(task_id)
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,185 @@
1
+ require 'thread'
2
+
3
+ module Pindo
4
+ module TaskSystem
5
+ # 单任务多行显示器
6
+ # 管理单个任务的多行滚动显示,采用 FIFO 队列机制
7
+ class MultiLineTaskDisplay
8
+ attr_reader :task_id, :task_name, :status, :lines
9
+
10
+ # 任务状态枚举
11
+ STATUS_RUNNING = :running
12
+ STATUS_SUCCESS = :success
13
+ STATUS_ERROR = :error
14
+ STATUS_PENDING = :pending
15
+
16
+ # 初始化显示器
17
+ # @param task_id [String] 任务 ID
18
+ # @param task_name [String] 任务名称
19
+ # @param max_lines [Integer] 最大显示行数(默认 5)
20
+ def initialize(task_id, task_name, max_lines: 5)
21
+ @task_id = task_id
22
+ # 防止任务名称中有换行符破坏显示
23
+ @task_name = task_name.to_s.gsub("\n", " ")
24
+ @max_lines = max_lines
25
+ @status = STATUS_RUNNING
26
+ @lines = [] # 滚动缓冲区(FIFO 队列)
27
+ @mutex = Mutex.new
28
+ end
29
+
30
+ # 添加新行(滚动)
31
+ # 新行添加到队列尾部,如果超过 max_lines,则移除最旧的行
32
+ # 支持多行消息自动拆分
33
+ # @param message [String] 消息内容
34
+ def add_line(message)
35
+ @mutex.synchronize do
36
+ timestamp = Time.now.strftime('%H:%M:%S')
37
+
38
+ # 处理包含换行符的消息,确保每一行都独立计数
39
+ str_message = message.to_s
40
+
41
+ # 如果消息为空,至少添加一行空行(或者忽略,这里选择忽略空消息以保持整洁)
42
+ return if str_message.empty?
43
+
44
+ sub_messages = str_message.split("\n")
45
+ if sub_messages.empty?
46
+ # 处理仅包含换行符的情况
47
+ sub_messages = [""]
48
+ end
49
+
50
+ sub_messages.each do |msg|
51
+ line = "#{timestamp} #{msg}"
52
+ @lines << line
53
+
54
+ # 保持最多 max_lines 行(FIFO)
55
+ @lines.shift if @lines.size > @max_lines
56
+ end
57
+ end
58
+ end
59
+
60
+ # 更新最后一行(不滚动,用于进度百分比)
61
+ # 为了保证行数计算准确,这里强行将换行符替换为空格
62
+ # @param message [String] 更新消息
63
+ def update_last_line(message)
64
+ @mutex.synchronize do
65
+ timestamp = Time.now.strftime('%H:%M:%S')
66
+
67
+ # 替换换行符,保证只占用一行
68
+ clean_message = message.to_s.gsub("\n", " ")
69
+ line = "#{timestamp} #{clean_message}"
70
+
71
+ if @lines.empty?
72
+ @lines << line
73
+ else
74
+ @lines[-1] = line
75
+ end
76
+ end
77
+ end
78
+
79
+ # 标记任务成功
80
+ # @param message [String, nil] 成功消息
81
+ def mark_success(message = nil)
82
+ @mutex.synchronize do
83
+ @status = STATUS_SUCCESS
84
+ add_line_unsafe(message) if message
85
+ end
86
+ end
87
+
88
+ # 标记任务失败
89
+ # @param message [String] 错误消息
90
+ def mark_error(message)
91
+ @mutex.synchronize do
92
+ @status = STATUS_ERROR
93
+ add_line_unsafe("错误: #{message}")
94
+ end
95
+ end
96
+
97
+ # 渲染为终端字符串
98
+ # @param max_lines [Integer, nil] 最大显示行数(可选覆盖默认值)
99
+ # @param max_width [Integer, nil] 最大显示宽度(可选,用于截断长行以防止换行破坏布局)
100
+ # @return [String] 终端显示字符串
101
+ def render(max_lines: nil, max_width: nil)
102
+ @mutex.synchronize do
103
+ lines_output = []
104
+
105
+ # 状态图标和颜色
106
+ icon = case @status
107
+ when STATUS_RUNNING then '⣾'
108
+ when STATUS_SUCCESS then '✔'
109
+ when STATUS_ERROR then '✖'
110
+ when STATUS_PENDING then '⊙'
111
+ else '?'
112
+ end
113
+
114
+ color_code = case @status
115
+ when STATUS_SUCCESS then "\e[32m" # 绿色
116
+ when STATUS_ERROR then "\e[31m" # 红色
117
+ when STATUS_PENDING then "\e[33m" # 黄色
118
+ else "\e[34m" # 蓝色(运行中)
119
+ end
120
+
121
+ # 任务名称行
122
+ # 截断任务名称(保留一些空间给图标)
123
+ task_line = " #{color_code}#{icon} #{@task_name}\e[0m"
124
+ lines_output << task_line
125
+
126
+ # 消息行(树形结构)
127
+ display_lines = max_lines ? @lines.last(max_lines) : @lines
128
+ display_lines.each_with_index do |line, index|
129
+ prefix = if index == display_lines.size - 1
130
+ '└─' # 最后一行
131
+ else
132
+ '├─' # 中间行
133
+ end
134
+
135
+ full_line = " #{prefix} #{line}"
136
+
137
+ # 如果指定了最大宽度,进行截断
138
+ if max_width && max_width > 0
139
+ # 简单截断防止换行
140
+ # 注意:这里假设全角字符占2宽度的情况未被完美处理,但对于防止严重错乱已足够
141
+ if full_line.length > max_width
142
+ # 保留最后 3 个字符为 "..."
143
+ full_line = full_line[0, max_width - 3] + "..."
144
+ end
145
+ end
146
+
147
+ lines_output << full_line
148
+ end
149
+
150
+ lines_output.join("\n")
151
+ end
152
+ end
153
+
154
+ # 计算占用的行数
155
+ # @return [Integer] 占用的终端行数
156
+ def line_count
157
+ @mutex.synchronize do
158
+ 1 + @lines.size # 任务名称行 + 消息行
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ # 内部方法:添加新行(不加锁,由调用者保证线程安全)
165
+ # 支持多行消息拆分
166
+ def add_line_unsafe(message)
167
+ timestamp = Time.now.strftime('%H:%M:%S')
168
+
169
+ str_message = message.to_s
170
+ return if str_message.empty?
171
+
172
+ sub_messages = str_message.split("\n")
173
+ if sub_messages.empty?
174
+ sub_messages = [""]
175
+ end
176
+
177
+ sub_messages.each do |msg|
178
+ line = "#{timestamp} #{msg}"
179
+ @lines << line
180
+ @lines.shift if @lines.size > @max_lines
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,95 @@
1
+ module Pindo
2
+ module TaskSystem
3
+ # Stdout 重定向器
4
+ # 用于在任务执行期间捕获标准输出和标准错误,将其路由到输出管理器
5
+ class StdoutRedirector
6
+ # 为任务重定向 stdout/stderr
7
+ # @param task_id [String] 任务 ID
8
+ # @param output_manager [OutputSink] 输出管理器
9
+ # @yield 执行任务的代码块
10
+ def self.redirect_for_task(task_id, output_manager)
11
+ # 保存原始 stdout/stderr
12
+ original_stdout = $stdout
13
+ original_stderr = $stderr
14
+
15
+ begin
16
+ # 重定向到自定义 IO
17
+ $stdout = CustomIO.new(task_id, output_manager, :stdout)
18
+ $stderr = CustomIO.new(task_id, output_manager, :stderr)
19
+
20
+ yield
21
+ ensure
22
+ # 恢复原始 stdout/stderr
23
+ $stdout = original_stdout
24
+ $stderr = original_stderr
25
+ end
26
+ end
27
+ end
28
+
29
+ # 自定义 IO 对象
30
+ # 拦截所有 puts/print 输出,将其路由到输出管理器
31
+ class CustomIO
32
+ # 初始化自定义 IO
33
+ # @param task_id [String] 任务 ID
34
+ # @param output_manager [OutputSink] 输出管理器
35
+ # @param stream_type [Symbol] 流类型(:stdout 或 :stderr)
36
+ def initialize(task_id, output_manager, stream_type)
37
+ @task_id = task_id
38
+ @output_manager = output_manager
39
+ @stream_type = stream_type
40
+ end
41
+
42
+ # 写入数据
43
+ # @param message [String] 消息内容
44
+ # @return [Integer] 写入的字节数
45
+ def write(message)
46
+ # 捕获输出,写入日志文件(跳过空行)
47
+ unless message.strip.empty?
48
+ # 根据流类型选择日志级别
49
+ if @stream_type == :stderr
50
+ @output_manager.log_error(@task_id, message.chomp)
51
+ else
52
+ @output_manager.log_detail(@task_id, message.chomp)
53
+ end
54
+ end
55
+ message.length
56
+ end
57
+
58
+ # puts 方法
59
+ # @param args [Array] 参数列表
60
+ def puts(*args)
61
+ if args.empty?
62
+ write("\n")
63
+ else
64
+ args.each { |arg| write("#{arg}\n") }
65
+ end
66
+ nil
67
+ end
68
+
69
+ # print 方法
70
+ # @param args [Array] 参数列表
71
+ def print(*args)
72
+ write(args.join)
73
+ nil
74
+ end
75
+
76
+ # flush 方法(无操作)
77
+ def flush
78
+ # 不需要处理
79
+ end
80
+
81
+ # 委托其他方法给 STDOUT
82
+ def method_missing(method, *args, &block)
83
+ STDOUT.send(method, *args, &block)
84
+ end
85
+
86
+ # 响应方法检查
87
+ # @param method [Symbol] 方法名
88
+ # @param include_private [Boolean] 是否包含私有方法
89
+ # @return [Boolean] 是否响应该方法
90
+ def respond_to_missing?(method, include_private = false)
91
+ STDOUT.respond_to?(method, include_private) || super
92
+ end
93
+ end
94
+ end
95
+ end