pipemaster 0.3.1

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/CHANGELOG ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://gemcutter.org"
2
+ gem "gemcutter"
3
+ gem "rake"
4
+ gem "unicorn", "0.96.1"
5
+ gem "yard"
data/LICENSE ADDED
@@ -0,0 +1,55 @@
1
+ Pipemaster is copyrighted free software by all contributors, see logs in
2
+ revision control for names and email addresses of all of them. You can
3
+ redistribute it and/or modify it under either the terms of the
4
+ {GPL2}[http://www.gnu.org/licenses/gpl-2.0.txt] (see link:COPYING) or
5
+ the conditions below:
6
+
7
+ 1. You may make and give away verbatim copies of the source form of the
8
+ software without restriction, provided that you duplicate all of the
9
+ original copyright notices and associated disclaimers.
10
+
11
+ 2. You may modify your copy of the software in any way, provided that
12
+ you do at least ONE of the following:
13
+
14
+ a) place your modifications in the Public Domain or otherwise make them
15
+ Freely Available, such as by posting said modifications to Usenet or an
16
+ equivalent medium, or by allowing the author to include your
17
+ modifications in the software.
18
+
19
+ b) use the modified software only within your corporation or
20
+ organization.
21
+
22
+ c) rename any non-standard executables so the names do not conflict with
23
+ standard executables, which must also be provided.
24
+
25
+ d) make other distribution arrangements with the author.
26
+
27
+ 3. You may distribute the software in object code or executable
28
+ form, provided that you do at least ONE of the following:
29
+
30
+ a) distribute the executables and library files of the software,
31
+ together with instructions (in the manual page or equivalent) on where
32
+ to get the original distribution.
33
+
34
+ b) accompany the distribution with the machine-readable source of the
35
+ software.
36
+
37
+ c) give non-standard executables non-standard names, with
38
+ instructions on where to get the original software distribution.
39
+
40
+ d) make other distribution arrangements with the author.
41
+
42
+ 4. You may modify and include the part of the software into any other
43
+ software (possibly commercial). But some files in the distribution
44
+ are not written by the author, so that they are not under this terms.
45
+
46
+ 5. The scripts and library files supplied as input to or produced as
47
+ output from the software do not automatically fall under the
48
+ copyright of the software, but belong to whomever generated them,
49
+ and may be sold commercially, and may be aggregated with this
50
+ software.
51
+
52
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
53
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
54
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
55
+ PURPOSE.
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ You pipe from here, to a process that runs over there. Pipemaster forks,
2
+ redirects and runs your command.
3
+
4
+ == Why
5
+
6
+ I've got a short task I want to perform. To validate an incoming email, parse
7
+ the message and store the results in the database(*). I setup Postfix to pipe
8
+ incoming emails into a Ruby script.
9
+
10
+ Ruby processes are fairly cheap, until you get into loading the mail library,
11
+ the database library, the ORM, the application logic, the ... you get the
12
+ picture.
13
+
14
+ I use Pipemaster to fire up the main process once, and the Pipemaster client to
15
+ run commands on that server. Still have to make sure the code is light and
16
+ fast, but I did eliminate the significant initialization overhead.
17
+
18
+ As you can guess, Pipemaster supports piping input and output streams, and uses
19
+ forking (sorry Windows; for JRuby see Nailgun).
20
+
21
+ * Processing emails as they come allows the application to reject unauthorized
22
+ senders immediately by replying with an SMTP code. The alternative, accepting
23
+ the email and later on sending a bounce, leads to backscatter (see
24
+ http://en.wikipedia.org/wiki/Backscatter_(e-mail)).
25
+
26
+
27
+ == Using Pipemaster
28
+
29
+ Step 1: Create a Pipemaster file. For example:
30
+
31
+ #!highlight/ruby
32
+ command :echo do |*args|
33
+ first, rest = $stdin.read.split
34
+ $stdout << [first, args, rest].flatten.join(" ")
35
+ end
36
+
37
+ Step 2: Fire up the Pipemaster server:
38
+
39
+ $ pipemaster --server
40
+ I, [2010-02-18T12:57:57.739230 #5460] INFO -- : master process ready
41
+ I, [2010-02-18T12:57:57.739606 #5460] INFO -- : listening on addr=127.0.0.1:7887 fd=3
42
+
43
+ Step 3: For a new shell, execute a command:
44
+
45
+ $ echo "Stand down!" | ruby -Ilib bin/pipemaster echo upside
46
+ Stand upside down!
47
+
48
+
49
+ == License
50
+
51
+ Pipemaster is copyright of Assaf Arkin. It is based on the awesome Unicorn Web
52
+ server and therefore uses the same license.
53
+
54
+ Unicorn is copyright 2009 by all contributors (see logs in git).
55
+ It is based on Mongrel and carries the same license.
56
+
57
+ Mongrel is copyright 2007 Zed A. Shaw and contributors. It is licensed
58
+ under the Ruby license and the GPL2. See the included LICENSE file for
59
+ details.
60
+
61
+ Pipemaster is 100% Free Software.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require "rake/testtask"
2
+
3
+ spec = Gem::Specification.load(File.expand_path("pipemaster.gemspec", File.dirname(__FILE__)))
4
+
5
+ desc "Push new release to gemcutter and git tag"
6
+ task :push do
7
+ sh "git push"
8
+ puts "Tagging version #{spec.version} .."
9
+ sh "git tag v#{spec.version}"
10
+ sh "git push --tag"
11
+ puts "Building and pushing gem .."
12
+ sh "gem build #{spec.name}.gemspec"
13
+ sh "gem push #{spec.name}-#{spec.version}.gem"
14
+ end
15
+
16
+ desc "Install #{spec.name} locally"
17
+ task :install do
18
+ sh "gem build #{spec.name}.gemspec"
19
+ sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir])
20
+ sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem"
21
+ end
22
+
23
+
24
+ task :default=>:test
25
+ desc "Run all tests using Redis mock (also default task)"
26
+ Rake::TestTask.new do |task|
27
+ task.test_files = FileList['test/unit/test_*.rb']
28
+ if Rake.application.options.trace
29
+ #task.warning = true
30
+ task.verbose = true
31
+ elsif Rake.application.options.silent
32
+ task.ruby_opts << "-W0"
33
+ else
34
+ task.verbose = true
35
+ end
36
+ end
37
+
38
+
39
+ begin
40
+ require "yard"
41
+ YARD::Rake::YardocTask.new(:yardoc) do |task|
42
+ task.files = FileList["lib/**/*.rb"]
43
+ task.options = "--output", "html", "--title", "Pipemaster #{spec.version}", "--main", "README.rdoc", "--files", "CHANGELOG"
44
+ end
45
+ rescue LoadError
46
+ end
47
+
48
+ desc "Create documentation in html directory"
49
+ task :docs=>[:yardoc]
50
+ desc "Remove temporary files and directories"
51
+ task(:clobber) { rm_rf "html" }
data/bin/pipemaster ADDED
@@ -0,0 +1,100 @@
1
+ #!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
2
+ # -*- encoding: binary -*-
3
+ require "pipemaster"
4
+ require "optparse"
5
+
6
+ ENV["PIPE_ENV"] ||= "development"
7
+ server = false
8
+ daemonize = false
9
+ listeners = []
10
+ options = { :listeners => listeners }
11
+
12
+ opts = OptionParser.new("", 24, ' ') do |opts|
13
+ opts.banner = "Usage: #{File.basename($0)}\n" \
14
+ "[ruby options] [pipemaster options] command [args]\n" \
15
+ "[ruby options] [pipemaster options] --server [Pipefile]"
16
+
17
+ opts.separator "Ruby options:"
18
+
19
+ lineno = 1
20
+ opts.on("-e", "--eval LINE", "evaluate a LINE of code") do |line|
21
+ eval line, TOPLEVEL_BINDING, "-e", lineno
22
+ lineno += 1
23
+ end
24
+
25
+ opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") do
26
+ $DEBUG = true
27
+ end
28
+
29
+ opts.on("-w", "--warn", "turn warnings on for your script") do
30
+ $-w = true
31
+ end
32
+
33
+ opts.on("-I", "--include PATH",
34
+ "specify $LOAD_PATH (may be used more than once)") do |path|
35
+ $LOAD_PATH.unshift(*path.split(/:/))
36
+ end
37
+
38
+ opts.on("-r", "--require LIBRARY",
39
+ "require the library, before executing your script") do |library|
40
+ require library
41
+ end
42
+
43
+ opts.separator "Pipemaster options:"
44
+
45
+ opts.on("-s", "--socket {HOST:PORT|PATH}",
46
+ "communicate on HOST:PORT or PATH",
47
+ "for server, this may be specified multiple times",
48
+ "(default: #{Pipemaster::DEFAULT_LISTEN})") do |address|
49
+ listeners << address
50
+ end
51
+
52
+ opts.on("-E", "--env ENVIRONMENT",
53
+ "use ENVIRONMENT for defaults (default: development)") do |e|
54
+ ENV["PIPE_ENV"] = e
55
+ end
56
+
57
+ opts.on("-S", "--server", "run as server (default: client)") do |s|
58
+ server = s ? true : false
59
+ end
60
+
61
+ opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
62
+ daemonize = d ? true : false
63
+ end
64
+
65
+ opts.separator "Common options:"
66
+
67
+ opts.on_tail("-h", "--help", "Show this message") do
68
+ puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
69
+ exit
70
+ end
71
+
72
+ opts.on_tail("-v", "--version", "Show version") do
73
+ puts "Pipemaster v#{Pipemaster::VERSION}"
74
+ exit
75
+ end
76
+
77
+ opts.parse! ARGV
78
+ end
79
+
80
+ if server
81
+
82
+ gem "unicorn"
83
+ require "pipemaster/server"
84
+ require "unicorn/launcher"
85
+ config = ARGV[0] || "Pipefile"
86
+ abort "configuration file #{config} not found" unless File.exist?(config)
87
+ options[:config_file] = config
88
+ options[:working_directory] = File.dirname(config)
89
+ Unicorn::Launcher.daemonize!(options) if daemonize
90
+ Pipemaster.run(options)
91
+
92
+ else
93
+
94
+ require "pipemaster/client"
95
+ client = Pipemaster::Client.new(listeners.first)
96
+ client.input = $stdin unless $stdin.isatty
97
+ client.output = $stdout
98
+ exit client.request(*ARGV)
99
+
100
+ end
data/lib/pipemaster.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Pipemaster
2
+ # Version number.
3
+ VERSION = Gem::Specification.load(File.expand_path("../pipemaster.gemspec", File.dirname(__FILE__))).version.to_s
4
+
5
+ DEFAULT_HOST = "127.0.0.1"
6
+ DEFAULT_PORT = 7887
7
+ DEFAULT_LISTEN = "#{DEFAULT_HOST}:#{DEFAULT_PORT}"
8
+ end
@@ -0,0 +1,121 @@
1
+ require "socket"
2
+
3
+ module Pipemaster
4
+ # Pipemaster client. Use this to send commands to the Pipemaster server.
5
+ #
6
+ # For example:
7
+ # c = Pipemaster::Client.new
8
+ # c.request "motd"
9
+ # puts c.output.string
10
+ # => "Toilet out of order please use floor below."
11
+ #
12
+ # c = Pipemaster::Client.new(9988)
13
+ # c.input = File.open("image.png")
14
+ # c.output = File.open("thumbnail.png", "w")
15
+ # c.request "transform", "thumbnail"
16
+ class Client
17
+ BUFFER_SIZE = 16 * 1024
18
+
19
+ # Address can be "x.x.x.x:port", ":port" or UNIX socket file name.
20
+ def initialize(address)
21
+ @address = expand_addr(address || DEFAULT_LISTEN)
22
+ end
23
+
24
+ # Set this to supply an input stream (for commands that read from $stdin).
25
+ attr_accessor :input
26
+
27
+ # Set this to supply an output stream, or read the output (defaults to
28
+ # StringIO).
29
+ attr_accessor :output
30
+
31
+ # Make a request. First argument is the command name. All other arguments
32
+ # are optional. Returns the exit code (usually 0). Will raise IOError if
33
+ # it can't talk to the server, or the server closed the connection
34
+ # prematurely.
35
+ def request(command, *args)
36
+ # Connect and send arguments.
37
+ socket = connect(@address)
38
+ socket.sync = true
39
+ header = ([command] + args).join("\0")
40
+ socket << [header.size].pack("N") << header
41
+ socket.flush
42
+
43
+ @output ||= StringIO.new
44
+
45
+ # If there's input, stream it over to the socket, while streaming the
46
+ # response back.
47
+ inputbuf, stdoutbuf = "", ""
48
+ if @input
49
+ while selected = select([@input, socket])
50
+ begin
51
+ if selected.first.include?(@input)
52
+ @input.readpartial(BUFFER_SIZE, inputbuf)
53
+ socket.write inputbuf
54
+ elsif selected.first.include?(socket)
55
+ raise IOError, "Server closed socket" if socket.eof?
56
+ @output.write stdoutbuf if stdoutbuf
57
+ stdoutbuf = socket.readpartial(BUFFER_SIZE)
58
+ end
59
+ rescue EOFError
60
+ break
61
+ end
62
+ end
63
+ end
64
+ socket.close_write # tell other side there's no more input
65
+
66
+ # Read remaining response and stream to output. Remember that very last
67
+ # byte is return code.
68
+ while selected = select([socket])
69
+ break if socket.eof?
70
+ @output.write stdoutbuf if stdoutbuf
71
+ stdoutbuf = socket.readpartial(BUFFER_SIZE)
72
+ end
73
+ if stdoutbuf && stdoutbuf.size > 0
74
+ status = stdoutbuf[-1]
75
+ @output.write stdoutbuf[0..-2]
76
+ return status.ord
77
+ else
78
+ raise IOError, "Server closed socket" if socket.eof?
79
+ end
80
+ ensure
81
+ socket.close rescue nil
82
+ end
83
+
84
+ private
85
+
86
+ def connect(address)
87
+ if address[0] == ?/
88
+ UNIXSocket.open(address)
89
+ elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/
90
+ TCPSocket.open($1, $2.to_i)
91
+ else
92
+ raise ArgumentError, "Don't know how to bind: #{address}"
93
+ end
94
+ end
95
+
96
+ # expands "unix:path/to/foo" to a socket relative to the current path
97
+ # expands pathnames of sockets if relative to "~" or "~username"
98
+ # expands "*:port and ":port" to "127.0.0.1:port"
99
+ def expand_addr(address) #:nodoc
100
+ return "0.0.0.0:#{address}" if Integer === address
101
+ return address unless String === address
102
+
103
+ case address
104
+ when %r{\Aunix:(.*)\z}
105
+ File.expand_path($1)
106
+ when %r{\A~}
107
+ File.expand_path(address)
108
+ when %r{\A(?:\*:)?(\d+)\z}
109
+ "127.0.0.1:#$1"
110
+ when %r{\A(.*):(\d+)\z}
111
+ # canonicalize the name
112
+ packed = Socket.pack_sockaddr_in($2.to_i, $1)
113
+ Socket.unpack_sockaddr_in(packed).reverse!.join(':')
114
+ else
115
+ address
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,228 @@
1
+ require 'socket'
2
+ require 'logger'
3
+
4
+ module Pipemaster
5
+
6
+ # Implements a simple DSL for configuring a Pipemaster server.
7
+ class Configurator < Unicorn::Configurator
8
+
9
+ # Default settings for Pipemaster
10
+ DEFAULTS = {
11
+ :logger => Logger.new($stderr),
12
+ :after_fork => lambda { |server, worker|
13
+ server.logger.info("spawned pid=#{$$}")
14
+ },
15
+ :before_fork => lambda { |server, worker|
16
+ server.logger.info("spawning...")
17
+ },
18
+ :before_exec => lambda { |server|
19
+ server.logger.info("forked child re-executing...")
20
+ },
21
+ :pid => nil,
22
+ :commands => {},
23
+ :timeout => 60
24
+ }
25
+
26
+ def initialize(defaults = {}) #:nodoc:
27
+ self.set = Hash.new(:unset)
28
+ use_defaults = defaults.delete(:use_defaults)
29
+ self.config_file = defaults.delete(:config_file)
30
+ set.merge!(DEFAULTS) if use_defaults
31
+ defaults.each { |key, value| self.send(key, value) }
32
+ Hash === set[:listener_opts] or
33
+ set[:listener_opts] = Hash.new { |hash,key| hash[key] = {} }
34
+ Array === set[:listeners] or set[:listeners] = []
35
+ reload
36
+ end
37
+
38
+ # Sets object to the +new+ Logger-like object. The new logger-like
39
+ # object must respond to the following methods:
40
+ # +debug+, +info+, +warn+, +error+, +fatal+, +close+
41
+ def logger(new)
42
+ super
43
+ end
44
+
45
+ def commands(hash)
46
+ set[:commands] = hash
47
+ end
48
+
49
+ # Defines a command.
50
+ def command(name, &block)
51
+ set[:commands][name.to_sym] = block
52
+ end
53
+
54
+ autoload :Etc, 'etc'
55
+ # Change user/group ownership of the master process.
56
+ def user(user, group = nil)
57
+ # we do not protect the caller, checking Process.euid == 0 is
58
+ # insufficient because modern systems have fine-grained
59
+ # capabilities. Let the caller handle any and all errors.
60
+ uid = Etc.getpwnam(user).uid
61
+ gid = Etc.getgrnam(group).gid if group
62
+ Unicorn::Util.chown_logs(uid, gid)
63
+ if gid && Process.egid != gid
64
+ Process.initgroups(user, gid)
65
+ Process::GID.change_privilege(gid)
66
+ end
67
+ Process.euid != uid and Process::UID.change_privilege(uid)
68
+ end
69
+
70
+ # Sets the working directory for Pipemaster. Defaults to the location
71
+ # of the Pipefile. This may be a symlink.
72
+ def working_directory(path)
73
+ # just let chdir raise errors
74
+ path = File.expand_path(path)
75
+ if config_file &&
76
+ config_file[0] != ?/ &&
77
+ ! test(?r, "#{path}/#{config_file}")
78
+ raise ArgumentError,
79
+ "pipefile=#{config_file} would not be accessible in" \
80
+ " working_directory=#{path}"
81
+ end
82
+ Dir.chdir(path)
83
+ Server::START_CTX[:cwd] = ENV["PWD"] = path
84
+ end
85
+
86
+ # Sets after_fork hook to a given block. This block will be called by
87
+ # the worker after forking.
88
+ def after_fork(*args, &block)
89
+ set_hook(:after_fork, block_given? ? block : args[0])
90
+ end
91
+
92
+ # Sets before_fork hook to a given block. This block will be called by
93
+ # the worker before forking.
94
+ def before_fork(*args, &block)
95
+ set_hook(:before_fork, block_given? ? block : args[0])
96
+ end
97
+
98
+ # Sets the before_exec hook to a given Proc object. This
99
+ # Proc object will be called by the master process right
100
+ # before exec()-ing the new binary. This is useful
101
+ # for freeing certain OS resources that you do NOT wish to
102
+ # share with the reexeced child process.
103
+ # There is no corresponding after_exec hook (for obvious reasons).
104
+ def before_exec(*args, &block)
105
+ set_hook(:before_exec, block_given? ? block : args[0], 1)
106
+ end
107
+
108
+ # Sets listeners to the given +addresses+, replacing or augmenting the
109
+ # current set. This is for internal API use only, do not use it in your
110
+ # Pipemaster config file. Use listen instead.
111
+ def listeners(addresses) # :nodoc:
112
+ Array === addresses or addresses = Array(addresses)
113
+ addresses.map! { |addr| expand_addr(addr) }
114
+ set[:listeners] = addresses
115
+ end
116
+
117
+ # Does nothing interesting for now.
118
+ def timeout(seconds)
119
+ end
120
+
121
+ # Does nothing interesting for now.
122
+ def worker_processes(nr)
123
+ end
124
+
125
+ # Adds an +address+ to the existing listener set.
126
+ #
127
+ # The following options may be specified (but are generally not needed):
128
+ #
129
+ # +:backlog+: this is the backlog of the listen() syscall.
130
+ #
131
+ # Some operating systems allow negative values here to specify the
132
+ # maximum allowable value. In most cases, this number is only
133
+ # recommendation and there are other OS-specific tunables and
134
+ # variables that can affect this number. See the listen(2)
135
+ # syscall documentation of your OS for the exact semantics of
136
+ # this.
137
+ #
138
+ # If you are running pipemaster on multiple machines, lowering this number
139
+ # can help your load balancer detect when a machine is overloaded
140
+ # and give requests to a different machine.
141
+ #
142
+ # Default: 1024
143
+ #
144
+ # +:rcvbuf+, +:sndbuf+: maximum receive and send buffer sizes of sockets
145
+ #
146
+ # These correspond to the SO_RCVBUF and SO_SNDBUF settings which
147
+ # can be set via the setsockopt(2) syscall. Some kernels
148
+ # (e.g. Linux 2.4+) have intelligent auto-tuning mechanisms and
149
+ # there is no need (and it is sometimes detrimental) to specify them.
150
+ #
151
+ # See the socket API documentation of your operating system
152
+ # to determine the exact semantics of these settings and
153
+ # other operating system-specific knobs where they can be
154
+ # specified.
155
+ #
156
+ # Defaults: operating system defaults
157
+ #
158
+ # +:tcp_nodelay+: disables Nagle's algorithm on TCP sockets
159
+ #
160
+ # This has no effect on UNIX sockets.
161
+ #
162
+ # Default: operating system defaults (usually Nagle's algorithm enabled)
163
+ #
164
+ # +:tcp_nopush+: enables TCP_CORK in Linux or TCP_NOPUSH in FreeBSD
165
+ #
166
+ # This will prevent partial TCP frames from being sent out.
167
+ # Enabling +tcp_nopush+ is generally not needed or recommended as
168
+ # controlling +tcp_nodelay+ already provides sufficient latency
169
+ # reduction whereas Pipemaster does not know when the best times are
170
+ # for flushing corked sockets.
171
+ #
172
+ # This has no effect on UNIX sockets.
173
+ #
174
+ # +:tries+: times to retry binding a socket if it is already in use
175
+ #
176
+ # A negative number indicates we will retry indefinitely, this is
177
+ # useful for migrations and upgrades when individual workers
178
+ # are binding to different ports.
179
+ #
180
+ # Default: 5
181
+ #
182
+ # +:delay+: seconds to wait between successive +tries+
183
+ #
184
+ # Default: 0.5 seconds
185
+ #
186
+ # +:umask+: sets the file mode creation mask for UNIX sockets
187
+ #
188
+ # Typically UNIX domain sockets are created with more liberal
189
+ # file permissions than the rest of the application. By default,
190
+ # we create UNIX domain sockets to be readable and writable by
191
+ # all local users to give them the same accessibility as
192
+ # locally-bound TCP listeners.
193
+ #
194
+ # This has no effect on TCP listeners.
195
+ #
196
+ # Default: 0 (world read/writable)
197
+ def listen(address, opt = {})
198
+ super
199
+ end
200
+
201
+ # Sets the +path+ for the PID file of the Pipemaster master process
202
+ def pid(path)
203
+ set_path(:pid, path)
204
+ end
205
+
206
+ # Allow redirecting $stderr to a given path. Unlike doing this from
207
+ # the shell, this allows the Pipemaster process to know the path its
208
+ # writing to and rotate the file if it is used for logging. The
209
+ # file will be opened with the File::APPEND flag and writes
210
+ # synchronized to the kernel (but not necessarily to _disk_) so
211
+ # multiple processes can safely append to it.
212
+ def stderr_path(path)
213
+ set_path(:stderr_path, path)
214
+ end
215
+
216
+ # Same as stderr_path, except for $stdout
217
+ def stdout_path(path)
218
+ set_path(:stdout_path, path)
219
+ end
220
+
221
+ private
222
+
223
+ def preload_app(bool)
224
+ end
225
+
226
+ end
227
+ end
228
+