process_bot 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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.