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.
- 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
|