howl.io.Process
run = (...) ->
with Process cmd: ...
\wait!
Process(opts)
raises an error if opts.cmd is missing or invalid
assert.raises 'cmd', -> Process {}
assert.raises 'cmd', -> Process cmd: 2
assert.not_error -> Process cmd: 'id'
assert.not_error -> Process cmd: {'echo', 'foo'}
returns a process object
assert.equal 'Process', typeof Process cmd: 'true'
raises an error for an unknown command
assert.raises 'howlblargh', -> Process cmd: {'howlblargh'}
sets .argv to the parsed command line
p = Process cmd: {'echo', 'foo'}
assert.same {'echo', 'foo'}, p.argv
p = Process cmd: 'echo "foo bar"'
assert.same { '/bin/sh', '-c', 'echo "foo bar"'}, p.argv
allows specifying a different shell
p = Process cmd: 'foo', shell: '/bin/echo'
assert.same { '/bin/echo', '-c', 'foo'}, p.argv
Process.open_pipe(cmd, opts)
it 'creates a process set up for piping', (done) ->
howl_async ->
p = Process.open_pipe {'sh', '-c', 'cat; echo foo >&2'}, stdin: 'reverb'
out, err = p\pump!
assert.equal 'reverb', out
assert.equal 'foo\n', err
assert.equal 'Process', typeof(p)
done!
Process.execute(cmd, opts)
it 'executes the specified command and return <out, err, process>', (done) ->
howl_async ->
out, err, p = Process.execute {'sh', '-c', 'cat; echo foo >&2'}, stdin: 'reverb'
assert.equal 'reverb', out
assert.equal 'foo\n', err
assert.equal 'Process', typeof(p)
done!
it "executes string commands using /bin/sh by default", (done) ->
howl_async ->
status, out = pcall Process.execute, 'echo $0'
assert.is_true status
assert.equal '/bin/sh\n', out
done!
it "allows specifying a different shell", (done) ->
howl_async ->
status, out, _, process = pcall Process.execute, 'blargh', shell: '/bin/echo'
assert.is_true status
assert.match out, 'blargh'
assert.equal 'blargh', process.command_line
done!
it 'opts.working_directory sets the working working directory', (done) ->
howl_async ->
with_tmpdir (dir) ->
out = Process.execute 'pwd', working_directory: dir
assert.equal dir.path, out.stripped
done!
it 'opts.env sets the process environment', (done) ->
howl_async ->
out = Process.execute {'env'}, env: { foo: 'bar' }
assert.equal 'foo=bar', out.stripped
done!
it 'works with large process outputs', (done) ->
howl_async ->
File.with_tmpfile (f) ->
file_contents = string.rep "xxxxxxxxxxxxxxxxxxxxxxxxxx yyyyyyyyyyyyyyyyyyy zzzzzzzzzzzzzzzzzzz\n", 5000
f.contents = file_contents
status, out = pcall Process.execute, "cat #{f.path}"
assert.is_true status
assert.equal file_contents, out
done!
pump(on_stdout, on_stderr)
(when the <on_stdout> handler is provided)
it 'invokes the handler for any stdout output before returning', (done) ->
howl_async ->
on_stdout = spy.new -> nil
p = Process cmd: 'echo foo', read_stdout: true
p\pump on_stdout
assert.is_true p.exited
assert.spy(on_stdout).was_called_with 'foo\n'
assert.spy(on_stdout).was_called_with nil
done!
(when the <on_stderr> handler is provided)
it 'invokes the handler for any stderr output before returning', (done) ->
howl_async ->
on_stderr = spy.new -> nil
p = Process cmd: 'echo err >&2', read_stderr: true
p\pump nil, on_stderr
assert.is_true p.exited
assert.spy(on_stderr).was_called_with 'err\n'
assert.spy(on_stderr).was_called_with nil
done!
(when both handlers are provided)
it 'invokes both handlers for any output before returning', (done) ->
howl_async ->
on_stdout = spy.new -> nil
on_stderr = spy.new -> nil
p = Process cmd: 'echo out; echo err >&2', read_stdout: true, read_stderr: true
p\pump on_stdout, on_stderr
assert.is_true p.exited
assert.spy(on_stdout).was_called_with 'out\n'
assert.spy(on_stdout).was_called_with nil
assert.spy(on_stderr).was_called_with 'err\n'
assert.spy(on_stderr).was_called_with nil
done!
(when handlers are not specified)
collects and returns <out> and <err> output
p = Process cmd: 'echo foo', read_stdout: true
stdout, stderr = p\pump!
assert.equals 'foo\n', stdout
assert.is_nil stderr
p = Process cmd: 'echo err >&2', read_stderr: true
stdout, stderr = p\pump!
assert.equals 'err\n', stderr
assert.is_nil stdout
p = Process cmd: 'echo out; echo err >&2', read_stdout: true, read_stderr: true
stdout, stderr = p\pump!
assert.equals 'out\n', stdout
assert.equals 'err\n', stderr
pump_lines(on_stdout, on_stderr)
it 'invokes the handler for any stdout output before returning', (done) ->
howl_async ->
on_stdout = spy.new -> nil
p = Process cmd: 'echo "foo\nbar"', read_stdout: true
p\pump_lines on_stdout
assert.is_true p.exited
assert.spy(on_stdout).was_called_with {'foo', 'bar'}
done!
it 'invokes the handler for any stderr output before returning', (done) ->
howl_async ->
on_stderr = spy.new -> nil
p = Process cmd: 'echo "err1\nerr2" >&2', read_stderr: true
p\pump_lines nil, on_stderr
assert.is_true p.exited
assert.spy(on_stderr).was_called_with {'err1', 'err2'}
done!
it 'handles CRLFs', (done) ->
howl_async ->
on_stdout = spy.new -> nil
p = Process cmd: 'echo "one\r\ntwo"', read_stdout: true
p\pump_lines on_stdout
assert.spy(on_stdout).was_called_with {'one', 'two'}
done!
it 'returns empty lines as empty lines', (done) ->
howl_async ->
on_stdout = spy.new -> nil
p = Process cmd: 'echo "one\n\nthree"', read_stdout: true
p\pump_lines on_stdout
assert.spy(on_stdout).was_called_with {'one', '', 'three'}
done!
it 'assembles lines correctly for larger reads', (done) ->
howl_async ->
File.with_tmpfile (f) ->
lines = ["line #{i}" for i = 1, 4000]
f.contents = table.concat lines, '\n'
passed_lines = {}
on_stdout = (_lines) ->
for l in *_lines
table.insert passed_lines, l
p = Process cmd: "cat '#{f.path}'", read_stdout: true
p\pump_lines on_stdout
for i = 1, 4000
assert.equal lines[i], passed_lines[i]
done!
(when handlers are not specified)
collects and returns <out> and <err> output as lines
p = Process cmd: 'echo "one\ntwo"', read_stdout: true
stdout, stderr = p\pump_lines!
assert.same {'one', 'two'}, stdout
assert.equals 0, #stderr
p = Process cmd: 'echo "one\ntwo" >&2', read_stderr: true
stdout, stderr = p\pump_lines!
assert.same {'one', 'two'}, stderr
assert.equals 0, #stdout
p = Process cmd: 'echo "one\ntwo"; echo "three" >&2', read_stdout: true, read_stderr: true
stdout, stderr = p\pump_lines!
assert.same {'one', 'two'}, stdout
assert.same {'three'}, stderr
wait()
it 'waits until the process is finished', (done) ->
settimeout 2
howl_async ->
File.with_tmpfile (file) ->
file\delete!
p = Process cmd: { 'sh', '-c', "sleep 1; touch '#{file.path}'" }
p\wait!
assert.is_true file.exists
done!
(signal handling)
send_signal(signal) and .signalled
it 'sends the specified signal to the process', (done) ->
howl_async ->
p = Process cmd: 'cat', write_stdin: true
p\send_signal 9
p\wait!
assert.is_true p.signalled
done!
it '.signalled is false for a non-signaled process', (done) ->
howl_async ->
p = Process cmd: 'id'
p\wait!
assert.is_false p.signalled
done!
it '.signal holds the signal used for terminating the process', (done) ->
howl_async ->
p = Process cmd: 'cat', write_stdin: true
p\send_signal 9
p\wait!
assert.equals 9, p.signal
done!
it '.signal_name holds the name of the signal used for terminating the process', (done) ->
howl_async ->
p = Process cmd: 'cat', write_stdin: true
p\send_signal 9
p\wait!
assert.equals 'KILL', p.signal_name
done!
it 'signals can be referred to by name as well', (done) ->
howl_async ->
p = Process cmd: 'cat', write_stdin: true
p\send_signal 'KILL'
p\wait!
assert.equals 9, p.signal
done!
.exit_status
is nil for a running process
p = Process cmd: { 'sh', '-c', "sleep 1; true" }
assert.is_nil p.exit_status
p\wait!
it 'is nil for a signalled process', (done) ->
howl_async ->
p = Process cmd: 'cat', write_stdin: true
p\send_signal 9
p\wait!
assert.is_nil p.exit_status
done!
it 'is set to the exit status for a normally exited process', (done) ->
howl_async ->
p = run 'echo foo'
assert.equals 0, p.exit_status
p = run {'sh', '-c', 'exit 1' }
assert.equals 1, p.exit_status
p = run {'sh', '-c', 'exit 2' }
assert.equals 2, p.exit_status
done!
.working_directory
(when provided during launch)
is the same directory
cwd = File '/bin'
p = Process(cmd: 'true', working_directory: cwd)
assert.equal cwd, p.working_directory
is always a File instance
p = Process(cmd: 'true', working_directory: '/bin')
assert.equal 'File', typeof p.working_directory
(when not provided)
is the current working directory
p = Process(cmd: 'true')
assert.equal File(glib.get_current_dir!), p.working_directory
.successful
it 'is true if the process exited cleanly with a zero exit code', (done) ->
howl_async ->
assert.is_true run('id').successful
done!
it 'is false if the process exited with a non-zero exit code', (done) ->
howl_async ->
assert.is_false run('false').successful
done!
it 'is false if the process exited due to a signal', (done) ->
howl_async ->
p = Process cmd: 'cat', write_stdin: true
p\send_signal 9
p\wait!
assert.is_false p.successful
done!
.stdout
it 'allows reading process output', (done) ->
howl_async ->
p = Process cmd: {'echo', 'one\ntwo'}, read_stdout: true
assert.equals 'one\ntwo\n', p.stdout\read!
assert.is_nil p.stdout\read!
done!
.stderr
it 'allows reading process error output', (done) ->
howl_async ->
p = Process cmd: {'sh', '-c', 'echo foo >&2'}, read_stderr: true
assert.equals 'foo\n', p.stderr\read!
done!
.stdin
howl_async ->
p = Process cmd: {'cat'}, write_stdin: true, read_stdout: true
with p.stdin
\write 'round-trip'
\close!
assert.equals 'round-trip', p.stdout\read!
p\wait!
done!
.command_line
(when the command is specified as a string)
it 'is the same', (done) ->
howl_async ->
assert.equal 'echo command "bar"', run('echo command "bar"').command_line
done!
(when the command is specified as a table)
it 'is a created shell command line', (done) ->
howl_async ->
assert.equal "echo command 'bar zed'", run({'echo', 'command', 'bar zed'}).command_line
done!
.exit_status_string
it 'provides the exit code for a normally terminated process', (done) ->
howl_async ->
assert.equals 'exited normally with code 0', run('id').exit_status_string
assert.equals 'exited normally with code 1', run('exit 1').exit_status_string
done!
it 'provides the signal name for a killed process', (done) ->
howl_async ->
p = Process cmd: {'cat'}, write_stdin: true, read_stdout: true
p\send_signal 'KILL'
p\wait!
assert.equals 'killed by signal 9 (KILL)', p.exit_status_string
done!
Process.running
it 'is a table of currently running processes, keyed by pid', (done) ->
howl_async ->
assert.same {}, Process.running
p = Process cmd: {'cat'}, write_stdin: true
assert.same {[p.pid]: p}, Process.running
p.stdin\close!
p\wait!
assert.same {}, Process.running
done!
(resource management)
it 'processes are collected correctly', (done) ->
howl_async ->
p = Process cmd: {'echo', 'one\ntwo'}, read_stdout: true
assert.equals 'one\ntwo\n', p.stdout\read!
p\wait!
assert.is_nil p.stdout\read!
list = setmetatable {p}, __mode: 'v'
p = nil
collect_memory!
assert.is_nil list[1]
done!