multi_process 1.1.1 → 1.2.1
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 +27 -3
- data/Gemfile +7 -3
- data/README.md +17 -13
- 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 +41 -13
- 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 -7
- 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 +15 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fa36d18413df27da497f7b827039f6f7316aea6b4d495e7dae6f7824779347a
|
|
4
|
+
data.tar.gz: 8aedd68f59d4e596cf905d71214db13693eca5c8bcdbe2ba65a380ad6da8da53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e86fea763485072ed00773827421524753b11f1e3b4d117dcbd1d0d78d05db78103db141fff9d7d2f229b4e5b5655ab6a6a3d2dddea8f624c9c9ff859abe30cd
|
|
7
|
+
data.tar.gz: 1eda65291d33b32948db1114300928341ba783d3e1148b9a633fdd470bc96de049b3944cb8fa47bdbec74e49b1552d61f0d1cd3d61ef1ec3e7b35876d175362e
|
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-22.04
|
|
9
|
+
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
ruby: ["3.2", "3.1", "3.0", "2.7"]
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
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-22.04
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: ruby/setup-ruby@v1
|
|
31
|
+
with:
|
|
32
|
+
ruby-version: "3.2"
|
|
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,44 @@
|
|
|
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.1] - 2024-01-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Remove dependency on ActiveSupport by @tylerhunt (#9)
|
|
14
|
+
- Ruby 3.2
|
|
15
|
+
|
|
16
|
+
## [1.2.0] - 2022-06-03
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `run!` and `wait!` to raise error if any process exits with an error code != 0
|
|
21
|
+
|
|
22
|
+
## [1.1.1] - 2020-12-21
|
|
23
|
+
|
|
8
24
|
### Fixed
|
|
25
|
+
|
|
9
26
|
- Replaced deprecated `#with_clean_env` method
|
|
10
27
|
|
|
11
28
|
## [1.1.0] - 2020-11-19
|
|
29
|
+
|
|
12
30
|
### Added
|
|
31
|
+
|
|
13
32
|
- Add support for IPv6 by using the hostname instead of the loopback IPv4 address (#2)
|
|
14
33
|
|
|
15
34
|
## 1.0.0 - 2019-05-13
|
|
35
|
+
|
|
16
36
|
### Fixed
|
|
37
|
+
|
|
17
38
|
- Possible concurrent hash modification while iterating (#1)
|
|
18
39
|
|
|
19
|
-
[Unreleased]: https://github.com/jgraichen/multi_process/compare/v1.1
|
|
40
|
+
[Unreleased]: https://github.com/jgraichen/multi_process/compare/v1.2.1...HEAD
|
|
41
|
+
[1.2.1]: https://github.com/jgraichen/multi_process/compare/v1.2.0...v1.2.1
|
|
42
|
+
[1.2.0]: https://github.com/jgraichen/multi_process/compare/v1.1.1...v1.2.0
|
|
43
|
+
[1.1.1]: https://github.com/jgraichen/multi_process/compare/v1.1.0...v1.1.1
|
|
20
44
|
[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: 'v11', require: false
|
data/README.md
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
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
|
+
```console
|
|
16
|
+
bundle
|
|
17
|
+
```
|
|
16
18
|
|
|
17
19
|
Or install it yourself as:
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
```console
|
|
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
|
|
32
36
|
group.stop # Stop processes
|
|
33
37
|
```
|
|
34
38
|
|
|
35
|
-
```
|
|
39
|
+
```text
|
|
36
40
|
(23311) rubyB | Output from B
|
|
37
41
|
(23308) rubyA | Output from A
|
|
38
42
|
(23314) rubyC | Output from C
|
|
@@ -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-2023 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,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
2
4
|
|
|
3
5
|
module MultiProcess
|
|
4
6
|
#
|
|
@@ -7,6 +9,8 @@ module MultiProcess
|
|
|
7
9
|
# {Process} basically is just a thin wrapper around {ChildProcess}.
|
|
8
10
|
#
|
|
9
11
|
class Process
|
|
12
|
+
extend Forwardable
|
|
13
|
+
|
|
10
14
|
# @!group Process
|
|
11
15
|
|
|
12
16
|
# Process title used in e.g. logger
|
|
@@ -20,14 +24,14 @@ module MultiProcess
|
|
|
20
24
|
|
|
21
25
|
def initialize(*args)
|
|
22
26
|
args.flatten!
|
|
23
|
-
opts = (
|
|
27
|
+
opts = (args.last.is_a?(Hash) ? args.pop : {})
|
|
24
28
|
|
|
25
29
|
@title = opts[:title].to_s || args.first.to_s.strip.split(/\s+/, 2)[0]
|
|
26
|
-
@command = args.map {
|
|
27
|
-
@childprocess = create_childprocess
|
|
30
|
+
@command = args.map {|arg| (/\A[\s"']+\z/.match?(arg) ? arg.inspect : arg).gsub '"', '\"' }.join(' ')
|
|
31
|
+
@childprocess = create_childprocess(*args)
|
|
28
32
|
|
|
29
|
-
@env = opts[:env] if
|
|
30
|
-
@env_clean = opts[:clean_env].nil? ? true :
|
|
33
|
+
@env = opts[:env] if opts[:env].is_a?(Hash)
|
|
34
|
+
@env_clean = opts[:clean_env].nil? ? true : !opts[:clean_env].nil?
|
|
31
35
|
|
|
32
36
|
self.receiver = opts[:receiver] || MultiProcess::Logger.global
|
|
33
37
|
|
|
@@ -37,7 +41,7 @@ module MultiProcess
|
|
|
37
41
|
|
|
38
42
|
# Delegate some methods to ChildProcess.
|
|
39
43
|
#
|
|
40
|
-
delegate
|
|
44
|
+
delegate %i[exited? alive? crashed? exit_code pid] => :childprocess
|
|
41
45
|
|
|
42
46
|
# Wait until process finished.
|
|
43
47
|
#
|
|
@@ -54,6 +58,20 @@ module MultiProcess
|
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
61
|
+
# Wait until process finished.
|
|
62
|
+
#
|
|
63
|
+
# If no timeout is given it will wait definitely.
|
|
64
|
+
#
|
|
65
|
+
# @param opts [Hash] Options.
|
|
66
|
+
# @option opts [Integer] :timeout Timeout to wait in seconds.
|
|
67
|
+
#
|
|
68
|
+
def wait!(opts = {})
|
|
69
|
+
wait(opts)
|
|
70
|
+
return if exit_code.zero?
|
|
71
|
+
|
|
72
|
+
raise ::MultiProcess::ProcessError.new(self, "Process #{pid} exited with code #{exit_code}")
|
|
73
|
+
end
|
|
74
|
+
|
|
57
75
|
# Start process.
|
|
58
76
|
#
|
|
59
77
|
# Started processes will be stopped when ruby VM exists by hooking into
|
|
@@ -63,7 +81,7 @@ module MultiProcess
|
|
|
63
81
|
return false if started?
|
|
64
82
|
|
|
65
83
|
at_exit { stop }
|
|
66
|
-
receiver
|
|
84
|
+
receiver&.message(self, :sys, command)
|
|
67
85
|
start_childprocess
|
|
68
86
|
@started = true
|
|
69
87
|
end
|
|
@@ -73,7 +91,7 @@ module MultiProcess
|
|
|
73
91
|
# Will call `ChildProcess#stop`.
|
|
74
92
|
#
|
|
75
93
|
def stop(*args)
|
|
76
|
-
childprocess.stop
|
|
94
|
+
childprocess.stop(*args) if started?
|
|
77
95
|
end
|
|
78
96
|
|
|
79
97
|
# Check if server is available. What available means can be defined
|
|
@@ -97,7 +115,7 @@ module MultiProcess
|
|
|
97
115
|
Timeout.timeout timeout do
|
|
98
116
|
sleep 0.2 until available?
|
|
99
117
|
end
|
|
100
|
-
rescue Timeout::Error
|
|
118
|
+
rescue Timeout::Error
|
|
101
119
|
raise Timeout::Error.new "Server #{id.inspect} on port #{port} didn't get up after #{timeout} seconds..."
|
|
102
120
|
end
|
|
103
121
|
|
|
@@ -116,6 +134,15 @@ module MultiProcess
|
|
|
116
134
|
wait opts
|
|
117
135
|
end
|
|
118
136
|
|
|
137
|
+
# Start process and wait until it's finished.
|
|
138
|
+
#
|
|
139
|
+
# Given arguments will be passed to {#wait!}.
|
|
140
|
+
#
|
|
141
|
+
def run!(opts = {})
|
|
142
|
+
start
|
|
143
|
+
wait!(opts)
|
|
144
|
+
end
|
|
145
|
+
|
|
119
146
|
# @!group Working Directory
|
|
120
147
|
|
|
121
148
|
# Working directory for child process.
|
|
@@ -150,7 +177,8 @@ module MultiProcess
|
|
|
150
177
|
# Set environment.
|
|
151
178
|
#
|
|
152
179
|
def env=(env)
|
|
153
|
-
|
|
180
|
+
raise ArgumentError.new 'Environment must be a Hash.' unless env.is_a?(Hash)
|
|
181
|
+
|
|
154
182
|
@env = env
|
|
155
183
|
end
|
|
156
184
|
|
|
@@ -178,7 +206,7 @@ module MultiProcess
|
|
|
178
206
|
# Create child process.
|
|
179
207
|
#
|
|
180
208
|
def create_childprocess(*args)
|
|
181
|
-
ChildProcess.new
|
|
209
|
+
ChildProcess.new(*args.flatten)
|
|
182
210
|
end
|
|
183
211
|
|
|
184
212
|
# Start child process.
|
|
@@ -186,7 +214,7 @@ module MultiProcess
|
|
|
186
214
|
# Can be used to hook in subclasses and modules.
|
|
187
215
|
#
|
|
188
216
|
def start_childprocess
|
|
189
|
-
env.each {
|
|
217
|
+
env.each {|k, v| childprocess.environment[k.to_s] = v.to_s }
|
|
190
218
|
childprocess.cwd = dir
|
|
191
219
|
|
|
192
220
|
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 =
|
|
6
|
+
MINOR = 2
|
|
5
7
|
PATCH = 1
|
|
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,14 +14,13 @@ 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/}) {
|
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f) }
|
|
19
22
|
spec.require_paths = ['lib']
|
|
20
23
|
|
|
21
|
-
spec.add_runtime_dependency 'activesupport', '>= 3.1'
|
|
22
24
|
spec.add_runtime_dependency 'childprocess'
|
|
23
25
|
spec.add_runtime_dependency 'nio4r', '~> 2.0'
|
|
24
|
-
|
|
25
|
-
spec.add_development_dependency 'bundler'
|
|
26
26
|
end
|
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,29 +1,15 @@
|
|
|
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.1
|
|
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: 2024-01-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: activesupport
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '3.1'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '3.1'
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: childprocess
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,20 +38,6 @@ dependencies:
|
|
|
52
38
|
- - "~>"
|
|
53
39
|
- !ruby/object:Gem::Version
|
|
54
40
|
version: '2.0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: bundler
|
|
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'
|
|
69
41
|
description: Handle multiple child processes.
|
|
70
42
|
email:
|
|
71
43
|
- jg@altimos.de
|
|
@@ -73,13 +45,17 @@ executables: []
|
|
|
73
45
|
extensions: []
|
|
74
46
|
extra_rdoc_files: []
|
|
75
47
|
files:
|
|
48
|
+
- ".editorconfig"
|
|
49
|
+
- ".github/workflows/test.yml"
|
|
76
50
|
- ".gitignore"
|
|
51
|
+
- ".rubocop.yml"
|
|
77
52
|
- CHANGELOG.md
|
|
78
53
|
- Gemfile
|
|
79
54
|
- LICENSE.txt
|
|
80
55
|
- README.md
|
|
81
56
|
- Rakefile
|
|
82
57
|
- lib/multi_process.rb
|
|
58
|
+
- lib/multi_process/errors.rb
|
|
83
59
|
- lib/multi_process/group.rb
|
|
84
60
|
- lib/multi_process/logger.rb
|
|
85
61
|
- lib/multi_process/loop.rb
|
|
@@ -91,15 +67,20 @@ files:
|
|
|
91
67
|
- lib/multi_process/string_receiver.rb
|
|
92
68
|
- lib/multi_process/version.rb
|
|
93
69
|
- multi_process.gemspec
|
|
70
|
+
- renovate.json
|
|
94
71
|
- spec/files/env.rb
|
|
72
|
+
- spec/files/fail.rb
|
|
95
73
|
- spec/files/sleep.rb
|
|
96
74
|
- spec/files/test.rb
|
|
75
|
+
- spec/multi_process/group_spec.rb
|
|
76
|
+
- spec/multi_process/process_spec.rb
|
|
97
77
|
- spec/multi_process_spec.rb
|
|
98
78
|
- spec/spec_helper.rb
|
|
99
79
|
homepage: https://github.com/jgraichen/multi_process
|
|
100
80
|
licenses:
|
|
101
81
|
- GPLv3
|
|
102
|
-
metadata:
|
|
82
|
+
metadata:
|
|
83
|
+
rubygems_mfa_required: 'true'
|
|
103
84
|
post_install_message:
|
|
104
85
|
rdoc_options: []
|
|
105
86
|
require_paths:
|
|
@@ -108,20 +89,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
108
89
|
requirements:
|
|
109
90
|
- - ">="
|
|
110
91
|
- !ruby/object:Gem::Version
|
|
111
|
-
version: '
|
|
92
|
+
version: '2.7'
|
|
112
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
94
|
requirements:
|
|
114
95
|
- - ">="
|
|
115
96
|
- !ruby/object:Gem::Version
|
|
116
97
|
version: '0'
|
|
117
98
|
requirements: []
|
|
118
|
-
rubygems_version: 3.
|
|
99
|
+
rubygems_version: 3.4.22
|
|
119
100
|
signing_key:
|
|
120
101
|
specification_version: 4
|
|
121
102
|
summary: Handle multiple child processes.
|
|
122
|
-
test_files:
|
|
123
|
-
- spec/files/env.rb
|
|
124
|
-
- spec/files/sleep.rb
|
|
125
|
-
- spec/files/test.rb
|
|
126
|
-
- spec/multi_process_spec.rb
|
|
127
|
-
- spec/spec_helper.rb
|
|
103
|
+
test_files: []
|