posix-spawn 0.3.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.
@@ -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