process_bot 0.1.29 → 0.1.30

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: 5e091db49a7e39df21edb300dc0ebb57cb5b98a73be033b220d24f72a7f72c36
4
+ data.tar.gz: 59e9442735c2a07cc568e8f9b0f1ed3c3660fa407d1f85ed7326348722ed7dbc
5
5
  SHA512:
6
- metadata.gz: 9d14944e3d8976d0077f20e83560558bd9af1d58247c849ef7cc03d4205c017925108fc2360cdd99059480f7aaf05e5fdaf464ea19fded430a540f168136963b
7
- data.tar.gz: d22b2ec8c5c7483bad6bee7ab51060164f811b601f448e1f81bdd8b86e2c0b57eae39779e05347984c065a45449b59058586b490f3a3ebf421fc80f1a865998d
6
+ metadata.gz: 3459f12d21d2d221c152ed047cd51dc4c9134e285cfc52847997099ace48a1ddd60fc8c7c54d5a6b6ca8535b2054e47f9ae5b72da010188e56eca304194b30e6
7
+ data.tar.gz: 8b7bd9482806698b84f3ea356b00c2f71d58850249fb0e2aabb28591495094e727324eb6bad5f0e5ee4740adef932c58f91f232d90e8ea8488f7f453a9bde138
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/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,6 @@
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.
2
4
  - Stop accepting new control commands during shutdown so in-flight responses complete reliably.
3
5
  - Stream ProcessBot logs to connected control clients for Capistrano output.
4
6
  - 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.30)
5
5
  knjrbfw (>= 0.0.116)
6
6
  pry
7
7
  rake
@@ -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.1)
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,50 @@ 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
+ entry[:id] == id && entry[:application_basename] == basename
78
+ end
79
+ end
80
+
81
+ def duplicate_id_error_message(id, duplicates)
82
+ details = duplicates.map { |entry| "PID #{entry[:pid]} on port #{entry[:port]}" }.join(", ")
83
+ example_port = duplicates.first[:port]
84
+ handler = options.fetch(:handler, "custom")
85
+ release_path = options.fetch(:release_path, "/")
86
+
87
+ "Another process_bot with id=#{id.inspect} is already running for this application (#{details}). " \
88
+ "Stop it (e.g. `process_bot --command stop --port #{example_port} --id #{id} " \
89
+ "--handler #{handler} --release-path #{release_path}`) " \
90
+ "or kill that PID before starting a new instance."
91
+ end
92
+
93
+ def safe_application_basename
94
+ options.application_basename
95
+ rescue KeyError
96
+ nil
97
+ end
98
+
53
99
  def actually_start_tcp_server(host, port)
54
100
  TCPServer.new(host, port)
55
101
  end
@@ -169,7 +215,14 @@ class ProcessBot::ControlSocket
169
215
  end
170
216
 
171
217
  def used_process_bot_ports
172
- ports = []
218
+ running_process_bot_entries.filter_map { |entry| entry[:port] }.uniq
219
+ end
220
+
221
+ # Parsed `{application_basename:, id:, pid:, port:}` entries for every
222
+ # running process_bot visible to `ps`, extracted from each instance's
223
+ # JSON process title.
224
+ def running_process_bot_entries
225
+ entries = []
173
226
 
174
227
  Knj::Unix_proc.list("grep" => "ProcessBot") do |process|
175
228
  process_command = process.data.fetch("cmd")
@@ -182,10 +235,15 @@ class ProcessBot::ControlSocket
182
235
  next
183
236
  end
184
237
 
185
- port = process_data["port"]
186
- ports << port.to_i if port
238
+ pid = process.data["pid"] || process.pid
239
+ entries << {
240
+ application_basename: process_data["application_basename"],
241
+ id: process_data["id"]&.to_s,
242
+ pid: pid,
243
+ port: process_data["port"]&.to_i
244
+ }
187
245
  end
188
246
 
189
- ports.uniq
247
+ entries
190
248
  end
191
249
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.29".freeze
2
+ VERSION = "0.1.30".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,13 @@
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.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: knjrbfw
@@ -187,7 +186,6 @@ metadata:
187
186
  source_code_uri: https://github.com/kaspernj/process_bot
188
187
  changelog_uri: https://github.com/kaspernj/process_bot/blob/master/CHANGELOG.md
189
188
  rubygems_mfa_required: 'true'
190
- post_install_message:
191
189
  rdoc_options: []
192
190
  require_paths:
193
191
  - lib
@@ -195,15 +193,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
193
  requirements:
196
194
  - - ">="
197
195
  - !ruby/object:Gem::Version
198
- version: 2.7.0
196
+ version: 3.2.0
199
197
  required_rubygems_version: !ruby/object:Gem::Requirement
200
198
  requirements:
201
199
  - - ">="
202
200
  - !ruby/object:Gem::Version
203
201
  version: '0'
204
202
  requirements: []
205
- rubygems_version: 3.1.6
206
- signing_key:
203
+ rubygems_version: 3.6.9
207
204
  specification_version: 4
208
205
  summary: Run and control processes.
209
206
  test_files: []