process_bot 0.1.2 → 0.1.4

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: 918adb2b2275f38d3b22eb926a5da3b021f667d4a7290a3b0b1d9ea7bb56a2c2
4
- data.tar.gz: 03c557694d0a91f78f25dc4689f2b99c46b626ebeed5816f8e2e0346e29dd482
3
+ metadata.gz: 24914d42c043171014a4dacebea43987204236a2dadb14d2fcc4f8bda936e628
4
+ data.tar.gz: b9248c4189e0d52826728401e940bed7ba46e330fb7f391d587ba00f23b160e3
5
5
  SHA512:
6
- metadata.gz: 2053d076158df45b723855cee991d47852efb18b34ace65e24005798c7d9d1f1ffff126d1fc7409ba3554e4bfa3bf9855c13dfec5bcfa14ac79aac57d9409acf
7
- data.tar.gz: 5622bc3069e029485876c7329bd0658b3bf7b3c4b265a40e51421bab04e0b898367f91c864dbe7caf1de93b317e9ba1fcd4787971fd4dc9e28528bf4f14a8df8
6
+ metadata.gz: a92b6568aaf9e799f5d318cd63c900185c4f6cacb4578c7f6fc3769933e571b27b7f48f27d7c33a93ce700d5e3990ea53c0f50a5b39c31ae9153130502c770bc
7
+ data.tar.gz: 3eb45c1dd31b202ac80b0c1b7925e60ed682fd5f42210a768459c6b456d84b9a76cc7618b67ead9f576482ded5b9d47501039d51d158a5125bfab258097c43ff
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ AllCops:
2
2
  DisplayCopNames: true
3
3
  DisplayStyleGuide: true
4
4
  NewCops: enable
5
- TargetRubyVersion: 2.6
5
+ TargetRubyVersion: 2.7
6
6
 
7
7
  require:
8
8
  - rubocop-performance
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.8
data/Gemfile CHANGED
@@ -5,9 +5,14 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in process_bot.gemspec
6
6
  gemspec
7
7
 
8
+ gem "pry"
8
9
  gem "rake"
9
10
  gem "rspec"
10
- gem "rubocop"
11
11
  gem "string-cases"
12
12
 
13
- gem "pry"
13
+ group :development do
14
+ gem "rubocop"
15
+ gem "rubocop-performance"
16
+ gem "rubocop-rake"
17
+ gem "rubocop-rspec"
18
+ end
data/Gemfile.lock CHANGED
@@ -1,63 +1,96 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- process_bot (0.1.2)
4
+ process_bot (0.1.4)
5
+ knjrbfw (>= 0.0.116)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
10
  ast (2.4.2)
10
11
  coderay (1.1.3)
12
+ datet (0.0.25)
11
13
  diff-lcs (1.5.0)
12
- json (2.6.2)
14
+ http2 (0.0.36)
15
+ string-cases (~> 0)
16
+ json (2.7.0)
17
+ knjrbfw (0.0.116)
18
+ datet
19
+ http2
20
+ php4r
21
+ ruby_process
22
+ tsafe
23
+ wref (>= 0.0.8)
24
+ language_server-protocol (3.17.0.3)
13
25
  method_source (1.0.0)
14
- parallel (1.22.1)
15
- parser (3.1.2.1)
26
+ parallel (1.23.0)
27
+ parser (3.2.2.4)
16
28
  ast (~> 2.4.1)
17
- pry (0.14.1)
29
+ racc
30
+ php4r (0.0.4)
31
+ datet
32
+ http2
33
+ string-strtr
34
+ pry (0.14.2)
18
35
  coderay (~> 1.1)
19
36
  method_source (~> 1.0)
37
+ racc (1.7.3)
20
38
  rainbow (3.1.1)
21
- rake (13.0.6)
22
- regexp_parser (2.6.0)
23
- rexml (3.2.5)
24
- rspec (3.11.0)
25
- rspec-core (~> 3.11.0)
26
- rspec-expectations (~> 3.11.0)
27
- rspec-mocks (~> 3.11.0)
28
- rspec-core (3.11.0)
29
- rspec-support (~> 3.11.0)
30
- rspec-expectations (3.11.0)
39
+ rake (13.1.0)
40
+ regexp_parser (2.8.2)
41
+ rexml (3.2.6)
42
+ rspec (3.12.0)
43
+ rspec-core (~> 3.12.0)
44
+ rspec-expectations (~> 3.12.0)
45
+ rspec-mocks (~> 3.12.0)
46
+ rspec-core (3.12.0)
47
+ rspec-support (~> 3.12.0)
48
+ rspec-expectations (3.12.0)
31
49
  diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.11.0)
33
- rspec-mocks (3.11.1)
50
+ rspec-support (~> 3.12.0)
51
+ rspec-mocks (3.12.0)
34
52
  diff-lcs (>= 1.2.0, < 2.0)
35
- rspec-support (~> 3.11.0)
36
- rspec-support (3.11.0)
37
- rubocop (1.36.0)
53
+ rspec-support (~> 3.12.0)
54
+ rspec-support (3.12.0)
55
+ rubocop (1.58.0)
38
56
  json (~> 2.3)
57
+ language_server-protocol (>= 3.17.0)
39
58
  parallel (~> 1.10)
40
- parser (>= 3.1.2.1)
59
+ parser (>= 3.2.2.4)
41
60
  rainbow (>= 2.2.2, < 4.0)
42
61
  regexp_parser (>= 1.8, < 3.0)
43
62
  rexml (>= 3.2.5, < 4.0)
44
- rubocop-ast (>= 1.20.1, < 2.0)
63
+ rubocop-ast (>= 1.30.0, < 2.0)
45
64
  ruby-progressbar (~> 1.7)
46
- unicode-display_width (>= 1.4.0, < 3.0)
47
- rubocop-ast (1.21.0)
48
- parser (>= 3.1.1.0)
49
- rubocop-performance (1.15.0)
65
+ unicode-display_width (>= 2.4.0, < 3.0)
66
+ rubocop-ast (1.30.0)
67
+ parser (>= 3.2.1.0)
68
+ rubocop-capybara (2.19.0)
69
+ rubocop (~> 1.41)
70
+ rubocop-factory_bot (2.24.0)
71
+ rubocop (~> 1.33)
72
+ rubocop-performance (1.19.1)
50
73
  rubocop (>= 1.7.0, < 2.0)
