posix-spawn 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/COPYING +28 -0
- data/Gemfile +2 -0
- data/HACKING +26 -0
- data/README.md +228 -0
- data/Rakefile +36 -0
- data/TODO +23 -0
- data/bin/posix-spawn-benchmark +117 -0
- data/ext/extconf.rb +6 -0
- data/ext/posix-spawn.c +406 -0
- data/lib/posix-spawn.rb +1 -0
- data/lib/posix/spawn.rb +472 -0
- data/lib/posix/spawn/child.rb +212 -0
- data/lib/posix/spawn/version.rb +5 -0
- data/posix-spawn.gemspec +24 -0
- data/test/test_backtick.rb +36 -0
- data/test/test_child.rb +107 -0
- data/test/test_popen.rb +18 -0
- data/test/test_spawn.rb +360 -0
- data/test/test_system.rb +29 -0
- metadata +89 -0
data/ext/extconf.rb
ADDED
data/ext/posix-spawn.c
ADDED
@@ -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: */
|
data/lib/posix-spawn.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "posix/spawn"
|
data/lib/posix/spawn.rb
ADDED
@@ -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
|