process_bot 0.1.26 → 0.1.28

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: 6e46c5f20791400dfc63138936340fc46e514d52947d2d2ba78cb208c521f442
4
- data.tar.gz: 0f3ec322eb916178092eb5e7b19cab1a92421a23767fea310bf4c69319e72ca9
3
+ metadata.gz: e123dbde56775d5539bb91d48be68c1f83a09a2b001c6a61a1334aa3dba2b1ab
4
+ data.tar.gz: 34f9ff90352cad06e742ba69f7d4402d095678359caded93b20204f89b804a44
5
5
  SHA512:
6
- metadata.gz: bd08e3680f9c436c00fed3c5ed760542cbbb4fba517021493f6d17de799ed4e781c86d652baa230f0433794a36f61861de8f52c846ff64dfda651b73ba42c3bc
7
- data.tar.gz: 1b91173a14173732de83e7e00f47e95a10a0272768f5810f9ca789f12cb181fd3282bd4c0feab33a3c3f6b0074bcfac6bbb55ca753bb2e8df9c70f4cd7121464
6
+ metadata.gz: 8cbc29f75c5ffa3358f8614c11698fc4ac6548efa5a00a4d63a1004300707c80b395eb9229b877b06cc93c1d3b7e10987fa99d02f1942c67a133db277d9af49b
7
+ data.tar.gz: 8042fcbf747ba1162120b87e54d64bd1ac972615eaca4288865864ea76747e27d94b697b8de97d0a38ede453abbc9c30e5a54fdc3af32052f9ff466c82b9d311
data/AGENTS.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - Made graceful shutdown waiting optional and defaulted Capistrano to not wait.
10
10
  - Kept graceful handling synchronous and verified `bundle exec rspec`.
11
11
  - Enabled ProcessBot logging by default for Capistrano hooks (configurable via `process_bot_log`).
12
- - Always run RuboCop against changed or created Ruby files.
12
+ - Always run RuboCop against changed or created Ruby files before pushing or opening a PR.
13
13
  - Added `graceful_no_wait` command and Capistrano task for non-blocking graceful shutdowns.
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.
data/CHANGELOG.md CHANGED
@@ -8,8 +8,8 @@
8
8
  - Add optional Sidekiq restart overlap and a new ProcessBot restart command.
9
9
  - Guard stop-related process scanning when subprocess PID/PGID is unavailable and fail stop loudly.
10
10
  - Wait briefly for subprocess PID assignment during stop; raise if PID is still missing so stop cannot silently succeed.
11
-
12
11
  - Require an active runner for custom stop commands to avoid constructing a fresh runner with no PID.
12
+ - Buffer subprocess output by line before broadcasting it to control clients so Capistrano does not receive one-character log chunks.
13
13
 
14
14
  ## [0.1.0] - 2022-04-03
15
15
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- process_bot (0.1.26)
4
+ process_bot (0.1.28)
5
5
  knjrbfw (>= 0.0.116)
6
6
  pry
7
7
  rake
@@ -58,7 +58,7 @@ GEM
58
58
  rspec-expectations (3.13.5)
59
59
  diff-lcs (>= 1.2.0, < 2.0)
60
60
  rspec-support (~> 3.13.0)
61
- rspec-mocks (3.13.7)
61
+ rspec-mocks (3.13.8)
62
62
  diff-lcs (>= 1.2.0, < 2.0)
63
63
  rspec-support (~> 3.13.0)
64
64
  rspec-support (3.13.7)
data/Rakefile CHANGED
@@ -7,40 +7,6 @@ require "rubocop/rake_task"
7
7
  RSpec::Core::RakeTask.new(:spec)
8
8
  RuboCop::RakeTask.new
9
9
 
