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.
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