Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.4.0 - 01-07-2026

- Stream command output directly to stdout instead of buffering

1.3.0 - 07-08-2025

- Add --params option
Expand Down
25 changes: 16 additions & 9 deletions lib/pups/exec_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,24 @@ def spawn(command)
return pid
end

IO.popen(command, "w+") do |f|
if stdin
# need a way to get stdout without blocking
Pups.log.info(stdin)
f.write stdin
f.close
else
Pups.log.info(f.readlines.join)
end
opts = { out: $stdout, err: $stderr }

if stdin
reader, writer = IO.pipe
opts[:in] = reader
end

pid = Process.spawn(command, opts)

if stdin
reader.close
Pups.log.info(stdin)
writer.write(stdin)
writer.close
end

Process.wait(pid)

unless $CHILD_STATUS == 0
err =
Pups::ExecError.new(
Expand Down
2 changes: 1 addition & 1 deletion lib/pups/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Pups
VERSION = "1.3.0"
VERSION = "1.4.0"
end
39 changes: 39 additions & 0 deletions test/exec_command_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,44 @@ def test_can_terminate_rogues

assert_raises(Errno::ECHILD) { Process.waitpid(pid, Process::WNOHANG) }
end

def test_stdout_streaming
# Starts a long-running process that outputs immediately
# and then watches a file before quitting.
# If stdout is properly streamed, we should see the output
# immediately rather than waiting for the process to end.

signal_file = Tempfile.new("signal")
signal_path = signal_file.path
signal_file.close

reader, writer = IO.pipe
original_stdout = $stdout.dup

$stdout.reopen(writer)

cmd = ExecCommand.new({})
cmd.add("echo 'streamed output'; while [ ! -s #{signal_path} ]; do sleep 0.1; done")

thread = Thread.new { cmd.run }

# Wait for output - if streaming works, it appears immediately
# If buffered, it would never appear since command loops until signaled
ready = IO.select([reader], nil, nil, 2)
assert ready, "Output should be available before command completes"

output = reader.read_nonblock(1000)
assert_includes(output, "streamed output")
ensure
$stdout.reopen(original_stdout) if original_stdout

writer&.close
reader&.close

File.write(signal_path, "done")
signal_file&.unlink

thread.join(2)
end
end
end