51
74
  rubocop-ast (>= 0.4.0)
52
75
  rubocop-rake (0.6.0)
53
76
  rubocop (~> 1.0)
54
- rubocop-rspec (2.11.1)
55
- rubocop (~> 1.19)
56
- ruby-progressbar (1.11.0)
77
+ rubocop-rspec (2.25.0)
78
+ rubocop (~> 1.40)
79
+ rubocop-capybara (~> 2.17)
80
+ rubocop-factory_bot (~> 2.22)
81
+ ruby-progressbar (1.13.0)
82
+ ruby_process (0.0.13)
83
+ string-cases
84
+ tsafe
85
+ wref
57
86
  string-cases (0.0.4)
58
- unicode-display_width (2.3.0)
87
+ string-strtr (0.0.3)
88
+ tsafe (0.0.12)
89
+ unicode-display_width (2.5.0)
90
+ wref (0.0.8)
59
91
 
60
92
  PLATFORMS
93
+ ruby
61
94
  x86_64-linux
62
95
 
63
96
  DEPENDENCIES
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # ProcessBot
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/process_bot`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Run your app through ProcessBot for automatic restart if crashing, but still support normal deployment through Capistrano.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ In the future ProcessBot will also watch memory usage and restart processes if leaking memory automatically and gracefully.
6
6
 
7
7
  ## Installation
8
8
 
@@ -12,17 +12,27 @@ Add this line to your application's Gemfile:
12
12
  gem 'process_bot'
13
13
  ```
14
14
 
15
- And then execute:
16
-
17
- $ bundle install
18
-
19
- Or install it yourself as:
15
+ Add to your `Capfile`:
16
+ ```ruby
17
+ require "process_bot"
18
+ install_plugin ProcessBot::Capistrano::Sidekiq
19
+ install_plugin ProcessBot::Capistrano::Puma
20
+ ```
20
21
 
21
- $ gem install process_bot
22
+ Add to your `deploy.rb`:
23
+ ```ruby
24
+ after "deploy:starting", "process_bot:sidekiq:graceful"
25
+ after "deploy:published", "process_bot:sidekiq:start"
26
+ after "deploy:failed", "process_bot:sidekiq:start"
27
+ ```
22
28
 
23
29
  ## Usage
24
30
 
25
- TODO: Write usage instructions here
31
+ Run commands in the command line like this:
32
+
33
+ ```bash
34
+ cap production process_bot:sidekiq:graceful
35
+ ```
26
36
 
27
37
  ## Development
28
38
 
data/exe/process_bot CHANGED
@@ -18,6 +18,12 @@ while argv_i < ARGV.length
18
18
 
19
19
  if (match = arg.match(/\A--(.+)\Z/))
20
20
  key = match[1].tr("-", "_").to_sym
21
+
22
+ if key == :path
23
+ puts "Path: #{File.realpath("#{__dir__}/..")}"
24
+ exit
25
+ end
26
+
21
27
  argv_i += 1
22
28
  value = ARGV.fetch(argv_i)
23
29
 
@@ -29,8 +35,6 @@ while argv_i < ARGV.length
29
35
  argv_i += 1
30
36
  end
31
37
 
32
- pp options.options
33
-
34
38
  ProcessBot::Process
35
39
  .new(options)
36
40
  .execute!
@@ -14,62 +14,51 @@ namespace :load do
14
14
  set :sidekiq_options_per_process, nil
15
15
  set :sidekiq_user, nil
16
16
  # Rbenv, Chruby, and RVM integration
17
- set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a.concat(%w[sidekiq sidekiqctl])
18
- set :rvm_map_bins, fetch(:rvm_map_bins).to_a.concat(%w[sidekiq sidekiqctl])
19
- set :chruby_map_bins, fetch(:chruby_map_bins).to_a.concat(%w[sidekiq sidekiqctl])
17
+ set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a + ["sidekiq", "sidekiqctl"]
18
+ set :rvm_map_bins, fetch(:rvm_map_bins).to_a + ["sidekiq", "sidekiqctl"]
19
+ set :chruby_map_bins, fetch(:chruby_map_bins).to_a + ["sidekiq", "sidekiqctl"]
20
20
  # Bundler integration
21
- set :bundle_bins, fetch(:bundle_bins).to_a.concat(%w[sidekiq sidekiqctl])
21
+ set :bundle_bins, fetch(:bundle_bins).to_a + ["sidekiq", "sidekiqctl"]
22
22
  end
23
23
  end
24
24
 
25
25
  namespace :process_bot do
26
26
  namespace :sidekiq do
27
- desc "Quiet sidekiq (stop fetching new tasks from Redis)"
28
- task :quiet do
27
+ desc "Stop Sidekiq and ProcessBot gracefully (stop fetching new tasks from Redis and then quit when nothing is running)"
28
+ task :graceful do
29
29
  on roles fetch(:sidekiq_roles) do |role|
30
30
  git_plugin.switch_user(role) do
31
- git_plugin.running_sidekiq_processes.each do |sidekiq_process|
32
- git_plugin.stop_sidekiq(pid: sidekiq_process.fetch(:pid), signal: "TSTP")
31
+ git_plugin.running_process_bot_processes.each do |process_bot_process|
32
+ git_plugin.process_bot_command(process_bot_process, :graceful)
33
33
  end
34
34
  end
35
35
  end
36
36
  end
37
37
 
38
- desc "Stop Sidekiq (graceful shutdown within timeout, put unfinished tasks back to Redis)"
38
+ desc "Stop Sidekiq and ProcessBot (graceful shutdown within timeout, put unfinished tasks back to Redis)"
39
39
  task :stop do
40
40
  on roles fetch(:sidekiq_roles) do |role|
41
41
  git_plugin.switch_user(role) do
