multi_process 0.1.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 +7 -0
- data/.gitignore +18 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +674 -0
- data/README.md +67 -0
- data/Rakefile +5 -0
- data/lib/multi_process.rb +15 -0
- data/lib/multi_process/group.rb +136 -0
- data/lib/multi_process/logger.rb +71 -0
- data/lib/multi_process/nil_receiver.rb +13 -0
- data/lib/multi_process/process.rb +200 -0
- data/lib/multi_process/process/bundle_exec.rb +13 -0
- data/lib/multi_process/process/rails.rb +74 -0
- data/lib/multi_process/receiver.rb +90 -0
- data/lib/multi_process/version.rb +11 -0
- data/multi_process.gemspec +25 -0
- data/spec/files/sleep.rb +2 -0
- data/spec/files/test.rb +6 -0
- data/spec/multi_process_spec.rb +41 -0
- data/spec/spec_helper.rb +27 -0
- metadata +109 -0
data/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# MultiProcess
|
|
2
|
+
|
|
3
|
+
Handle multiple processes.
|
|
4
|
+
|
|
5
|
+
TODO: Just experiment.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
gem 'multi_process'
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install multi_process
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
receiver = MultiProcess::Logger $stdout, $stderr, sys: false
|
|
25
|
+
group = MultiProcess::Group.new receiver: receiver
|
|
26
|
+
group << MultiProcess::Process.new %w(ruby test.rb), title: 'rubyA'
|
|
27
|
+
group << MultiProcess::Process.new %w(ruby test.rb), title: 'rubyB'
|
|
28
|
+
group << MultiProcess::Process.new %w(ruby test.rb), title: 'rubyC'
|
|
29
|
+
group.start # Start in background
|
|
30
|
+
group.run # Block until finished
|
|
31
|
+
group.wait # Wait until finished
|
|
32
|
+
group.stop # Stop processes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
(23311) rubyB | Output from B
|
|
37
|
+
(23308) rubyA | Output from A
|
|
38
|
+
(23314) rubyC | Output from C
|
|
39
|
+
(23314) rubyC | Output from C
|
|
40
|
+
(23311) rubyB | Output from B
|
|
41
|
+
(23308) rubyA | Output from A
|
|
42
|
+
``
|
|
43
|
+
|
|
44
|
+
## Contributing
|
|
45
|
+
|
|
46
|
+
1. Fork it ( http://github.com/jgraichen/multi_process/fork )
|
|
47
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
48
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
49
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
50
|
+
5. Create new Pull Request
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
Copyright (C) 2014 Jan Graichen
|
|
55
|
+
|
|
56
|
+
This program is free software: you can redistribute it and/or modify
|
|
57
|
+
it under the terms of the GNU General Public License as published by
|
|
58
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
59
|
+
(at your option) any later version.
|
|
60
|
+
|
|
61
|
+
This program is distributed in the hope that it will be useful,
|
|
62
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
63
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
64
|
+
GNU General Public License for more details.
|
|
65
|
+
|
|
66
|
+
You should have received a copy of the GNU General Public License
|
|
67
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'multi_process/version'
|
|
2
|
+
require 'childprocess'
|
|
3
|
+
|
|
4
|
+
module MultiProcess
|
|
5
|
+
DEFAULT_TIMEOUT = 60
|
|
6
|
+
|
|
7
|
+
require 'multi_process/group'
|
|
8
|
+
require 'multi_process/receiver'
|
|
9
|
+
require 'multi_process/nil_receiver'
|
|
10
|
+
require 'multi_process/logger'
|
|
11
|
+
|
|
12
|
+
require 'multi_process/process'
|
|
13
|
+
require 'multi_process/process/rails'
|
|
14
|
+
require 'multi_process/process/bundle_exec'
|
|
15
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module MultiProcess
|
|
2
|
+
|
|
3
|
+
# Store and run a group of processes.
|
|
4
|
+
#
|
|
5
|
+
class Group
|
|
6
|
+
|
|
7
|
+
# Return list of processes.
|
|
8
|
+
attr_reader :processes
|
|
9
|
+
|
|
10
|
+
# Receiver all processes in group should use.
|
|
11
|
+
#
|
|
12
|
+
# If changed only affect new added processes.
|
|
13
|
+
#
|
|
14
|
+
attr_accessor :receiver
|
|
15
|
+
|
|
16
|
+
# Create new process group.
|
|
17
|
+
#
|
|
18
|
+
# @param opts [ Hash ] Options
|
|
19
|
+
# @option otps [ Receiver ] :receiver Receiver to use for new added
|
|
20
|
+
# processes. Defaults to `MultiProcess::Logger.global`.
|
|
21
|
+
#
|
|
22
|
+
def initialize(opts = {})
|
|
23
|
+
@processes = []
|
|
24
|
+
@receiver = opts[:receiver] ? opts[:receiver] : MultiProcess::Logger.global
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add new process or list of processes.
|
|
28
|
+
#
|
|
29
|
+
# If group was already started added processes will also be started.
|
|
30
|
+
#
|
|
31
|
+
# @param process [Process, Array<Process>] New process or processes.
|
|
32
|
+
#
|
|
33
|
+
def <<(process)
|
|
34
|
+
Array(process).flatten.each do |process|
|
|
35
|
+
processes << process
|
|
36
|
+
# process.receiver = receiver
|
|
37
|
+
|
|
38
|
+
if started?
|
|
39
|
+
start process
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Start all process in group.
|
|
45
|
+
#
|
|
46
|
+
# Call blocks until all processes are started.
|
|
47
|
+
#
|
|
48
|
+
# @param opts [ Hash ] Options.
|
|
49
|
+
# @option opts [ Integer ] :delay Delay in seconds between starting processes.
|
|
50
|
+
#
|
|
51
|
+
def start(opts = {})
|
|
52
|
+
processes.each do |process|
|
|
53
|
+
unless process.started?
|
|
54
|
+
process.start
|
|
55
|
+
sleep opts[:delay] if opts[:delay]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if group was already started.
|
|
61
|
+
#
|
|
62
|
+
# @return [ Boolean ] True if group was already started.
|
|
63
|
+
#
|
|
64
|
+
def started?
|
|
65
|
+
processes.any? &:started?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Stop all processes.
|
|
69
|
+
#
|
|
70
|
+
def stop
|
|
71
|
+
processes.each do |process|
|
|
72
|
+
process.stop
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Wait until all process terminated.
|
|
77
|
+
#
|
|
78
|
+
# @param opts [ Hash ] Options.
|
|
79
|
+
# @option opts [ Integer ] :timeout Timeout in seconds to wait before
|
|
80
|
+
# raising {Timeout::Error}.
|
|
81
|
+
#
|
|
82
|
+
def wait(opts = {})
|
|
83
|
+
opts[:timeout] ||= 30
|
|
84
|
+
|
|
85
|
+
::Timeout::timeout(opts[:timeout]) do
|
|
86
|
+
processes.each{|p| p.wait}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Start all process and wait for them to terminate.
|
|
91
|
+
#
|
|
92
|
+
# Given options will be passed to {#wait}.
|
|
93
|
+
#
|
|
94
|
+
# If timeout is given process will be terminated using {#stop}
|
|
95
|
+
# when timeout error is raised.
|
|
96
|
+
#
|
|
97
|
+
def run(opts = {})
|
|
98
|
+
start
|
|
99
|
+
wait opts
|
|
100
|
+
ensure
|
|
101
|
+
stop
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if group is alive e.g. if at least on process is alive.
|
|
105
|
+
#
|
|
106
|
+
# @return [ Boolean ] True if group is alive.
|
|
107
|
+
#
|
|
108
|
+
def alive?
|
|
109
|
+
processes.any? &:alive?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if group is available. The group is available if all
|
|
113
|
+
# processes are available.
|
|
114
|
+
#
|
|
115
|
+
def available?
|
|
116
|
+
!processes.any?{|p| !p.available? }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Wait until group is available. This implies waiting until
|
|
120
|
+
# all processes in group are available.
|
|
121
|
+
#
|
|
122
|
+
# Processes will not be stopped if timeout occurs.
|
|
123
|
+
#
|
|
124
|
+
# @param opts [ Hash ] Options.
|
|
125
|
+
# @option opts [ Integer ] :timeout Timeout in seconds to wait for processes
|
|
126
|
+
# to become available. Defaults to {MultiProcess::DEFAULT_TIMEOUT}.
|
|
127
|
+
#
|
|
128
|
+
def available!(opts = {})
|
|
129
|
+
timeout = opts[:timeout] ? opts[:timeout].to_i : MultiProcess::DEFAULT_TIMEOUT
|
|
130
|
+
|
|
131
|
+
Timeout.timeout timeout do
|
|
132
|
+
processes.each{|p| p.available! }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module MultiProcess
|
|
2
|
+
|
|
3
|
+
# Can create pipes and multiplex pipe content to put into
|
|
4
|
+
# given IO objects e.g. multiple output from multiple
|
|
5
|
+
# processes to current stdout.
|
|
6
|
+
#
|
|
7
|
+
class Logger < Receiver
|
|
8
|
+
|
|
9
|
+
# Create new logger.
|
|
10
|
+
#
|
|
11
|
+
# @param out [IO] IO to push formatted output from
|
|
12
|
+
# default created logger pipes.
|
|
13
|
+
# @param err [IO] IO to push formatted output from
|
|
14
|
+
# error sources.
|
|
15
|
+
#
|
|
16
|
+
def initialize(*args)
|
|
17
|
+
@opts = Hash === args.last ? args.pop : Hash.new
|
|
18
|
+
@out = args[0] || $stdout
|
|
19
|
+
@err = args[1] || $stderr
|
|
20
|
+
|
|
21
|
+
super()
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def received(process, name, line)
|
|
27
|
+
case name
|
|
28
|
+
when :err, :stderr
|
|
29
|
+
output process, line, io: @err, delimiter: 'E>'
|
|
30
|
+
when :out, :stdout
|
|
31
|
+
output process, line
|
|
32
|
+
when :sys
|
|
33
|
+
output(process, line, delimiter: '$>')# if @opts[:sys]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read(pipe)
|
|
38
|
+
pipe.gets
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
def output(process, line, opts = {})
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
opts[:delimiter] ||= ' |'
|
|
45
|
+
name = if opts[:name]
|
|
46
|
+
opts[:name].to_s.dup
|
|
47
|
+
else
|
|
48
|
+
max = @readers.values.map{|h| h[:process] ? h[:process].title.length : 0 }.max
|
|
49
|
+
process ? process.title.to_s.rjust(max, ' ') : (' ' * max)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
io = opts[:io] || @out
|
|
53
|
+
if @last_name == name
|
|
54
|
+
io.print " #{' ' * name.length} #{opts[:delimiter]} "
|
|
55
|
+
else
|
|
56
|
+
io.print " #{name} #{opts[:delimiter]} "
|
|
57
|
+
end
|
|
58
|
+
io.puts line
|
|
59
|
+
io.flush
|
|
60
|
+
|
|
61
|
+
@last_name = name
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
def global
|
|
67
|
+
@global ||= self.new $stdout, $stderr
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
|
+
|
|
3
|
+
module MultiProcess
|
|
4
|
+
|
|
5
|
+
# Describes a single process that can be configured and run.
|
|
6
|
+
#
|
|
7
|
+
# {Process} basically is just a thin wrapper around {ChildProcess}.
|
|
8
|
+
#
|
|
9
|
+
class Process
|
|
10
|
+
#@!group Process
|
|
11
|
+
|
|
12
|
+
# Process title used in e.g. logger
|
|
13
|
+
attr_reader :title
|
|
14
|
+
|
|
15
|
+
# Command as full string.
|
|
16
|
+
attr_reader :command
|
|
17
|
+
|
|
18
|
+
# ChildProcess object.
|
|
19
|
+
attr_reader :childprocess
|
|
20
|
+
|
|
21
|
+
def initialize(*args)
|
|
22
|
+
args.flatten!
|
|
23
|
+
opts = (Hash === args.last ? args.pop : {})
|
|
24
|
+
|
|
25
|
+
@title = opts[:title].to_s || args.first.to_s.strip.split(/\s+/, 2)[0]
|
|
26
|
+
@command = args.map{ |arg| (arg =~ /\A[\s"']+\z/ ? arg.inspect : arg).gsub '"', '\"' }.join(' ')
|
|
27
|
+
@childprocess = create_childprocess *args
|
|
28
|
+
|
|
29
|
+
@env = opts[:env] if Hash === opts[:env]
|
|
30
|
+
@env_clean = opts[:clean_env].nil? ? true : !!opts[:clean_env]
|
|
31
|
+
|
|
32
|
+
self.receiver = MultiProcess::Logger.global
|
|
33
|
+
|
|
34
|
+
self.dir = Dir.pwd
|
|
35
|
+
self.dir = opts[:dir].to_s if opts[:dir]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Delegate some methods to ChildProcess.
|
|
39
|
+
#
|
|
40
|
+
delegate :exited?, :alive?, :crashed?, :exit_code, :pid, to: :childprocess
|
|
41
|
+
|
|
42
|
+
# Wait until process finished.
|
|
43
|
+
#
|
|
44
|
+
# If no timeout is given it will wait definitely.
|
|
45
|
+
#
|
|
46
|
+
# @param opts [Hash] Options.
|
|
47
|
+
# @option opts [Integer] :timeout Timeout to wait in seconds.
|
|
48
|
+
#
|
|
49
|
+
def wait(opts = {})
|
|
50
|
+
if opts[:timeout]
|
|
51
|
+
childprocess.wait_for_exit opts[:timeout]
|
|
52
|
+
else
|
|
53
|
+
childprocess.wait
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Start process.
|
|
58
|
+
#
|
|
59
|
+
# Started processes will be stopped when ruby VM exists by hooking into
|
|
60
|
+
# `at_exit`.
|
|
61
|
+
#
|
|
62
|
+
def start
|
|
63
|
+
return false if started?
|
|
64
|
+
|
|
65
|
+
at_exit { stop }
|
|
66
|
+
receiver.message(self, :sys, self.command) if receiver
|
|
67
|
+
start_childprocess
|
|
68
|
+
@started = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Stop process.
|
|
72
|
+
#
|
|
73
|
+
# Will call `ChildProcess#stop`.
|
|
74
|
+
#
|
|
75
|
+
def stop(*args)
|
|
76
|
+
childprocess.stop *args if started?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if server is available. What available means can be defined
|
|
80
|
+
# by subclasses e.g. a server process can check if server port is reachable.
|
|
81
|
+
#
|
|
82
|
+
# By default is process if available if alive? returns true.
|
|
83
|
+
#
|
|
84
|
+
def available?
|
|
85
|
+
alive?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Wait until process is available. See {#available?}.
|
|
89
|
+
#
|
|
90
|
+
# @param opts [Hash] Options.
|
|
91
|
+
# @option opts [Integer] :timeout Timeout in seconds. Will raise
|
|
92
|
+
# Timeout::Error if timeout is reached.
|
|
93
|
+
#
|
|
94
|
+
def available!(opts = {})
|
|
95
|
+
timeout = opts[:timeout] ? opts[:timeout].to_i : MultiProcess::DEFAULT_TIMEOUT
|
|
96
|
+
|
|
97
|
+
Timeout.timeout timeout do
|
|
98
|
+
sleep 0.2 until available?
|
|
99
|
+
end
|
|
100
|
+
rescue Timeout::Error => ex
|
|
101
|
+
raise Timeout::Error.new "Server #{id.inspect} on port #{port} didn't get up after #{timeout} seconds..."
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if process was started.
|
|
105
|
+
#
|
|
106
|
+
def started?
|
|
107
|
+
!!@started
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Start process and wait until it's finished.
|
|
111
|
+
#
|
|
112
|
+
# Given arguments will be passed to {#wait}.
|
|
113
|
+
#
|
|
114
|
+
def run(opts = {})
|
|
115
|
+
start
|
|
116
|
+
wait opts
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#@!group Working Directory
|
|
120
|
+
|
|
121
|
+
# Working directory for child process.
|
|
122
|
+
attr_reader :dir
|
|
123
|
+
|
|
124
|
+
# Set process working directory. Only affect process if set before
|
|
125
|
+
# starting.
|
|
126
|
+
#
|
|
127
|
+
def dir=(dir)
|
|
128
|
+
@dir = ::File.expand_path(dir.to_s)
|
|
129
|
+
self.env['PWD'] = @dir
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#@!group Environment
|
|
133
|
+
|
|
134
|
+
# Check if environment will be cleaned up for process.
|
|
135
|
+
#
|
|
136
|
+
# Currently that includes wrapping the process start in
|
|
137
|
+
# `Bundler.with_clean_env` to remove bundler environment
|
|
138
|
+
# variables.
|
|
139
|
+
#
|
|
140
|
+
def clean_env?
|
|
141
|
+
!!@env_clean
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Return current environment.
|
|
145
|
+
#
|
|
146
|
+
def env
|
|
147
|
+
@env ||= Hash.new
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Set environment.
|
|
151
|
+
#
|
|
152
|
+
def env=(env)
|
|
153
|
+
raise ArgumentError.new 'Environment must be a Hash.' unless hash === env
|
|
154
|
+
@env = env
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
#@!group Receiver
|
|
158
|
+
|
|
159
|
+
# Current receiver. Defaults to `MultiProcess::Logger.global`.
|
|
160
|
+
#
|
|
161
|
+
attr_reader :receiver
|
|
162
|
+
|
|
163
|
+
# Set receiver that should receive process output.
|
|
164
|
+
#
|
|
165
|
+
def receiver=(receiver)
|
|
166
|
+
if @receiver
|
|
167
|
+
childprocess.io.stdout.close
|
|
168
|
+
childprocess.io.stderr.close
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
childprocess.io.stdout = receiver.pipe(self, :out) if receiver
|
|
172
|
+
childprocess.io.stderr = receiver.pipe(self, :err) if receiver
|
|
173
|
+
@receiver = receiver
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# Create child process.
|
|
179
|
+
#
|
|
180
|
+
def create_childprocess(*args)
|
|
181
|
+
ChildProcess.new *args.flatten
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Start child process.
|
|
185
|
+
#
|
|
186
|
+
# Can be used to hook in subclasses and modules.
|
|
187
|
+
#
|
|
188
|
+
def start_childprocess
|
|
189
|
+
env.each{|k, v| childprocess.environment[k.to_s] = v.to_s }
|
|
190
|
+
|
|
191
|
+
Dir.chdir(dir) do
|
|
192
|
+
if clean_env?
|
|
193
|
+
Bundler.with_clean_env { childprocess.start }
|
|
194
|
+
else
|
|
195
|
+
childprocess.start
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|