pindo 5.4.1 → 5.5.1

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/client/aws3sclient.rb +2 -2
  3. data/lib/pindo/client/httpclient.rb +1 -2
  4. data/lib/pindo/command/android/autobuild.rb +40 -18
  5. data/lib/pindo/command/android/build.rb +37 -16
  6. data/lib/pindo/command/android/debug.rb +34 -20
  7. data/lib/pindo/command/appstore/iap.rb +15 -1
  8. data/lib/pindo/command/appstore/itcapp.rb +15 -1
  9. data/lib/pindo/command/appstore/metadata.rb +15 -1
  10. data/lib/pindo/command/appstore/screenshots.rb +15 -1
  11. data/lib/pindo/command/appstore/upload.rb +15 -1
  12. data/lib/pindo/command/deploy/build.rb +35 -20
  13. data/lib/pindo/command/deploy/bundleid.rb +15 -1
  14. data/lib/pindo/command/deploy/cert.rb +19 -2
  15. data/lib/pindo/command/deploy/check.rb +15 -1
  16. data/lib/pindo/command/deploy/configproj.rb +15 -1
  17. data/lib/pindo/command/deploy/confusecode.rb +15 -1
  18. data/lib/pindo/command/deploy/confuseproj.rb +11 -1
  19. data/lib/pindo/command/deploy/fabric.rb +10 -1
  20. data/lib/pindo/command/deploy/getitcinfo.rb +11 -1
  21. data/lib/pindo/command/deploy/iap.rb +12 -1
  22. data/lib/pindo/command/deploy/initconfig.rb +11 -1
  23. data/lib/pindo/command/deploy/itcinfo.rb +12 -1
  24. data/lib/pindo/command/deploy/pullconfig.rb +11 -1
  25. data/lib/pindo/command/deploy/pushconfig.rb +11 -1
  26. data/lib/pindo/command/deploy/quswark.rb +12 -1
  27. data/lib/pindo/command/deploy/quswauth.rb +10 -1
  28. data/lib/pindo/command/deploy/reportbug.rb +11 -1
  29. data/lib/pindo/command/deploy/resign.rb +12 -1
  30. data/lib/pindo/command/deploy/updateconfig.rb +11 -1
  31. data/lib/pindo/command/deploy/uploadipa.rb +11 -1
  32. data/lib/pindo/command/env/dreamstudio.rb +13 -1
  33. data/lib/pindo/command/env/quarkenv.rb +13 -1
  34. data/lib/pindo/command/env/swarkenv.rb +13 -1
  35. data/lib/pindo/command/env/workhard.rb +13 -1
  36. data/lib/pindo/command/gplay/iap.rb +21 -5
  37. data/lib/pindo/command/gplay/itcapp.rb +19 -5
  38. data/lib/pindo/command/gplay/metadata.rb +23 -5
  39. data/lib/pindo/command/gplay/screenshots.rb +23 -5
  40. data/lib/pindo/command/gplay/upload.rb +21 -5
  41. data/lib/pindo/command/gplay.rb +8 -8
  42. data/lib/pindo/command/ios/adhoc.rb +18 -3
  43. data/lib/pindo/command/ios/autobuild.rb +22 -7
  44. data/lib/pindo/command/ios/autoresign.rb +18 -3
  45. data/lib/pindo/command/ios/build.rb +18 -4
  46. data/lib/pindo/command/ipa/autoresign.rb +35 -4
  47. data/lib/pindo/command/ipa/import.rb +19 -1
  48. data/lib/pindo/command/ipa/output.rb +39 -4
  49. data/lib/pindo/command/{pgyer → jps}/apptest.rb +35 -24
  50. data/lib/pindo/command/jps/bind.rb +191 -0
  51. data/lib/pindo/command/{pgyer → jps}/comment.rb +19 -19
  52. data/lib/pindo/command/{pgyer → jps}/download.rb +20 -20
  53. data/lib/pindo/command/{pgyer → jps}/login.rb +9 -9
  54. data/lib/pindo/command/{pgyer → jps}/resign.rb +40 -25
  55. data/lib/pindo/command/{pgyer → jps}/upload.rb +60 -43
  56. data/lib/pindo/command/jps.rb +18 -0
  57. data/lib/pindo/command/lib/forcepush.rb +15 -1
  58. data/lib/pindo/command/lib/push.rb +15 -1
  59. data/lib/pindo/command/lib/update.rb +15 -1
  60. data/lib/pindo/command/repo/clone.rb +11 -1
  61. data/lib/pindo/command/repo/create.rb +13 -1
  62. data/lib/pindo/command/repo/login.rb +14 -3
  63. data/lib/pindo/command/repo/search.rb +14 -2
  64. data/lib/pindo/command/unity/apk.rb +14 -28
  65. data/lib/pindo/command/unity/autobuild.rb +13 -13
  66. data/lib/pindo/command/unity/ipa.rb +15 -29
  67. data/lib/pindo/command/unity/web.rb +15 -15
  68. data/lib/pindo/command/utils/boss.rb +19 -2
  69. data/lib/pindo/command/utils/clearcert.rb +18 -2
  70. data/lib/pindo/command/utils/device.rb +19 -2
  71. data/lib/pindo/command/utils/icon.rb +20 -2
  72. data/lib/pindo/command/utils/renewcert.rb +28 -3
  73. data/lib/pindo/command/utils/renewproj.rb +25 -2
  74. data/lib/pindo/command/utils/tgate.rb +28 -3
  75. data/lib/pindo/command/utils/xcassets.rb +20 -2
  76. data/lib/pindo/command/web/autobuild.rb +18 -3
  77. data/lib/pindo/command.rb +1 -1
  78. data/lib/pindo/module/pgyer/pgyerhelper.rb +185 -85
  79. data/lib/pindo/version.rb +1 -1
  80. metadata +30 -12
  81. data/lib/pindo/client/pgyer_feishu_oauth_cli.rb +0 -669
  82. data/lib/pindo/client/pgyerclient.rb +0 -466
  83. data/lib/pindo/client/pgyeruploadclient.rb +0 -517
  84. data/lib/pindo/command/pgyer.rb +0 -18