42
- git_plugin.running_sidekiq_processes.each do |sidekiq_process|
43
- git_plugin.stop_sidekiq(pid: sidekiq_process.fetch(:pid), signal: "TERM")
42
+ git_plugin.running_process_bot_processes.each do |process_bot_data|
43
+ git_plugin.process_bot_command(process_bot_data, :stop)
44
44
  end
45
45
  end
46
46
  end
47
47
  end
48
48
 
49
- desc "Stops Sidekiq after a set amount of time"
50
- task :stop_after_time do
51
- on roles fetch(:sidekiq_roles) do |role|
52
- git_plugin.switch_user(role) do
53
- git_plugin.running_sidekiq_processes.each do |sidekiq_process|
54
- git_plugin.stop_sidekiq_after_time(pid: sidekiq_process.fetch(:pid), signal: "TERM")
55
- end
56
- end
57
- end
58
- end
59
-
60
- desc "Start sidekiq"
49
+ desc "Start Sidekiq and ProcessBot"
61
50
  task :start do
62
51
  on roles fetch(:sidekiq_roles) do |role|
63
52
  git_plugin.switch_user(role) do
64
53
  fetch(:sidekiq_processes).times do |idx|
65
- puts "Starting Sidekiq #{idx}"
54
+ puts "Starting Sidekiq with ProcessBot #{idx}"
66
55
  git_plugin.start_sidekiq(idx)
67
56
  end
68
57
  end
69
58
  end
70
59
  end
71
60
 
72
- desc "Restart sidekiq"
61
+ desc "Restart Sidekiq and ProcessBot"
73
62
  task :restart do
74
63
  invoke! "process_bot:sidekiq:stop"
75
64
  invoke! "process_bot:sidekiq:start"
@@ -1,5 +1,3 @@
1
- require_relative "sidekiq_helpers"
2
-
3
1
  class ProcessBot::Capistrano::Sidekiq < Capistrano::Plugin
4
2
  include ProcessBot::Capistrano::SidekiqHelpers
5
3
 
@@ -1,4 +1,6 @@
1
- module ProcessBot::Capistrano::SidekiqHelpers
1
+ require "json"
2
+
3
+ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLength
2
4
  def sidekiq_require
3
5
  "--require #{fetch(:sidekiq_require)}" if fetch(:sidekiq_require)
4
6
  end
@@ -30,41 +32,62 @@ module ProcessBot::Capistrano::SidekiqHelpers
30
32
  end
31
33
  end
32
34
 
33
- VALID_SIGNALS = ["TERM", "TSTP"].freeze
34
- def stop_sidekiq(pid:, signal:)
35
- raise "Invalid PID: #{pid}" unless pid.to_s.match?(/\A\d+\Z/)
36
- raise "Invalid signal: #{signal}" unless VALID_SIGNALS.include?(signal)
35
+ def process_bot_command(process_bot_data, command) # rubocop:disable Metrics/AbcSize
36
+ raise "No port in process bot data? #{process_bot_data}" unless process_bot_data["port"]
37
37
 
38
- backend.execute "kill -#{signal} #{pid}"
39
- end
38
+ mode = "exec"
39
+
40
+ if mode == "runner"
41
+ args = {command: command, port: process_bot_data.fetch("port")}
42
+
43
+ if command == :graceful && !fetch(:process_bot_wait_for_gracefully_stopped).nil?
44
+ args["wait_for_gracefully_stopped"] = fetch(:process_bot_wait_for_gracefully_stopped)
45
+ end
40
46
 
41
- def stop_sidekiq_after_time(pid:, signal:)
42
- raise "Invalid PID: #{pid}" unless pid.to_s.match?(/\A\d+\Z/)
43
- raise "Invalid signal: #{signal}" unless VALID_SIGNALS.include?(signal)
47
+ escaped_args = JSON.generate(args).gsub("\"", "\\\"")
48
+ rails_runner_command = "require 'process_bot'; ProcessBot::Process.new(ProcessBot::Options.from_args(#{escaped_args})).execute!"
44
49
 
45
- time = ENV["STOP_AFTER_TIME"] || fetch(:sidekiq_stop_after_time)
46
- raise "Invalid time: #{time}" unless time.to_s.match?(/\A\d+\Z/)
50
+ backend_command = "cd #{release_path} && " \
51
+ "#{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec rails runner \"#{rails_runner_command}\""
52
+ elsif mode == "exec"
53
+ backend_command = "cd #{release_path} && " \
54
+ "#{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec process_bot " \
55
+ "--command #{command} " \
56
+ "--port #{process_bot_data.fetch("port")}"
57
+
58
+ if command == :graceful && !fetch(:process_bot_wait_for_gracefully_stopped).nil?
59
+ backend_command << " --wait-for-gracefully-stopped #{fetch(:process_bot_wait_for_gracefully_stopped)}"
60
+ end
61
+ else
62
+ raise "Unknown mode: #{mode}"
63
+ end
47
64
 
48
- backend.execute "screen -dmS stopsidekiq#{pid} bash -c \"sleep #{time} && kill -#{signal} #{pid}\""
65
+ backend.execute backend_command
49
66
  end
50
67
 
51
- def running_sidekiq_processes
68
+ def running_process_bot_processes
52
69
  sidekiq_app_name = fetch(:sidekiq_app_name, fetch(:application))
53
70
  raise "No :sidekiq_app_name was set" unless sidekiq_app_name
54
71
 
55
72
  begin
56
- processes_output = backend.capture("ps a | egrep 'sidekiq ([0-9]+\.[0-9]+\.[0-9]+) #{Regexp.escape(sidekiq_app_name)}'")
73
+ processes_output = backend.capture("ps a | grep ProcessBot | grep sidekiq | grep -v '/usr/bin/SCREEN' | grep '#{Regexp.escape(sidekiq_app_name)}'")
57
74
  rescue SSHKit::Command::Failed
58
75
  # Fails when output is empty (when no processes found through grep)
59
- puts "No Sidekiq processes found"
76
+ puts "No ProcessBot Sidekiq processes found"
60
77
  return []
