Module: POSIX::Spawn

Extended by:
Spawn
Included in:
Spawn, Child
Defined in:
lib/posix/spawn.rb,
lib/posix/spawn/child.rb,
lib/posix/spawn/version.rb,
ext/posix-spawn.c

Overview

The POSIX::Spawn module implements a compatible subset of Ruby 1.9’s Process::spawn and related methods using the IEEE Std 1003.1 posix_spawn(2) system interfaces where available, or a pure Ruby fork/exec based implementation when not.

In Ruby 1.9, a versatile new process spawning interface was added (Process::spawn) as the foundation for enhanced versions of existing process-related methods like Kernel#system, Kernel#‘, and IO#popen. These methods are backward compatible with their Ruby 1.8 counterparts but support a large number of new options. The POSIX::Spawn module implements many of these methods with support for most of Ruby 1.9’s features.

The argument signatures for all of these methods follow a new convention, making it possible to take advantage of Process::spawn features:

spawn([env], command, [argv1, ...], [options])
system([env], command, [argv1, ...], [options])
popen([[env], command, [argv1, ...]], mode="r", [options])

The env, command, and options arguments are described below.

Environment

If a hash is given in the first argument (env), the child process’s environment becomes a merge of the parent’s and any modifications specified in the hash. When a value in env is nil, the variable is unset in the child:

# set FOO as BAR and unset BAZ.
spawn({"FOO" => "BAR", "BAZ" => nil}, 'echo', 'hello world')

Command

The command and optional argvN string arguments specify the command to execute and any program arguments. When only command is given and includes a space character, the command text is executed by the system shell interpreter, as if by:

/bin/sh -c 'command'

When command does not include a space character, or one or more argvN arguments are given, the command is executed as if by execve(2) with each argument forming the new program’s argv.

NOTE: Use of the shell variation is generally discouraged unless you indeed want to execute a shell program. Specifying an explicitly argv is typically more secure and less error prone in most cases.

Options

When a hash is given in the last argument (options), it specifies a current directory and zero or more fd redirects for the child process.

The :chdir option specifies the current directory:

spawn(command, :chdir => "/var/tmp")

The :in, :out, :err, a Fixnum, an IO object or an Array option specify fd redirection. For example, stderr can be merged into stdout as follows:

spawn(command, :err => :out)
spawn(command, 2 => 1)
spawn(command, STDERR => :out)
spawn(command, STDERR => STDOUT)

The key is a fd in the newly spawned child process (stderr in this case). The value is a fd in the parent process (stdout in this case).

You can also specify a filename for redirection instead of an fd:

spawn(command, :in => "/dev/null")   # read mode
spawn(command, :out => "/dev/null")  # write mode
spawn(command, :err => "log")        # write mode
spawn(command, 3 => "/dev/null")     # read mode

When redirecting to stdout or stderr, the files are opened in write mode; otherwise, read mode is used.

It’s also possible to control the open flags and file permissions directly by passing an array value:

spawn(command, :in=>["file"])       # read mode assumed
spawn(command, :in=>["file", "r"])  # explicit read mode
spawn(command, :out=>["log", "w"])  # explicit write mode, 0644 assumed
spawn(command, :out=>["log", "w", 0600])
spawn(command, :out=>["log", File::APPEND | File::CREAT, 0600])

The array is a [filename, open_mode, perms] tuple. open_mode can be a string or an integer. When open_mode is omitted or nil, File::RDONLY is assumed. The perms element should be an integer. When perms is omitted or nil, 0644 is assumed.

