multi_process 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|