10
- def bump_patch_version(version_path)
11
- version_content = File.read(version_path)
12
- version_match = version_content.match(/VERSION = "(\d+)\.(\d+)\.(\d+)"\.freeze/)
13
- raise "Could not find current version in #{version_path}" unless version_match
14
-
15
- major = version_match[1].to_i
16
- minor = version_match[2].to_i
17
- patch = version_match[3].to_i + 1
18
-
19
- new_version = "#{major}.#{minor}.#{patch}"
20
- new_content = version_content.sub(version_match[0], "VERSION = \"#{new_version}\".freeze")
21
- File.write(version_path, new_content)
22
-
23
- new_version
24
- end
25
-
26
- namespace :release do
27
- desc "Bump patch version, run bundle, commit version bump, build gem, and push gem"
28
- task :patch do
29
- version_path = "lib/process_bot/version.rb"
30
- new_version = bump_patch_version(version_path)
31
-
32
- puts "Bumped version to #{new_version}"
33
-
34
- sh "bundle install"
35
- sh "git add #{version_path}"
36
- sh "git commit -m \"Bump version to #{new_version}\""
37
- sh "bundle exec rake build"
38
-
39
- gem_path = "pkg/process_bot-#{new_version}.gem"
40
- raise "Expected gem file was not built: #{gem_path}" unless File.exist?(gem_path)
41
-
42
- sh "gem push #{gem_path}"
43
- end
44
- end
10
+ Dir[File.expand_path("lib/tasks/**/*.rake", __dir__)].each { |f| load f }
45
11
 
46
12
  task default: %i[spec rubocop]
@@ -48,7 +48,14 @@ class ProcessBot::Process::Handlers::Custom
48
48
  end
49
49
 
50
50
  def stop(**_args)
51
+ runner = process.active_runner
52
+
53
+ unless runner
54
+ logger.logs "No active runner to stop"
55
+ return
56
+ end
57
+
51
58
  logger.logs "Stop related processes"
52
- process.active_runner!.stop_related_processes
59
+ runner.stop_related_processes
53
60
  end
54
61
  end
@@ -3,6 +3,8 @@ require "knjrbfw"
3
3
  class ProcessBot::Process::Runner
4
4
  attr_reader :command, :exit_status, :handler_instance, :handler_name, :logger, :monitor, :options, :pid, :stop_time, :subprocess_pid
5
5
 
6
+ READ_CHUNK_SIZE = 4096
7
+
6
8
  def initialize(command:, handler_instance:, handler_name:, logger:, options:)
7
9
  @command = command
8
10
  @handler_instance = handler_instance
@@ -16,6 +18,17 @@ class ProcessBot::Process::Runner
16
18
  logger.log(output, type: type)
17
19
  end
18
20
 
21
+ def stream_output(io, type:)
22
+ buffer = +""
23
+
24
+ loop do
25
+ buffer << io.readpartial(READ_CHUNK_SIZE)
26
+ flush_complete_lines(type: type, buffer: buffer)
27
+ end
28
+ rescue EOFError, Errno::EIO
29
+ flush_remaining_output(type: type, buffer: buffer)
30
+ end
31
+
19
32
  def running?
20
33
  !stop_time
21
34
  end
@@ -31,13 +44,7 @@ class ProcessBot::Process::Runner
31
44
  logger.logs "Command running with PID #{pid}: #{command}"
32
45
 
33
46
  stdout_reader_thread = Thread.new do
34
- stdout.each_char do |chunk|
35
- monitor.synchronize do
36
- output(type: :stdout, output: chunk)
37
- end
38
- end
39
- rescue Errno::EIO
40
- # Process done
47
+ stream_output(stdout, type: :stdout)
41
48
  ensure
42
49
  status = Process::Status.wait(subprocess_pid, 0)
43
50
 
@@ -46,11 +53,7 @@ class ProcessBot::Process::Runner
46
53
  end
47
54
 
48
55
  stderr_reader_thread = Thread.new do