The :close It’s possible to direct an fd be closed in the child process. This is important for implementing ‘popen`-style logic and other forms of IPC between processes using `IO.pipe`:

rd, wr = IO.pipe
pid = spawn('echo', 'hello world', rd => :close, :stdout => wr)
wr.close
output = rd.read
Process.wait(pid)

Spawn Implementation

The POSIX::Spawn#spawn method uses the best available implementation given the current platform and Ruby version. In order of preference, they are:

1. The posix_spawn based C extension method (pspawn).
2. Process::spawn when available (Ruby 1.9 only).
3. A simple pure-Ruby fork/exec based spawn implementation compatible
   with Ruby >= 1.8.7.

Defined Under Namespace

Classes: Child, MaximumOutputExceeded, TimeoutExceeded

Constant Summary collapse

VERSION =
'0.3.2'

Instance Method Summary collapse

Instance Method Details

#_pspawn(env, argv, options) ⇒ Object

POSIX::Spawn#_pspawn(env, argv, options)

env - Hash of the new environment. argv - The [[cmdname, argv0], argv1, …] exec array. options - The options hash with fd redirect and close operations.

Returns the pid of the newly spawned process.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'ext/posix-spawn.c', line 279

static VALUE
rb_posixspawn_pspawn(VALUE self, VALUE env, VALUE argv, VALUE options)
{
	int i, ret;
	char **envp = NULL;
	VALUE dirname;
	VALUE cmdname;
	VALUE unsetenv_others_p = Qfalse;
	char *file;
	char *cwd = NULL;
	pid_t pid;
	posix_spawn_file_actions_t fops;
	posix_spawnattr_t attr;

	/* argv is a [[cmdname, argv0], argv1, argvN, ...] array. */
	if (TYPE(argv) != T_ARRAY ||
	    TYPE(RARRAY_PTR(argv)[0]) != T_ARRAY ||
	    RARRAY_LEN(RARRAY_PTR(argv)[0]) != 2)
		rb_raise(rb_eArgError, "Invalid command name");

	long argc = RARRAY_LEN(argv);
	char *cargv[argc + 1];

	cmdname = RARRAY_PTR(argv)[0];
	file = StringValuePtr(RARRAY_PTR(cmdname)[0]);

	cargv[0] = StringValuePtr(RARRAY_PTR(cmdname)[1]);
	for (i = 1; i < argc; i++)
		cargv[i] = StringValuePtr(RARRAY_PTR(argv)[i]);
	cargv[argc] = NULL;

	if (TYPE(options) == T_HASH) {
		unsetenv_others_p = rb_hash_delete(options, ID2SYM(rb_intern("unsetenv_others")));
	}

	if (RTEST(env)) {
		/*
		 * Make sure env is a hash, and all keys and values are strings.
		 * We do this before allocating space for the new environment to
		 * prevent a leak when raising an exception after the calloc() below.
		 */
		Check_Type(env, T_HASH);
		rb_hash_foreach(env, each_env_check_i, 0);

		if (RHASH_SIZE(env) > 0) {
			int size = 0;

			char **curr = environ;
			if (curr) {
				while (*curr != NULL) ++curr, ++size;
			}

			if (unsetenv_others_p == Qtrue) {
				/*
				 * ignore the parent's environment by pretending it had
				 * no entries. the loop below will do nothing.
				 */
				size = 0;
			}

			char **new_env = calloc(size+RHASH_SIZE(env)+1, sizeof(char*));
			for (i = 0; i < size; i++) {
				new_env[i] = strdup(environ[i]);
			}
			envp = new_env;

			rb_hash_foreach(env, each_env_i, (VALUE)envp);
		}
	}

	posixspawn_file_actions_init(&fops, options);

	posix_spawnattr_init(&attr);
#if defined(POSIX_SPAWN_USEVFORK) || defined(__linux__)
	/* Force USEVFORK on linux. If this is undefined, it's probably because
	 * you forgot to define _GNU_SOURCE at the top of this file.
	 */
	posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK);
#endif

	if (RTEST(dirname = rb_hash_delete(options, ID2SYM(rb_intern("chdir"))))) {
		char *new_cwd = StringValuePtr(dirname);
		cwd = getcwd(NULL, 0);
		chdir(new_cwd);
	}

	if (RHASH_SIZE(options) == 0) {
		rb_enable_interrupt();
		ret = posix_spawnp(&pid, file, &fops, &attr, cargv, envp ? envp : environ);
		rb_disable_interrupt();
		if (cwd) {
			chdir(cwd);
			free(cwd);
		}
	} else {
		ret = -1;
	}

	posix_spawn_file_actions_destroy(&fops);
	posix_spawnattr_destroy(&attr);
	if (envp) {
		char **ep = envp;
		while (*ep != NULL) free(*ep), ++ep;
		free(envp);
	}

	if (RHASH_SIZE(options) > 0) {
		rb_raise(rb_eArgError, "Invalid option: %s", RSTRING_PTR(rb_inspect(rb_funcall(options, rb_intern("first"), 0))));
		return -1;
	}

	if (ret != 0) {
		errno = ret;
		rb_sys_fail("posix_spawnp");
	}

	return INT2FIX(pid);
}

#`(cmd) ⇒ Object

Executes a command in a subshell using the system’s shell interpreter and returns anything written to the new process’s stdout. This method is compatible with Kernel#‘.

Returns the String output of the command.



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/posix/spawn.rb', line 229

