process_bot 0.1.29 → 0.1.31

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26b4a49cb5489140c90c0712ed2a9b686a43d531ab1290ee63fec7710371ca31
4
- data.tar.gz: 857a0fc1d15a79dbb87a92f25ded46706953b0215e64f47d135eb96ba9cf90d3
3
+ metadata.gz: c2183ea1e8ec1a90a39ec3d3d7d2753150f99e164f31e3650d83a40a784d1a67
4
+ data.tar.gz: 0b009604dcd29677832a88cd814b29e7259cef2709d22f5ef140673d298a0981
5
5
  SHA512:
6
- metadata.gz: 9d14944e3d8976d0077f20e83560558bd9af1d58247c849ef7cc03d4205c017925108fc2360cdd99059480f7aaf05e5fdaf464ea19fded430a540f168136963b
7
- data.tar.gz: d22b2ec8c5c7483bad6bee7ab51060164f811b601f448e1f81bdd8b86e2c0b57eae39779e05347984c065a45449b59058586b490f3a3ebf421fc80f1a865998d
6
+ metadata.gz: f6239960b599dc220ab6e8519f7edd7061556b69e0918d48cd655a25bd1239307b6f7b836599811cc3b69dd72ad40a830988aef00d3cb92fdfbbd9ebf02428c2
7
+ data.tar.gz: 4566c50a189d90b5c638369db712d63355700894a6643f17b44e4b0544bee9fdb949e13046c9f24c21652742e4352d8139bf80a5c79612d007a8114f06a4fe9c
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ AllCops:
2
2
  DisplayCopNames: true
3
3
  DisplayStyleGuide: true
4
4
  NewCops: enable
5
- TargetRubyVersion: 2.7
5
+ TargetRubyVersion: 3.2
6
6
 
7
7
  plugins:
8
8
  - rubocop-performance
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.8
1
+ 3.2.5
data/AGENTS.md CHANGED
@@ -14,3 +14,8 @@
14
14
  - Always add or update tests for new/changed functionality, and run them.
15
15
  - Added coverage for graceful_no_wait and Capistrano wait defaults.
16
16
  - Bumped version to 0.1.20 for log streaming updates.
17
+
18
+ ## Release policy
19
+ - Do not bump the version in `lib/process_bot/version.rb` (and do not touch the `process_bot (x.y.z)` line in `Gemfile.lock`) as part of a feature or fix PR.
20
+ - Version bumps, CHANGELOG version headings, and the rubygems push are driven by the release rake tasks (`bundle exec rake release:patch`, `release:minor`, or `release:major`), run by the release maintainer after the PR lands on master.
21
+ - PRs should land their code change and add CHANGELOG notes under the `## [Unreleased]` heading only. Leave version numbers to the release workflow.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
1
  ## [Unreleased]
2
+ - Drop Ruby 2.x support. Minimum Ruby is now 3.2; CI covers 3.2/3.3/3.4 and RuboCop's `TargetRubyVersion` follows. Shorthand anonymous forwarding (`*, **, &`) is applied where it replaces redundant `*args, **opts, &block` pass-throughs.
3
+ - Fail `start_tcp_server` when another ProcessBot with the same `--id` is already running instead of silently drifting to a new port. Port drift across unrelated services is still supported; drift for a duplicate id was the root cause of Capistrano deploys leaving a stale previous-release ProcessBot alive on a drifted port while every subsequent `stop --port X` hit the wrong instance.
4
+ - Treat legacy ProcessBot titles without `application_basename` as duplicates when they share the same `--id` and control port, and include the same legacy match in forced stop diagnostics.
2
5
  - Stop accepting new control commands during shutdown so in-flight responses complete reliably.
3
6
  - Stream ProcessBot logs to connected control clients for Capistrano output.
4
7
  - Sanitize broadcast log output to keep JSON encoding safe.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- process_bot (0.1.29)
4
+ process_bot (0.1.31)
5
5
  knjrbfw (>= 0.0.116)
6
6
  pry
7
7
  rake
@@ -18,7 +18,7 @@ GEM
18
18
  http2 (0.0.36)
19
19
  string-cases (~> 0)
20
20
  io-console (0.8.2)
21
- json (2.19.3)
21
+ json (2.19.5)
22
22
  knjrbfw (0.0.116)
23
23
  datet
24
24
  http2
@@ -29,7 +29,7 @@ GEM
29
29
  language_server-protocol (3.17.0.5)
30
30
  lint_roller (1.1.0)
31
31
  method_source (1.1.0)
32
- parallel (1.27.0)
32
+ parallel (1.28.0)
33
33
  parser (3.3.11.1)
34
34
  ast (~> 2.4.1)
