multi_process 1.1.1 → 1.2.1
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 +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: []
|