multi_process 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -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,13 @@
1
+ module MultiProcess
2
+
3
+ # Receiver implementation that does nothing on every input.
4
+ #
5
+ class NilReceiver < Receiver
6
+
7
+ # Do nothing.
8
+ #
9
+ def received(process, name, message)
10
+ nil
11
+ end
12
+ end
13
+ 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