35
35
  racc
@@ -44,9 +44,9 @@ GEM
44
44
  reline (>= 0.6.0)
45
45
  racc (1.8.1)
46
46
  rainbow (3.1.1)
47
- rake (13.3.1)
47
+ rake (13.4.2)
48
48
  ref (2.0.0)
49
- regexp_parser (2.11.3)
49
+ regexp_parser (2.12.0)
50
50
  reline (0.6.3)
51
51
  io-console (~> 0.5)
52
52
  rspec (3.13.2)
@@ -62,11 +62,11 @@ GEM
62
62
  diff-lcs (>= 1.2.0, < 2.0)
63
63
  rspec-support (~> 3.13.0)
64
64
  rspec-support (3.13.7)
65
- rubocop (1.86.0)
65
+ rubocop (1.86.2)
66
66
  json (~> 2.3)
67
67
  language_server-protocol (~> 3.17.0.2)
68
68
  lint_roller (~> 1.1.0)
69
- parallel (~> 1.10)
69
+ parallel (>= 1.10)
70
70
  parser (>= 3.3.0.2)
71
71
  rainbow (>= 2.2.2, < 4.0)
72
72
  regexp_parser (>= 2.9.3, < 3.0)
@@ -23,12 +23,12 @@ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLe
23
23
  fetch(:sidekiq_log)
24
24
  end
25
25
 
26
- def switch_user(role, &block)
26
+ def switch_user(role, &)
27
27
  su_user = sidekiq_user(role)
28
28
  if su_user == role.user
29
29
  yield
30
30
  else
31
- as su_user, &block
31
+ as(su_user, &)
32
32
  end
33
33
  end
34
34
 
@@ -28,6 +28,8 @@ class ProcessBot::ControlSocket
28
28
  end
29
29
 
30
30
  def start_tcp_server
31
+ ensure_no_duplicate_id!
32
+
31
33
  used_ports = used_process_bot_ports
32
34
  attempts = 0
33
35
 
@@ -50,6 +52,63 @@ class ProcessBot::ControlSocket
50
52
  end
51
53
  end
52
54
 
55
+ # Prevent a second process_bot with the same `--id` under the same
56
+ # application from starting while the first is still alive. The
57
+ # `start_tcp_server` loop silently drifts to a free port when the
58
+ # requested one is in use; drift is intentional when unrelated
59
+ # process_bots share a host, but it's a bug when a Capistrano deploy's
60
+ # stop failed to clean up the previous release's process_bot and the
61
+ # new release's start drifts around the zombie. Scope the match by
62
+ # `application_basename` (derived from `release_path`) so that two
63
+ # unrelated apps on the same host can reuse a generic id like
64
+ # `sidekiq-main` without falsely blocking each other.
65
+ def ensure_no_duplicate_id!
66
+ id = options[:id]
67
+ return if id.nil? || id.to_s.strip.empty?
68
+
69
+ duplicates = find_duplicate_id_entries(id.to_s, safe_application_basename)
70
+ return if duplicates.empty?
71
+
72
+ raise duplicate_id_error_message(id, duplicates)
73
+ end
74
+
75
+ def find_duplicate_id_entries(id, basename)
76
+ running_process_bot_entries.select do |entry|
77
+ duplicate_id_entry_matches?(entry, id, basename)
78
+ end
79
+ end
80
+
81
+ def duplicate_id_entry_matches?(entry, id, basename)
82
+ return false unless entry[:id] == id
83
+ return true if basename && entry[:application_basename] == basename
84
+
85
+ entry[:application_basename].nil? && same_control_port?(entry)
86
+ end
87
+
88
+ def same_control_port?(entry)
89
+ return false unless entry[:port] && options[:port]
90
+
91
+ entry[:port].to_i == options[:port].to_i
92
+ end
93
+
94
+ def duplicate_id_error_message(id, duplicates)
95
+ details = duplicates.map { |entry| "PID #{entry[:pid]} on port #{entry[:port]}" }.join(", ")
96
+ example_port = duplicates.first[:port]
97
+ handler = options.fetch(:handler, "custom")
98
+ release_path = options.fetch(:release_path, "/")
99
+
100
+ "Another process_bot with id=#{id.inspect} is already running for this application (#{details}). " \
101
+ "Stop it (e.g. `process_bot --command stop --port #{example_port} --id #{id} " \
102
+ "--handler #{handler} --release-path #{release_path}`) " \
103
+ "or kill that PID before starting a new instance."
104
+ end
105
+
106
+ def safe_application_basename
107
+ options.application_basename
108
+ rescue KeyError
109
+ nil
110
+ end
111
+
53
112
  def actually_start_tcp_server(host, port)