49
- stderr_reader.each_char do |chunk|
50
- monitor.synchronize do
51
- output(type: :stderr, output: chunk)
52
- end
53
- end
56
+ stream_output(stderr_reader, type: :stderr)
54
57
  end
55
58
 
56
59
  find_sidekiq_pid if handler_name == "sidekiq"
@@ -182,4 +185,37 @@ class ProcessBot::Process::Runner
182
185
  break
183
186
  end
184
187
  end
188
+
189
+ def flush_complete_lines(type:, buffer:)
190
+ loop do
191
+ separator_index = next_separator_index(buffer)
192
+ break unless separator_index
193
+
194
+ monitor.synchronize do
195
+ output(type: type, output: buffer.slice!(0, separator_index + 1))
196
+ end
197
+ end
198
+
199
+ return if buffer.bytesize < READ_CHUNK_SIZE
200
+
201
+ monitor.synchronize do
202
+ output(type: type, output: buffer.dup)
203
+ end
204
+
205
+ buffer.clear
206
+ end
207
+
208
+ def flush_remaining_output(type:, buffer:)
209
+ return if buffer.empty?
210
+
211
+ monitor.synchronize do
212
+ output(type: type, output: buffer.dup)
213
+ end
214
+
215
+ buffer.clear
216
+ end
217
+
218
+ def next_separator_index(buffer)
219
+ buffer.index(/[\r\n]/)
220
+ end
185
221
  end
@@ -3,7 +3,7 @@ require "json"
3
3
  require "monitor"
4
4
  require "string-cases"
5
5
 
6
- class ProcessBot::Process
6
+ class ProcessBot::Process # rubocop:disable Metrics/ClassLength
7
7
  extend Forwardable
8
8
 
9
9
  def_delegator :handler_instance, :graceful
@@ -131,8 +131,15 @@ class ProcessBot::Process
131
131
  def send_control_command(command, **command_options)
132
132
  logger.logs "Sending #{command} command"
133
133
  response = client.send_command(command: command, options: options.options.merge(command_options))
134
- raise "No response from ProcessBot while sending #{command}" if response == :nil
134
+
135
+ if response == :nil
136
+ handle_missing_control_command_response(command)
137
+ return if options[:ignore_no_process_bot]
138
+
139
+ raise "No response from ProcessBot while sending #{command}"
140
+ end
135
141
  rescue Errno::ECONNREFUSED => e
142
+ handle_missing_control_command_response(command)
136
143
  raise e unless options[:ignore_no_process_bot]
137
144
  end
138
145
 
@@ -274,6 +281,65 @@ class ProcessBot::Process
274
281
  start_runner_instance
275
282
  end
276
283
 
284
+ def handle_missing_control_command_response(command)
285
+ return unless command == "stop"
286
+
287
+ matching_processes = matching_process_bot_processes
288
+ log_missing_control_response_diagnostics(matching_processes)
289
+ force_stop_process_bot_if_configured(matching_processes)
290
+ end
291
+
292
+ def log_missing_control_response_diagnostics(matching_processes)
293
+ logger.logs "Control command response missing; attempting diagnostics for application=#{options[:application].inspect} id=#{options[:id].inspect}"
294
+ logger.logs "Matching process_bot lines:\n#{matching_process_bot_processes_text(matching_processes)}"
295
+ end
296
+
297
+ def matching_process_bot_processes_text(lines)
298
+ return "(none)" if lines.empty?
299
+
300
+ lines.join("\n")
301
+ end
302
+
303
+ def matching_process_bot_processes
304
+ ps_output = Knj::Os.shellcmd("ps -eo pid,args")
305
+
306
+ ps_output
307
+ .to_s
308
+ .split("\n")
309
+ .select { |line| process_bot_process_line_matches?(line) }
310
+ end
311
+
312
+ def process_bot_process_line_matches?(line)
313
+ line.include?("ProcessBot {") &&
314
+ line.include?("\"application\":\"#{options[:application]}\"") &&
315
+ line.include?("\"id\":\"#{options[:id]}\"")
316
+ end
317
+
318
+ def force_stop_process_bot_if_configured(matching_processes)
319
+ return unless truthy_option?(:force_stop_on_no_response)
320
+
321
+ matching_processes.each do |line|
322
+ pid = line.strip.split(/\s+/, 2).first
323
+ next unless pid&.match?(/\A\d+\z/)
324
+
325
+ logger.logs "Force-stopping unresponsive process_bot PID #{pid}"
326
+ Process.kill("TERM", Integer(pid, 10))
327
+ rescue Errno::ESRCH
328
+ logger.logs "Process bot PID #{pid} already gone during force stop"
329
+ end
330
+ end
331
+
332
+ def truthy_option?(key)
333
+ value = options[key]
334
+ return false if value.nil?
335
+ return value if value == true || value == false
336
+
337
+ normalized = value.to_s.strip.downcase
338
+ return false if normalized == "false" || normalized == "0" || normalized == ""
339
+
340
+ true
341
+ end
342
+
277
343
  private
