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 +4 -4
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/AGENTS.md +5 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile.lock +7 -7
- data/lib/process_bot/capistrano/sidekiq_helpers.rb +2 -2
- data/lib/process_bot/control_socket.rb +79 -4
- data/lib/process_bot/options.rb +11 -9
- data/lib/process_bot/process/handlers/custom.rb +4 -4
- data/lib/process_bot/process/handlers/sidekiq.rb +4 -4
- data/lib/process_bot/process.rb +50 -4
- data/lib/process_bot/version.rb +1 -1
- data/lib/tasks/release.rake +2 -1
- data/peak_flow.yml +11 -5
- data/process_bot.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2183ea1e8ec1a90a39ec3d3d7d2753150f99e164f31e3650d83a40a784d1a67
|
|
4
|
+
data.tar.gz: 0b009604dcd29677832a88cd814b29e7259cef2709d22f5ef140673d298a0981
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6239960b599dc220ab6e8519f7edd7061556b69e0918d48cd655a25bd1239307b6f7b836599811cc3b69dd72ad40a830988aef00d3cb92fdfbbd9ebf02428c2
|
|
7
|
+
data.tar.gz: 4566c50a189d90b5c638369db712d63355700894a6643f17b44e4b0544bee9fdb949e13046c9f24c21652742e4352d8139bf80a5c79612d007a8114f06a4fe9c
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
47
|
+
rake (13.4.2)
|
|
48
48
|
ref (2.0.0)
|
|
49
|
-
regexp_parser (2.
|
|
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.
|
|
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 (
|
|
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, &
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
ports << port.to_i if port
|
|
251
|
+
entries << process_bot_entry_from_process_data(process, process_data)
|
|
187
252
|
end
|
|
188
253
|
|
|
189
|
-
|
|
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
|
data/lib/process_bot/options.rb
CHANGED
|
@@ -36,17 +36,19 @@ class ProcessBot::Options
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def application_basename
|
|
39
|
-
@application_basename ||=
|
|
40
|
-
|
|
39
|
+
@application_basename ||= application_basename_from_release_path
|
|
40
|
+
end
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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(
|
|
29
|
-
options.fetch(
|
|
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(
|
|
43
|
-
options.set(
|
|
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(
|
|
60
|
-
options.fetch(
|
|
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(
|
|
74
|
-
options.set(
|
|
73
|
+
def set(*, **)
|
|
74
|
+
options.set(*, **)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def send_tstp_or_return
|
data/lib/process_bot/process.rb
CHANGED
|
@@ -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 = {
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
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)
|
data/lib/process_bot/version.rb
CHANGED
data/lib/tasks/release.rake
CHANGED
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.
|
|
6
|
-
name: Ruby 2.
|
|
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.
|
|
12
|
-
name: Ruby 3.
|
|
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:
|
|
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.
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
205
|
+
rubygems_version: 3.4.19
|
|
206
206
|
signing_key:
|
|
207
207
|
specification_version: 4
|
|
208
208
|
summary: Run and control processes.
|