multi_process 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +12 -0
- data/.github/workflows/test.yml +38 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +26 -3
- data/Gemfile +7 -3
- data/README.md +16 -12
- data/Rakefile +3 -1
- data/lib/multi_process/errors.rb +14 -0
- data/lib/multi_process/group.rb +55 -11
- data/lib/multi_process/logger.rb +13 -13
- data/lib/multi_process/loop.rb +2 -0
- data/lib/multi_process/nil_receiver.rb +2 -0
- data/lib/multi_process/process/bundle_exec.rb +4 -2
- data/lib/multi_process/process/rails.rb +9 -13
- data/lib/multi_process/process.rb +38 -12
- data/lib/multi_process/receiver.rb +11 -11
- data/lib/multi_process/string_receiver.rb +3 -1
- data/lib/multi_process/version.rb +4 -2
- data/lib/multi_process.rb +4 -0
- data/multi_process.gemspec +7 -3
- data/renovate.json +6 -0
- data/spec/files/env.rb +2 -1
- data/spec/files/fail.rb +3 -0
- data/spec/files/sleep.rb +1 -0
- data/spec/files/test.rb +1 -0
- data/spec/multi_process/group_spec.rb +31 -0
- data/spec/multi_process/process_spec.rb +17 -0
- data/spec/multi_process_spec.rb +25 -23
- data/spec/spec_helper.rb +3 -1
- metadata +17 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dddbd3442f480abb21f2e64ce86365471a4f04e6832e385f07e9d52a45038e95
|
4
|
+
data.tar.gz: fb7b5bcb5f053cacac6d3c2cbe2ff84ef200622fa06b8c045db6105051807ab2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92843a792257de7057e597f062ac53c690ccddd223dfdcfae94e42917a74cf0b299def05a691b880737cb7e51670ca6b343bf788e743201b312c8a32ce2fb0df
|
7
|
+
data.tar.gz: c3db19666a44e40c06d4f321079bddf8182e5c2169cd616cc4855fbb3a0e90818bbbd27b180f2d3791e1927df7899bcdd5f83dcc0c323a60b4230ebb9c61a222
|
data/.editorconfig
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# vim: ft=yaml
|
2
|
+
|
3
|
+
name: test
|
4
|
+
on: push
|
5
|
+
jobs:
|
6
|
+
rspec:
|
7
|
+
name: ruby-${{ matrix.ruby }}
|
8
|
+
runs-on: ubuntu-20.04
|
9
|
+
|
10
|
+
strategy:
|
11
|
+
fail-fast: false
|
12
|
+
matrix:
|
13
|
+
ruby: ["3.1", "3.0", "2.7"]
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v2
|
17
|
+
- uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: ${{ matrix.ruby }}
|
20
|
+
bundler-cache: True
|
21
|
+
|
22
|
+
- run: bundle exec rspec --color
|
23
|
+
|
24
|
+
rubocop:
|
25
|
+
name: rubocop
|
26
|
+
runs-on: ubuntu-20.04
|
27
|
+
|
28
|
+
steps:
|
29
|
+
- uses: actions/checkout@v2
|
30
|
+
- uses: ruby/setup-ruby@v1
|
31
|
+
with:
|
32
|
+
ruby-version: 3.1
|
33
|
+
bundler-cache: True
|
34
|
+
env:
|
35
|
+
BUNDLE_JOBS: 4
|
36
|
+
BUNDLE_RETRY: 3
|
37
|
+
|
38
|
+
- run: bundle exec rubocop --parallel --color
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,13 +1,36 @@
|
|
1
1
|
# Changelog
|
2
|
+
|
2
3
|
All notable changes to this project will be documented in this file.
|
3
4
|
|
4
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [1.2.0] - 2022-06-03
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- `run!` and `wait!` to raise error if any process exits with an error code != 0
|
14
|
+
|
15
|
+
## [1.1.1] - 2020-12-21
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
|
19
|
+
- Replaced deprecated `#with_clean_env` method
|
20
|
+
|
21
|
+
## [1.1.0] - 2020-11-19
|
22
|
+
|
23
|
+
### Added
|
24
|
+
|
25
|
+
- Add support for IPv6 by using the hostname instead of the loopback IPv4 address (#2)
|
26
|
+
|
9
27
|
## 1.0.0 - 2019-05-13
|
28
|
+
|
10
29
|
### Fixed
|
30
|
+
|
11
31
|
- Possible concurrent hash modification while iterating (#1)
|
12
32
|
|
13
|
-
[
|
33
|
+
[unreleased]: https://github.com/jgraichen/multi_process/compare/v1.2.0...HEAD
|
34
|
+
[1.2.0]: https://github.com/jgraichen/multi_process/compare/v1.1.1...v1.2.0
|
35
|
+
[1.1.1]: https://github.com/jgraichen/multi_process/compare/v1.1.0...v1.1.1
|
36
|
+
[1.1.0]: https://github.com/jgraichen/multi_process/compare/v1.0.0...v1.1.0
|
data/Gemfile
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
gem 'rspec', '>= 3.0.0.beta1'
|
3
|
+
source 'https://rubygems.org'
|
5
4
|
|
6
5
|
# Specify your gem's dependencies in multi_process.gemspec
|
7
6
|
gemspec
|
7
|
+
|
8
|
+
gem 'rake'
|
9
|
+
gem 'rake-release', '~> 1.3'
|
10
|
+
gem 'rspec', '~> 3.11'
|
11
|
+
gem 'rubocop-config', github: 'jgraichen/rubocop-config', ref: 'v9', require: false
|
data/README.md
CHANGED
@@ -1,31 +1,35 @@
|
|
1
1
|
# MultiProcess
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Just experiment.
|
3
|
+
Run multiple processes.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
7
|
Add this line to your application's Gemfile:
|
10
8
|
|
11
|
-
|
9
|
+
```ruby
|
10
|
+
gem 'multi_process'
|
11
|
+
```
|
12
12
|
|
13
13
|
And then execute:
|
14
14
|
|
15
|
-
|
15
|
+
```
|
16
|
+
$ bundle
|
17
|
+
```
|
16
18
|
|
17
19
|
Or install it yourself as:
|
18
20
|
|
19
|
-
|
21
|
+
```
|
22
|
+
$ gem install multi_process
|
23
|
+
```
|
20
24
|
|
21
25
|
## Usage
|
22
26
|
|
23
|
-
```
|
27
|
+
```ruby
|
24
28
|
receiver = MultiProcess::Logger $stdout, $stderr, sys: false
|
25
29
|
group = MultiProcess::Group.new receiver: receiver
|
26
|
-
group << MultiProcess::Process.new %w
|
27
|
-
group << MultiProcess::Process.new %w
|
28
|
-
group << MultiProcess::Process.new %w
|
30
|
+
group << MultiProcess::Process.new %w[ruby test.rb], title: 'rubyA'
|
31
|
+
group << MultiProcess::Process.new %w[ruby test.rb], title: 'rubyB'
|
32
|
+
group << MultiProcess::Process.new %w[ruby test.rb], title: 'rubyC'
|
29
33
|
group.start # Start in background
|
30
34
|
group.run # Block until finished
|
31
35
|
group.wait # Wait until finished
|
@@ -43,7 +47,7 @@ group.stop # Stop processes
|
|
43
47
|
|
44
48
|
## Contributing
|
45
49
|
|
46
|
-
1. Fork it (
|
50
|
+
1. Fork it (http://github.com/jgraichen/multi_process/fork)
|
47
51
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
48
52
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
49
53
|
4. Push to the branch (`git push origin my-new-feature`)
|
@@ -51,7 +55,7 @@ group.stop # Stop processes
|
|
51
55
|
|
52
56
|
## License
|
53
57
|
|
54
|
-
Copyright
|
58
|
+
Copyright © 2019 Jan Graichen
|
55
59
|
|
56
60
|
This program is free software: you can redistribute it and/or modify
|
57
61
|
it under the terms of the GNU General Public License as published by
|
data/Rakefile
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MultiProcess
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ProcessError < Error
|
7
|
+
attr_reader :process
|
8
|
+
|
9
|
+
def initialize(process, *args, **kwargs)
|
10
|
+
@process = process
|
11
|
+
super(*args, **kwargs)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/multi_process/group.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiProcess
|
2
4
|
#
|
3
5
|
# Store and run a group of processes.
|
@@ -24,7 +26,7 @@ module MultiProcess
|
|
24
26
|
#
|
25
27
|
def initialize(receiver: nil, partition: nil)
|
26
28
|
@processes = []
|
27
|
-
@receiver = receiver
|
29
|
+
@receiver = receiver || MultiProcess::Logger.global
|
28
30
|
@partition = partition ? partition.to_i : 0
|
29
31
|
@mutex = Mutex.new
|
30
32
|
end
|
@@ -87,6 +89,21 @@ module MultiProcess
|
|
87
89
|
end
|
88
90
|
end
|
89
91
|
|
92
|
+
# Wait until all process terminated.
|
93
|
+
#
|
94
|
+
# Raise an error if a process exists unsuccessfully.
|
95
|
+
#
|
96
|
+
# @param opts [ Hash ] Options.
|
97
|
+
# @option opts [ Integer ] :timeout Timeout in seconds to wait before raising {Timeout::Error}.
|
98
|
+
#
|
99
|
+
def wait!(timeout: nil)
|
100
|
+
if timeout
|
101
|
+
::Timeout.timeout(timeout) { wait! }
|
102
|
+
else
|
103
|
+
processes.each(&:wait!)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
90
107
|
# Start all process and wait for them to terminate.
|
91
108
|
#
|
92
109
|
# Given options will be passed to {#start} and {#wait}.
|
@@ -96,17 +113,32 @@ module MultiProcess
|
|
96
113
|
# when timeout error is raised.
|
97
114
|
#
|
98
115
|
def run(delay: nil, timeout: nil)
|
99
|
-
if partition
|
100
|
-
|
101
|
-
Thread.new do
|
102
|
-
while (process = next_process)
|
103
|
-
process.run
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end.each(&:join)
|
116
|
+
if partition.positive?
|
117
|
+
run_partition(&:run)
|
107
118
|
else
|
108
|
-
start
|
109
|
-
wait
|
119
|
+
start(delay: delay)
|
120
|
+
wait(timeout: timeout)
|
121
|
+
end
|
122
|
+
ensure
|
123
|
+
stop
|
124
|
+
end
|
125
|
+
|
126
|
+
# Start all process and wait for them to terminate.
|
127
|
+
#
|
128
|
+
# Given options will be passed to {#start} and {#wait}. {#start}
|
129
|
+
# will only be called if partition is zero.
|
130
|
+
#
|
131
|
+
# If timeout is given process will be terminated using {#stop} when
|
132
|
+
# timeout error is raised.
|
133
|
+
#
|
134
|
+
# An error will be raised if any process exits unsuccessfully.
|
135
|
+
#
|
136
|
+
def run!(delay: nil, timeout: nil)
|
137
|
+
if partition.positive?
|
138
|
+
run_partition(&:run!)
|
139
|
+
else
|
140
|
+
start(delay: delay)
|
141
|
+
wait!(timeout: timeout)
|
110
142
|
end
|
111
143
|
ensure
|
112
144
|
stop
|
@@ -151,5 +183,17 @@ module MultiProcess
|
|
151
183
|
processes[@index - 1]
|
152
184
|
end
|
153
185
|
end
|
186
|
+
|
187
|
+
def run_partition
|
188
|
+
Array.new(partition) do
|
189
|
+
Thread.new do
|
190
|
+
Thread.current.report_on_exception = false
|
191
|
+
|
192
|
+
while (process = next_process)
|
193
|
+
yield process
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end.each(&:join)
|
197
|
+
end
|
154
198
|
end
|
155
199
|
end
|
data/lib/multi_process/logger.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiProcess
|
2
4
|
# Can create pipes and multiplex pipe content to put into
|
3
5
|
# given IO objects e.g. multiple output from multiple
|
@@ -12,7 +14,7 @@ module MultiProcess
|
|
12
14
|
# error sources.
|
13
15
|
#
|
14
16
|
def initialize(*args)
|
15
|
-
@opts =
|
17
|
+
@opts = args.last.is_a?(Hash) ? args.pop : {}
|
16
18
|
@out = args[0] || $stdout
|
17
19
|
@err = args[1] || $stderr
|
18
20
|
|
@@ -25,12 +27,12 @@ module MultiProcess
|
|
25
27
|
|
26
28
|
def received(process, name, line)
|
27
29
|
case name
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
when :err, :stderr
|
31
|
+
output process, line, io: @err, delimiter: 'E>'
|
32
|
+
when :out, :stdout
|
33
|
+
output process, line
|
34
|
+
when :sys
|
35
|
+
output(process, line, delimiter: '$>') if @opts[:sys]
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
@@ -49,15 +51,13 @@ module MultiProcess
|
|
49
51
|
private
|
50
52
|
|
51
53
|
def output(process, line, opts = {})
|
52
|
-
opts[:delimiter]
|
54
|
+
opts[:delimiter] ||= ' |'
|
53
55
|
name = if opts[:name]
|
54
56
|
opts[:name].to_s.dup
|
57
|
+
elsif process
|
58
|
+
process.title.to_s.rjust(@colwidth, ' ')
|
55
59
|
else
|
56
|
-
|
57
|
-
process.title.to_s.rjust(@colwidth, ' ')
|
58
|
-
else
|
59
|
-
(' ' * @colwidth)
|
60
|
-
end
|
60
|
+
(' ' * @colwidth)
|
61
61
|
end
|
62
62
|
|
63
63
|
io = opts[:io] || @out
|
data/lib/multi_process/loop.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class MultiProcess::Process
|
2
4
|
# Provides functionality to wrap command in with bundle
|
3
5
|
# execute.
|
4
6
|
#
|
5
7
|
module BundleExec
|
6
8
|
def initialize(*args)
|
7
|
-
opts =
|
8
|
-
super %w
|
9
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
10
|
+
super %w[bundle exec] + args, opts
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class MultiProcess::Process
|
2
4
|
# Provides functionality for a process that is a rails server
|
3
5
|
# process.
|
@@ -12,21 +14,15 @@ class MultiProcess::Process
|
|
12
14
|
#
|
13
15
|
attr_reader :server
|
14
16
|
|
15
|
-
# Port server should be running on.
|
16
|
-
#
|
17
|
-
# Default will be a free port determined when process is created.
|
18
|
-
#
|
19
|
-
attr_reader :port
|
20
|
-
|
21
17
|
def initialize(opts = {})
|
22
18
|
self.server = opts[:server] if opts[:server]
|
23
19
|
self.port = opts[:port] if opts[:port]
|
24
20
|
|
25
|
-
super
|
21
|
+
super(*server_command, opts)
|
26
22
|
end
|
27
23
|
|
28
24
|
def server_command
|
29
|
-
['rails', 'server', server, '--port', port].
|
25
|
+
['rails', 'server', server, '--port', port].compact.map(&:to_s)
|
30
26
|
end
|
31
27
|
|
32
28
|
def server=(server)
|
@@ -34,7 +30,7 @@ class MultiProcess::Process
|
|
34
30
|
end
|
35
31
|
|
36
32
|
def port=(port)
|
37
|
-
@port = port.to_i
|
33
|
+
@port = port.to_i.zero? ? free_port : port.to_i
|
38
34
|
end
|
39
35
|
|
40
36
|
def port
|
@@ -42,9 +38,9 @@ class MultiProcess::Process
|
|
42
38
|
end
|
43
39
|
|
44
40
|
def available?
|
45
|
-
|
41
|
+
raise ArgumentError.new "Cannot check availability for port #{port}." if port.zero?
|
46
42
|
|
47
|
-
TCPSocket.new('
|
43
|
+
TCPSocket.new('localhost', port).close
|
48
44
|
true
|
49
45
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
50
46
|
false
|
@@ -67,10 +63,10 @@ class MultiProcess::Process
|
|
67
63
|
|
68
64
|
def free_port
|
69
65
|
socket = Socket.new(:INET, :STREAM, 0)
|
70
|
-
socket.bind(Addrinfo.tcp('
|
66
|
+
socket.bind(Addrinfo.tcp('localhost', 0))
|
71
67
|
socket.local_address.ip_port
|
72
68
|
ensure
|
73
|
-
socket
|
69
|
+
socket&.close
|
74
70
|
end
|
75
71
|
end
|
76
72
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext/module/delegation'
|
2
4
|
|
3
5
|
module MultiProcess
|
@@ -20,14 +22,14 @@ module MultiProcess
|
|
20
22
|
|
21
23
|
def initialize(*args)
|
22
24
|
args.flatten!
|
23
|
-
opts = (
|
25
|
+
opts = (args.last.is_a?(Hash) ? args.pop : {})
|
24
26
|
|
25
27
|
@title = opts[:title].to_s || args.first.to_s.strip.split(/\s+/, 2)[0]
|
26
|
-
@command = args.map {
|
27
|
-
@childprocess = create_childprocess
|
28
|
+
@command = args.map {|arg| (/\A[\s"']+\z/.match?(arg) ? arg.inspect : arg).gsub '"', '\"' }.join(' ')
|
29
|
+
@childprocess = create_childprocess(*args)
|
28
30
|
|
29
|
-
@env = opts[:env] if
|
30
|
-
@env_clean = opts[:clean_env].nil? ? true :
|
31
|
+
@env = opts[:env] if opts[:env].is_a?(Hash)
|
32
|
+
@env_clean = opts[:clean_env].nil? ? true : !opts[:clean_env].nil?
|
31
33
|
|
32
34
|
self.receiver = opts[:receiver] || MultiProcess::Logger.global
|
33
35
|
|
@@ -54,6 +56,20 @@ module MultiProcess
|
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
59
|
+
# Wait until process finished.
|
60
|
+
#
|
61
|
+
# If no timeout is given it will wait definitely.
|
62
|
+
#
|
63
|
+
# @param opts [Hash] Options.
|
64
|
+
# @option opts [Integer] :timeout Timeout to wait in seconds.
|
65
|
+
#
|
66
|
+
def wait!(opts = {})
|
67
|
+
wait(opts)
|
68
|
+
return if exit_code.zero?
|
69
|
+
|
70
|
+
raise ::MultiProcess::ProcessError.new(self, "Process #{pid} exited with code #{exit_code}")
|
71
|
+
end
|
72
|
+
|
57
73
|
# Start process.
|
58
74
|
#
|
59
75
|
# Started processes will be stopped when ruby VM exists by hooking into
|
@@ -63,7 +79,7 @@ module MultiProcess
|
|
63
79
|
return false if started?
|
64
80
|
|
65
81
|
at_exit { stop }
|
66
|
-
receiver
|
82
|
+
receiver&.message(self, :sys, command)
|
67
83
|
start_childprocess
|
68
84
|
@started = true
|
69
85
|
end
|
@@ -73,7 +89,7 @@ module MultiProcess
|
|
73
89
|
# Will call `ChildProcess#stop`.
|
74
90
|
#
|
75
91
|
def stop(*args)
|
76
|
-
childprocess.stop
|
92
|
+
childprocess.stop(*args) if started?
|
77
93
|
end
|
78
94
|
|
79
95
|
# Check if server is available. What available means can be defined
|
@@ -97,7 +113,7 @@ module MultiProcess
|
|
97
113
|
Timeout.timeout timeout do
|
98
114
|
sleep 0.2 until available?
|
99
115
|
end
|
100
|
-
rescue Timeout::Error
|
116
|
+
rescue Timeout::Error
|
101
117
|
raise Timeout::Error.new "Server #{id.inspect} on port #{port} didn't get up after #{timeout} seconds..."
|
102
118
|
end
|
103
119
|
|
@@ -116,6 +132,15 @@ module MultiProcess
|
|
116
132
|
wait opts
|
117
133
|
end
|
118
134
|
|
135
|
+
# Start process and wait until it's finished.
|
136
|
+
#
|
137
|
+
# Given arguments will be passed to {#wait!}.
|
138
|
+
#
|
139
|
+
def run!(opts = {})
|
140
|
+
start
|
141
|
+
wait!(opts)
|
142
|
+
end
|
143
|
+
|
119
144
|
# @!group Working Directory
|
120
145
|
|
121
146
|
# Working directory for child process.
|
@@ -150,7 +175,8 @@ module MultiProcess
|
|
150
175
|
# Set environment.
|
151
176
|
#
|
152
177
|
def env=(env)
|
153
|
-
|
178
|
+
raise ArgumentError.new 'Environment must be a Hash.' unless env.is_a?(Hash)
|
179
|
+
|
154
180
|
@env = env
|
155
181
|
end
|
156
182
|
|
@@ -178,7 +204,7 @@ module MultiProcess
|
|
178
204
|
# Create child process.
|
179
205
|
#
|
180
206
|
def create_childprocess(*args)
|
181
|
-
ChildProcess.new
|
207
|
+
ChildProcess.new(*args.flatten)
|
182
208
|
end
|
183
209
|
|
184
210
|
# Start child process.
|
@@ -186,11 +212,11 @@ module MultiProcess
|
|
186
212
|
# Can be used to hook in subclasses and modules.
|
187
213
|
#
|
188
214
|
def start_childprocess
|
189
|
-
env.each {
|
215
|
+
env.each {|k, v| childprocess.environment[k.to_s] = v.to_s }
|
190
216
|
childprocess.cwd = dir
|
191
217
|
|
192
218
|
if clean_env?
|
193
|
-
Bundler.
|
219
|
+
Bundler.with_original_env { childprocess.start }
|
194
220
|
else
|
195
221
|
childprocess.start
|
196
222
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiProcess
|
2
4
|
# Can handle input from multiple processes and run custom
|
3
5
|
# actions on event and output.
|
@@ -14,12 +16,12 @@ module MultiProcess
|
|
14
16
|
|
15
17
|
Loop.instance.watch(reader) do |action, monitor|
|
16
18
|
case action
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
when :registered
|
20
|
+
connected(process, name)
|
21
|
+
when :ready
|
22
|
+
received(process, name, read(monitor.io))
|
23
|
+
when :eof
|
24
|
+
removed(process, name)
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
@@ -40,7 +42,7 @@ module MultiProcess
|
|
40
42
|
# Must be overridden by subclass.
|
41
43
|
#
|
42
44
|
def received(_process, _name, _message)
|
43
|
-
|
45
|
+
raise NotImplementedError.new 'Subclass responsibility.'
|
44
46
|
end
|
45
47
|
|
46
48
|
# Read content from pipe. Can be used to provide custom reading
|
@@ -55,12 +57,10 @@ module MultiProcess
|
|
55
57
|
# Called after pipe for process and name was removed because it
|
56
58
|
# reached EOF.
|
57
59
|
#
|
58
|
-
def removed(_process, _name)
|
59
|
-
end
|
60
|
+
def removed(_process, _name); end
|
60
61
|
|
61
62
|
# Called after new pipe for process and name was created.
|
62
63
|
#
|
63
|
-
def connected(_process, _name)
|
64
|
-
end
|
64
|
+
def connected(_process, _name); end
|
65
65
|
end
|
66
66
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiProcess
|
2
4
|
# Receiver implementation storing process output
|
3
5
|
# in string.
|
@@ -9,7 +11,7 @@ module MultiProcess
|
|
9
11
|
|
10
12
|
def get(name)
|
11
13
|
@strings ||= {}
|
12
|
-
@strings[name.to_s] ||= ''
|
14
|
+
@strings[name.to_s] ||= +''
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MultiProcess
|
2
4
|
module VERSION
|
3
5
|
MAJOR = 1
|
4
|
-
MINOR =
|
6
|
+
MINOR = 2
|
5
7
|
PATCH = 0
|
6
8
|
STAGE = nil
|
7
|
-
STRING = [MAJOR, MINOR, PATCH, STAGE].
|
9
|
+
STRING = [MAJOR, MINOR, PATCH, STAGE].compact.join('.')
|
8
10
|
|
9
11
|
def self.to_s
|
10
12
|
STRING
|
data/lib/multi_process.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'multi_process/version'
|
2
4
|
require 'childprocess'
|
3
5
|
|
4
6
|
module MultiProcess
|
5
7
|
DEFAULT_TIMEOUT = 60
|
6
8
|
|
9
|
+
require 'multi_process/errors'
|
10
|
+
|
7
11
|
require 'multi_process/loop'
|
8
12
|
require 'multi_process/group'
|
9
13
|
require 'multi_process/receiver'
|
data/multi_process.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'multi_process/version'
|
5
6
|
|
@@ -13,8 +14,11 @@ Gem::Specification.new do |spec|
|
|
13
14
|
spec.homepage = 'https://github.com/jgraichen/multi_process'
|
14
15
|
spec.license = 'GPLv3'
|
15
16
|
|
17
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
18
|
+
spec.required_ruby_version = '>= 2.7'
|
19
|
+
|
16
20
|
spec.files = `git ls-files -z`.split("\x0")
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/}) {
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f) }
|
18
22
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
23
|
spec.require_paths = ['lib']
|
20
24
|
|
data/renovate.json
ADDED
data/spec/files/env.rb
CHANGED
data/spec/files/fail.rb
ADDED
data/spec/files/sleep.rb
CHANGED
data/spec/files/test.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe MultiProcess::Group do
|
6
|
+
subject(:group) { MultiProcess::Group.new }
|
7
|
+
|
8
|
+
before do
|
9
|
+
group << MultiProcess::Process.new(command)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#run!' do
|
13
|
+
context 'with failing command' do
|
14
|
+
let(:command) { %w[ruby spec/files/fail.rb] }
|
15
|
+
|
16
|
+
it 'does raise an error' do
|
17
|
+
expect { group.run! }.to raise_error(MultiProcess::ProcessError, /Process \d+ exited with code 1/)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with partition and failing command' do
|
22
|
+
subject(:group) { MultiProcess::Group.new(partition: 1) }
|
23
|
+
|
24
|
+
let(:command) { %w[ruby spec/files/fail.rb] }
|
25
|
+
|
26
|
+
it 'does raise an error' do
|
27
|
+
expect { group.run! }.to raise_error(MultiProcess::ProcessError, /Process \d+ exited with code 1/)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe MultiProcess::Process do
|
6
|
+
subject(:process) { MultiProcess::Process.new(command) }
|
7
|
+
|
8
|
+
describe '#run!' do
|
9
|
+
context 'with failing command' do
|
10
|
+
let(:command) { %w[ruby spec/files/fail.rb] }
|
11
|
+
|
12
|
+
it 'does raise an error' do
|
13
|
+
expect { process.run! }.to raise_error(MultiProcess::ProcessError, /Process \d+ exited with code 1/)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/multi_process_spec.rb
CHANGED
@@ -1,62 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe MultiProcess do
|
4
|
-
it '
|
6
|
+
it 'runs processes (I)' do
|
5
7
|
reader, writer = IO.pipe
|
6
8
|
|
7
9
|
logger = MultiProcess::Logger.new writer, collapse: false
|
8
10
|
group = MultiProcess::Group.new receiver: logger
|
9
|
-
group << MultiProcess::Process.new(%w
|
10
|
-
group << MultiProcess::Process.new(%w
|
11
|
-
group << MultiProcess::Process.new(%w
|
11
|
+
group << MultiProcess::Process.new(%w[ruby spec/files/test.rb A], title: 'rubyA')
|
12
|
+
group << MultiProcess::Process.new(%w[ruby spec/files/test.rb B], title: 'rubyBB')
|
13
|
+
group << MultiProcess::Process.new(%w[ruby spec/files/test.rb C], title: 'rubyCCC')
|
12
14
|
group.run
|
13
15
|
|
14
|
-
expect(reader.read_nonblock(4096).split("\n")).to match_array <<-
|
16
|
+
expect(reader.read_nonblock(4096).split("\n")).to match_array <<-OUTPUT.gsub(/^\s+\./, '').split("\n")
|
15
17
|
. rubyBB | Output from B
|
16
18
|
. rubyA | Output from A
|
17
19
|
. rubyA | Output from A
|
18
20
|
. rubyCCC | Output from C
|
19
21
|
. rubyCCC | Output from C
|
20
22
|
. rubyBB | Output from B
|
21
|
-
|
23
|
+
OUTPUT
|
22
24
|
end
|
23
25
|
|
24
|
-
it '
|
26
|
+
it 'runs processes (II)' do
|
25
27
|
start = Time.now
|
26
28
|
|
27
29
|
group = MultiProcess::Group.new receiver: MultiProcess::NilReceiver.new
|
28
|
-
group << MultiProcess::Process.new(%w
|
29
|
-
group << MultiProcess::Process.new(%w
|
30
|
-
group << MultiProcess::Process.new(%w
|
30
|
+
group << MultiProcess::Process.new(%w[ruby spec/files/sleep.rb 5000], title: 'rubyA')
|
31
|
+
group << MultiProcess::Process.new(%w[ruby spec/files/sleep.rb 5000], title: 'rubyB')
|
32
|
+
group << MultiProcess::Process.new(%w[ruby spec/files/sleep.rb 5000], title: 'rubyC')
|
31
33
|
group.start
|
32
34
|
sleep 1
|
33
35
|
group.stop
|
34
36
|
|
35
37
|
group.processes.each do |p|
|
36
|
-
expect(p).
|
38
|
+
expect(p).not_to be_alive
|
37
39
|
end
|
38
40
|
expect(Time.now - start).to be < 2
|
39
41
|
end
|
40
42
|
|
41
|
-
it '
|
43
|
+
it 'partitions processes' do
|
42
44
|
group = MultiProcess::Group.new partition: 4, receiver: MultiProcess::NilReceiver.new
|
43
|
-
group << MultiProcess::Process.new(%w
|
44
|
-
group << MultiProcess::Process.new(%w
|
45
|
-
group << MultiProcess::Process.new(%w
|
46
|
-
group << MultiProcess::Process.new(%w
|
47
|
-
group << MultiProcess::Process.new(%w
|
48
|
-
group << MultiProcess::Process.new(%w
|
49
|
-
group << MultiProcess::Process.new(%w
|
50
|
-
group << MultiProcess::Process.new(%w
|
45
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyA')
|
46
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyB')
|
47
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyC')
|
48
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyD')
|
49
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyE')
|
50
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyF')
|
51
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyG')
|
52
|
+
group << MultiProcess::Process.new(%w[ruby sleep.rb 1], dir: 'spec/files', title: 'rubyH')
|
51
53
|
|
52
54
|
start = Time.now
|
53
55
|
group.run
|
54
|
-
expect(Time.now - start).to be_within(0.
|
56
|
+
expect(Time.now - start).to be_within(0.5).of(2)
|
55
57
|
end
|
56
58
|
|
57
|
-
it '
|
59
|
+
it 'envs processes' do
|
58
60
|
receiver = MultiProcess::StringReceiver.new
|
59
|
-
process = MultiProcess::Process.new(%w
|
61
|
+
process = MultiProcess::Process.new(%w[ruby spec/files/env.rb TEST], env: {'TEST' => 'abc'}, receiver: receiver)
|
60
62
|
process.run
|
61
63
|
|
62
64
|
expect(receiver.get(:out)).to eq "ENV: abc\n"
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rspec'
|
2
4
|
|
3
5
|
require 'bundler'
|
@@ -5,7 +7,7 @@ Bundler.require
|
|
5
7
|
|
6
8
|
require 'multi_process'
|
7
9
|
|
8
|
-
Dir[File.expand_path('spec/support/**/*.rb')].each {
|
10
|
+
Dir[File.expand_path('spec/support/**/*.rb')].sort.each {|f| require f }
|
9
11
|
|
10
12
|
RSpec.configure do |config|
|
11
13
|
# ## Mock Framework
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: multi_process
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Graichen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -73,13 +73,17 @@ executables: []
|
|
73
73
|
extensions: []
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
|
+
- ".editorconfig"
|
77
|
+
- ".github/workflows/test.yml"
|
76
78
|
- ".gitignore"
|
79
|
+
- ".rubocop.yml"
|
77
80
|
- CHANGELOG.md
|
78
81
|
- Gemfile
|
79
82
|
- LICENSE.txt
|
80
83
|
- README.md
|
81
84
|
- Rakefile
|
82
85
|
- lib/multi_process.rb
|
86
|
+
- lib/multi_process/errors.rb
|
83
87
|
- lib/multi_process/group.rb
|
84
88
|
- lib/multi_process/logger.rb
|
85
89
|
- lib/multi_process/loop.rb
|
@@ -91,15 +95,20 @@ files:
|
|
91
95
|
- lib/multi_process/string_receiver.rb
|
92
96
|
- lib/multi_process/version.rb
|
93
97
|
- multi_process.gemspec
|
98
|
+
- renovate.json
|
94
99
|
- spec/files/env.rb
|
100
|
+
- spec/files/fail.rb
|
95
101
|
- spec/files/sleep.rb
|
96
102
|
- spec/files/test.rb
|
103
|
+
- spec/multi_process/group_spec.rb
|
104
|
+
- spec/multi_process/process_spec.rb
|
97
105
|
- spec/multi_process_spec.rb
|
98
106
|
- spec/spec_helper.rb
|
99
107
|
homepage: https://github.com/jgraichen/multi_process
|
100
108
|
licenses:
|
101
109
|
- GPLv3
|
102
|
-
metadata:
|
110
|
+
metadata:
|
111
|
+
rubygems_mfa_required: 'true'
|
103
112
|
post_install_message:
|
104
113
|
rdoc_options: []
|
105
114
|
require_paths:
|
@@ -108,20 +117,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
117
|
requirements:
|
109
118
|
- - ">="
|
110
119
|
- !ruby/object:Gem::Version
|
111
|
-
version: '
|
120
|
+
version: '2.7'
|
112
121
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
122
|
requirements:
|
114
123
|
- - ">="
|
115
124
|
- !ruby/object:Gem::Version
|
116
125
|
version: '0'
|
117
126
|
requirements: []
|
118
|
-
rubygems_version: 3.
|
127
|
+
rubygems_version: 3.3.7
|
119
128
|
signing_key:
|
120
129
|
specification_version: 4
|
121
130
|
summary: Handle multiple child processes.
|
122
131
|
test_files:
|
123
132
|
- spec/files/env.rb
|
133
|
+
- spec/files/fail.rb
|
124
134
|
- spec/files/sleep.rb
|
125
135
|
- spec/files/test.rb
|
136
|
+
- spec/multi_process/group_spec.rb
|
137
|
+
- spec/multi_process/process_spec.rb
|
126
138
|
- spec/multi_process_spec.rb
|
127
139
|
- spec/spec_helper.rb
|