pindo 5.11.1 → 5.11.3

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.
@@ -0,0 +1,278 @@
1
+ require_relative '../pindo_task'
2
+
3
+ module Pindo
4
+ module TaskSystem
5
+ class UploadTask < PindoTask
6
+ attr_reader :platform, :file_path, :upload_url
7
+
8
+ def self.task_type
9
+ :upload
10
+ end
11
+
12
+ def self.execution_mode
13
+ ExecutionMode::CONCURRENT # 上传可以并发执行
14
+ end
15
+
16
+ def self.execution_type
17
+ ExecutionType::ASYNC # 在新线程异步执行
18
+ end
19
+
20
+ def initialize(platform, file_path, options = {})
21
+ @platform = platform
22
+ @file_path = file_path
23
+ @project_name = options[:project_name]
24
+ @upload_helper = options[:upload_helper]
25
+ @upload_url = nil
26
+
27
+ name = case platform
28
+ when 'ios', 'ipa'
29
+ "上传 IPA"
30
+ when 'android', 'apk'
31
+ "上传 APK"
32
+ when 'web', 'html'
33
+ "上传 HTML"
34
+ else
35
+ "上传 #{platform.upcase}"
36
+ end
37
+
38
+ super(name, options)
39
+ end
40
+
41
+ def validate
42
+ # 如果文件路径为空,暂时跳过验证(可能从构建任务结果获取)
43
+ return true if @file_path.nil?
44
+
45
+ # 验证文件是否存在
46
+ unless File.exist?(@file_path)
47
+ @error = "上传文件不存在:#{@file_path}"
48
+ return false
49
+ end
50
+
51
+ true
52
+ end
53
+
54
+ protected
55
+
56
+ def do_work
57
+ # 如果文件路径为空,尝试从 context 获取
58
+ if @file_path.nil?
59
+ @file_path = @context[:output_path]
60
+ end
61
+
62
+ # 再次验证文件
63
+ unless @file_path && File.exist?(@file_path)
64
+ raise "上传文件不存在或未指定:#{@file_path}"
65
+ end
66
+
67
+ update_progress(0, "准备上传...")
68
+
69
+ # 初始化上传 helper
70
+ @upload_helper ||= PgyerHelper.share_instace
71
+
72
+ case @platform
73
+ when 'ios', 'ipa'
74
+ upload_ipa
75
+ when 'android', 'apk'
76
+ upload_apk
77
+ when 'web', 'html'
78
+ upload_web
79
+ else
80
+ raise "Unsupported platform: #{@platform}"
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def upload_ipa
87
+ update_progress(10, "准备上传 IPA...")
88
+
89
+ # 获取文件大小
90
+ file_size = File.size(@file_path) / (1024.0 * 1024.0)
91
+ update_progress(20, "文件大小: #{file_size.round(2)} MB")
92
+
93
+ # 准备上传参数
94
+ upload_params = prepare_upload_params
95
+
96
+ update_progress(30, "开始上传到测试平台...")
97
+
98
+ # 执行上传
99
+ begin
100
+ # 模拟进度更新
101
+ upload_with_progress do |progress|
102
+ update_progress(30 + (progress * 0.6).to_i, "上传中...")
103
+ end
104
+
105
+ # 实际上传逻辑
106
+ result = @upload_helper.upload_ipa(
107
+ ipa_path: @file_path,
108
+ project_name: @project_name || upload_params[:project_name]
109
+ )
110
+
111
+ @upload_url = result[:url] if result && result[:url]
112
+
113
+ update_progress(95, "处理上传结果...")
114
+ rescue => e
115
+ raise "IPA 上传失败: #{e.message}"
116
+ end
117
+
118
+ update_progress(100, "IPA 上传成功")
119
+ {
120
+ success: true,
121
+ platform: 'ios',
122
+ file_path: @file_path,
123
+ upload_url: @upload_url
124
+ }
125
+ end
126
+
127
+ def upload_apk
128
+ update_progress(10, "准备上传 APK...")
129
+
130
+ # 获取文件大小
131
+ file_size = File.size(@file_path) / (1024.0 * 1024.0)
132
+ update_progress(20, "文件大小: #{file_size.round(2)} MB")
133
+
134
+ # 准备上传参数
135
+ upload_params = prepare_upload_params
136
+
137
+ update_progress(30, "开始上传到测试平台...")
138
+
139
+ # 执行上传
140
+ begin
141
+ # 模拟进度更新
142
+ upload_with_progress do |progress|
143
+ update_progress(30 + (progress * 0.6).to_i, "上传中...")
144
+ end
145
+
146
+ # 实际上传逻辑
147
+ result = @upload_helper.upload_apk(
148
+ apk_path: @file_path,
149
+ project_name: @project_name || upload_params[:project_name]
150
+ )
151
+
152
+ @upload_url = result[:url] if result && result[:url]
153
+
154
+ update_progress(95, "处理上传结果...")
155
+ rescue => e
156
+ raise "APK 上传失败: #{e.message}"
157
+ end
158
+
159
+ update_progress(100, "APK 上传成功")
160
+ {
161
+ success: true,
162
+ platform: 'android',
163
+ file_path: @file_path,
164
+ upload_url: @upload_url
165
+ }
166
+ end
167
+
168
+ def upload_web
169
+ update_progress(10, "准备上传 HTML...")
170
+
171
+ # Web 可能是文件夹,获取大小
172
+ if File.directory?(@file_path)
173
+ dir_size = calculate_directory_size(@file_path) / (1024.0 * 1024.0)
174
+ update_progress(20, "文件夹大小: #{dir_size.round(2)} MB")
175
+ else
176
+ file_size = File.size(@file_path) / (1024.0 * 1024.0)
177
+ update_progress(20, "文件大小: #{file_size.round(2)} MB")
178
+ end
179
+
180
+ # 准备上传参数
181
+ upload_params = prepare_upload_params
182
+
183
+ update_progress(30, "开始上传到测试平台...")
184
+
185
+ # 执行上传
186
+ begin
187
+ # 模拟进度更新
188
+ upload_with_progress do |progress|
189
+ update_progress(30 + (progress * 0.6).to_i, "上传中...")
190
+ end
191
+
192
+ # 实际上传逻辑(可能需要压缩文件夹)
193
+ if File.directory?(@file_path)
194
+ # 压缩文件夹
195
+ update_progress(40, "压缩 Web 文件...")
196
+ zip_path = compress_directory(@file_path)
197
+
198
+ result = @upload_helper.upload_web(
199
+ web_path: zip_path,
200
+ project_name: @project_name || upload_params[:project_name]
201
+ )
202
+
203
+ # 清理临时文件
204
+ File.delete(zip_path) if File.exist?(zip_path)
205
+ else
206
+ result = @upload_helper.upload_web(
207
+ web_path: @file_path,
208
+ project_name: @project_name || upload_params[:project_name]
209
+ )
210
+ end
211
+
212
+ @upload_url = result[:url] if result && result[:url]
213
+
214
+ update_progress(95, "处理上传结果...")
215
+ rescue => e
216
+ raise "HTML 上传失败: #{e.message}"
217
+ end
218
+
219
+ update_progress(100, "HTML 上传成功")
220
+ {
221
+ success: true,
222
+ platform: 'web',
223
+ file_path: @file_path,
224
+ upload_url: @upload_url
225
+ }
226
+ end
227
+
228
+ def prepare_upload_params
229
+ params = {}
230
+
231
+ # 从 context 获取项目信息
232
+ params[:project_name] = @context[:project_name] || @project_name
233
+
234
+ # 其他参数
235
+ params[:version] = @context[:version]
236
+ params[:build_number] = @context[:build_number]
237
+ params[:description] = @context[:description]
238
+
239
+ params
240
+ end
241
+
242
+ def upload_with_progress(&block)
243
+ # 模拟进度回调
244
+ # 实际实现中,应该根据上传 API 的进度回调来更新
245
+ 10.times do |i|
246
+ sleep(0.2)
247
+ block.call(i / 10.0) if block_given?
248
+ end
249
+ end
250
+
251
+ def calculate_directory_size(path)
252
+ size = 0
253
+ Dir.glob(File.join(path, '**', '*')).each do |file|
254
+ size += File.size(file) if File.file?(file)
255
+ end
256
+ size
257
+ end
258
+
259
+ def compress_directory(dir_path)
260
+ require 'zip'
261
+ zip_path = "#{dir_path}.zip"
262
+
263
+ Zip::File.open(zip_path, Zip::File::CREATE) do |zipfile|
264
+ Dir.glob(File.join(dir_path, '**', '**')).each do |file|
265
+ zipfile.add(file.sub(dir_path + '/', ''), file)
266
+ end
267
+ end
268
+
269
+ zip_path
270
+ rescue LoadError
271
+ # 如果没有 zip gem,使用系统命令
272
+ zip_path = "#{dir_path}.zip"
273
+ system("zip -r '#{zip_path}' '#{dir_path}'")
274
+ zip_path
275
+ end
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,237 @@
1
+ require 'securerandom'
2
+ require 'thread'
3
+
4
+ module Pindo
5
+ module TaskSystem
6
+ # 任务取消异常
7
+ class TaskCancelledException < StandardError; end
8
+ # 执行模式:控制并发策略
9
+ module ExecutionMode
10
+ EXCLUSIVE = :exclusive # 独占:阻塞所有其他任务
11
+ TYPE_EXCLUSIVE = :type_exclusive # 类型独占:同类型串行
12
+ CONCURRENT = :concurrent # 并发:可与任意任务并行
13
+ end
14
+
15
+ # 执行类型:控制在哪个线程执行
16
+ module ExecutionType
17
+ SYNC = :sync # 同步执行(在主线程)
18
+ ASYNC = :async # 异步执行(在新线程)
19
+ end
20
+
21
+ # 任务状态
22
+ module TaskStatus
23
+ PENDING = :pending # 待执行
24
+ TRIGGERED = :triggered # 已触发
25
+ RUNNING = :running # 执行中
26
+ SUCCESS = :success # 成功
27
+ FAILED = :failed # 失败
28
+ CANCELLED = :cancelled # 取消
29
+ end
30
+
31
+ # 任务优先级
32
+ module TaskPriority
33
+ LOW = 0
34
+ NORMAL = 5
35
+ HIGH = 10
36
+ CRITICAL = 15
37
+ end
38
+
39
+ # PindoTask 基类
40
+ class PindoTask
41
+ attr_accessor :id, :name, :status, :error, :result, :progress, :progress_message
42
+ attr_reader :type, :execution_mode, :execution_type, :priority, :dependencies
43
+ attr_accessor :context, :metadata, :thread
44
+ attr_reader :created_at, :started_at, :finished_at
45
+
46
+ def initialize(name, options = {})
47
+ @id = SecureRandom.uuid
48
+ @name = name
49
+ @type = self.class.task_type
50
+ @execution_mode = options[:execution_mode] || self.class.execution_mode
51
+ @execution_type = options[:execution_type] || self.class.execution_type
52
+ @priority = options[:priority] || TaskPriority::NORMAL
53
+ @status = TaskStatus::PENDING
54
+ @dependencies = options[:dependencies] || []
55
+ @context = options[:context] || {}
56
+ @metadata = options[:metadata] || {}
57
+ @progress = 0
58
+ @progress_message = nil
59
+ @error = nil
60
+ @result = nil
61
+ @thread = nil # 执行线程
62
+ @created_at = Time.now
63
+ @started_at = nil
64
+ @finished_at = nil
65
+ @mutex = Mutex.new
66
+ @condition = ConditionVariable.new
67
+ @callbacks = {
68
+ before: [],
69
+ after: [],
70
+ on_success: [],
71
+ on_failure: [],
72
+ on_progress: []
73
+ }
74
+ end
75
+
76
+ # 子类配置
77
+ def self.task_type
78
+ raise NotImplementedError, "Subclass must define task_type"
79
+ end
80
+
81
+ def self.execution_mode
82
+ ExecutionMode::TYPE_EXCLUSIVE # 默认同类型串行
83
+ end
84
+
85
+ def self.execution_type
86
+ ExecutionType::ASYNC # 默认异步执行
87
+ end
88
+
89
+ # 同步执行(在当前线程)
90
+ def do_task
91
+ execute_internal
92
+ end
93
+
94
+ # 异步执行(在新线程)
95
+ def async_do_task
96
+ @thread = Thread.new do
97
+ begin
98
+ execute_internal
99
+ rescue => e
100
+ @error = e
101
+ @status = TaskStatus::FAILED
102
+ puts "[Task Error] #{@name}: #{e.message}".red if defined?(String.red)
103
+ end
104
+ end
105
+ end
106
+
107
+ # 等待异步任务完成
108
+ def wait
109
+ @thread&.join
110
+ end
111
+
112
+ # 检查是否完成
113
+ def finished?
114
+ [TaskStatus::SUCCESS, TaskStatus::FAILED, TaskStatus::CANCELLED].include?(@status)
115
+ end
116
+
117
+ # 是否正在运行
118
+ def running?
119
+ @status == TaskStatus::RUNNING
120
+ end
121
+
122
+ # 验证任务是否可以执行
123
+ def validate
124
+ true
125
+ end
126
+
127
+ # 取消任务
128
+ def cancel
129
+ @mutex.synchronize do
130
+ if @status == TaskStatus::PENDING || @status == TaskStatus::TRIGGERED
131
+ @status = TaskStatus::CANCELLED
132
+ @condition.signal
133
+ true
134
+ else
135
+ false
136
+ end
137
+ end
138
+ end
139
+
140
+ # 执行时间
141
+ def execution_time
142
+ return nil unless @started_at
143
+ end_time = @finished_at || Time.now
144
+ end_time - @started_at
145
+ end
146
+
147
+ # 更新进度
148
+ def update_progress(value, message = nil)
149
+ # 检查是否已取消
150
+ check_cancelled!
151
+
152
+ @progress = value.clamp(0, 100)
153
+ @progress_message = message
154
+ run_callbacks(:on_progress)
155
+ end
156
+
157
+ # 检查是否已取消,如果已取消则抛出异常
158
+ def check_cancelled!
159
+ if @status == TaskStatus::CANCELLED
160
+ raise TaskCancelledException.new("任务已被取消: #{@name}")
161
+ end
162
+ end
163
+
164
+ # 检查是否已取消(不抛异常)
165
+ def cancelled?
166
+ @status == TaskStatus::CANCELLED
167
+ end
168
+
169
+ # 添加回调
170
+ def on(event, &block)
171
+ @callbacks[event] << block if @callbacks[event]
172
+ end
173
+
174
+ protected
175
+
176
+ # 子类必须实现
177
+ def do_work
178
+ raise NotImplementedError, "Subclass must implement do_work method"
179
+ end
180
+
181
+ private
182
+
183
+ # 内部执行逻辑
184
+ def execute_internal
185
+ begin
186
+ @mutex.synchronize do
187
+ return if @status == TaskStatus::CANCELLED
188
+ @status = TaskStatus::RUNNING
189
+ @started_at = Time.now
190
+ end
191
+
192
+ run_callbacks(:before)
193
+
194
+ # 子类实现具体逻辑
195
+ @result = do_work
196
+
197
+ @mutex.synchronize do
198
+ @status = TaskStatus::SUCCESS
199
+ @condition.signal
200
+ end
201
+
202
+ run_callbacks(:on_success)
203
+ rescue TaskCancelledException => e
204
+ # 任务被取消
205
+ @mutex.synchronize do
206
+ @status = TaskStatus::CANCELLED
207
+ @error = e
208
+ @condition.signal
209
+ end
210
+
211
+ puts "[Task] #{@name} 已被取消".yellow if defined?(String.yellow)
212
+ # 不抛出异常,正常结束
213
+ rescue => e
214
+ @mutex.synchronize do
215
+ @error = e
216
+ @status = TaskStatus::FAILED
217
+ @condition.signal
218
+ end
219
+
220
+ run_callbacks(:on_failure)
221
+ raise if should_raise_on_failure?
222
+ ensure
223
+ @finished_at = Time.now
224
+ run_callbacks(:after)
225
+ end
226
+ end
227
+
228
+ def run_callbacks(event)
229
+ @callbacks[event].each { |cb| cb.call(self) }
230
+ end
231
+
232
+ def should_raise_on_failure?
233
+ @metadata[:raise_on_failure] != false
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,84 @@
1
+ module Pindo
2
+ module TaskSystem
3
+ # 任务系统配置
4
+ module TaskConfig
5
+ # Unity 导出路径配置
6
+ UNITY_EXPORT_PATHS = {
7
+ ios: 'GoodPlatform/iOS',
8
+ android: 'GoodPlatform/Android',
9
+ web: 'GoodPlatform/Web',
10
+ webgl: 'GoodPlatform/WebGL'
11
+ }.freeze
12
+
13
+ # 构建输出搜索路径模式
14
+ BUILD_OUTPUT_PATTERNS = {
15
+ ipa: [
16
+ 'build/**/*.ipa',
17
+ 'output/**/*.ipa',
18
+ 'GoodPlatform/iOS/**/*.ipa',
19
+ '**/*.ipa'
20
+ ].freeze,
21
+
22
+ apk: [
23
+ 'build/**/*.apk',
24
+ 'output/**/*.apk',
25
+ 'GoodPlatform/Android/**/*.apk',
26
+ 'app/build/outputs/apk/**/*.apk',
27
+ '**/*.apk'
28
+ ].freeze,
29
+
30
+ aab: [
31
+ 'build/**/*.aab',
32
+ 'output/**/*.aab',
33
+ 'GoodPlatform/Android/**/*.aab',
34
+ 'app/build/outputs/bundle/**/*.aab',
35
+ '**/*.aab'
36
+ ].freeze,
37
+
38
+ web: [
39
+ 'build/web',
40
+ 'build/webgl',
41
+ 'output/web',
42
+ 'output/webgl',
43
+ 'GoodPlatform/Web',
44
+ 'GoodPlatform/WebGL',
45
+ 'Build/WebGL',
46
+ 'dist',
47
+ 'public'
48
+ ].freeze,
49
+
50
+ web_zip: [
51
+ 'build/**/*web*.zip',
52
+ 'output/**/*web*.zip',
53
+ '**/*webgl*.zip'
54
+ ].freeze
55
+ }.freeze
56
+
57
+ # 需要过滤的文件名模式(测试包、调试包等)
58
+ EXCLUDED_PATTERNS = [
59
+ 'test',
60
+ 'debug',
61
+ 'unsigned',
62
+ 'unaligned'
63
+ ].freeze
64
+
65
+ # 平台映射
66
+ PLATFORM_MAPPING = {
67
+ 'ipa' => 'ios',
68
+ 'apk' => 'android',
69
+ 'aab' => 'android',
70
+ 'html' => 'web'
71
+ }.freeze
72
+
73
+ # 显示名称映射
74
+ DISPLAY_NAMES = {
75
+ 'ios' => 'iOS',
76
+ 'android' => 'Android',
77
+ 'web' => 'WebGL',
78
+ 'ipa' => 'IPA',
79
+ 'apk' => 'APK',
80
+ 'html' => 'HTML'
81
+ }.freeze
82
+ end
83
+ end
84
+ end