278
344
 
279
345
  attr_reader :current_runner_instance, :runner_events, :runner_monitor
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.26".freeze
2
+ VERSION = "0.1.28".freeze
3
3
  end
@@ -0,0 +1,177 @@
1
+ require "English"
2
+ require "fileutils"
3
+ require "pathname"
4
+ require "rubygems/version"
5
+ require "shellwords"
6
+
7
+ class ProcessBotRubygemsRelease
8
+ VERSION_FILE = Pathname.new(File.expand_path("../process_bot/version.rb", __dir__)) unless const_defined?(:VERSION_FILE)
9
+
10
+ def call
11
+ ensure_clean_worktree!
12
+ checkout_master!
13
+ fetch!
14
+ merge!
15
+
16
+ next_version = determine_next_version
17
+
18
+ bump_version!(next_version)
19
+ commit!(next_version)
20
+ push!
21
+ gem_file = build_gem!(next_version)
22
+ push_gem!(gem_file)
23
+ delete_gem_file!(gem_file)
24
+ rescue StandardError
25
+ warn "Release failed."
26
+ raise
27
+ end
28
+
29
+ private
30
+
31
+ def ensure_clean_worktree!
32
+ dirty_entries = git_status_lines.grep_v(%r{\A\?\? process_bot-[^/]+\.gem\z})
33
+ return if dirty_entries.empty?
34
+
35
+ raise "Working tree must be clean before releasing:\n#{dirty_entries.join("\n")}"
36
+ end
37
+
38
+ def checkout_master!
39
+ run!("git", "checkout", "master")
40
+ end
41
+
42
+ def fetch!
43
+ run!("git", "fetch", remote_name)
44
+ end
45
+
46
+ def merge!
47
+ run!("git", "merge", "--ff-only", "#{remote_name}/master")
48
+ end
49
+
50
+ def determine_next_version
51
+ requested_version || bumped_version
52
+ end
53
+
54
+ def requested_version
55
+ version = ENV["VERSION"]&.strip
56
+ return if version.to_s.empty?
57
+
58
+ Gem::Version.new(version)
59
+ version
60
+ end
61
+
62
+ def bumped_version
63
+ major, minor, patch = version_segments
64
+
65
+ case bump_type
66
+ when "major"
67
+ format_version(major + 1, 0, 0)
68
+ when "minor"
69
+ format_version(major, minor + 1, 0)
70
+ when "patch"
71
+ format_version(major, minor, patch + 1)
72
+ else
73
+ raise "Unsupported BUMP=#{bump_type.inspect}. Use patch, minor, major, or VERSION=x.y.z."
74
+ end
75
+ end
76
+
77
+ def version_segments
78
+ @version_segments ||= begin
79
+ segments = Gem::Version.new(current_version).segments
80
+ segments << 0 while segments.length < 3
81
+ segments
82
+ end
83
+ end
84
+
85
+ def current_version
86
+ @current_version ||= VERSION_FILE.read[/VERSION = "([^"]+)"\.freeze/, 1] || raise("Could not find current version")
87
+ end
88
+
89
+ def format_version(major, minor, patch)
90
+ [major, minor, patch].join(".")
91
+ end
92
+
93
+ def bump_version!(next_version)
94
+ raise "Next version must differ from current version" if next_version == current_version
95
+
96
+ VERSION_FILE.write(
97
+ VERSION_FILE.read.sub(
98
+ /VERSION = "[^"]+"\.freeze/,
99
+ %(VERSION = "#{next_version}".freeze)
100
+ )
101
+ )
102
+
103
+ run!("git", "add", VERSION_FILE.to_s)
104
+ end
105
+
106
+ def commit!(next_version)
107
+ run!("git", "commit", "-m", "Release #{next_version}")
108
+ end
109
+
110
+ def push!
111
+ run!("git", "push", remote_name, "master")
112
+ end
113
+
114
+ def build_gem!(next_version)
115
+ gem_file = "process_bot-#{next_version}.gem"
116
+ run!("gem", "build", "process_bot.gemspec")
117
+ gem_file
118
+ end
119
+
120
+ def push_gem!(gem_file)
121
+ run!("gem", "push", gem_file)
122
+ end
123
+
124
+ def delete_gem_file!(gem_file)
125
+ FileUtils.rm_f(gem_file)
126
+ end
127
+
128
+ def git_status_lines
129
+ capture!("git", "status", "--porcelain").split("\n").reject(&:empty?)
130
+ end
131
+
132
+ def bump_type
133
+ ENV.fetch("BUMP", "patch")
134
+ end
135
+
136
+ def remote_name
137
+ ENV.fetch("REMOTE", "origin")
138
+ end
139
+
140
+ def capture!(*command)
141
+ output = `#{command.map { |part| Shellwords.escape(part) }.join(" ")}`
142
+ raise "Command failed: #{command.join(' ')}" unless $CHILD_STATUS&.success?
143
+
144
+ output
145
+ end
146
+
147
+ def run!(*command)
148
+ return if system(*command)
149
+
150
+ raise "Command failed: #{command.join(' ')}"
151
+ end
152
+ end
153
+
154
+ namespace :release do
155
+ desc "Release a patch version from master by fetching, fast-forward merging, bumping version, pushing, and publishing"
156
+ task :patch do
157
+ ENV["BUMP"] = "patch"
158
+ ProcessBotRubygemsRelease.new.call
159
+ end
160
+
161
+ desc "Release a minor version from master by fetching, fast-forward merging, bumping version, pushing, and publishing"
162
+ task :minor do
163
+ ENV["BUMP"] = "minor"
164
+ ProcessBotRubygemsRelease.new.call
165
+ end
166
+
167
+ desc "Release a major version from master by fetching, fast-forward merging, bumping version, pushing, and publishing"
168
+ task :major do
169
+ ENV["BUMP"] = "major"
170
+ ProcessBotRubygemsRelease.new.call
171
+ end
172
+
173
+ desc "Release the gem from master by fetching, fast-forward merging, bumping version, pushing, and publishing"
174
+ task :rubygems do
175
+ ProcessBotRubygemsRelease.new.call
176
+ end
177
+ end
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.26
4
+ version: 0.1.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-12 00:00:00.000000000 Z
11
+ date: 2026-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: knjrbfw
@@ -175,6 +175,7 @@ files:
175
175
  - lib/process_bot/process/runner.rb
176
176
  - lib/process_bot/process/runner_instance.rb
177
177
  - lib/process_bot/version.rb
178
+ - lib/tasks/release.rake
178
179
  - peak_flow.yml
179
180
  - process_bot.gemspec
180
181
  homepage: https://github.com/kaspernj/process_bot