def `(cmd)
  r, w = IO.pipe
  pid = spawn(['/bin/sh', '/bin/sh'], '-c', cmd, :out => w, r => :close)

  if pid > 0
    w.close
    out = r.read
    ::Process.waitpid(pid)
    out
  else
    ''
  end
ensure
  [r, w].each{ |io| io.close rescue nil }
end

#fspawn(*args) ⇒ Object

Spawn a child process with a variety of options using a pure Ruby fork + exec. Supports the standard spawn interface as described in the POSIX::Spawn module documentation.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/posix/spawn.rb', line 160

def fspawn(*args)
  env, argv, options = extract_process_spawn_arguments(*args)

  if badopt = options.find{ |key,val| !fd?(key) && ![:chdir,:unsetenv_others].include?(key) }
    raise ArgumentError, "Invalid option: #{badopt[0].inspect}"
  elsif !argv.is_a?(Array) || !argv[0].is_a?(Array) || argv[0].size != 2
    raise ArgumentError, "Invalid command name"
  end

  fork do
    begin
      # handle FD => {FD, :close, [file,mode,perms]} options
      options.map do |key, val|
        if fd?(key)
          key = fd_to_io(key)

          if fd?(val)
            val = fd_to_io(val)
            key.reopen(val)
          elsif val == :close
            if key.respond_to?(:close_on_exec=)
              key.close_on_exec = true
            else
              key.close
            end
          elsif val.is_a?(Array)
            file, mode_string, perms = *val
            key.reopen(File.open(file, mode_string, perms))
          end
        end
      end

      # setup child environment
      ENV.replace({}) if options[:unsetenv_others] == true
      env.each { |k, v| ENV[k] = v }

      # { :chdir => '/' } in options means change into that dir
      ::Dir.chdir(options[:chdir]) if options[:chdir]

      # do the deed
      ::Kernel::exec(*argv)
    ensure
      exit!(127)
    end
  end
end

#popen4(*argv) ⇒ Object

Spawn a child process with all standard IO streams piped in and out of the spawning process. Supports the standard spawn interface as described in the POSIX::Spawn module documentation.

Returns a [pid, stdin, stderr, stdout] tuple, where pid is the new process’s pid, stdin is a writeable IO object, and stdout / stderr are readable IO objects. The caller should take care to close all IO objects when finished and the child process’s status must be collected by a call to Process::waitpid or equivalent.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/posix/spawn.rb', line 254

def popen4(*argv)
  # create some pipes (see pipe(2) manual -- the ruby docs suck)
  ird, iwr = IO.pipe
  ord, owr = IO.pipe
  erd, ewr = IO.pipe

  # spawn the child process with either end of pipes hooked together
  opts =
    ((argv.pop if argv[-1].is_a?(Hash)) || {}).merge(
      # redirect fds        # close other sides
      :in  => ird,          iwr  => :close,
      :out => owr,          ord  => :close,
      :err => ewr,          erd  => :close
    )
  pid = spawn(*(argv + [opts]))

  [pid, iwr, ord, erd]
ensure
  # we're in the parent, close child-side fds
  [ird, owr, ewr].each { |fd| fd.close }
end

#pspawn(*args) ⇒ Object

Spawn a child process with a variety of options using the posix_spawn(2) systems interfaces. Supports the standard spawn interface as described in the POSIX::Spawn module documentation.

Raises NotImplementedError when the posix_spawn_ext module could not be loaded due to lack of platform support.

Raises:

  • (NotImplementedError)


151
152
153
154
155
# File 'lib/posix/spawn.rb', line 151

def pspawn(*args)
  env, argv, options = extract_process_spawn_arguments(*args)
  raise NotImplementedError unless respond_to?(:_pspawn)
  _pspawn(env, argv, options)
end

#spawn(*args) ⇒ Object

Spawn a child process with a variety of options using the best available implementation for the current platform and Ruby version.

spawn(, command, [argv1, …], [options])

env - Optional hash specifying the new process’s environment. command - A string command name, or shell program, used to determine the

program to execute.

argvN - Zero or more string program arguments (argv). options - Optional hash of operations to perform before executing the

new child process.

Returns the integer pid of the newly spawned process.

Raises any number of Errno

exceptions on failure.



135
136
137
138
139
140
141
142
143
# File 'lib/posix/spawn.rb', line 135

def spawn(*args)
  if respond_to?(:_pspawn)
    pspawn(*args)
  elsif ::Process.respond_to?(:spawn)
    ::Process::spawn(*args)
  else
    fspawn(*args)
  end
end

#system(*args) ⇒ Object

Executes a command and waits for it to complete. The command’s exit status is available as $?. Supports the standard spawn interface as described in the POSIX::Spawn module documentation.

This method is compatible with Kernel#system.

Returns true if the command returns a zero exit status, or false for non-zero exit.



215
216
217
218
219
220
221
222
# File 'lib/posix/spawn.rb', line 215

def system(*args)
  pid = spawn(*args)
  return false if pid <= 0
  ::Process.waitpid(pid)
  $?.exitstatus == 0
rescue Errno::ENOENT
  false
end