54
113
  TCPServer.new(host, port)
55
114
  end
@@ -169,7 +228,14 @@ class ProcessBot::ControlSocket
169
228
  end
170
229
 
171
230
  def used_process_bot_ports
172
- ports = []
231
+ running_process_bot_entries.filter_map { |entry| entry[:port] }.uniq
232
+ end
233
+
234
+ # Parsed `{application_basename:, id:, pid:, port:}` entries for every
235
+ # running process_bot visible to `ps`, extracted from each instance's
236
+ # JSON process title.
237
+ def running_process_bot_entries
238
+ entries = []
173
239
 
174
240
  Knj::Unix_proc.list("grep" => "ProcessBot") do |process|
175
241
  process_command = process.data.fetch("cmd")
@@ -182,10 +248,19 @@ class ProcessBot::ControlSocket
182
248
  next
183
249
  end
184
250
 
185
- port = process_data["port"]
186
- ports << port.to_i if port
251
+ entries << process_bot_entry_from_process_data(process, process_data)
187
252
  end
188
253
 
189
- ports.uniq
254
+ entries
255
+ end
256
+
257
+ def process_bot_entry_from_process_data(process, process_data)
258
+ {
259
+ application: process_data["application"],
260
+ application_basename: process_data["application_basename"],
261
+ id: process_data["id"]&.to_s,
262
+ pid: process.data["pid"] || process.pid,
263
+ port: process_data["port"]&.to_i
264
+ }
190
265
  end
191
266
  end
@@ -36,17 +36,19 @@ class ProcessBot::Options
36
36
  end
37
37
 
38
38
  def application_basename
39
- @application_basename ||= begin
40
- app_path_parts = release_path.split("/")
39
+ @application_basename ||= application_basename_from_release_path
40
+ end
41
41
 
42
- if release_path.include?("/releases/")
43
- app_path_parts.pop(2)
44
- elsif release_path.end_with?("/current")
45
- app_path_parts.pop
46
- end
42
+ def application_basename_from_release_path
43
+ app_path_parts = release_path.split("/")
44
+ deployment_marker_index = [
45
+ app_path_parts.rindex("releases"),
46
+ app_path_parts.rindex("current")
47
+ ].compact.max
47
48
 
48
- app_path_parts.last
49
- end
49
+ return app_path_parts[0...deployment_marker_index].last if deployment_marker_index
50
+
51
+ app_path_parts.last
50
52
  end
51
53
 
52
54
  def possible_process_titles
@@ -25,8 +25,8 @@ class ProcessBot::Process::Handlers::Custom
25
25
  !value || value == "false"
26
26
  end
27
27
 
28
- def fetch(*args, **opts)
29
- options.fetch(*args, **opts)
28
+ def fetch(*, **)
29
+ options.fetch(*, **)
30
30
  end
31
31
 
32
32
  def logger
@@ -39,8 +39,8 @@ class ProcessBot::Process::Handlers::Custom
39
39
  set(key, value)
40
40
  end
41
41
 
42
- def set(*args, **opts)
43
- options.set(*args, **opts)
42
+ def set(*, **)
43
+ options.set(*, **)
44
44
  end
45
45
 
46
46
  def start_command
@@ -56,8 +56,8 @@ class ProcessBot::Process::Handlers::Sidekiq
56
56
  update_current_pid(new_pid)
57
57
  end
58
58
 
59
- def fetch(*args, **opts)
60
- options.fetch(*args, **opts)
59
+ def fetch(*, **)
60
+ options.fetch(*, **)
61
61
  end
62
62
 
63
63
  def logger
@@ -70,8 +70,8 @@ class ProcessBot::Process::Handlers::Sidekiq
70
70
  set(key, value)
71
71
  end
72
72
 
73
- def set(*args, **opts)
74
- options.set(*args, **opts)
73
+ def set(*, **)
74
+ options.set(*, **)
75
75
  end
76
76
 
77
77
  def send_tstp_or_return
@@ -163,11 +163,31 @@ class ProcessBot::Process # rubocop:disable Metrics/ClassLength
163
163
  end
164
164
 
165
165
  def update_process_title
166
- process_args = {application: options[:application], handler: handler_name, id: options[:id], pid: current_pid, port: port}
166
+ process_args = {
167
+ application: options[:application],
168
+ application_basename: safe_application_basename,
169
+ handler: handler_name,
170
+ id: options[:id],
171
+ pid: current_pid,
172
+ port: port
173
+ }
167
174
  @current_process_title = "ProcessBot #{JSON.generate(process_args)}"
168
175
  Process.setproctitle(current_process_title)
169
176
  end
170
177
 