61
78
  end
62
79
 
80
+ parse_process_bot_process_from_ps(processes_output)
81
+ end
82
+
83
+ def parse_process_bot_process_from_ps(processes_output)
63
84
  processes = []
64
- processes_output.scan(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/).each do |process_output|
65
- sidekiq_pid = process_output[0]
85
+ processes_output.scan(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+ProcessBot (\{([^\n]+?)\})$/).each do |process_output|
86
+ process_bot_data = JSON.parse(process_output[4])
87
+ process_bot_pid = process_output[0]
88
+ process_bot_data["process_bot_pid"] = process_bot_pid
66
89
 
67
- processes << {pid: sidekiq_pid}
90
+ processes << process_bot_data
68
91
  end
69
92
 
70
93
  processes
@@ -86,7 +109,7 @@ module ProcessBot::Capistrano::SidekiqHelpers
86
109
  backend.capture(:echo, SSHKit.config.command_map[:bundle]).strip
87
110
  end
88
111
 
89
- def start_sidekiq(idx = 0) # rubocop:disable Metrics/AbcSize
112
+ def start_sidekiq(idx = 0) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
113
  releases = backend.capture(:ls, "-x", releases_path).split
91
114
  releases << release_timestamp.to_s if release_timestamp
92
115
  releases.uniq
@@ -95,13 +118,19 @@ module ProcessBot::Capistrano::SidekiqHelpers
95
118
  raise "Invalid release timestamp: #{release_timestamp}" unless latest_release_version
96
119
 
97
120
  args = [
121
+ "--command", "start",
98
122
  "--id", "sidekiq-#{latest_release_version}-#{idx}",
123
+ "--application", fetch(:sidekiq_app_name, fetch(:application)),
99
124
  "--handler", "sidekiq",
100
125
  "--bundle-prefix", SSHKit.config.command_map.prefix[:bundle].join(" "),
101
126
  "--sidekiq-environment", fetch(:sidekiq_env),
102
- "--port", 7050 + idx
127
+ "--port", idx + 7050,
128
+ "--release-path", release_path
103
129
  ]
104
- args += ["--log-file-path", fetch(:sidekiq_log)] if fetch(:sidekiq_log)
130
+
131
+ # Use screen for logging everything which is why this is disabled
132
+ # args += ["--log-file-path", fetch(:sidekiq_log)] if fetch(:sidekiq_log)
133
+
105
134
  args += ["--sidekiq-require", fetch(:sidekiq_require)] if fetch(:sidekiq_require)
106
135
  args += ["--sidekiq-tag", fetch(:sidekiq_tag)] if fetch(:sidekiq_tag)
107
136
  args += ["--sidekiq-queues", Array(fetch(:sidekiq_queue)).join(",")] if fetch(:sidekiq_queue)
@@ -112,13 +141,18 @@ module ProcessBot::Capistrano::SidekiqHelpers
112
141
  end
113
142
  args += fetch(:sidekiq_options) if fetch(:sidekiq_options)
114
143
 
115
- screen_args = ["-dmS sidekiq-#{idx}-#{latest_release_version}"]
116
- screen_args << "-L -Logfile #{fetch(:sidekiq_log)}" if fetch(:sidekiq_log)
144
+ screen_args = ["-dmS process-bot--sidekiq--#{idx}-#{latest_release_version}"]
145
+
146
+ if (process_bot_sidekiq_log = fetch(:process_bot_sidekig_log))
147
+ screen_args << "-L -Logfile #{process_bot_sidekiq_log}_#{latest_release_version}_#{idx}.log"
148
+ elsif fetch(:sidekiq_log)
149
+ screen_args << "-L -Logfile #{fetch(:sidekiq_log)}"
150
+ end
117
151
 
118
152
  process_bot_args = args.compact.map { |arg| "\"#{arg}\"" }
119
153
 
120
154
  command = "/usr/bin/screen #{screen_args.join(" ")} " \
121
- "bash -c 'cd #{release_path} && #{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec process_bot #{process_bot_args.join(" ")}'"
155
+ "bash -c 'cd #{release_path} && exec #{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec process_bot #{process_bot_args.join(" ")}'"
122
156
 
123
157
  puts "WARNING: A known bug prevents Sidekiq from starting when pty is set (which it is)" if fetch(:pty)
124
158
  puts "ProcessBot Sidekiq command: #{command}"
@@ -1,4 +1,5 @@
1
1
  class ProcessBot::Capistrano
2
2
  autoload :Puma, "#{__dir__}/capistrano/puma"
3
3
  autoload :Sidekiq, "#{__dir__}/capistrano/sidekiq"
4
+ autoload :SidekiqHelpers, "#{__dir__}/capistrano/sidekiq_helpers"
4
5
  end
@@ -0,0 +1,41 @@
1
+ require "socket"
2
+
3
+ class ProcessBot::ClientSocket
4
+ attr_reader :options
5
+
6
+ def initialize(options:)
7
+ @options = options
8
+ end
9
+
10
+ def client
11
+ @client ||= Socket.tcp("localhost", options.fetch(:port).to_i, connect_timeout: 2)
12
+ end
13
+
14
+ def close
15
+ client.close
16
+ end
17
+
18
+ def logger
19
+ @logger ||= ProcessBot::Logger.new(options: options)
20
+ end
21
+
22
+ def send_command(data) # rubocop:disable Metrics/AbcSize
23
+ logger.log "Sending: #{data}"
24
+ client.puts(JSON.generate(data))
25
+ response_raw = client.gets
26
+
27
+ # Happens if process is interrupted
28
+ return :nil if response_raw.nil?
29
+
30
+ response = JSON.parse(response_raw)
31
+
32
+ return :success if response.fetch("type") == "success"
33
+
34
+ if response.fetch("type") == "error"
35
+ error = RuntimeError.new("Command raised an error: #{response.fetch("message")}")
36
+ error.set_backtrace(response.fetch("backtrace") + Thread.current.backtrace)
37
+
38
+ raise error
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,5 @@
1
+ require "socket"
2
+
1
3
  class ProcessBot::ControlSocket
