puma 4.3.6 → 5.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +60 -12
- data/LICENSE +23 -20
- data/README.md +17 -11
- data/docs/deployment.md +3 -1
- data/docs/fork_worker.md +31 -0
- data/docs/jungle/README.md +13 -0
- data/{tools → docs}/jungle/rc.d/README.md +0 -0
- data/{tools → docs}/jungle/rc.d/puma +0 -0
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/{tools → docs}/jungle/upstart/README.md +0 -0
- data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
- data/{tools → docs}/jungle/upstart/puma.conf +0 -0
- data/docs/signals.md +1 -0
- data/docs/systemd.md +1 -63
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/extconf.rb +4 -3
- data/ext/puma_http11/http11_parser.c +1 -3
- data/ext/puma_http11/http11_parser.rl +1 -3
- data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
- data/ext/puma_http11/puma_http11.c +2 -38
- data/lib/puma.rb +4 -0
- data/lib/puma/app/status.rb +16 -5
- data/lib/puma/binder.rb +62 -60
- data/lib/puma/cli.rb +7 -15
- data/lib/puma/client.rb +30 -15
- data/lib/puma/cluster.rb +179 -74
- data/lib/puma/configuration.rb +30 -42
- data/lib/puma/const.rb +2 -3
- data/lib/puma/control_cli.rb +27 -17
- data/lib/puma/detect.rb +8 -0
- data/lib/puma/dsl.rb +70 -34
- data/lib/puma/io_buffer.rb +9 -2
- data/lib/puma/jruby_restart.rb +0 -58
- data/lib/puma/launcher.rb +41 -29
- data/lib/puma/minissl.rb +13 -8
- data/lib/puma/null_io.rb +1 -1
- data/lib/puma/plugin.rb +1 -10
- data/lib/puma/rack/builder.rb +0 -4
- data/lib/puma/reactor.rb +6 -1
- data/lib/puma/runner.rb +5 -34
- data/lib/puma/server.rb +62 -177
- data/lib/puma/single.rb +7 -64
- data/lib/puma/state_file.rb +5 -2
- data/lib/puma/thread_pool.rb +85 -47
- data/lib/rack/handler/puma.rb +1 -3
- data/tools/{docker/Dockerfile → Dockerfile} +0 -0
- metadata +19 -23
- data/docs/tcp_mode.md +0 -96
- data/ext/puma_http11/io_buffer.c +0 -155
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
- data/lib/puma/tcp_logger.rb +0 -41
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
data/lib/puma/configuration.rb
CHANGED
@@ -54,9 +54,7 @@ module Puma
|
|
54
54
|
attr_reader :user_options, :file_options, :default_options
|
55
55
|
|
56
56
|
def [](key)
|
57
|
-
|
58
|
-
return file_options[key] if file_options.key?(key)
|
59
|
-
return default_options[key] if default_options.key?(key)
|
57
|
+
fetch(key)
|
60
58
|
end
|
61
59
|
|
62
60
|
def []=(key, value)
|
@@ -64,7 +62,11 @@ module Puma
|
|
64
62
|
end
|
65
63
|
|
66
64
|
def fetch(key, default_value = nil)
|
67
|
-
|
65
|
+
return user_options[key] if user_options.key?(key)
|
66
|
+
return file_options[key] if file_options.key?(key)
|
67
|
+
return default_options[key] if default_options.key?(key)
|
68
|
+
|
69
|
+
default_value
|
68
70
|
end
|
69
71
|
|
70
72
|
def all_of(key)
|
@@ -106,7 +108,7 @@ module Puma
|
|
106
108
|
#
|
107
109
|
# It also handles loading plugins.
|
108
110
|
#
|
109
|
-
# > Note: `:port` and `:host` are not valid keys. By
|
111
|
+
# > Note: `:port` and `:host` are not valid keys. By the time they make it to the
|
110
112
|
# configuration options they are expected to be incorporated into a `:binds` key.
|
111
113
|
# Under the hood the DSL maps `port` and `host` calls to `:binds`
|
112
114
|
#
|
@@ -137,6 +139,10 @@ module Puma
|
|
137
139
|
@file_dsl = DSL.new(@options.file_options, self)
|
138
140
|
@default_dsl = DSL.new(@options.default_options, self)
|
139
141
|
|
142
|
+
if !@options[:prune_bundler]
|
143
|
+
default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
|
144
|
+
end
|
145
|
+
|
140
146
|
if block
|
141
147
|
configure(&block)
|
142
148
|
end
|
@@ -167,22 +173,25 @@ module Puma
|
|
167
173
|
self
|
168
174
|
end
|
169
175
|
|
176
|
+
def default_max_threads
|
177
|
+
Puma.mri? ? 5 : 16
|
178
|
+
end
|
179
|
+
|
170
180
|
def puma_default_options
|
171
181
|
{
|
172
|
-
:min_threads => 0,
|
173
|
-
:max_threads =>
|
182
|
+
:min_threads => Integer(ENV['PUMA_MIN_THREADS'] || ENV['MIN_THREADS'] || 0),
|
183
|
+
:max_threads => Integer(ENV['PUMA_MAX_THREADS'] || ENV['MAX_THREADS'] || default_max_threads),
|
174
184
|
:log_requests => false,
|
175
185
|
:debug => false,
|
176
186
|
:binds => ["tcp://#{DefaultTCPHost}:#{DefaultTCPPort}"],
|
177
|
-
:workers => 0,
|
178
|
-
:daemon => false,
|
187
|
+
:workers => Integer(ENV['WEB_CONCURRENCY'] || 0),
|
179
188
|
:mode => :http,
|
180
189
|
:worker_timeout => DefaultWorkerTimeout,
|
181
190
|
:worker_boot_timeout => DefaultWorkerTimeout,
|
182
191
|
:worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
|
183
192
|
:remote_address => :socket,
|
184
193
|
:tag => method(:infer_tag),
|
185
|
-
:environment => -> { ENV['RACK_ENV'] || "development" },
|
194
|
+
:environment => -> { ENV['RACK_ENV'] || ENV['RAILS_ENV'] || "development" },
|
186
195
|
:rackup => DefaultRackup,
|
187
196
|
:logger => STDOUT,
|
188
197
|
:persistent_timeout => Const::PERSISTENT_TIMEOUT,
|
@@ -245,14 +254,6 @@ module Puma
|
|
245
254
|
def app
|
246
255
|
found = options[:app] || load_rackup
|
247
256
|
|
248
|
-
if @options[:mode] == :tcp
|
249
|
-
require 'puma/tcp_logger'
|
250
|
-
|
251
|
-
logger = @options[:logger]
|
252
|
-
quiet = !@options[:log_requests]
|
253
|
-
return TCPLogger.new(logger, found, quiet)
|
254
|
-
end
|
255
|
-
|
256
257
|
if @options[:log_requests]
|
257
258
|
require 'puma/commonlogger'
|
258
259
|
logger = @options[:logger]
|
@@ -275,8 +276,15 @@ module Puma
|
|
275
276
|
@plugins.create name
|
276
277
|
end
|
277
278
|
|
278
|
-
def run_hooks(key, arg)
|
279
|
-
@options.all_of(key).each
|
279
|
+
def run_hooks(key, arg, events)
|
280
|
+
@options.all_of(key).each do |b|
|
281
|
+
begin
|
282
|
+
b.call arg
|
283
|
+
rescue => e
|
284
|
+
events.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
|
285
|
+
events.debug e.backtrace.join("\n")
|
286
|
+
end
|
287
|
+
end
|
280
288
|
end
|
281
289
|
|
282
290
|
def self.temp_path
|
@@ -332,29 +340,9 @@ module Puma
|
|
332
340
|
end
|
333
341
|
|
334
342
|
def self.random_token
|
335
|
-
|
336
|
-
require 'openssl'
|
337
|
-
rescue LoadError
|
338
|
-
end
|
339
|
-
|
340
|
-
count = 16
|
341
|
-
|
342
|
-
bytes = nil
|
343
|
-
|
344
|
-
if defined? OpenSSL::Random
|
345
|
-
bytes = OpenSSL::Random.random_bytes(count)
|
346
|
-
elsif File.exist?("/dev/urandom")
|
347
|
-
File.open('/dev/urandom') { |f| bytes = f.read(count) }
|
348
|
-
end
|
349
|
-
|
350
|
-
if bytes
|
351
|
-
token = "".dup
|
352
|
-
bytes.each_byte { |b| token << b.to_s(16) }
|
353
|
-
else
|
354
|
-
token = (0..count).to_a.map { rand(255).to_s(16) }.join
|
355
|
-
end
|
343
|
+
require 'securerandom' unless defined?(SecureRandom)
|
356
344
|
|
357
|
-
|
345
|
+
SecureRandom.hex(16)
|
358
346
|
end
|
359
347
|
end
|
360
348
|
end
|
data/lib/puma/const.rb
CHANGED
@@ -100,8 +100,8 @@ module Puma
|
|
100
100
|
# too taxing on performance.
|
101
101
|
module Const
|
102
102
|
|
103
|
-
PUMA_VERSION = VERSION = "
|
104
|
-
CODE_NAME = "
|
103
|
+
PUMA_VERSION = VERSION = "5.0.0.beta1".freeze
|
104
|
+
CODE_NAME = "Spoony Bard".freeze
|
105
105
|
PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
|
106
106
|
|
107
107
|
FAST_TRACK_KA_TIMEOUT = 0.2
|
@@ -175,7 +175,6 @@ module Puma
|
|
175
175
|
PORT_443 = "443".freeze
|
176
176
|
LOCALHOST = "localhost".freeze
|
177
177
|
LOCALHOST_IP = "127.0.0.1".freeze
|
178
|
-
LOCALHOST_ADDR = "127.0.0.1:0".freeze
|
179
178
|
|
180
179
|
SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
|
181
180
|
HTTP_11 = "HTTP/1.1".freeze
|
data/lib/puma/control_cli.rb
CHANGED
@@ -11,7 +11,8 @@ require 'socket'
|
|
11
11
|
module Puma
|
12
12
|
class ControlCLI
|
13
13
|
|
14
|
-
COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats}
|
14
|
+
COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces refork}
|
15
|
+
PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}
|
15
16
|
|
16
17
|
def initialize(argv, stdout=STDOUT, stderr=STDERR)
|
17
18
|
@state = nil
|
@@ -22,7 +23,7 @@ module Puma
|
|
22
23
|
@control_auth_token = nil
|
23
24
|
@config_file = nil
|
24
25
|
@command = nil
|
25
|
-
@environment = ENV['RACK_ENV']
|
26
|
+
@environment = ENV['RACK_ENV'] || ENV['RAILS_ENV']
|
26
27
|
|
27
28
|
@argv = argv.dup
|
28
29
|
@stdout = stdout
|
@@ -81,6 +82,15 @@ module Puma
|
|
81
82
|
|
82
83
|
@command = argv.shift
|
83
84
|
|
85
|
+
# check presence of command
|
86
|
+
unless @command
|
87
|
+
raise "Available commands: #{COMMANDS.join(", ")}"
|
88
|
+
end
|
89
|
+
|
90
|
+
unless COMMANDS.include? @command
|
91
|
+
raise "Invalid command: #{@command}"
|
92
|
+
end
|
93
|
+
|
84
94
|
unless @config_file == '-'
|
85
95
|
environment = @environment || 'development'
|
86
96
|
|
@@ -99,16 +109,6 @@ module Puma
|
|
99
109
|
@pidfile ||= config.options[:pidfile]
|
100
110
|
end
|
101
111
|
end
|
102
|
-
|
103
|
-
# check present of command
|
104
|
-
unless @command
|
105
|
-
raise "Available commands: #{COMMANDS.join(", ")}"
|
106
|
-
end
|
107
|
-
|
108
|
-
unless COMMANDS.include? @command
|
109
|
-
raise "Invalid command: #{@command}"
|
110
|
-
end
|
111
|
-
|
112
112
|
rescue => e
|
113
113
|
@stdout.puts e.message
|
114
114
|
exit 1
|
@@ -145,8 +145,9 @@ module Puma
|
|
145
145
|
require 'openssl'
|
146
146
|
OpenSSL::SSL::SSLSocket.new(
|
147
147
|
TCPSocket.new(uri.host, uri.port),
|
148
|
-
OpenSSL::SSL::SSLContext.new
|
149
|
-
|
148
|
+
OpenSSL::SSL::SSLContext.new)
|
149
|
+
.tap { |ssl| ssl.sync_close = true } # default is false
|
150
|
+
.tap(&:connect)
|
150
151
|
when "tcp"
|
151
152
|
TCPSocket.new uri.host, uri.port
|
152
153
|
when "unix"
|
@@ -187,10 +188,16 @@ module Puma
|
|
187
188
|
end
|
188
189
|
|
189
190
|
message "Command #{@command} sent success"
|
190
|
-
message response.last if @command
|
191
|
+
message response.last if PRINTABLE_COMMANDS.include?(@command)
|
191
192
|
end
|
192
193
|
ensure
|
193
|
-
|
194
|
+
if server
|
195
|
+
if uri.scheme == "ssl"
|
196
|
+
server.sysclose
|
197
|
+
else
|
198
|
+
server.close unless server.closed?
|
199
|
+
end
|
200
|
+
end
|
194
201
|
end
|
195
202
|
|
196
203
|
def send_signal
|
@@ -231,6 +238,9 @@ module Puma
|
|
231
238
|
|
232
239
|
return
|
233
240
|
|
241
|
+
when "refork"
|
242
|
+
Process.kill "SIGURG", @pid
|
243
|
+
|
234
244
|
else
|
235
245
|
return
|
236
246
|
end
|
@@ -262,7 +272,7 @@ module Puma
|
|
262
272
|
exit 1
|
263
273
|
end
|
264
274
|
|
265
|
-
|
275
|
+
private
|
266
276
|
def start
|
267
277
|
require 'puma/cli'
|
268
278
|
|
data/lib/puma/detect.rb
CHANGED
data/lib/puma/dsl.rb
CHANGED
@@ -210,20 +210,6 @@ module Puma
|
|
210
210
|
@options[:clean_thread_locals] = which
|
211
211
|
end
|
212
212
|
|
213
|
-
# Daemonize the server into the background. It's highly recommended to
|
214
|
-
# use this in combination with +pidfile+ and +stdout_redirect+.
|
215
|
-
#
|
216
|
-
# The default is "false".
|
217
|
-
#
|
218
|
-
# @example
|
219
|
-
# daemonize
|
220
|
-
#
|
221
|
-
# @example
|
222
|
-
# daemonize false
|
223
|
-
def daemonize(which=true)
|
224
|
-
@options[:daemon] = which
|
225
|
-
end
|
226
|
-
|
227
213
|
# When shutting down, drain the accept socket of pending
|
228
214
|
# connections and process them. This loops over the accept
|
229
215
|
# socket until there are no more read events and then stops
|
@@ -257,7 +243,7 @@ module Puma
|
|
257
243
|
when :immediately
|
258
244
|
0
|
259
245
|
else
|
260
|
-
|
246
|
+
Float(val)
|
261
247
|
end
|
262
248
|
|
263
249
|
@options[:force_shutdown_after] = i
|
@@ -322,13 +308,7 @@ module Puma
|
|
322
308
|
# @example
|
323
309
|
# rackup '/u/apps/lolcat/config.ru'
|
324
310
|
def rackup(path)
|
325
|
-
@options[:rackup]
|
326
|
-
end
|
327
|
-
|
328
|
-
# Run Puma in TCP mode
|
329
|
-
#
|
330
|
-
def tcp_mode!
|
331
|
-
@options[:mode] = :tcp
|
311
|
+
@options[:rackup] ||= path.to_s
|
332
312
|
end
|
333
313
|
|
334
314
|
def early_hints(answer=true)
|
@@ -419,8 +399,16 @@ module Puma
|
|
419
399
|
@options[:state] = path.to_s
|
420
400
|
end
|
421
401
|
|
402
|
+
# Use +permission+ to restrict permissions for the state file.
|
403
|
+
#
|
404
|
+
# @example
|
405
|
+
# state_permission 0600
|
406
|
+
def state_permission(permission)
|
407
|
+
@options[:state_permission] = permission
|
408
|
+
end
|
409
|
+
|
422
410
|
# How many worker processes to run. Typically this is set to
|
423
|
-
#
|
411
|
+
# the number of available cores.
|
424
412
|
#
|
425
413
|
# The default is 0.
|
426
414
|
#
|
@@ -512,6 +500,28 @@ module Puma
|
|
512
500
|
|
513
501
|
alias_method :after_worker_boot, :after_worker_fork
|
514
502
|
|
503
|
+
# When `fork_worker` is enabled, code to run in Worker 0
|
504
|
+
# before all other workers are re-forked from this process,
|
505
|
+
# after the server has temporarily stopped serving requests
|
506
|
+
# (once per complete refork cycle).
|
507
|
+
#
|
508
|
+
# This can be used to trigger extra garbage-collection to maximize
|
509
|
+
# copy-on-write efficiency, or close any connections to remote servers
|
510
|
+
# (database, Redis, ...) that were opened while the server was running.
|
511
|
+
#
|
512
|
+
# This can be called multiple times to add several hooks.
|
513
|
+
#
|
514
|
+
# @note Cluster mode with `fork_worker` enabled only.
|
515
|
+
# @example
|
516
|
+
# on_refork do
|
517
|
+
# 3.times {GC.start}
|
518
|
+
# end
|
519
|
+
|
520
|
+
def on_refork(&block)
|
521
|
+
@options[:before_refork] ||= []
|
522
|
+
@options[:before_refork] << block
|
523
|
+
end
|
524
|
+
|
515
525
|
# Code to run out-of-band when the worker is idle.
|
516
526
|
# These hooks run immediately after a request has finished
|
517
527
|
# processing and there are no busy threads on the worker.
|
@@ -536,17 +546,6 @@ module Puma
|
|
536
546
|
@options[:directory] = dir.to_s
|
537
547
|
end
|
538
548
|
|
539
|
-
# DEPRECATED: The directory to operate out of.
|
540
|
-
def worker_directory(dir)
|
541
|
-
$stderr.puts "worker_directory is deprecated. Please use `directory`"
|
542
|
-
directory dir
|
543
|
-
end
|
544
|
-
|
545
|
-
# Run the app as a raw TCP app instead of an HTTP rack app.
|
546
|
-
def tcp_mode
|
547
|
-
@options[:mode] = :tcp
|
548
|
-
end
|
549
|
-
|
550
549
|
# Preload the application before starting the workers; this conflicts with
|
551
550
|
# phased restart feature. This is off by default.
|
552
551
|
#
|
@@ -695,6 +694,16 @@ module Puma
|
|
695
694
|
@options[:shutdown_debug] = val
|
696
695
|
end
|
697
696
|
|
697
|
+
|
698
|
+
# Attempts to route traffic to less-busy workers by causing them to delay
|
699
|
+
# listening on the socket, allowing workers which are not processing any
|
700
|
+
# requests to pick up new requests first.
|
701
|
+
#
|
702
|
+
# Only works on MRI. For all other interpreters, this setting does nothing.
|
703
|
+
def wait_for_less_busy_worker(val=0.005)
|
704
|
+
@options[:wait_for_less_busy_worker] = val.to_f
|
705
|
+
end
|
706
|
+
|
698
707
|
# Control how the remote address of the connection is set. This
|
699
708
|
# is configurable because to calculate the true socket peer address
|
700
709
|
# a kernel syscall is required which for very fast rack handlers
|
@@ -736,5 +745,32 @@ module Puma
|
|
736
745
|
end
|
737
746
|
end
|
738
747
|
|
748
|
+
# When enabled, workers will be forked from worker 0 instead of from the master process.
|
749
|
+
# This option is similar to `preload_app` because the app is preloaded before forking,
|
750
|
+
# but it is compatible with phased restart.
|
751
|
+
#
|
752
|
+
# This option also enables the `refork` command (SIGURG), which optimizes copy-on-write performance
|
753
|
+
# in a running app.
|
754
|
+
#
|
755
|
+
# A refork will automatically trigger once after the specified number of requests
|
756
|
+
# (default 1000), or pass 0 to disable auto refork.
|
757
|
+
#
|
758
|
+
# @note Cluster mode only.
|
759
|
+
def fork_worker(after_requests=1000)
|
760
|
+
@options[:fork_worker] = Integer(after_requests)
|
761
|
+
end
|
762
|
+
|
763
|
+
# When enabled, Puma will GC 4 times before forking workers.
|
764
|
+
# If available (Ruby 2.7+), we will also call GC.compact.
|
765
|
+
# Not recommended for non-MRI Rubies.
|
766
|
+
#
|
767
|
+
# Based on the work of Koichi Sasada and Aaron Patterson, this option may
|
768
|
+
# decrease memory utilization of preload-enabled cluster-mode Pumas. It will
|
769
|
+
# also increase time to boot and fork. See your logs for details on how much
|
770
|
+
# time this adds to your boot process. For most apps, it will be less than one
|
771
|
+
# second.
|
772
|
+
def nakayoshi_fork(enabled=false)
|
773
|
+
@options[:nakayoshi_fork] = enabled
|
774
|
+
end
|
739
775
|
end
|
740
776
|
end
|
data/lib/puma/io_buffer.rb
CHANGED
data/lib/puma/jruby_restart.rb
CHANGED
@@ -22,63 +22,5 @@ module Puma
|
|
22
22
|
execlp(cmd, *argv)
|
23
23
|
raise SystemCallError.new(FFI.errno)
|
24
24
|
end
|
25
|
-
|
26
|
-
PermKey = 'PUMA_DAEMON_PERM'
|
27
|
-
RestartKey = 'PUMA_DAEMON_RESTART'
|
28
|
-
|
29
|
-
# Called to tell things "Your now always in daemon mode,
|
30
|
-
# don't try to reenter it."
|
31
|
-
#
|
32
|
-
def self.perm_daemonize
|
33
|
-
ENV[PermKey] = "1"
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.daemon?
|
37
|
-
ENV.key?(PermKey) || ENV.key?(RestartKey)
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.daemon_init
|
41
|
-
return true if ENV.key?(PermKey)
|
42
|
-
|
43
|
-
return false unless ENV.key? RestartKey
|
44
|
-
|
45
|
-
master = ENV[RestartKey]
|
46
|
-
|
47
|
-
# In case the master disappears early
|
48
|
-
begin
|
49
|
-
Process.kill "SIGUSR2", master.to_i
|
50
|
-
rescue SystemCallError => e
|
51
|
-
end
|
52
|
-
|
53
|
-
ENV[RestartKey] = ""
|
54
|
-
|
55
|
-
setsid
|
56
|
-
|
57
|
-
null = File.open "/dev/null", "w+"
|
58
|
-
STDIN.reopen null
|
59
|
-
STDOUT.reopen null
|
60
|
-
STDERR.reopen null
|
61
|
-
|
62
|
-
true
|
63
|
-
end
|
64
|
-
|
65
|
-
def self.daemon_start(dir, argv)
|
66
|
-
ENV[RestartKey] = Process.pid.to_s
|
67
|
-
|
68
|
-
if k = ENV['PUMA_JRUBY_DAEMON_OPTS']
|
69
|
-
ENV['JRUBY_OPTS'] = k
|
70
|
-
end
|
71
|
-
|
72
|
-
cmd = argv.first
|
73
|
-
argv = ([:string] * argv.size).zip(argv).flatten
|
74
|
-
argv << :string
|
75
|
-
argv << nil
|
76
|
-
|
77
|
-
chdir(dir)
|
78
|
-
ret = fork
|
79
|
-
return ret if ret != 0
|
80
|
-
execlp(cmd, *argv)
|
81
|
-
raise SystemCallError.new(FFI.errno)
|
82
|
-
end
|
83
25
|
end
|
84
26
|
end
|