multi_process 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.editorconfig +12 -0
- data/.github/workflows/test.yml +38 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +19 -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 +7 -11
- data/lib/multi_process/process.rb +37 -11
- data/lib/multi_process/receiver.rb +11 -11
- data/lib/multi_process/string_receiver.rb +3 -1
- data/lib/multi_process/version.rb +5 -3
- 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,20 +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
|
+
|
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
|
+
|
8
17
|
### Fixed
|
18
|
+
|
9
19
|
- Replaced deprecated `#with_clean_env` method
|
10
20
|
|
11
21
|
## [1.1.0] - 2020-11-19
|
22
|
+
|
12
23
|
### Added
|
24
|
+
|
13
25
|
- Add support for IPv6 by using the hostname instead of the loopback IPv4 address (#2)
|
14
26
|
|
15
27
|
## 1.0.0 - 2019-05-13
|
28
|
+
|
16
29
|
### Fixed
|
30
|
+
|
17
31
|
- Possible concurrent hash modification while iterating (#1)
|
18
32
|
|
19
|
-
[
|
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
|
20
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,7 +38,7 @@ 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
43
|
TCPSocket.new('localhost', port).close
|
48
44
|
true
|
@@ -70,7 +66,7 @@ class MultiProcess::Process
|
|
70
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,7 +212,7 @@ 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?
|
@@ -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 =
|
5
|
-
PATCH =
|
6
|
+
MINOR = 2
|
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
|