2
4
  attr_reader :options, :process, :server
3
5
 
@@ -6,14 +8,25 @@ class ProcessBot::ControlSocket
6
8
  @process = process
7
9
  end
8
10
 
11
+ def logger
12
+ @logger ||= ProcessBot::Logger.new(options: options)
13
+ end
14
+
9
15
  def port
10
16
  options.fetch(:port).to_i
11
17
  end
12
18
 
13
19
  def start
14
- require "socket"
20
+ @server = TCPServer.new("localhost", port)
21
+ run_client_loop
15
22
 
16
- @server = TCPServer.new(port)
23
+ logger.logs "TCPServer started"
24
+
25
+ options.events.call(:on_socket_opened, port: port)
26
+ end
27
+
28
+ def stop
29
+ server.close
17
30
  end
18
31
 
19
32
  def run_client_loop
@@ -26,14 +39,48 @@ class ProcessBot::ControlSocket
26
39
  end
27
40
  end
28
41
 
29
- def handle_client(client)
30
- command = JSON.parse(client.gets)
31
- type = command.fetch("type")
42
+ def handle_client(client) # rubocop:disable Metrics/AbcSize
43
+ loop do
44
+ data = client.gets
45
+ break if data.nil? # Client disconnected
46
+
47
+ command = JSON.parse(data)
48
+ command_type = command.fetch("command")
49
+
50
+ if command_type == "graceful" || command_type == "stop"
51
+ begin
52
+ command_options = if command["options"]
53
+ symbolize_keys(command.fetch("options"))
54
+ else
55
+ {}
56
+ end
57
+
58
+ logger.logs "Command #{command_type} with options #{command_options}"
32
59
 
33
- if type == "stop"
34
- process.stop
35
- else
36
- client.puts(JSON.generate(type: "error", message: "Unknown type: #{type}"))
60
+ process.__send__(command_type, **command_options)
61
+ client.puts(JSON.generate(type: "success"))
62
+ rescue => e # rubocop:disable Style/RescueStandardError
63
+ logger.logs e.message, type: :stderr
64
+ logger.logs e.backtrace, type: :stderr
65
+
66
+ client.puts(JSON.generate(type: "error", message: e.message, backtrace: e.backtrace))
67
+
68
+ raise e
69
+ end
70
+ else
71
+ client.puts(JSON.generate(type: "error", message: "Unknown command: #{command_type}", backtrace: Thread.current.backtrace))
72
+ end
73
+ end
74
+ end
75
+
76
+ def symbolize_keys(hash)
77
+ new_hash = {}
78
+ hash.each do |key, value|
79
+ next if key == "port"
80
+
81
+ new_hash[key.to_sym] = value
37
82
  end
83
+
84
+ new_hash
38
85
  end
39
86
  end
@@ -1,22 +1,38 @@
1
1
  class ProcessBot::Logger
2
- attr_reader :fp_log, :options
2
+ attr_reader :options
3
3
 
4
4
  def initialize(options:)
5
5
  @options = options
6
+ end
7
+
8
+ def log(output, type: :stdout)
9
+ if type == :stdout || (type == :debug && options[:debug])
10
+ $stdout.print output
11
+ elsif type == :stderr
12
+ $stderr.print output
13
+ else
14
+ raise "Unknown type: #{type}"
15
+ end
16
+
17
+ return unless log_to_file?
6
18
 
7
- open_file
19
+ fp_log.write(output)
20
+ fp_log.flush
8
21
  end
9
22
 
10
- def log(output)
11
- fp_log&.write(output)
12
- fp_log&.flush
23
+ def logs(output, **args)
24
+ log("#{output}\n", **args)
13
25
  end
14
26
 
15
27
  def log_file_path
16
28
  options.fetch(:log_file_path)
17
29
  end
18
30
 
19
- def open_file
20
- @fp_log = File.open(log_file_path, "a")
31
+ def log_to_file?
32
+ options.present?(:log_file_path)
33
+ end
34
+
35
+ def fp_log
36
+ @fp_log ||= File.open(log_file_path, "a") if log_to_file?
21
37
  end
22
38
  end
@@ -1,12 +1,73 @@
1
1
  class ProcessBot::Options
2
2
  attr_reader :options
3
3
 
4
+ def self.from_args(args)
5
+ options = ProcessBot::Options.new
6
+
7
+ args.each do |key, value|
8
+ options.set(key, value)
9
+ end
10
+
11
+ options
12
+ end
13
+
4
14
  def initialize(options = {})
5
15
  @options = options
6
16
  end
7
17
 
8
- def fetch(*args, **opts, &blk)
9
- options.fetch(*args, **opts, &blk)
18
+ def [](key)
19
+ options[key]
20
+ end
21
+
22
+ def events
23
+ @events ||= begin
24
+ require "knjrbfw"
25
+
26
+ event_handler = ::Knj::Event_handler.new
27
+ event_handler.add_event(name: :on_process_started)
28
+ event_handler.add_event(name: :on_socket_opened)
29
+ event_handler
30
+ end
31
+ end
32
+
33
+ def fetch(...)
34
+ options.fetch(...)
35
+ end
36
+
37
+ def application_basename
38
+ @application_basename ||= begin
39
+ app_path_parts = release_path.split("/")
40
+
41
+ if release_path.include?("/releases/")
42
+ app_path_parts.pop(2)
43
+ elsif release_path.end_with?("/current")
44
+ app_path_parts.pop
45
+ end
46
+
47
+ app_path_parts.last
48
+ end
49
+ end
50
+
51
+ def possible_process_titles
52
+ possible_names = []
53
+
54
+ # Sidekiq name can by current Rails root base name
55
+ possible_names << application_basename
56
+
57
+ # Sidekiq name can be set tag name (but we wrongly read application for some reason?)
58
+ possible_names << options.fetch(:application)
59
+
60
+ possible_names
61
+ end
62
+
63
+ def possible_process_titles_joined_regex
64
+ possible_process_titles_joined_regex = ""
65
+ possible_process_titles.each_with_index do |possible_name, index|
66
+ possible_process_titles_joined_regex << "|" if index >= 1
67
+ possible_process_titles_joined_regex << Regexp.escape(possible_name)
68
+ end
69
+
70
+ possible_process_titles_joined_regex
10
71
  end