@@ -1,669 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'webrick'
4
- require 'json'
5
- require 'net/http'
6
- require 'uri'
7
- require 'securerandom'
8
- require 'digest'
9
- require 'base64'
10
- require 'fileutils'
11
- require 'pindo/base/aeshelper'
12
- require 'pindo/config/pindouserlocalconfig'
13
- require 'pindo/client/httpclient'
14
-
15
- module Pindo
16
-
17
- class PgyerFeishuOAuthCLI
18
- attr_reader :access_token, :username, :expires_at
19
-
20
- def initialize(client_id)
21
- @client_id = client_id
22
- @feishu_auth_url = 'https://passport.feishu.cn/suite/passport/oauth/authorize?'
23
-
24
- # 保持原有的redirect_uri,飞书OAuth流程中仍使用这个URL
25
- # 前端UI会检测state=terminal_login并自动跳转到localhost:8899
26
- @redirect_uri = 'https://pgyerapps.com/login'
27
- @pgyer_api_endpoint = 'https://api.pgyerapps.com/api/user/lark_qr_login'
28
-
29
- @state = 'terminal_login'
30
- @server_port = 8899
31
-
32
- @larkScopeList = [
33
- 'task:task:write',
34
- 'task:section:write',
35
- 'task:custom_field:write',
36
- 'task:tasklist:write'
37
- ];
38
-
39
- @access_token = nil
40
- @username = nil
41
- @expires_at = nil
42
- end
43
-
44
- # 启动授权流程
45
- def authorize
46
- # 不再检查已存储的令牌,直接开始授权流程
47
- authorization_uri = build_authorization_uri
48
- puts "🚀 正在打开浏览器进行飞书 OAuth 授权..."
49
- puts "\n📋 授权链接(如浏览器未自动打开,请手动复制访问):"
50
- puts "-" * 60
51
- puts authorization_uri
52
- puts "-" * 60
53
-
54
- # 在浏览器中打开授权URL
55
- open_browser(authorization_uri)
56
-
57
- # 启动本地服务器处理回调
58
- code = start_callback_server
59
-
60
- # 如果自动获取失败,提示用户手动输入
61
- if code.nil?
62
- # 停止任何可能的 spinner 动画
63
- if defined?(Funlog) && Funlog.instance.instance_variable_get(:@spinner_log)
64
- Funlog.instance.instance_variable_set(:@spinner_log, nil)
65
- end
66
-
67
- loop do
68
- puts "\n⚠️ 自动获取授权码失败,请选择操作:"
69
- puts " 1. 输入授权码(复制 code= 后的内容)"
70
- puts " 2. 输入完整回调 URL"
71
- puts " 3. 重新打开授权网页"
72
- puts " 4. 退出登录"
73
- print "请选择 (1-4): "
74
- STDOUT.flush # 确保提示符立即显示
75
-
76
- begin
77
- choice = STDIN.gets&.chomp
78
-
79
- # 处理 Ctrl+C 中断
80
- if choice.nil?
81
- puts "\n\n🚪 用户中断操作"
82
- return :user_cancelled
83
- end
84
-
85
- case choice
86
- when "1"
87
- puts "\n请输入授权码:"
88
- print "🔑 "
89
- STDOUT.flush # 确保提示符立即显示
90
- code_input = STDIN.gets&.chomp
91
- if code_input.nil?
92
- puts "\n🚪 用户中断操作"
93
- return :user_cancelled
94
- elsif !code_input.empty?
95
- code = code_input
96
- break
97
- else
98
- puts "⚠️ 授权码不能为空,请重新选择"
99
- end
100
- when "2"
101
- puts "\n请输入完整回调 URL:"
102
- print "🔗 "
103
- STDOUT.flush # 确保提示符立即显示
104
- url_input = STDIN.gets&.chomp
105
- if url_input.nil?
106
- puts "\n🚪 用户中断操作"
107
- return :user_cancelled
108
- elsif url_input.start_with?("http")
109
- # 尝试从 URL 中提取 code
110
- begin
111
- uri = URI(url_input)
112
- query_params = URI.decode_www_form(uri.query || '').to_h
113
- code = query_params['code']
114
- if code
115
- puts "✅ 成功从 URL 提取授权码"
116
- break
117
- else
118
- puts "❌ URL 中未找到授权码,请重新选择"
119
- end
120
- rescue => e
121
- puts "❌ 解析 URL 失败: #{e.message}"
122
- end
123
- else
124
- puts "❌ 无效的 URL 格式,请重新选择"
125
- end
126
- when "3"
127
- # 重新打开授权网页
128
- puts "\n🔄 重新打开授权网页..."
129
- open_browser(authorization_uri)
130
- # 重新启动服务器尝试获取授权码
131
- code = start_callback_server
132
- if code
133
- break
134
- end
135
- # 如果还是失败,继续循环让用户选择
136
- when "4"
137
- puts "\n🚪 已取消登录"
138
- return :user_cancelled
139
- else
140
- puts "❌ 无效的选择,请输入 1-4"
141
- end
142
- rescue Interrupt
143
- puts "\n\n🚪 用户中断操作"
144
- return :user_cancelled
145
- end
146
- end
147
- end
148
-
149
- if code
150
- puts "\n✅ 成功获取授权码"
151
- print "🔄 正在验证飞书身份..."
152
- STDOUT.flush
153
- if login_pgyer_with_feishu(code:code)
154
- puts " ✅"
155
- puts "🎉 登录成功!欢迎使用 Pgyer"
156
- # 登录成功后,返回true,外部程序可以通过访问access_token属性获取token
157
- return true
158
- end
159
- return false
160
- else
161
- puts "\n❌ 授权失败"
162
- return false
163
- end
164
- end
165
-
166
- # 验证Pgyer令牌
167
- def validate_pgyer_token(token = nil, expires_at = nil)
168
- token_to_check = token || @access_token
169
- expiration_time = expires_at || @expires_at
170
-
171
- # 首先检查token是否存在
172
- return false unless token_to_check
173
-
174
- # 然后检查token是否过期
175
- if expiration_time && Time.now.to_i > expiration_time
176
- puts "令牌已过期,需要重新登录"
177
- return false
178
- end
179
-
180
- # 最后验证token是否有效
181
- uri = URI("https://www.pgyer.com/api/user/profile")
182
- request = Net::HTTP::Get.new(uri)
183
- request['Authorization'] = "Bearer #{token_to_check}"
184
-
185
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
186
- http.request(request)
187
- end
188
-
189
- return response.code == '200'
190
- end
191
-
192
- # 使用code登录Pgyer, 并且Pgyer返回token
193
- def login_pgyer_with_feishu(code: nil)
194
- return false unless code
195
-
196
- # 构造请求体
197
- body_params = {
198
- code: code,
199
- redirectUri: @redirect_uri,
200
- scope: @larkScopeList.join(' ')
201
- }
202
-
203
- # 使用HttpClient发送请求
204
- con = HttpClient.create_instance_with_proxy
205
-
206
- begin
207
- res = con.post do |req|
208
- req.url @pgyer_api_endpoint
209
- req.headers['Content-Type'] = 'application/json'
210
- req.body = body_params.to_json
211
- end
212
-
213
- # 处理响应
214
- result = nil
215
- if !res.body.nil?
216
- begin
217
- result = JSON.parse(res.body)
218
- # puts "解析后的响应: #{result.inspect}"
219
-
220
- if result['code'] == 200 && !result['data'].nil? && !result['data']['token'].nil?
221
- @access_token = result['data']['token']
222
- @username = result['data']['username'] if result['data']['username']
223
- # 设置token有效期为7天后
224
- @expires_at = Time.now.to_i + 6 * 24 * 60 * 60 # 7天的秒数
225
- return true
226
- else
227
- error_msg = result['meta'] && result['meta']['message'] ? result['meta']['message'] : '未知错误'
228
- puts " ❌"
229
- puts "登录失败: #{error_msg}"
230
- return false
231
- end
232
- rescue => e
233
- puts " ❌"
234
- puts "响应解析失败: #{e.message}"
235
- return false
236
- end
237
- else
238
- puts " ❌"
239
- puts "服务器返回空响应"
240
- return false
241
- end
242
- rescue => e
243
- puts " ❌"
244
- puts "网络请求失败: #{e.message}"
245
- return false
246
- end
247
- end
248
-
249
- private
250
-
251
- # 获取跨平台的 null 设备路径
252
- def get_null_device
253
- case RbConfig::CONFIG['host_os']
254
- when /mswin|mingw|cygwin/
255
- 'NUL'
256
- else
257
- '/dev/null'
258
- end
259
- end
260
-
261
-
262
- # 构建授权URI
263
- def build_authorization_uri
264
- uri = URI(@feishu_auth_url)
265
-
266
- params = {
267
- 'client_id' => @client_id,
268
- 'redirect_uri' => @redirect_uri,
269
- 'response_type' => 'code',
270
- 'state' => @state,
271
- 'scope' => @larkScopeList.join(' ')
272
- }
273
-
274
- uri.query = URI.encode_www_form(params)
275
- uri.to_s
276
- end
277
-
278
- # 在浏览器中打开URL
279
- def open_browser(url)
280
- case RbConfig::CONFIG['host_os']
281
- when /mswin|mingw|cygwin/
282
- # Windows: 使用双引号包围URL避免参数解析问题
283
- system("start \"\" \"#{url}\"")
284
- when /darwin/
285
- system("open", url)
286
- when /linux|bsd/
287
- system("xdg-open", url)
288
- else
289
- puts "无法自动打开浏览器,请手动访问: #{url}"
290
- end
291
- end
292
-
293
- # 启动本地服务器处理回调
294
- def start_callback_server
295
- code = nil
296
-
297
- # 检查并处理端口占用
298
- unless ensure_port_available(@server_port)
299
- puts "✗ 无法使用端口 #{@server_port},登录失败"
300
- return nil
301
- end
302
-
303
- puts "\n🌐 本地服务器已启动(端口: #{@server_port})"
304
- puts "💡 提示: 按 Ctrl+C 可中断并选择其他操作"
305
- puts "🔄 等待飞书授权回调..."
306
-
307
- begin
308
- # 使用本地8899端口处理回调,不管redirect_uri配置如何
309
- server = WEBrick::HTTPServer.new(
310
- Port: @server_port,
311
- BindAddress: '127.0.0.1',
312
- Logger: WEBrick::Log.new(get_null_device),
313
- AccessLog: []
314
- )
315
- rescue Errno::EADDRINUSE
316
- puts "✗ 端口 #{@server_port} 仍被占用,无法启动服务器"
317
- return nil
318
- rescue Errno::ENOENT
319
- puts "✗ 启动服务器失败: 系统找不到指定的路径或文件"
320
- return nil
321
- rescue => e
322
- puts "✗ 启动服务器失败: #{e.message}"
323
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
324
- puts " 提示: 如果是Windows系统,请确保防火墙允许Ruby访问网络"
325
- end
326
- return nil
327
- end
328
-
329
- # 处理根路径的请求
330
- server.mount_proc '/' do |req, res|
331
- begin
332
- # puts "接收到请求: #{req.request_line}"
333
- # puts "请求参数: #{req.query_string}"
334
-
335
- # 安全解析请求参数
336
- begin
337
- query_params = URI.decode_www_form(req.query_string || '').to_h
338
- # 只在有code时输出日志,避免多余信息
339
- # puts "解析的参数: #{query_params.inspect}" if query_params['code']
340
- rescue => e
341
- # puts "解析请求参数失败: #{e.message}"
342
- query_params = {}
343
- end
344
-
345
- # if query_params['state'] != @state
346
- # puts "状态不匹配: 接收到 #{query_params['state']},期望 #{@state}"
347
- # res.content_type = "text/html; charset=UTF-8"
348
- # res.body = "状态不匹配,可能存在CSRF攻击风险"
349
- # elsif query_params['error']
350
- if query_params['error']
351
- # puts "授权错误: #{query_params['error']}"
352
- res.content_type = "text/html; charset=UTF-8"
353
- res.body = "授权错误: #{query_params['error']}"
354
- elsif query_params['code']
355
- code = query_params['code']
356
- # 成功获取时不输出,避免重复
357
- # puts "成功获取授权码: #{code}"
358
- res.content_type = "text/html; charset=UTF-8"
359
- res.body = <<-HTML
360
- <!DOCTYPE html>
361
- <html>
362
- <head>
363
- <meta charset="UTF-8">
364
- <title>飞书授权成功</title>
365
- <style>
366
- body { font-family: Arial, sans-serif; text-align: center; padding: 40px; }
367
- .container { max-width: 600px; margin: 0 auto; }
368
- .success { color: #4CAF50; }
369
- .code { font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px; word-break: break-all; }
370
- .countdown { font-weight: bold; color: #FF5722; }
371
- .autoclose-banner {
372
- background-color: #333; color: white; padding: 10px;
373
- position: fixed; top: 0; left: 0; right: 0;
374
- display: flex; justify-content: space-between; align-items: center;
375
- }
376
- .close-btn {
377
- background: #f44336; color: white; border: none;
378
- padding: 5px 10px; cursor: pointer; border-radius: 3px;
379
- }
380
- </style>
381
- <script>
382
- // 尝试多种方法关闭窗口
383
- function attemptClose() {
384
- try {
385
- // 方法1: 最基本的关闭尝试
386
- window.close();
387
-
388
- // 方法4: 尝试使用opener关系
389
- if (window.opener) {
390
- window.opener.focus();
391
- window.close();
392
- }
393
-
394
- // 如果以上方法都失败了,显示手动关闭提示
395
- setTimeout(function() {
396
- document.getElementById('close-message').style.display = 'block';
397
- document.getElementById('countdown-container').style.display = 'none';
398
- }, 1000);
399
- } catch (e) {
400
- console.error("关闭窗口失败:", e);
401
- document.getElementById('close-message').style.display = 'block';
402
- document.getElementById('countdown-container').style.display = 'none';
403
- }
404
- }
405
-
406
- // 倒计时函数
407
- var secondsLeft = 3;
408
- function updateCountdown() {
409
- document.getElementById('countdown').innerText = secondsLeft;
410
- if (secondsLeft <= 0) {
411
- attemptClose();
412
- } else {
413
- secondsLeft -= 1;
414
- setTimeout(updateCountdown, 1000);
415
- }
416
- }
417
-
418
- // 初始化
419
- window.onload = function() {
420
- updateCountdown();
421
- }
422
- </script>
423
- </head>
424
- <body>
425
- <div class="autoclose-banner">
426
- <span>授权成功! 此窗口将自动关闭 (<span id="countdown" class="countdown">3</span>)</span>
427
- <button class="close-btn" onclick="attemptClose()">立即关闭</button>
428
- </div>
429
-
430
- <div class="container">
431
- <h1 class="success">飞书授权成功!</h1>
432
- <p>已获取授权码,正在返回命令行...</p>
433
-
434
- <div id="countdown-container">
435
- <p>此窗口将在 <span id="countdown-text" class="countdown">3</span> 秒后自动关闭</p>
436
- </div>
437
-
438
- <div id="close-message" style="display: none;">
439
- <p>自动关闭失败,请手动关闭此窗口</p>
440
- <button class="close-btn" onclick="attemptClose()">尝试再次关闭</button>
441
- </div>
442
-
443
- <p>授权码:</p>
444
- <div class="code">#{code}</div>
445
- </div>
446
-
447
- <script>
448
- // 同步倒计时显示
449
- document.getElementById('countdown-text').innerText = secondsLeft;
450
- </script>
451
- </body>
452
- </html>
453
- HTML
454
- else
455
- # 忽略没有参数的请求(如favicon.ico等)
456
- # puts "未获取到授权码"
457
- res.content_type = "text/html; charset=UTF-8"
458
- res.body = "等待授权中..."
459
- end
460
-
461
- # 只在获取到code时关闭服务器
462
- if code
463
- Thread.new do
464
- sleep 4 # 给用户时间看到成功页面
465
- server.shutdown
466
- end
467
- end
468
- rescue => e
469
- # 静默处理错误,避免干扰用户
470
- # puts "处理请求时出错: #{e.class} - #{e.message}"
471
-
472
- # 确保即使出错也返回有效的响应
473
- res.content_type = "text/html; charset=UTF-8"
474
- res.body = "处理请求时出错"
475
- end
476
- end
477
-
478
- # 捕获Ctrl+C以允许用户中断
479
- trap('INT') { server.shutdown }
480
-
481
- # puts "正在监听http://localhost:8899等待飞书重定向..."
482
- # puts "您也可以手动访问: http://localhost:8899/?code=YOUR_CODE&state=terminal_login"
483
-
484
- # 在线程中运行服务器,最多等待3分钟
485
- thread = Thread.new { server.start }
486
- begin
487
- thread.join(180) # 最多等待3分钟
488
- rescue Timeout::Error
489
- puts "授权超时"
490
- server.shutdown
491
- end
492
-
493
- # 返回获取到的code(如果有的话)
494
- code
495
- end
496
-
497
- # 确保端口可用,处理端口占用问题
498
- def ensure_port_available(port)
499
- return true unless port_occupied?(port)
500
-
501
- puts "⚠️ 端口 #{port} 被占用,正在检查占用进程..."
502
-
503
- # 获取占用端口的进程信息
504
- process_info = get_port_process_info(port)
505
-
506
- if process_info.nil?
507
- puts "✗ 无法获取端口占用信息"
508
- return false
509
- end
510
-
511
- puts "占用进程信息:"
512
- puts " PID: #{process_info[:pid]}"
513
- puts " 进程名: #{process_info[:name]}"
514
- puts " 命令: #{process_info[:command]}"
515
-
516
- # 检查是否是自己的进程(可能是之前未正常关闭的实例)
517
- if process_info[:name].include?('ruby') && (process_info[:command].include?('pindo') || process_info[:command].include?('pgyer'))
518
- puts "检测到可能是 Pindo/Pgyer 之前的遗留进程"
519
- if ask_user_kill_process?
520
- return kill_process_by_pid(process_info[:pid])
521
- end
522
- else
523
- puts "端口被其他应用程序占用"
524
- if ask_user_kill_process?
525
- return kill_process_by_pid(process_info[:pid])
526
- end
527
- end
528
-
529
- return false
530
- end
531
-
532
- # 检查端口是否被占用
533
- def port_occupied?(port)
534
- require 'socket'
535
-
536
- begin
537
- server = TCPServer.new('127.0.0.1', port)
538
- server.close
539
- false # 端口未被占用
540
- rescue Errno::EADDRINUSE
541
- true # 端口被占用
542
- rescue => e
543
- puts "⚠️ 检查端口时出错: #{e.message}"
544
- true # 出错时假设端口被占用,避免冲突
545
- end
546
- end
547
-
548
- # 获取占用指定端口的进程信息
549
- def get_port_process_info(port)
550
- begin
551
- # macOS/Linux 使用 lsof 命令
552
- if system('which lsof > /dev/null 2>&1')
553
- output = `lsof -ti :#{port} 2>/dev/null`.strip
554
- return nil if output.empty?
555
-
556
- pid = output.split("\n").first
557
- return nil if pid.nil? || pid.empty?
558
-
559
- # 获取进程详细信息
560
- ps_output = `ps -p #{pid} -o pid,comm,args 2>/dev/null`.lines
561
- return nil if ps_output.length < 2
562
-
563
- process_line = ps_output[1].strip
564
- parts = process_line.split(/\s+/, 3)
565
-
566
- return {
567
- pid: parts[0],
568
- name: parts[1] || 'unknown',
569
- command: parts[2] || 'unknown'
570
- }
571
- end
572
-
573
- # Windows 使用 netstat 命令
574
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
575
- output = `netstat -ano | findstr :#{port}`.strip
576
- return nil if output.empty?
577
-
578
- lines = output.split("\n")
579
- listening_line = lines.find { |line| line.include?('LISTENING') }
580
- return nil unless listening_line
581
-
582
- pid = listening_line.split.last
583
- return nil if pid.nil? || pid.empty?
584
-
585
- # 获取进程名称
586
- task_output = `tasklist /FI "PID eq #{pid}" /FO CSV /NH 2>nul`.strip
587
- if !task_output.empty?
588
- process_name = task_output.split(',').first.gsub('"', '')
589
- return {
590
- pid: pid,
591
- name: process_name,
592
- command: process_name
593
- }
594
- end
595
- end
596
-
597
- return nil
598
- rescue => e
599
- puts "获取进程信息时出错: #{e.message}"
600
- return nil
601
- end
602
- end
603
-
604
- # 询问用户是否终止占用端口的进程
605
- def ask_user_kill_process?
606
- puts "\n是否终止占用端口的进程?"
607
- puts " y/yes - 终止进程并继续 (可能影响其他应用)"
608
- puts " n/no - 取消登录 (默认)"
609
- print "> "
610
-
611
- begin
612
- response = STDIN.gets&.chomp&.downcase
613
- return ['y', 'yes'].include?(response)
614
- rescue Interrupt
615
- puts "\n\n用户中断操作"
616
- return false
617
- rescue => e
618
- puts "无法读取用户输入: #{e.message}"
619
- return false
620
- end
621
- end
622
-
623
- # 根据 PID 终止进程
624
- def kill_process_by_pid(pid)
625
- return false if pid.nil? || pid.empty?
626
-
627
- begin
628
- puts "正在终止进程 #{pid}..."
629
-
630
- # 跨平台的进程终止命令
631
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
632
- # Windows
633
- success = system("taskkill /PID #{pid} /F >nul 2>&1")
634
- else
635
- # macOS/Linux - 先尝试优雅终止,再强制终止
636
- success = system("kill -TERM #{pid} 2>/dev/null")
637
- sleep(2)
638
- # 检查进程是否还存在
639
- if system("kill -0 #{pid} 2>/dev/null")
640
- puts "优雅终止失败,使用强制终止..."
641
- success = system("kill -KILL #{pid} 2>/dev/null")
642
- end
643
- end
644
-
645
- if success
646
- puts "✓ 进程 #{pid} 已终止"
647
- sleep(1) # 等待端口释放
648
-
649
- # 再次检查端口是否可用
650
- unless port_occupied?(@server_port)
651
- puts "✓ 端口 #{@server_port} 现在可用"
652
- return true
653
- else
654
- puts "⚠️ 端口仍被占用,可能需要等待更长时间"
655
- return false
656
- end
657
- else
658
- puts "✗ 终止进程失败,可能需要管理员权限"
659
- return false
660
- end
661
- rescue => e
662
- puts "终止进程时出错: #{e.message}"
663
- return false
664
- end
665
- end
666
-
667
- end
668
-
669
- end