posix-spawn 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require 'mkmf'
2
+
3
+ # warnings save lives
4
+ $CFLAGS << " -Wall "
5
+
6
+ create_makefile('posix_spawn_ext')
@@ -0,0 +1,406 @@
1
+ /* we want GNU extensions like POSIX_SPAWN_USEVFORK */
2
+ #ifndef _GNU_SOURCE
3
+ #define _GNU_SOURCE
4
+ #endif
5
+
6
+ #include <errno.h>
7
+ #include <fcntl.h>
8
+ #include <spawn.h>
9
+ #include <stdio.h>
10
+ #include <string.h>
11
+ #include <sys/stat.h>
12
+ #include <unistd.h>
13
+ #include <ruby.h>
14
+
15
+ #ifdef RUBY_VM
16
+ #include <ruby/st.h>
17
+ extern void rb_enable_interrupt(void);
18
+ extern void rb_disable_interrupt(void);
19
+ #else
20
+ #include <node.h>
21
+ #include <st.h>
22
+ #define rb_enable_interrupt()
23
+ #define rb_disable_interrupt()
24
+ #endif
25
+
26
+ #ifndef RARRAY_LEN
27
+ #define RARRAY_LEN(ary) RARRAY(ary)->len
28
+ #endif
29
+ #ifndef RARRAY_PTR
30
+ #define RARRAY_PTR(ary) RARRAY(ary)->ptr
31
+ #endif
32
+ #ifndef RHASH_SIZE
33
+ #define RHASH_SIZE(hash) RHASH(hash)->tbl->num_entries
34
+ #endif
35
+
36
+ #ifdef __APPLE__
37
+ #include <crt_externs.h>
38
+ #define environ (*_NSGetEnviron())
39
+ #else
40
+ extern char **environ;
41
+ #endif
42
+
43
+ static VALUE rb_mPOSIX;
44
+ static VALUE rb_mPOSIXSpawn;
45
+
46
+ /* Determine the fd number for a Ruby object VALUE.
47
+ *
48
+ * obj - This can be any valid Ruby object, but only the following return
49
+ * an actual fd number:
50
+ * - The symbols :in, :out, or :err for fds 0, 1, or 2.
51
+ * - An IO object. (IO#fileno is returned)
52
+ * - A Fixnum.
53
+ *
54
+ * Returns the fd number >= 0 if one could be established, or -1 if the object
55
+ * does not map to an fd.
56
+ */
57
+ static int
58
+ posixspawn_obj_to_fd(VALUE obj)
59
+ {
60
+ int fd = -1;
61
+ switch (TYPE(obj)) {
62
+ case T_FIXNUM:
63
+ /* Fixnum fd number */
64
+ fd = FIX2INT(obj);
65
+ break;
66
+
67
+ case T_SYMBOL:
68
+ /* (:in|:out|:err) */
69
+ if (SYM2ID(obj) == rb_intern("in")) fd = 0;
70
+ else if (SYM2ID(obj) == rb_intern("out")) fd = 1;
71
+ else if (SYM2ID(obj) == rb_intern("err")) fd = 2;
72
+ break;
73
+
74
+ case T_FILE:
75
+ /* IO object */
76
+ fd = FIX2INT(rb_funcall(obj, rb_intern("fileno"), 0));
77
+ break;
78
+
79
+ case T_OBJECT:
80
+ /* some other object */
81
+ if (rb_respond_to(obj, rb_intern("to_io"))) {
82
+ obj = rb_funcall(obj, rb_intern("to_io"), 0);
83
+ fd = FIX2INT(rb_funcall(obj, rb_intern("fileno"), 0));
84
+ }
85
+ break;
86
+ }
87
+ return fd;
88
+ }
89
+
90
+ /*
91
+ * Hash iterator that sets up the posix_spawn_file_actions_t with addclose
92
+ * operations. Only hash pairs whose value is :close are processed. Keys may
93
+ * be the :in, :out, :err, an IO object, or a Fixnum fd number.
94
+ *
95
+ * Returns ST_DELETE when an addclose operation was added; ST_CONTINUE when
96
+ * no operation was performed.
97
+ */
98
+ static int
99
+ posixspawn_file_actions_addclose(VALUE key, VALUE val, posix_spawn_file_actions_t *fops)
100
+ {
101
+ int fd;
102
+
103
+ /* we only care about { (IO|FD|:in|:out|:err) => :close } */
104
+ if (TYPE(val) != T_SYMBOL || SYM2ID(val) != rb_intern("close"))
105
+ return ST_CONTINUE;
106
+
107
+ fd = posixspawn_obj_to_fd(key);
108
+ if (fd >= 0) {
109
+ posix_spawn_file_actions_addclose(fops, fd);
110
+ return ST_DELETE;
111
+ } else {
112
+ return ST_CONTINUE;
113
+ }
114
+ }
115
+
116
+ /*
117
+ * Hash iterator that sets up the posix_spawn_file_actions_t with adddup2 +
118
+ * close operations for all redirects. Only hash pairs whose key and value
119
+ * represent fd numbers are processed.
120
+ *
121
+ * Returns ST_DELETE when an adddup2 operation was added; ST_CONTINUE when
122
+ * no operation was performed.
123
+ */
124
+ static int
125
+ posixspawn_file_actions_adddup2(VALUE key, VALUE val, posix_spawn_file_actions_t *fops)
126
+ {
127
+ int fd, newfd;
128
+
129
+ newfd = posixspawn_obj_to_fd(key);
130
+ if (newfd < 0)
131
+ return ST_CONTINUE;
132
+
133
+ fd = posixspawn_obj_to_fd(val);
134
+ if (fd < 0)
135
+ return ST_CONTINUE;
136
+
137
+ posix_spawn_file_actions_adddup2(fops, fd, newfd);
138
+ return ST_DELETE;
139
+ }
140
+
141
+ /*
142
+ * Hash iterator that sets up the posix_spawn_file_actions_t with adddup2 +
143
+ * clone operations for all file redirects. Only hash pairs whose key is an
144
+ * fd number and value is a valid three-tuple [file, flags, mode] are
145
+ * processed.
146
+ *
147
+ * Returns ST_DELETE when an adddup2 operation was added; ST_CONTINUE when
148
+ * no operation was performed.
149
+ */
150
+ static int
151
+ posixspawn_file_actions_addopen(VALUE key, VALUE val, posix_spawn_file_actions_t *fops)
152
+ {
153
+ int fd;
154
+ char *path;
155
+ int oflag;
156
+ mode_t mode;
157
+
158
+ fd = posixspawn_obj_to_fd(key);
159
+ if (fd < 0)
160
+ return ST_CONTINUE;
161
+
162
+ if (TYPE(val) != T_ARRAY || RARRAY_LEN(val) != 3)
163
+ return ST_CONTINUE;
164
+
165
+ path = StringValuePtr(RARRAY_PTR(val)[0]);
166
+ oflag = FIX2INT(RARRAY_PTR(val)[1]);
167
+ mode = FIX2INT(RARRAY_PTR(val)[2]);
168
+
169
+ posix_spawn_file_actions_addopen(fops, fd, path, oflag, mode);
170
+ return ST_DELETE;
171
+ }
172
+
173
+ /*
174
+ * Main entry point for iterating over the options hash to perform file actions.
175
+ * This function dispatches to the addclose and adddup2 functions, stopping once
176
+ * an operation was added.
177
+ *
178
+ * Returns ST_DELETE if one of the handlers performed an operation; ST_CONTINUE
179
+ * if not.
180
+ */
181
+ static int
182
+ posixspawn_file_actions_operations_iter(VALUE key, VALUE val, posix_spawn_file_actions_t *fops)
183
+ {
184
+ int act;
185
+
186
+ act = posixspawn_file_actions_addclose(key, val, fops);
187
+ if (act != ST_CONTINUE) return act;
188
+
189
+ act = posixspawn_file_actions_adddup2(key, val, fops);
190
+ if (act != ST_CONTINUE) return act;
191
+
192
+ act = posixspawn_file_actions_addopen(key, val, fops);
193
+ if (act != ST_CONTINUE) return act;
194
+
195
+ return ST_CONTINUE;
196
+ }
197
+
198
+ /*
199
+ * Initialize the posix_spawn_file_actions_t structure and add operations from
200
+ * the options hash. Keys in the options Hash that are processed by handlers are
201
+ * removed.
202
+ *
203
+ * Returns nothing.
204
+ */
205
+ static void
206
+ posixspawn_file_actions_init(posix_spawn_file_actions_t *fops, VALUE options)
207
+ {
208
+ posix_spawn_file_actions_init(fops);
209
+ rb_hash_foreach(options, posixspawn_file_actions_operations_iter, (VALUE)fops);
210
+ }
211
+
212
+ static int
213
+ each_env_check_i(VALUE key, VALUE val, VALUE arg)
214
+ {
215
+ StringValuePtr(key);
216
+ if (!NIL_P(val)) StringValuePtr(val);
217
+ return ST_CONTINUE;
218
+ }
219
+
220
+ static int
221
+ each_env_i(VALUE key, VALUE val, VALUE arg)
222
+ {
223
+ char *name = StringValuePtr(key);
224
+ size_t len = strlen(name);
225
+
226
+ /*
227
+ * Delete any existing values for this variable before inserting the new value.
228
+ * This implementation was copied from glibc's unsetenv().
229
+ */
230
+ char **ep = (char **)arg;
231
+ while (*ep != NULL)
232
+ if (!strncmp (*ep, name, len) && (*ep)[len] == '=')
233
+ {
234
+ /* Found it. Remove this pointer by moving later ones back. */
235
+ char **dp = ep;
236
+
237
+ do
238
+ dp[0] = dp[1];
239
+ while (*dp++);
240
+ /* Continue the loop in case NAME appears again. */
241
+ }
242
+ else
243
+ ++ep;
244
+
245
+ /*
246
+ * Insert the new value if we have one. We can assume there is space
247
+ * at the end of the list, since ep was preallocated to be big enough
248
+ * for the new entries.
249
+ */
250
+ if (RTEST(val)) {
251
+ char **ep = (char **)arg;
252
+ char *cval = StringValuePtr(val);
253
+
254
+ size_t cval_len = strlen(cval);
255
+ size_t ep_len = len + 1 + cval_len + 1; /* +2 for null terminator and '=' separator */
256
+
257
+ /* find the last entry */
258
+ while (*ep != NULL) ++ep;
259
+ *ep = malloc(ep_len);
260
+
261
+ strncpy(*ep, name, len);
262
+ (*ep)[len] = '=';
263
+ strncpy(*ep + len + 1, cval, cval_len);
264
+ (*ep)[ep_len-1] = 0;
265
+ }
266
+
267
+ return ST_CONTINUE;
268
+ }
269
+
270
+ /*
271
+ * POSIX::Spawn#_pspawn(env, argv, options)
272
+ *
273
+ * env - Hash of the new environment.
274
+ * argv - The [[cmdname, argv0], argv1, ...] exec array.
275
+ * options - The options hash with fd redirect and close operations.
276
+ *
277
+ * Returns the pid of the newly spawned process.
278
+ */
279
+ static VALUE
280
+ rb_posixspawn_pspawn(VALUE self, VALUE env, VALUE argv, VALUE options)
281
+ {
282
+ int i, ret;
283
+ char **envp = NULL;
284
+ VALUE dirname;
285
+ VALUE cmdname;
286
+ VALUE unsetenv_others_p = Qfalse;
287
+ char *file;
288
+ char *cwd = NULL;
289
+ pid_t pid;
290
+ posix_spawn_file_actions_t fops;
291
+ posix_spawnattr_t attr;
292
+
293
+ /* argv is a [[cmdname, argv0], argv1, argvN, ...] array. */
294
+ if (TYPE(argv) != T_ARRAY ||
295
+ TYPE(RARRAY_PTR(argv)[0]) != T_ARRAY ||
296
+ RARRAY_LEN(RARRAY_PTR(argv)[0]) != 2)
297
+ rb_raise(rb_eArgError, "Invalid command name");
298
+
299
+ long argc = RARRAY_LEN(argv);
300
+ char *cargv[argc + 1];
301
+
302
+ cmdname = RARRAY_PTR(argv)[0];
303
+ file = StringValuePtr(RARRAY_PTR(cmdname)[0]);
304
+
305
+ cargv[0] = StringValuePtr(RARRAY_PTR(cmdname)[1]);
306
+ for (i = 1; i < argc; i++)
307
+ cargv[i] = StringValuePtr(RARRAY_PTR(argv)[i]);
308
+ cargv[argc] = NULL;
309
+
310
+ if (TYPE(options) == T_HASH) {
311
+ unsetenv_others_p = rb_hash_delete(options, ID2SYM(rb_intern("unsetenv_others")));
312
+ }
313
+
314
+ if (RTEST(env)) {
315
+ /*
316
+ * Make sure env is a hash, and all keys and values are strings.
317
+ * We do this before allocating space for the new environment to
318
+ * prevent a leak when raising an exception after the calloc() below.
319
+ */
320
+ Check_Type(env, T_HASH);
321
+ rb_hash_foreach(env, each_env_check_i, 0);
322
+
323
+ if (RHASH_SIZE(env) > 0) {
324
+ int size = 0;
325
+
326
+ char **curr = environ;
327
+ if (curr) {
328
+ while (*curr != NULL) ++curr, ++size;
329
+ }
330
+
331
+ if (unsetenv_others_p == Qtrue) {
332
+ /*
333
+ * ignore the parent's environment by pretending it had
334
+ * no entries. the loop below will do nothing.
335
+ */
336
+ size = 0;
337
+ }
338
+
339
+ char **new_env = calloc(size+RHASH_SIZE(env)+1, sizeof(char*));
340
+ for (i = 0; i < size; i++) {
341
+ new_env[i] = strdup(environ[i]);
342
+ }
343
+ envp = new_env;
344
+
345
+ rb_hash_foreach(env, each_env_i, (VALUE)envp);
346
+ }
347
+ }
348
+
349
+ posixspawn_file_actions_init(&fops, options);
350
+
351
+ posix_spawnattr_init(&attr);
352
+ #if defined(POSIX_SPAWN_USEVFORK) || defined(__linux__)
353
+ /* Force USEVFORK on linux. If this is undefined, it's probably because
354
+ * you forgot to define _GNU_SOURCE at the top of this file.
355
+ */
356
+ posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
357
+ #endif
358
+
359
+ if (RTEST(dirname = rb_hash_delete(options, ID2SYM(rb_intern("chdir"))))) {
360
+ char *new_cwd = StringValuePtr(dirname);
361
+ cwd = getcwd(NULL, 0);
362
+ chdir(new_cwd);
363
+ }
364
+
365
+ if (RHASH_SIZE(options) == 0) {
366
+ rb_enable_interrupt();
367
+ ret = posix_spawnp(&pid, file, &fops, &attr, cargv, envp ? envp : environ);
368
+ rb_disable_interrupt();
369
+ if (cwd) {
370
+ chdir(cwd);
371
+ free(cwd);
372
+ }
373
+ } else {
374
+ ret = -1;
375
+ }
376
+
377
+ posix_spawn_file_actions_destroy(&fops);
378
+ posix_spawnattr_destroy(&attr);
379
+ if (envp) {
380
+ char **ep = envp;
381
+ while (*ep != NULL) free(*ep), ++ep;
382
+ free(envp);
383
+ }
384
+
385
+ if (RHASH_SIZE(options) > 0) {
386
+ rb_raise(rb_eArgError, "Invalid option: %s", RSTRING_PTR(rb_inspect(rb_funcall(options, rb_intern("first"), 0))));
387
+ return -1;
388
+ }
389
+
390
+ if (ret != 0) {
391
+ errno = ret;
392
+ rb_sys_fail("posix_spawnp");
393
+ }
394
+
395
+ return INT2FIX(pid);
396
+ }
397
+
398
+ void
399
+ Init_posix_spawn_ext()
400
+ {
401
+ rb_mPOSIX = rb_define_module("POSIX");
402
+ rb_mPOSIXSpawn = rb_define_module_under(rb_mPOSIX, "Spawn");
403
+ rb_define_method(rb_mPOSIXSpawn, "_pspawn", rb_posixspawn_pspawn, 3);
404
+ }
405
+
406
+ /* vim: set noexpandtab sts=0 ts=4 sw=4: */
@@ -0,0 +1 @@
1
+ require "posix/spawn"
@@ -0,0 +1,472 @@
1
+ require 'posix_spawn_ext'
2
+ require 'posix/spawn/version'
3
+ require 'posix/spawn/child'
4
+
5
+ module POSIX
6
+ # The POSIX::Spawn module implements a compatible subset of Ruby 1.9's
7
+ # Process::spawn and related methods using the IEEE Std 1003.1 posix_spawn(2)
8
+ # system interfaces where available, or a pure Ruby fork/exec based
9
+ # implementation when not.
10
+ #
11
+ # In Ruby 1.9, a versatile new process spawning interface was added
12
+ # (Process::spawn) as the foundation for enhanced versions of existing
13
+ # process-related methods like Kernel#system, Kernel#`, and IO#popen. These
14
+ # methods are backward compatible with their Ruby 1.8 counterparts but
15
+ # support a large number of new options. The POSIX::Spawn module implements
16
+ # many of these methods with support for most of Ruby 1.9's features.
17
+ #
18
+ # The argument signatures for all of these methods follow a new convention,
19
+ # making it possible to take advantage of Process::spawn features:
20
+ #
21
+ # spawn([env], command, [argv1, ...], [options])
22
+ # system([env], command, [argv1, ...], [options])
23
+ # popen([[env], command, [argv1, ...]], mode="r", [options])
24
+ #
25
+ # The env, command, and options arguments are described below.
26
+ #
27
+ # == Environment
28
+ #
29
+ # If a hash is given in the first argument (env), the child process's
30
+ # environment becomes a merge of the parent's and any modifications
31
+ # specified in the hash. When a value in env is nil, the variable is
32
+ # unset in the child:
33
+ #
34
+ # # set FOO as BAR and unset BAZ.
35
+ # spawn({"FOO" => "BAR", "BAZ" => nil}, 'echo', 'hello world')
36
+ #
37
+ # == Command
38
+ #
39
+ # The command and optional argvN string arguments specify the command to
40
+ # execute and any program arguments. When only command is given and
41
+ # includes a space character, the command text is executed by the system
42
+ # shell interpreter, as if by:
43
+ #
44
+ # /bin/sh -c 'command'
45
+ #
46
+ # When command does not include a space character, or one or more argvN
47
+ # arguments are given, the command is executed as if by execve(2) with
48
+ # each argument forming the new program's argv.
49
+ #
50
+ # NOTE: Use of the shell variation is generally discouraged unless you
51
+ # indeed want to execute a shell program. Specifying an explicitly argv is
52
+ # typically more secure and less error prone in most cases.
53
+ #
54
+ # == Options
55
+ #
56
+ # When a hash is given in the last argument (options), it specifies a
57
+ # current directory and zero or more fd redirects for the child process.
58
+ #
59
+ # The :chdir option specifies the current directory:
60
+ #
61
+ # spawn(command, :chdir => "/var/tmp")
62
+ #
63
+ # The :in, :out, :err, a Fixnum, an IO object or an Array option specify
64
+ # fd redirection. For example, stderr can be merged into stdout as follows:
65
+ #
66
+ # spawn(command, :err => :out)
67
+ # spawn(command, 2 => 1)
68
+ # spawn(command, STDERR => :out)
69
+ # spawn(command, STDERR => STDOUT)
70
+ #
71
+ # The key is a fd in the newly spawned child process (stderr in this case).
72
+ # The value is a fd in the parent process (stdout in this case).
73
+ #
74
+ # You can also specify a filename for redirection instead of an fd:
75
+ #
76
+ # spawn(command, :in => "/dev/null") # read mode
77
+ # spawn(command, :out => "/dev/null") # write mode
78
+ # spawn(command, :err => "log") # write mode
79
+ # spawn(command, 3 => "/dev/null") # read mode
80
+ #
81
+ # When redirecting to stdout or stderr, the files are opened in write mode;
82
+ # otherwise, read mode is used.
83
+ #
84
+ # It's also possible to control the open flags and file permissions
85
+ # directly by passing an array value:
86
+ #
87
+ # spawn(command, :in=>["file"]) # read mode assumed
88
+ # spawn(command, :in=>["file", "r"]) # explicit read mode
89
+ # spawn(command, :out=>["log", "w"]) # explicit write mode, 0644 assumed
90
+ # spawn(command, :out=>["log", "w", 0600])
91
+ # spawn(command, :out=>["log", File::APPEND | File::CREAT, 0600])
92
+ #
93
+ # The array is a [filename, open_mode, perms] tuple. open_mode can be a
94
+ # string or an integer. When open_mode is omitted or nil, File::RDONLY is
95
+ # assumed. The perms element should be an integer. When perms is omitted or
96
+ # nil, 0644 is assumed.
97
+ #
98
+ # The :close It's possible to direct an fd be closed in the child process. This is
99
+ # important for implementing `popen`-style logic and other forms of IPC between
100
+ # processes using `IO.pipe`:
101
+ #
102
+ # rd, wr = IO.pipe
103
+ # pid = spawn('echo', 'hello world', rd => :close, :stdout => wr)
104
+ # wr.close
105
+ # output = rd.read
106
+ # Process.wait(pid)
107
+ #
108
+ # == Spawn Implementation
109
+ #
110
+ # The POSIX::Spawn#spawn method uses the best available implementation given
111
+ # the current platform and Ruby version. In order of preference, they are:
112
+ #
113
+ # 1. The posix_spawn based C extension method (pspawn).
114
+ # 2. Process::spawn when available (Ruby 1.9 only).
115
+ # 3. A simple pure-Ruby fork/exec based spawn implementation compatible
116
+ # with Ruby >= 1.8.7.
117
+ #
118
+ module Spawn
119
+ extend self
120
+
121
+ # Spawn a child process with a variety of options using the best
122
+ # available implementation for the current platform and Ruby version.
123
+ #
124
+ # spawn([env], command, [argv1, ...], [options])
125
+ #
126
+ # env - Optional hash specifying the new process's environment.
127
+ # command - A string command name, or shell program, used to determine the
128
+ # program to execute.
129
+ # argvN - Zero or more string program arguments (argv).
130
+ # options - Optional hash of operations to perform before executing the
131
+ # new child process.
132
+ #
133
+ # Returns the integer pid of the newly spawned process.
134
+ # Raises any number of Errno:: exceptions on failure.
135
+ def spawn(*args)
136
+ if respond_to?(:_pspawn)
137
+ pspawn(*args)
138
+ elsif ::Process.respond_to?(:spawn)
139
+ ::Process::spawn(*args)
140
+ else
141
+ fspawn(*args)
142
+ end
143
+ end
144
+
145
+ # Spawn a child process with a variety of options using the posix_spawn(2)
146
+ # systems interfaces. Supports the standard spawn interface as described in
147
+ # the POSIX::Spawn module documentation.
148
+ #
149
+ # Raises NotImplementedError when the posix_spawn_ext module could not be
150
+ # loaded due to lack of platform support.
151
+ def pspawn(*args)
152
+ env, argv, options = extract_process_spawn_arguments(*args)
153
+ raise NotImplementedError unless respond_to?(:_pspawn)
154
+ _pspawn(env, argv, options)
155
+ end
156
+
157
+ # Spawn a child process with a variety of options using a pure
158
+ # Ruby fork + exec. Supports the standard spawn interface as described in
159
+ # the POSIX::Spawn module documentation.
160
+ def fspawn(*args)
161
+ env, argv, options = extract_process_spawn_arguments(*args)
162
+
163
+ if badopt = options.find{ |key,val| !fd?(key) && ![:chdir,:unsetenv_others].include?(key) }
164
+ raise ArgumentError, "Invalid option: #{badopt[0].inspect}"
165
+ elsif !argv.is_a?(Array) || !argv[0].is_a?(Array) || argv[0].size != 2
166
+ raise ArgumentError, "Invalid command name"
167
+ end
168
+
169
+ fork do
170
+ begin
171
+ # handle FD => {FD, :close, [file,mode,perms]} options
172
+ options.map do |key, val|
173
+ if fd?(key)
174
+ key = fd_to_io(key)
175
+
176
+ if fd?(val)
177
+ val = fd_to_io(val)
178
+ key.reopen(val)
179
+ elsif val == :close
180
+ if key.respond_to?(:close_on_exec=)
181
+ key.close_on_exec = true
182
+ else
183
+ key.close
184
+ end
185
+ elsif val.is_a?(Array)
186
+ file, mode_string, perms = *val
187
+ key.reopen(File.open(file, mode_string, perms))
188
+ end
189
+ end
190
+ end
191
+
192
+ # setup child environment
193
+ ENV.replace({}) if options[:unsetenv_others] == true
194
+ env.each { |k, v| ENV[k] = v }
195
+
196
+ # { :chdir => '/' } in options means change into that dir
197
+ ::Dir.chdir(options[:chdir]) if options[:chdir]
198
+
199
+ # do the deed
200
+ ::Kernel::exec(*argv)
201
+ ensure
202
+ exit!(127)
203
+ end
204
+ end
205
+ end
206
+
207
+ # Executes a command and waits for it to complete. The command's exit
208
+ # status is available as $?. Supports the standard spawn interface as
209
+ # described in the POSIX::Spawn module documentation.
210
+ #
211
+ # This method is compatible with Kernel#system.
212
+ #
213
+ # Returns true if the command returns a zero exit status, or false for
214
+ # non-zero exit.
215
+ def system(*args)
216
+ pid = spawn(*args)
217
+ return false if pid <= 0
218
+ ::Process.waitpid(pid)
219
+ $?.exitstatus == 0
220
+ rescue Errno::ENOENT
221
+ false
222
+ end
223
+
224
+ # Executes a command in a subshell using the system's shell interpreter
225
+ # and returns anything written to the new process's stdout. This method
226
+ # is compatible with Kernel#`.
227
+ #
228
+ # Returns the String output of the command.
229
+ def `(cmd)
230
+ r, w = IO.pipe
231
+ pid = spawn(['/bin/sh', '/bin/sh'], '-c', cmd, :out => w, r => :close)
232
+
233
+ if pid > 0
234
+ w.close
235
+ out = r.read
236
+ ::Process.waitpid(pid)
237
+ out
238
+ else
239
+ ''
240
+ end
241
+ ensure
242
+ [r, w].each{ |io| io.close rescue nil }
243
+ end
244
+
245
+ # Spawn a child process with all standard IO streams piped in and out of
246
+ # the spawning process. Supports the standard spawn interface as described
247
+ # in the POSIX::Spawn module documentation.
248
+ #
249
+ # Returns a [pid, stdin, stderr, stdout] tuple, where pid is the new
250
+ # process's pid, stdin is a writeable IO object, and stdout / stderr are
251
+ # readable IO objects. The caller should take care to close all IO objects
252
+ # when finished and the child process's status must be collected by a call
253
+ # to Process::waitpid or equivalent.
254
+ def popen4(*argv)
255
+ # create some pipes (see pipe(2) manual -- the ruby docs suck)
256
+ ird, iwr = IO.pipe
257
+ ord, owr = IO.pipe
258
+ erd, ewr = IO.pipe
259
+
260
+ # spawn the child process with either end of pipes hooked together
261
+ opts =
262
+ ((argv.pop if argv[-1].is_a?(Hash)) || {}).merge(
263
+ # redirect fds # close other sides
264
+ :in => ird, iwr => :close,
265
+ :out => owr, ord => :close,
266
+ :err => ewr, erd => :close
267
+ )
268
+ pid = spawn(*(argv + [opts]))
269
+
270
+ [pid, iwr, ord, erd]
271
+ ensure
272
+ # we're in the parent, close child-side fds
273
+ [ird, owr, ewr].each { |fd| fd.close }
274
+ end
275
+
276
+ ##
277
+ # Process::Spawn::Child Exceptions
278
+
279
+ # Exception raised when the total number of bytes output on the command's
280
+ # stderr and stdout streams exceeds the maximum output size (:max option).
281
+ # Currently
282
+ class MaximumOutputExceeded < StandardError
283
+ end
284
+
285
+ # Exception raised when timeout is exceeded.
286
+ class TimeoutExceeded < StandardError
287
+ end
288
+
289
+ private
290
+
291
+ # Turns the various varargs incantations supported by Process::spawn into a
292
+ # simple [env, argv, options] tuple. This just makes life easier for the
293
+ # extension functions.
294
+ #
295
+ # The following method signature is supported:
296
+ # Process::spawn([env], command, ..., [options])
297
+ #
298
+ # The env and options hashes are optional. The command may be a variable
299
+ # number of strings or an Array full of strings that make up the new process's
300
+ # argv.
301
+ #
302
+ # Returns an [env, argv, options] tuple. All elements are guaranteed to be
303
+ # non-nil. When no env or options are given, empty hashes are returned.
304
+ def extract_process_spawn_arguments(*args)
305
+ # pop the options hash off the end if it's there
306
+ options =
307
+ if args[-1].respond_to?(:to_hash)
308
+ args.pop.to_hash
309
+ else
310
+ {}
311
+ end
312
+ flatten_process_spawn_options!(options)
313
+ normalize_process_spawn_redirect_file_options!(options)
314
+
315
+ # shift the environ hash off the front if it's there and account for
316
+ # possible :env key in options hash.
317
+ env =
318
+ if args[0].respond_to?(:to_hash)
319
+ args.shift.to_hash
320
+ else
321
+ {}
322
+ end
323
+ env.merge!(options.delete(:env)) if options.key?(:env)
324
+
325
+ # remaining arguments are the argv supporting a number of variations.
326
+ argv = adjust_process_spawn_argv(args)
327
+
328
+ [env, argv, options]
329
+ end
330
+
331
+ # Convert { [fd1, fd2, ...] => (:close|fd) } options to individual keys,
332
+ # like: { fd1 => :close, fd2 => :close }. This just makes life easier for the
333
+ # spawn implementations.
334
+ #
335
+ # options - The options hash. This is modified in place.
336
+ #
337
+ # Returns the modified options hash.
338
+ def flatten_process_spawn_options!(options)
339
+ options.to_a.each do |key, value|
340
+ if key.respond_to?(:to_ary)
341
+ key.to_ary.each { |fd| options[fd] = value }
342
+ options.delete(key)
343
+ end
344
+ end
345
+ end
346
+
347
+ # Mapping of string open modes to integer oflag versions.
348
+ OFLAGS = {
349
+ "r" => File::RDONLY,
350
+ "r+" => File::RDWR | File::CREAT,
351
+ "w" => File::WRONLY | File::CREAT | File::TRUNC,
352
+ "w+" => File::RDWR | File::CREAT | File::TRUNC,
353
+ "a" => File::WRONLY | File::APPEND | File::CREAT,
354
+ "a+" => File::RDWR | File::APPEND | File::CREAT
355
+ }
356
+
357
+ # Convert variations of redirecting to a file to a standard tuple.
358
+ #
359
+ # :in => '/some/file' => ['/some/file', 'r', 0644]
360
+ # :out => '/some/file' => ['/some/file', 'w', 0644]
361
+ # :err => '/some/file' => ['/some/file', 'w', 0644]
362
+ # STDIN => '/some/file' => ['/some/file', 'r', 0644]
363
+ #
364
+ # Returns the modified options hash.
365
+ def normalize_process_spawn_redirect_file_options!(options)
366
+ options.to_a.each do |key, value|
367
+ next if !fd?(key)
368
+
369
+ # convert string and short array values to
370
+ if value.respond_to?(:to_str)
371
+ value = default_file_reopen_info(key, value)
372
+ elsif value.respond_to?(:to_ary) && value.size < 3
373
+ defaults = default_file_reopen_info(key, value[0])
374
+ value += defaults[value.size..-1]
375
+ else
376
+ value = nil
377
+ end
378
+
379
+ # replace string open mode flag maybe and replace original value
380
+ if value
381
+ value[1] = OFLAGS[value[1]] if value[1].respond_to?(:to_str)
382
+ options[key] = value
383
+ end
384
+ end
385
+ end
386
+
387
+ # The default [file, flags, mode] tuple for a given fd and filename. The
388
+ # default flags vary based on the what fd is being redirected. stdout and
389
+ # stderr default to write, while stdin and all other fds default to read.
390
+ #
391
+ # fd - The file descriptor that is being redirected. This may be an IO
392
+ # object, integer fd number, or :in, :out, :err for one of the standard
393
+ # streams.
394
+ # file - The string path to the file that fd should be redirected to.
395
+ #
396
+ # Returns a [file, flags, mode] tuple.
397
+ def default_file_reopen_info(fd, file)
398
+ case fd
399
+ when :in, STDIN, $stdin, 0
400
+ [file, "r", 0644]
401
+ when :out, STDOUT, $stdout, 1
402
+ [file, "w", 0644]
403
+ when :err, STDERR, $stderr, 2
404
+ [file, "w", 0644]
405
+ else
406
+ [file, "r", 0644]
407
+ end
408
+ end
409
+
410
+ # Determine whether object is fd-like.
411
+ #
412
+ # Returns true if object is an instance of IO, Fixnum >= 0, or one of the
413
+ # the symbolic names :in, :out, or :err.
414
+ def fd?(object)
415
+ case object
416
+ when Fixnum
417
+ object >= 0
418
+ when :in, :out, :err, STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, IO
419
+ true
420
+ else
421
+ object.respond_to?(:to_io) && !object.to_io.nil?
422
+ end
423
+ end
424
+
425
+ # Convert a fd identifier to an IO object.
426
+ #
427
+ # Returns nil or an instance of IO.
428
+ def fd_to_io(object)
429
+ case object
430
+ when STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr
431
+ object
432
+ when :in, 0
433
+ STDIN
434
+ when :out, 1
435
+ STDOUT
436
+ when :err, 2
437
+ STDERR
438
+ when Fixnum
439
+ object >= 0 ? IO.for_fd(object) : nil
440
+ when IO
441
+ object
442
+ else
443
+ object.respond_to?(:to_io) ? object.to_io : nil
444
+ end
445
+ end
446
+
447
+ # Converts the various supported command argument variations into a
448
+ # standard argv suitable for use with exec. This includes detecting commands
449
+ # to be run through the shell (single argument strings with spaces).
450
+ #
451
+ # The args array may follow any of these variations:
452
+ #
453
+ # 'true' => [['true', 'true']]
454
+ # 'echo', 'hello', 'world' => [['echo', 'echo'], 'hello', 'world']
455
+ # 'echo hello world' => [['/bin/sh', '/bin/sh'], '-c', 'echo hello world']
456
+ # ['echo', 'fuuu'], 'hello' => [['echo', 'fuuu'], 'hello']
457
+ #
458
+ # Returns a [[cmdname, argv0], argv1, ...] array.
459
+ def adjust_process_spawn_argv(args)
460
+ if args.size == 1 && args[0] =~ /[ |>]/
461
+ # single string with these characters means run it through the shell
462
+ [['/bin/sh', '/bin/sh'], '-c', args[0]]
463
+ elsif !args[0].respond_to?(:to_ary)
464
+ # [argv0, argv1, ...]
465
+ [[args[0], args[0]], *args[1..-1]]
466
+ else
467
+ # [[cmdname, argv0], argv1, ...]
468
+ args
469
+ end
470
+ end
471
+ end
472
+ end