11
72
 
12
73
  def present?(key)
@@ -15,6 +76,10 @@ class ProcessBot::Options
15
76
  false
16
77
  end
17
78
 
79
+ def release_path
80
+ @release_path ||= fetch(:release_path)
81
+ end
82
+
18
83
  def set(key, value)
19
84
  options[key] = value
20
85
  end
@@ -1,16 +1,38 @@
1
1
  class ProcessBot::Process::Handlers::Sidekiq
2
- attr_reader :options
2
+ attr_reader :options, :process
3
3
 
4
- def initialize(options)
5
- @options = options
4
+ def initialize(process)
5
+ @process = process
6
+ @options = process.options
7
+ end
8
+
9
+ def current_pid
10
+ process.current_pid
11
+ end
12
+
13
+ def daemonize
14
+ logger.logs "Daemonize!"
15
+
16
+ pid = Process.fork do
17
+ Process.daemon
18
+ yield
19
+ end
6
20
 
7
- set_defaults
21
+ Process.detach(pid) if pid
22
+ end
23
+
24
+ def false_value?(value)
25
+ !value || value == "false"
8
26
  end
9
27
 
10
28
  def fetch(*args, **opts)
11
29
  options.fetch(*args, **opts)
12
30
  end
13
31
 
32
+ def logger
33
+ @logger ||= ProcessBot::Logger.new(options: options)
34
+ end
35
+
14
36
  def set_option(key, value)
15
37
  raise "Unknown option for Sidekiq handler: #{key}" unless options.key?(key)
16
38
 
@@ -21,35 +43,95 @@ class ProcessBot::Process::Handlers::Sidekiq
21
43
  options.set(*args, **opts)
22
44
  end
23
45
 
24
- def set_defaults
25
- set :sidekiq_default_hooks, true
26
- set :sidekiq_pid, -> { File.join(shared_path, "tmp", "pids", "sidekiq.pid") }
27
- set :sidekiq_timeout, 10
28
- set :sidekiq_roles, fetch(:sidekiq_role, :app)
29
- set :sidekiq_processes, 1
30
- set :sidekiq_options_per_process, nil
31
- end
32
-
33
- def command # rubocop:disable Metrics/AbcSize
46
+ def start_command # rubocop:disable Metrics/AbcSize
34
47
  args = []
35
48
 
36
49
  options.options.each do |key, value|
37
- if (match = key.to_s.match(/\Asidekiq-(.+)\Z/))
38
- sidekiq_key = match[1]
39
-
40
- if sidekiq_key == "queue"
41
- value.split(",").each do |queue|
42
- args.push "--queue #{value}"
43
- end
44
- else
45
- args.push "--#{sidekiq_key} #{value}"
50
+ next unless (match = key.to_s.match(/\Asidekiq_(.+)\Z/))
51
+
52
+ sidekiq_key = match[1]
53
+
54
+ if sidekiq_key == "queue"
55
+ value.split(",").each do |queue|
56
+ args.push "--queue #{queue}"
46
57
  end
58
+ else
59
+ args.push "--#{sidekiq_key} #{value}"
47
60
  end
48
61
  end
49
62
 
50
- command = ""
63
+ command = "bash -c 'cd #{options.fetch(:release_path)} && exec "
51
64
  command << "#{options.fetch(:bundle_prefix)} " if options.present?(:bundle_prefix)
52
65
  command << "bundle exec sidekiq #{args.compact.join(' ')}"
66
+ command << "'"
53
67
  command
54
68
  end
