I am developing a wrapper for Terraform, which at some point during its execution, it may request user input. So, my application must forward everything typed on its stdin to the subprocess' stdin. The following solution works on Linux, but in Windows the subprocess (Terraform) seems to never receive the input:
require 'open3'
def exec(cmd)
Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
stdout_thread = Thread.new do
IO.copy_stream(stdout, STDOUT)
end
stderr_thread = Thread.new do
IO.copy_stream(stderr, STDERR)
end
stdin_thread = Thread.new do
IO.copy_stream(STDIN, stdin)
end
puts "Return code: #{thread.value}"
stdin_thread.join
stdout_thread.join
stderr_thread.join
end
end
exec('terraform destroy')
This solution actually works on Windows when executing some applications that require user input different than Terraform. But, the following two implementations (in Go and Python) are capable of forwarding their stdin to Terraform on Windows. So, it might be the case that my Ruby code has some issue, or perhaps Ruby's implementation for Windows has some limitation when dealing with process execution and input forwarding.
Is anyone aware of such a limitation?
Python example:
import subprocess
import sys
with subprocess.Popen(['terraform', 'destroy'],
stdin=sys.stdin, stdout=sys.stdout) as proc:
proc.wait()
Go example:
package main
import (
"io"
"log"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("terraform", "destroy")
stdin, err := cmd.StdinPipe()
if err != nil { log.Fatal(err) }
stdout, err := cmd.StdoutPipe()
if err != nil { log.Fatal(err) }
stderr, err := cmd.StderrPipe()
if err != nil { log.Fatal(err) }
go func() {
defer stdout.Close()
io.Copy(os.Stdout, stdout)
}()
go func() {
defer stderr.Close()
io.Copy(os.Stderr, stderr)
}()
go func() {
defer stdin.Close()
io.Copy(stdin, os.Stdin)
}()
err = cmd.Run()
log.Printf("Command finished with error: %v", err)
}
The following code snippet based on IO.popen
seems to work. It executes a command, and it returns the command output as an array containing the output lines. Optionally, the output is written to stdout too.
def run(cmd, directory: Dir.pwd, print_output: true)
out = IO.popen(cmd, err: %i[child out], chdir: directory) do |io|
begin
out = ''
loop do
chunk = io.readpartial(4096)
print chunk if print_output
out += chunk
end
rescue EOFError; end
out
end
$?.exitstatus.zero? || (raise "Error running command #{cmd}")
out.split("\n")
.map { |line| line.tr("\r\n", '') }
end