178
+ # Capistrano-style release paths (`.../<app>/releases/<timestamp>`)
179
+ # resolve to a stable per-app basename like `awesome-tasks` that is
180
+ # consistent across deploys of the same app and distinct across apps
181
+ # sharing a host. `ControlSocket#ensure_no_duplicate_id!` uses it to
182
+ # scope the duplicate-id guard by `(application_basename, id)` so an
183
+ # unrelated app on the same host can safely reuse an id like
184
+ # `sidekiq-main`. Returns nil when release_path isn't set.
185
+ def safe_application_basename
186
+ options.application_basename
187
+ rescue KeyError
188
+ nil
189
+ end
190
+
171
191
  def with_control_command
172
192
  control_command_monitor.synchronize do
173
193
  @control_commands_in_flight += 1
@@ -314,9 +334,35 @@ class ProcessBot::Process # rubocop:disable Metrics/ClassLength
314
334
  end
315
335
 
316
336
  def process_bot_process_line_matches?(line)
317
- line.include?("ProcessBot {") &&
318
- line.include?("\"application\":\"#{options[:application]}\"") &&
319
- line.include?("\"id\":\"#{options[:id]}\"")
337
+ process_data = process_bot_data_from_line(line)
338
+ return false unless process_data
339
+ return false unless process_data["id"]&.to_s == options[:id].to_s
340
+
341
+ process_bot_application_matches?(process_data) || legacy_process_bot_port_matches?(process_data)
342
+ end
343
+
344
+ def process_bot_data_from_line(line)
345
+ match = line.match(/ProcessBot (\{.+\})/)
346
+ return unless match
347
+
348
+ JSON.parse(match[1])
349
+ rescue JSON::ParserError
350
+ nil
351
+ end
352
+
353
+ def process_bot_application_matches?(process_data)
354
+ application = options[:application]
355
+ return true if application && process_data["application"] == application
356
+
357
+ basename = safe_application_basename
358
+ basename && process_data["application_basename"] == basename
359
+ end
360
+
361
+ def legacy_process_bot_port_matches?(process_data)
362
+ return false unless process_data["port"] && options[:port]
363
+
364
+ process_data["application_basename"].nil? &&
365
+ process_data["port"].to_i == options[:port].to_i
320
366
  end
321
367
 
322
368
  def force_stop_process_bot_if_configured(matching_processes)
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.29".freeze
2
+ VERSION = "0.1.31".freeze
3
3
  end
@@ -104,7 +104,8 @@ private
104
104
  )
105
105
  )
106
106
 
107
- run!("git", "add", VERSION_FILE.to_s)
107
+ run!("bundle", "lock")
108
+ run!("git", "add", VERSION_FILE.to_s, "Gemfile.lock")
108
109
  end
109
110
 
110
111
  def commit!(next_version)
data/peak_flow.yml CHANGED
@@ -2,19 +2,25 @@ rvm: true
2
2
  builds:
3
3
  build_1:
4
4
  environment:
5
- RUBY_VERSION: 2.7.8
6
- name: Ruby 2.7.8
5
+ RUBY_VERSION: 3.2.5
6
+ name: Ruby 3.2.5
7
7
  script:
8
8
  - bundle exec rspec
9
9
  build_2:
10
10
  environment:
11
- RUBY_VERSION: 3.2.2
12
- name: Ruby 3.2.2
11
+ RUBY_VERSION: 3.3.5
12
+ name: Ruby 3.3.5
13
13
  script:
14
14
  - bundle exec rspec
15
15
  build_3:
16
16
  environment:
17
- RUBY_VERSION: 2.7.8
17
+ RUBY_VERSION: 3.4.5
18
+ name: Ruby 3.4.5
19
+ script:
20
+ - bundle exec rspec
21
+ build_4:
22
+ environment:
23
+ RUBY_VERSION: 3.4.5
18
24
  name: Rubocop
19
25
  script:
20
26
  - bundle exec rubocop
data/process_bot.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Run and control processes."
13
13
  spec.homepage = "https://github.com/kaspernj/process_bot"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.0"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.29
4
+ version: 0.1.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: knjrbfw
@@ -195,14 +195,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
195
  requirements:
196
196
  - - ">="
197
197
  - !ruby/object:Gem::Version
198
- version: 2.7.0
198
+ version: 3.2.0
199
199
  required_rubygems_version: !ruby/object:Gem::Requirement
200
200
  requirements:
201
201
  - - ">="
202
202
  - !ruby/object:Gem::Version
203
203
  version: '0'
204
204
  requirements: []
205
- rubygems_version: 3.1.6
205
+ rubygems_version: 3.4.19
206
206
  signing_key:
207
207
  specification_version: 4
208
208
  summary: Run and control processes.