69
+
70
+ def graceful(**args)
71
+ wait_for_gracefully_stopped = args.fetch(:wait_for_gracefully_stopped, true)
72
+ process.set_stopped
73
+
74
+ unless current_pid
75
+ warn "Sidekiq not running with a PID"
76
+ return
77
+ end
78
+
79
+ Process.kill("TSTP", current_pid)
80
+
81
+ if false_value?(wait_for_gracefully_stopped)
82
+ logger.logs "Dont wait for gracefully stopped - doing that in fork..."
83
+
84
+ daemonize do
85
+ wait_for_no_jobs_and_stop_sidekiq
86
+ exit
87
+ end
88
+ else
89
+ logger.logs "Wait for gracefully stopped..."
90
+ wait_for_no_jobs_and_stop_sidekiq
91
+ end
92
+ end
93
+
94
+ def stop(**_args)
95
+ process.set_stopped
96
+
97
+ unless current_pid
98
+ warn "#{handler_name} not running with a PID"
99
+ return
100
+ end
101
+
102
+ Process.kill("TERM", current_pid)
103
+ end
104
+
105
+ def wait_for_no_jobs # rubocop:disable Metrics/AbcSize
106
+ loop do
107
+ found_process = false
108
+
109
+ Knj::Unix_proc.list("grep" => current_pid) do |process|
110
+ process_command = process.data.fetch("cmd")
111
+ process_pid = process.data.fetch("pid").to_i
112
+ next unless process_pid == current_pid
113
+
114
+ found_process = true
115
+ sidekiq_regex = /\Asidekiq (\d+).(\d+).(\d+) (#{options.possible_process_titles_joined_regex}) \[(\d+) of (\d+)(\]|) (.+?)(\]|)\Z/
116
+ match = process_command.match(sidekiq_regex)
117
+ raise "Couldnt match Sidekiq command: #{process_command} with Sidekiq regex: #{sidekiq_regex}" unless match
118
+
119
+ running_jobs = match[5].to_i
120
+
121
+ logger.logs "running_jobs: #{running_jobs}"
122
+
123
+ return if running_jobs.zero? # rubocop:disable Lint/NonLocalExitFromIterator
124
+ end
125
+
126
+ raise "Couldn't find running process with PID #{current_pid}" unless found_process
127
+
128
+ sleep 1
129
+ end
130
+ end
131
+
132
+ def wait_for_no_jobs_and_stop_sidekiq
133
+ logger.logs "Wait for no jobs and Stop sidekiq"
134
+ wait_for_no_jobs
135
+ stop
136
+ end
55
137
  end
@@ -1,5 +1,7 @@
1
+ require "knjrbfw"
2
+
1
3
  class ProcessBot::Process::Runner
2
- attr_reader :command, :exit_status, :logger, :monitor, :options, :stop_time
4
+ attr_reader :command, :exit_status, :logger, :monitor, :options, :pid, :stop_time, :subprocess_pid
3
5
 
4
6
  def initialize(command:, logger:, options:)
5
7
  @command = command
@@ -9,8 +11,12 @@ class ProcessBot::Process::Runner
9
11
  @output = []
10
12
  end
11
13
 
12
- def output(output:, type:) # rubocop:disable Lint/UnusedMethodArgument
13
- logger.log(output)
14
+ def output(output:, type:)
15
+ logger.log(output, type: type)
16
+ end
17
+
18
+ def running?
19
+ !stop_time
14
20
  end
15
21
 
16
22
  def run # rubocop:disable Metrics/AbcSize
@@ -20,8 +26,8 @@ class ProcessBot::Process::Runner
20
26
  require "pty"
21
27
 
22
28
  PTY.spawn(command, err: stderr_writer.fileno) do |stdout, _stdin, pid|
23
- @pid = pid
24
- logger.log "Command running with PID #{pid}: #{command}"
29
+ @subprocess_pid = pid
30
+ logger.logs "Command running with PID #{pid}: #{command}"
25
31
 
26
32
  stdout_reader_thread = Thread.new do
27
33
  stdout.each_char do |chunk|
@@ -32,7 +38,7 @@ class ProcessBot::Process::Runner
32
38
  rescue Errno::EIO
33
39
  # Process done
34
40
  ensure
35
- status = Process::Status.wait(@pid, 0)
41
+ status = Process::Status.wait(subprocess_pid, 0)
36
42
 
37
43
  @exit_status = status.exitstatus
38
44
  stderr_writer.close
@@ -46,10 +52,56 @@ class ProcessBot::Process::Runner
46
52
  end
47
53
  end
48
54
 
55
+ find_sidekiq_pid
56
+
49
57
  stdout_reader_thread.join
50
58
  stderr_reader_thread.join
51
59
 
52
60
  @stop_time = Time.new
53
61
  end
54
62
  end
63
+
64
+ def subprocess_pgid
65
+ @subprocess_pgid ||= Process.getpgid(subprocess_pid)
66
+ end
67
+
68
+ def sidekiq_app_name
69
+ options.fetch(:application)
70
+ end
71
+
72
+ def find_sidekiq_pid # rubocop:disable Metrics/AbcSize
73
+ Thread.new do
74
+ while running? && !pid
75
+ Knj::Unix_proc.list("grep" => "sidekiq") do |process|
76
+ cmd = process.data.fetch("cmd")
77
+
78
+ if /sidekiq ([0-9]+\.[0-9]+\.[0-9]+) (#{options.possible_process_titles_joined_regex})/.match?(cmd)
79
+ sidekiq_pid = process.data.fetch("pid").to_i
80
+
81
+ begin
82
+ sidekiq_pgid = Process.getpgid(sidekiq_pid)
83
+ rescue Errno::ESRCH
84
+ # Process no longer running
85
+ end
86
+
87
+ if subprocess_pgid == sidekiq_pgid
88
+ puts "FOUND PID: #{sidekiq_pid}"
89
+
90
+ @pid = sidekiq_pid
91
+ options.events.call(:on_process_started, pid: pid)
92
+
93
+ break
94
+ else
95
+ puts "PGID didn't match - Sidekiq: #{sidekiq_pgid} Own: #{subprocess_pgid}"
96
+ end
97
+ end
98
+ end
99
+
100
+ unless pid
101
+ puts "Waiting 1 second before trying to find Sidekiq PID again"
102
+ sleep 1
103
+ end
104
+ end
105
+ end
106
+ end
55
107
  end
@@ -1,35 +1,79 @@
1
+ require "forwardable"
2
+ require "json"
3
+ require "string-cases"
4
+
1
5
  class ProcessBot::Process
6
+ extend Forwardable
7
+
8
+ def_delegator :handler_instance, :graceful
9
+ def_delegator :handler_instance, :stop
10
+
2
11
  autoload :Handlers, "#{__dir__}/process/handlers"
3
12
  autoload :Runner, "#{__dir__}/process/runner"
4
13
 
5
- attr_reader :options, :stopped
14
+ attr_reader :current_pid, :current_process_title, :options, :port, :stopped
6
15
 
7
16
  def initialize(options)
8
17
  @options = options
9
18
  @stopped = false
10
- end
11
19
 
12
- def logger
13
- @logger ||= ProcessBot::Logger.new(options: options)
20
+ options.events.connect(:on_process_started, &method(:on_process_started)) # rubocop:disable Performance/MethodObjectAsBlock
21
+ options.events.connect(:on_socket_opened, &method(:on_socket_opened)) # rubocop:disable Performance/MethodObjectAsBlock
22
+
23
+ logger.logs("ProcessBot 1 - Options: #{options.options}")
14
24
  end
15
25
 
16
- def start_control_socket
17
- @control_socket = ProcessBot::ControlSocket.new(options: options, process: self)
18
- @control_socket.start
26
+ def execute!
27
+ command = options.fetch(:command)
28
+
29
+ if command == "start"
30
+ start
31
+ elsif command == "graceful" || command == "stop"
32
+ client.send_command(command: command, options: options.options)
33
+ else
34
+ raise "Unknown command: #{command}"
35
+ end
19
36
  end
20
37
 
21
- def stop
22
- @stopped = true
38
+ def client
39
+ @client ||= ProcessBot::ClientSocket.new(options: options)
23
40
  end
24
41
 
25
42
  def handler_class
26
43
  @handler_class ||= begin
27
- require_relative "process/handlers/#{options.fetch(:handler)}"
28
- ProcessBot::Process::Handlers.const_get(StringCases.snake_to_camel(options.fetch(:handler)))
44
+ require_relative "process/handlers/#{handler_name}"
45
+ ProcessBot::Process::Handlers.const_get(StringCases.snake_to_camel(handler_name))
29
46
  end
30
47
  end
31
48
 
32
- def execute!
49
+ def handler_instance
50
+ @handler_instance ||= handler_class.new(self)
51
+ end
52
+
53
+ def handler_name
54
+ @handler_name ||= options.fetch(:handler)
55
+ end
56
+
57
+ def logger
58
+ @logger ||= ProcessBot::Logger.new(options: options)
59
+ end
60
+
61
+ def on_process_started(_event_name, pid:)
62
+ @current_pid = pid
63
+ update_process_title
64
+ end
65
+
66
+ def on_socket_opened(_event_name, port:)
67
+ @port = port
68
+ update_process_title
69
+ end
70
+
71
+ def start_control_socket
72
+ @control_socket = ProcessBot::ControlSocket.new(options: options, process: self)
73
+ @control_socket.start
74
+ end
75
+
76
+ def start
33
77
  start_control_socket
34
78
 
35
79
  loop do
@@ -38,15 +82,24 @@ class ProcessBot::Process
38
82
  if stopped
39
83
  break
40
84
  else
41
- puts "Process stopped - starting again after 1 sec"
85
+ logger.logs "Process stopped - starting again after 1 sec"
42
86
  sleep 1
43
87
  end
44
88
  end
45
89
  end
46
90
 
91
+ def set_stopped
92
+ @stopped = true
93
+ end
94
+
47
95
  def run
48
- handler_instance = handler_class.new(options)
49
- runner = ProcessBot::Process::Runner.new(command: handler_instance.command, logger: logger, options: options)
96
+ runner = ProcessBot::Process::Runner.new(command: handler_instance.start_command, logger: logger, options: options)
50
97
  runner.run
51
98
  end
99
+
100
+ def update_process_title
101
+ process_args = {application: options[:application], handler: handler_name, id: options[:id], pid: current_pid, port: port}
102
+ @current_process_title = "ProcessBot #{JSON.generate(process_args)}"
103
+ Process.setproctitle(current_process_title)
104
+ end
52
105
  end
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.4".freeze
3
3
  end
data/lib/process_bot.rb CHANGED
@@ -4,6 +4,7 @@ module ProcessBot
4
4
  class Error < StandardError; end
5
5
 
6
6
  autoload :Capistrano, "#{__dir__}/process_bot/capistrano"
7
+ autoload :ClientSocket, "#{__dir__}/process_bot/client_socket"
7
8
  autoload :ControlSocket, "#{__dir__}/process_bot/control_socket"
8
9
  autoload :Logger, "#{__dir__}/process_bot/logger"
9
10
  autoload :Options, "#{__dir__}/process_bot/options"
data/peak_flow.yml CHANGED
@@ -1,4 +1,20 @@
1
1
  rvm: true
2
- script:
3
- - bundle exec rspec
4
- - bundle exec rubocop
2
+ builds:
3
+ build_1:
4
+ environment:
5
+ RUBY_VERSION: 2.7.8
6
+ name: Ruby 2.7.8
7
+ script:
8
+ - bundle exec rspec
9
+ build_2:
10
+ environment:
11
+ RUBY_VERSION: 3.2.2
12
+ name: Ruby 3.2.2
13
+ script:
14
+ - bundle exec rspec
15
+ build_3:
16
+ environment:
17
+ RUBY_VERSION: 2.7.8
18
+ name: Rubocop
19
+ script:
20
+ - 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.6.0"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
 
@@ -31,15 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- # Uncomment to register a new dependency of your gem
35
- # spec.add_dependency "example-gem", "~> 1.0"
34
+ spec.add_dependency "knjrbfw", ">= 0.0.116"
36
35
 
37
- spec.add_development_dependency "rubocop"
38
- spec.add_development_dependency "rubocop-performance"
39
- spec.add_development_dependency "rubocop-rake"
40
- spec.add_development_dependency "rubocop-rspec"
41
-
42
- # For more information and examples about making a new gem, check out our
43
- # guide at: https://bundler.io/guides/creating_gem.html
44
36
  spec.metadata["rubygems_mfa_required"] = "true"
45
37
  end
metadata CHANGED
@@ -1,71 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-18 00:00:00.000000000 Z
11
+ date: 2023-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rubocop
14
+ name: knjrbfw
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
19
+ version: 0.0.116
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rubocop-performance
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rubocop-rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop-rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
26
+ version: 0.0.116
69
27
  description: Run and control processes.
70
28
  email:
71
29
  - k@spernj.org
@@ -76,6 +34,7 @@ extra_rdoc_files: []
76
34
  files:
77
35
  - ".rspec"
78
36
  - ".rubocop.yml"
37
+ - ".ruby-version"
79
38
  - CHANGELOG.md
80
39
  - Gemfile
81
40
  - Gemfile.lock
@@ -91,6 +50,7 @@ files:
91
50
  - lib/process_bot/capistrano/sidekiq.rake
92
51
  - lib/process_bot/capistrano/sidekiq.rb
93
52
  - lib/process_bot/capistrano/sidekiq_helpers.rb
53
+ - lib/process_bot/client_socket.rb
94
54
  - lib/process_bot/control_socket.rb
95
55
  - lib/process_bot/logger.rb
96
56
  - lib/process_bot/options.rb
@@ -119,14 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
119
79
  requirements:
120
80
  - - ">="
121
81
  - !ruby/object:Gem::Version
122
- version: 2.6.0
82
+ version: 2.7.0
123
83
  required_rubygems_version: !ruby/object:Gem::Requirement
124
84
  requirements:
125
85
  - - ">="
126
86
  - !ruby/object:Gem::Version
127
87
  version: '0'
128
88
  requirements: []
129
- rubygems_version: 3.3.7
89
+ rubygems_version: 3.4.17
130
90
  signing_key:
131
91
  specification_version: 4
132
92
  summary: Run and control processes.