unipod 0.1.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.
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+ require 'singleton'
5
+
6
+ module UniPod
7
+ module Server
8
+ # 管理包构建任务的队列,使用后台线程异步处理Git克隆和tarball构建
9
+ class PackageBuilderQueue
10
+ include Singleton
11
+
12
+ # 表示要构建的包任务
13
+ class BuildTask
14
+ attr_reader :package_name, :version, :git_url, :git_tag, :git_branch, :priority, :created_at, :callbacks
15
+
16
+ def initialize(package_name, version, git_url, git_tag = nil, git_branch = nil, priority = 0)
17
+ @package_name = package_name
18
+ @version = version
19
+ @git_url = git_url
20
+ @git_tag = git_tag || "v#{version}"
21
+ @git_branch = git_branch || 'main'
22
+ @priority = priority # 优先级:0=普通,1=高优先级(当前请求的包)
23
+ @created_at = Time.now
24
+ @callbacks = []
25
+ end
26
+
27
+ # 添加任务完成后的回调函数
28
+ def add_callback(&block)
29
+ @callbacks << block if block_given?
30
+ end
31
+
32
+ # 执行所有回调
33
+ def execute_callbacks(success, tarball_path = nil)
34
+ @callbacks.each do |callback|
35
+ begin
36
+ callback.call(success, tarball_path)
37
+ rescue => e
38
+ # 忽略回调中的错误,避免影响队列处理
39
+ UI.error "执行包构建回调时出错: #{e.message}" if verbose?
40
+ end
41
+ end
42
+ end
43
+
44
+ # 生成任务的唯一键
45
+ def key
46
+ "#{@package_name}-#{@version}"
47
+ end
48
+
49
+ # 任务已等待时间(秒)
50
+ def wait_time
51
+ Time.now - @created_at
52
+ end
53
+
54
+ # 打印任务信息
55
+ def to_s
56
+ "BuildTask[#{@package_name}@#{@version}, git:#{@git_url}, tag:#{@git_tag}, priority:#{@priority}]"
57
+ end
58
+ end
59
+
60
+ def initialize
61
+ @queue = Queue.new
62
+ @running_tasks = {}
63
+ @tasks_mutex = Mutex.new
64
+ @worker_thread = nil
65
+ @shutdown = false
66
+ @cache_manager = nil
67
+ @verbose = false
68
+ end
69
+
70
+ # 启动工作线程
71
+ def start(cache_manager, options = {})
72
+ @cache_manager = cache_manager
73
+ @verbose = options[:verbose] || false
74
+
75
+ log_verbose "启动包构建队列工作线程..."
76
+
77
+ # 确保只启动一个工作线程
78
+ return if @worker_thread && @worker_thread.alive?
79
+
80
+ @shutdown = false
81
+ @worker_thread = Thread.new do
82
+ worker_loop
83
+ end
84
+ end
85
+
86
+ # 停止工作线程
87
+ def stop
88
+ log_verbose "正在停止包构建队列..."
89
+ @shutdown = true
90
+ @queue.clear
91
+
92
+ # 向队列发送关闭信号(nil任务)
93
+ @queue << nil
94
+
95
+ # 等待工作线程结束
96
+ if @worker_thread
97
+ begin
98
+ @worker_thread.join(5) # 最多等待5秒
99
+ rescue => e
100
+ log_verbose "等待工作线程结束时出错: #{e.message}"
101
+ end
102
+
103
+ # 如果线程仍在运行,强制终止
104
+ if @worker_thread.alive?
105
+ log_verbose "工作线程未及时结束,强制终止"
106
+ @worker_thread.kill
107
+ end
108
+
109
+ @worker_thread = nil
110
+ end
111
+
112
+ log_verbose "包构建队列已停止"
113
+ end
114
+
115
+ # 添加包构建任务到队列
116
+ # 如果同一个包版本已有任务,则不重复添加
117
+ def enqueue(package_name, version, git_url, git_tag = nil, git_branch = nil, priority = 0, &callback)
118
+ task = BuildTask.new(package_name, version, git_url, git_tag, git_branch, priority)
119
+ task.add_callback(&callback) if block_given?
120
+
121
+ @tasks_mutex.synchronize do
122
+ # 检查是否已有相同的任务正在运行
123
+ if @running_tasks[task.key]
124
+ existing_task = @running_tasks[task.key]
125
+ existing_task.add_callback(&callback) if block_given?
126
+ log_verbose "跳过已在处理的任务: #{task}"
127
+ return false
128
+ end
129
+
130
+ # 检查队列中是否已有相同的任务
131
+ existing_task = nil
132
+ @queue.size.times do
133
+ t = @queue.pop
134
+ if t && t.key == task.key
135
+ existing_task = t
136
+ # 如果新任务优先级更高,使用新任务
137
+ if priority > t.priority
138
+ log_verbose "用更高优先级的任务替换队列中的任务: #{task}"
139
+ t.add_callback(&callback) if block_given?
140
+ @queue << task
141
+ else
142
+ t.add_callback(&callback) if block_given?
143
+ @queue << t
144
+ end
145
+ else
146
+ @queue << t if t # 放回其他任务
147
+ end
148
+ end
149
+
150
+ # 如果队列中没有相同任务,添加新任务
151
+ if existing_task.nil?
152
+ log_verbose "添加新的包构建任务到队列: #{task}"
153
+ @queue << task
154
+ end
155
+ end
156
+
157
+ true
158
+ end
159
+
160
+ # 获取队列中的任务数量
161
+ def queue_size
162
+ @queue.size
163
+ end
164
+
165
+ # 获取当前正在处理的任务数量
166
+ def running_task_count
167
+ @tasks_mutex.synchronize { @running_tasks.size }
168
+ end
169
+
170
+ private
171
+
172
+ # 工作线程主循环
173
+ def worker_loop
174
+ log_verbose "包构建工作线程已启动"
175
+
176
+ while !@shutdown
177
+ begin
178
+ # 从队列获取任务
179
+ task = @queue.pop
180
+ break if @shutdown || task.nil?
181
+
182
+ # 更新正在运行的任务列表
183
+ @tasks_mutex.synchronize do
184
+ @running_tasks[task.key] = task
185
+ end
186
+
187
+ log_verbose "开始处理包构建任务: #{task}"
188
+ success = false
189
+ tarball_path = nil
190
+
191
+ begin
192
+ # 执行包构建
193
+ if @cache_manager
194
+ tarball_name = "#{task.package_name}-#{task.version}.tgz"
195
+
196
+ # 首先检查是否已存在tarball文件
197
+ existing_tarball = @cache_manager.get_package_tarball(task.package_name, tarball_name)
198
+ if existing_tarball && File.exist?(existing_tarball)
199
+ log_verbose "找到已存在的tarball文件: #{existing_tarball}"
200
+ success = true
201
+ tarball_path = existing_tarball
202
+ else
203
+ # 克隆并构建包
204
+ log_verbose "尝试克隆并构建包: #{task.package_name}@#{task.version} (#{task.git_url})"
205
+ if @cache_manager.clone_and_build_package(task.package_name, task.git_url, task.git_tag, task.git_branch)
206
+ # 检查构建结果
207
+ tarball_path = @cache_manager.get_package_tarball(task.package_name, tarball_name)
208
+ if tarball_path && File.exist?(tarball_path)
209
+ log_verbose "成功构建tarball: #{tarball_path}"
210
+ success = true
211
+ else
212
+ log_verbose "构建过程完成,但未找到tarball文件: #{tarball_name}"
213
+ end
214
+ else
215
+ log_verbose "包构建失败: #{task.package_name}@#{task.version}"
216
+ end
217
+ end
218
+ else
219
+ log_verbose "未设置缓存管理器,无法构建包: #{task.package_name}@#{task.version}"
220
+ end
221
+ rescue => e
222
+ log_verbose "处理包构建任务时出错: #{e.message}"
223
+ log_verbose e.backtrace.join("\n") if @verbose
224
+ end
225
+
226
+ # 执行回调
227
+ task.execute_callbacks(success, tarball_path)
228
+
229
+ # 更新正在运行的任务列表
230
+ @tasks_mutex.synchronize do
231
+ @running_tasks.delete(task.key)
232
+ end
233
+
234
+ log_verbose "包构建任务处理#{success ? '成功' : '失败'}: #{task}"
235
+
236
+ rescue => e
237
+ # 捕获循环中的任何错误,确保工作线程不会意外终止
238
+ log_verbose "工作线程出错: #{e.message}"
239
+ log_verbose e.backtrace.join("\n") if @verbose
240
+ end
241
+ end
242
+
243
+ log_verbose "包构建工作线程已退出"
244
+ end
245
+
246
+ def log_verbose(message)
247
+ UI.puts message if @verbose
248
+ end
249
+
250
+ def verbose?
251
+ @verbose
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,393 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webrick'
4
+ require 'json'
5
+ require 'fileutils'
6
+ require 'socket'
7
+ require 'rainbow'
8
+ require 'unipod/ui'
9
+ require_relative 'cache_manager'
10
+ require_relative 'server_handler'
11
+ require_relative 'download_handler'
12
+ require_relative 'package_builder_queue'
13
+
14
+ module UniPod
15
+ module Server
16
+ class VerdaccioServer
17
+ attr_reader :server, :port, :host
18
+
19
+ DEFAULT_PORT = 7748
20
+ DEFAULT_HOST = '0.0.0.0' # 绑定到所有IP地址
21
+
22
+ #-----------------------------------------------
23
+ # 初始化和服务器生命周期管理
24
+ #-----------------------------------------------
25
+
26
+ def initialize(options = {})
27
+ @port = options[:port] || DEFAULT_PORT
28
+ @host = options[:host] || DEFAULT_HOST
29
+ @verbose = options[:verbose] || false
30
+ @cache_manager = CacheManager.new
31
+ @request_count = 0 # 请求计数,用于调试
32
+
33
+ # 初始化请求处理程序
34
+ @server_handler = ServerHandler.new(@cache_manager, verbose: @verbose)
35
+ @download_handler = DownloadHandler.new(@cache_manager, verbose: @verbose)
36
+
37
+ # 初始化包构建队列
38
+ @builder_queue = PackageBuilderQueue.instance
39
+ end
40
+
41
+ def start
42
+ begin
43
+ # 确保正在运行的服务器已停止
44
+ stop_running_server(@port)
45
+
46
+ # 打印启动信息 - 这个信息保留,因为它是服务器实际启动的指示
47
+ # UI.puts "启动UniPod服务器: #{@host}:#{@port}" - 不再需要,Command类已经显示
48
+
49
+ # 创建HTTP服务器
50
+ create_http_server
51
+
52
+ # 注册路由处理
53
+ setup_routes
54
+
55
+ # 注册退出处理
56
+ register_shutdown_hooks
57
+
58
+ # 启动包构建队列
59
+ @builder_queue.start(@cache_manager, verbose: @verbose)
60
+
61
+ # 预加载包信息
62
+ preload_packages
63
+
64
+ # 显示缓存目录信息 - Command类已经显示类似信息,不需要重复
65
+ # display_cache_directories
66
+
67
+ # 启动服务器确认
68
+ UI.success "UniPod服务器已启动,地址:http://#{@host}:#{@port}"
69
+ UI.puts "服务器正在运行,按Ctrl+C停止..."
70
+
71
+ # 开始服务
72
+ @server.start
73
+ rescue => e
74
+ UI.error "启动服务器时出错: #{e.message}"
75
+ UI.error e.backtrace.join("\n") if @verbose
76
+ raise
77
+ end
78
+ end
79
+
80
+ def stop
81
+ begin
82
+ # 停止包构建队列
83
+ @builder_queue.stop if @builder_queue
84
+
85
+ # 停止HTTP服务器
86
+ @server.shutdown if @server
87
+ UI.puts "服务器已停止"
88
+ rescue => e
89
+ UI.error "停止服务器时出错: #{e.message}"
90
+ UI.error e.backtrace.join("\n") if @verbose
91
+ end
92
+ end
93
+
94
+ #-----------------------------------------------
95
+ # 服务器设置和配置
96
+ #-----------------------------------------------
97
+
98
+ private
99
+
100
+ def create_http_server
101
+ @server = WEBrick::HTTPServer.new(
102
+ Port: @port.to_i,
103
+ Host: @host,
104
+ Logger: create_logger,
105
+ AccessLog: create_access_log,
106
+ DoNotReverseLookup: true, # 提高性能
107
+ RequestTimeout: 120, # 增加超时时间
108
+ MaxClients: 10 # 减少线程数以降低内存使用
109
+ )
110
+ end
111
+
112
+ def register_shutdown_hooks
113
+ # 注册优雅退出的处理程序
114
+ [:INT, :TERM].each do |signal|
115
+ trap(signal) do
116
+ UI.puts "\n正在停止服务器..."
117
+ @server.shutdown
118
+ end
119
+ end
120
+ end
121
+
122
+ def create_logger
123
+ log_level = @verbose ? WEBrick::Log::DEBUG : WEBrick::Log::WARN
124
+ WEBrick::Log.new($stderr, log_level)
125
+ end
126
+
127
+ def create_access_log
128
+ @verbose ? [[$stderr, WEBrick::AccessLog::COMBINED_LOG_FORMAT]] : []
129
+ end
130
+
131
+ def stop_running_server(port)
132
+ begin
133
+ # 尝试使用系统命令终止占用端口的进程
134
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
135
+ # Windows系统
136
+ system("FOR /F \"tokens=5\" %a in ('netstat -ano ^| findstr :#{port}') do taskkill /F /PID %a")
137
+ else
138
+ # Unix类系统
139
+ system("lsof -i:#{port} | grep -v PID | awk '{print $2}' | xargs kill -9 2>/dev/null || true")
140
+ end
141
+ rescue => e
142
+ # 忽略错误,继续尝试启动服务器
143
+ log_verbose "尝试停止现有服务器时出错: #{e.message}"
144
+ end
145
+ end
146
+
147
+ # 预加载包信息,避免首次请求延迟
148
+ def preload_packages
149
+ Thread.new do
150
+ log_verbose "开始预加载包信息..."
151
+ @cache_manager.get_all_packages
152
+ log_verbose "包信息预加载完成!"
153
+ end
154
+ end
155
+
156
+ def display_cache_directories
157
+ # 使用Config类获取缓存目录路径
158
+ require 'unipod/config'
159
+ cache_dir = UniPod::Config.cache_dir rescue "未设置"
160
+ scoped_dir = File.join(cache_dir, 'scoped') rescue "未设置"
161
+ packages_dir = File.join(cache_dir, 'packages') rescue "未设置"
162
+ tarballs_dir = File.join(cache_dir, 'tarballs') rescue "未设置"
163
+
164
+ UI.puts "============ 缓存目录结构 ============"
165
+ UI.puts "UniPod缓存目录: #{cache_dir}"
166
+ UI.puts "索引仓库目录: #{scoped_dir}"
167
+ UI.puts "包Git仓库目录: #{packages_dir}"
168
+ UI.puts "包压缩文件目录: #{tarballs_dir}"
169
+ end
170
+
171
+ #-----------------------------------------------
172
+ # 路由设置和请求处理 - 仅保留Unity Package Manager使用的API
173
+ #-----------------------------------------------
174
+
175
+ def setup_routes
176
+ # 主路由处理所有请求
177
+ @server.mount_proc('/') do |req, res|
178
+ handle_main_route(req, res)
179
+ end
180
+ end
181
+
182
+ def handle_main_route(req, res)
183
+ # 添加请求ID
184
+ req_id = @request_count += 1
185
+
186
+ # 添加CORS头
187
+ @server_handler.add_cors_headers(res)
188
+
189
+ # 记录请求详情
190
+ log_request_details(req, req_id) if @verbose
191
+
192
+ # 处理预检请求
193
+ if req.request_method == 'OPTIONS'
194
+ handle_options_request(res, req_id)
195
+ return
196
+ end
197
+
198
+ # 处理请求
199
+ begin
200
+ handle_request(req, res, req_id)
201
+ rescue => e
202
+ @server_handler.handle_request_error(e, res, req_id)
203
+ end
204
+
205
+ # 记录响应
206
+ log_verbose "[#{req_id}] 响应状态码: #{res.status}"
207
+ log_verbose "[#{req_id}] ==========请求结束==========\n"
208
+ end
209
+
210
+ def handle_options_request(res, req_id)
211
+ res.status = 200
212
+ log_verbose "[#{req_id}] 处理OPTIONS请求"
213
+ end
214
+
215
+ def log_request_details(req, req_id)
216
+ puts "\n[#{req_id}] ==========请求详情==========\n"
217
+ puts "[#{req_id}] 请求方法: #{req.request_method}"
218
+ puts "[#{req_id}] 请求路径: #{req.path}"
219
+ puts "[#{req_id}] 查询字符串: #{req.query_string}"
220
+
221
+ # 打印请求头
222
+ puts "[#{req_id}] 请求头:"
223
+ req.header.each do |k, v|
224
+ puts "[#{req_id}] #{k}: #{v.join(', ')}"
225
+ end
226
+
227
+ # 打印查询参数
228
+ if req.query
229
+ puts "[#{req_id}] 解析后的查询参数:"
230
+ req.query.each do |k, v|
231
+ puts "[#{req_id}] #{k}: #{v}"
232
+ end
233
+ end
234
+ end
235
+
236
+ def handle_request(req, res, req_id = 0)
237
+ method = req.request_method
238
+ path = req.path
239
+
240
+ query_string = req.query_string.nil? ? '' : req.query_string
241
+ log_verbose "[#{req_id}] #{method} #{path}#{query_string.empty? ? '' : '?' + query_string}"
242
+
243
+ # 处理首页请求 - 提供简单的欢迎页面或API信息
244
+ if path == '/' && method == 'GET'
245
+ @server_handler.handle_root_request(res, req_id)
246
+ return
247
+ end
248
+
249
+ # 添加CORS头
250
+ @server_handler.add_cors_headers(res)
251
+
252
+ # 处理OPTIONS请求
253
+ if method == 'OPTIONS'
254
+ res.status = 200
255
+ return
256
+ end
257
+
258
+ # 只处理GET和HEAD请求,Unity Package Manager只使用这两种方法
259
+ if method == 'GET' || method == 'HEAD'
260
+ route_get_request(req, res, req_id)
261
+ else
262
+ handle_unsupported_method(method, res, req_id)
263
+ end
264
+ end
265
+
266
+ def route_get_request(req, res, req_id)
267
+ path = req.path
268
+
269
+ log_verbose "[#{req_id}] 收到GET请求: #{path}"
270
+ puts "\n🔍 [#{req_id}] API请求: GET #{path}"
271
+ puts "📝 请求参数: #{req.query.inspect}" if req.query && !req.query.empty?
272
+
273
+ begin
274
+ # 健康检查
275
+ if path == '/health' || path == '/healthz'
276
+ res.status = 200
277
+ res['Content-Type'] = 'application/json'
278
+ health_data = {
279
+ status: 'ok',
280
+ uptime: (Time.now - @start_time).to_i,
281
+ timestamp: Time.now.to_i
282
+ }
283
+ res.body = JSON.generate(health_data)
284
+ puts "✅ [#{req_id}] 健康检查响应: #{health_data.inspect}"
285
+ return
286
+ end
287
+
288
+ # 根路径 - 显示主页
289
+ if path == '/' || path == '/index.html'
290
+ @server_handler.handle_root_request(res, req_id)
291
+ puts "✅ [#{req_id}] 根路径请求处理完成"
292
+ return
293
+ end
294
+
295
+ # 确保CORS头被添加到所有响应
296
+ @server_handler.add_cors_headers(res)
297
+
298
+ # 搜索API - Unity Package Manager使用
299
+ if path == '/-/v1/search'
300
+ @server_handler.handle_search(req, res, req_id)
301
+ begin
302
+ response_data = JSON.parse(res.body)
303
+ puts "✅ [#{req_id}] 搜索API响应:"
304
+ puts JSON.pretty_generate(response_data)
305
+ rescue => e
306
+ puts "❌ [#{req_id}] 无法解析搜索响应JSON: #{e.message}"
307
+ end
308
+ # 包下载 - Unity Package Manager使用
309
+ elsif path =~ %r{^/([^/]+)/-/(.+\.tgz)$}
310
+ package_name = $1
311
+ tarball_name = $2
312
+ puts "📦 [#{req_id}] 包下载请求: 包名=#{package_name}, 文件=#{tarball_name}"
313
+ @download_handler.handle_package_download(package_name, tarball_name, res, req_id, req)
314
+ if res.status == 200
315
+ puts "✅ [#{req_id}] 包下载成功: #{package_name}/#{tarball_name}, 大小: #{res.body.bytesize} 字节"
316
+ else
317
+ begin
318
+ response_data = JSON.parse(res.body)
319
+ puts "❌ [#{req_id}] 包下载失败: #{res.status}"
320
+ puts JSON.pretty_generate(response_data)
321
+ rescue => e
322
+ puts "❌ [#{req_id}] 包下载失败: #{res.status}, 响应: #{res.body}"
323
+ end
324
+ end
325
+ # 单个包信息 - Unity Package Manager使用
326
+ elsif path =~ %r{^/([^/]+)$} && !path.include?('/-/')
327
+ package_name = $1
328
+ puts "📋 [#{req_id}] 包信息请求: 包名=#{package_name}"
329
+ @server_handler.handle_package_info(package_name, res, req_id, req)
330
+ begin
331
+ response_data = JSON.parse(res.body)
332
+ puts "✅ [#{req_id}] 包信息响应:"
333
+ puts JSON.pretty_generate(response_data)
334
+ rescue => e
335
+ puts "❌ [#{req_id}] 无法解析包信息响应JSON: #{e.message}"
336
+ end
337
+ # 未知路径
338
+ else
339
+ log_verbose "[#{req_id}] 未知GET路径: #{path}"
340
+ res.status = 404
341
+ res['Content-Type'] = 'application/json'
342
+ error_message = {
343
+ error: "Not Found",
344
+ code: "UNKNOWN_ROUTE",
345
+ message: "路径不存在: #{path}"
346
+ }
347
+ res.body = JSON.generate(error_message)
348
+ puts "❌ [#{req_id}] 未知路径响应: #{error_message.inspect}"
349
+ end
350
+ rescue => e
351
+ @server_handler.handle_request_error(e, res, req_id)
352
+ end
353
+ end
354
+
355
+ def handle_unsupported_method(method, res, req_id)
356
+ # 不支持的HTTP方法
357
+ res.status = 405
358
+ res['Content-Type'] = 'application/json'
359
+ res.body = JSON.generate({
360
+ error: "不支持的请求方法",
361
+ message: "Unity Package Manager只使用GET和HEAD方法,服务器不支持#{method}方法"
362
+ })
363
+ log_verbose "[#{req_id}] 不支持的请求方法: #{method}"
364
+ end
365
+
366
+ # 处理未知路径,返回404错误
367
+ def handle_unknown_path(path, res, req_id = 0)
368
+ log_verbose "[#{req_id}] 处理未知路径: #{path}"
369
+
370
+ # 返回404错误
371
+ res.status = 404
372
+ res['Content-Type'] = 'application/json'
373
+ @server_handler.add_cors_headers(res)
374
+
375
+ json_response = JSON.generate({
376
+ 'error' => 'Not Found',
377
+ 'path' => path,
378
+ 'message' => "请求的路径不存在。Unity Package Manager使用的API为: /-/v1/search, /[package-name], /[package-name]/-/[file.tgz]"
379
+ })
380
+ res.body = json_response
381
+ @server_handler.puts_formatted_json(json_response, req_id, "未知路径响应") if @verbose
382
+ end
383
+
384
+ #-----------------------------------------------
385
+ # 工具方法
386
+ #-----------------------------------------------
387
+
388
+ def log_verbose(message)
389
+ UI.puts message if @verbose
390
+ end
391
+ end
392
+ end
393
+ end