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.
- checksums.yaml +4 -4
- data/lib/pindo/base/funlog.rb +62 -5
- data/lib/pindo/base/git_handler.rb +83 -22
- data/lib/pindo/base/output_sink.rb +69 -0
- data/lib/pindo/command/android/autobuild.rb +57 -8
- data/lib/pindo/command/appstore/autobuild.rb +10 -1
- data/lib/pindo/command/ios/autobuild.rb +59 -7
- data/lib/pindo/command/jps/media.rb +164 -13
- data/lib/pindo/command/jps/upload.rb +14 -9
- data/lib/pindo/command/unity/autobuild.rb +64 -10
- data/lib/pindo/command/utils/tag.rb +9 -1
- data/lib/pindo/command/web/autobuild.rb +59 -10
- data/lib/pindo/module/android/android_build_helper.rb +6 -7
- data/lib/pindo/module/build/git_repo_helper.rb +29 -25
- data/lib/pindo/module/pgyer/pgyerhelper.rb +174 -77
- data/lib/pindo/module/task/core/concurrent_execution_strategy.rb +237 -0
- data/lib/pindo/module/task/core/dependency_checker.rb +123 -0
- data/lib/pindo/module/task/core/execution_strategy.rb +61 -0
- data/lib/pindo/module/task/core/resource_lock_manager.rb +190 -0
- data/lib/pindo/module/task/core/serial_execution_strategy.rb +60 -0
- data/lib/pindo/module/task/core/task_executor.rb +131 -0
- data/lib/pindo/module/task/core/task_queue.rb +221 -0
- data/lib/pindo/module/task/model/build/android_build_dev_task.rb +1 -1
- data/lib/pindo/module/task/model/build/android_build_task.rb +6 -2
- data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +2 -3
- data/lib/pindo/module/task/model/build/ios_build_task.rb +6 -0
- data/lib/pindo/module/task/model/build_task.rb +22 -0
- data/lib/pindo/module/task/model/git/git_commit_task.rb +11 -2
- data/lib/pindo/module/task/model/git_task.rb +6 -0
- data/lib/pindo/module/task/model/jps/jps_message_task.rb +9 -11
- data/lib/pindo/module/task/model/jps/jps_upload_media_task.rb +204 -103
- data/lib/pindo/module/task/model/jps_task.rb +0 -1
- data/lib/pindo/module/task/model/unity_task.rb +38 -2
- data/lib/pindo/module/task/output/multi_line_output_manager.rb +380 -0
- data/lib/pindo/module/task/output/multi_line_task_display.rb +185 -0
- data/lib/pindo/module/task/output/stdout_redirector.rb +95 -0
- data/lib/pindo/module/task/pindo_task.rb +133 -9
- data/lib/pindo/module/task/task_manager.rb +98 -268
- data/lib/pindo/module/task/task_reporter.rb +135 -0
- data/lib/pindo/module/task/task_resources/resource_instance.rb +90 -0
- data/lib/pindo/module/task/task_resources/resource_registry.rb +105 -0
- data/lib/pindo/module/task/task_resources/resource_type.rb +59 -0
- data/lib/pindo/module/task/task_resources/types/directory_based_resource.rb +63 -0
- data/lib/pindo/module/task/task_resources/types/global_exclusive_resource.rb +33 -0
- data/lib/pindo/module/task/task_resources/types/global_shared_resource.rb +34 -0
- data/lib/pindo/module/xcode/xcode_build_helper.rb +26 -8
- data/lib/pindo/options/groups/jps_options.rb +10 -0
- data/lib/pindo/options/groups/task_options.rb +39 -0
- data/lib/pindo/version.rb +3 -2
- 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
|