How to setup a pipe to a long running process?

cmd = "bash"
args = "-i"
reader,writer = IO.pipe
pp writer
p1 = Process.new(cmd, shell: false, input: writer, output: reader)
pp p1
sleep 2
writer.puts "pwd"
sleep 2
puts reader.gets
sleep
   
#<Process:0x7f09a79f3fc0
 @channel=nil,
 @error=nil,
 @input=nil,          # why is input nil here ?
 @output=nil,
 @pid=15373,
 @wait_count=0,
 @waitpid=#<Channel(Int32):0x7f09a79f3c80>>

Please learn to use markdown code blocks and preserve indentation while copy pasting code. Your code posts tend to be really hard to read.

Process.run with a block sets up pipes for you by default:

Process.run("ls", args: {"/"}) do |proc|
  puts proc.output.gets
end

If you set them explicitly, Process does not bother to retain a reference to them, you passed one in, so you got it already! Just reuse your writer variable.

Ok, I will try to write better readable postings.

I have some experience with Process.run and the default pipes.
My issue is that I want to use STDIN and STDOUT in the main process,
and at the same time use a pipe for communication with a spawned long running process like an interactive shell session(“bash -i”).

  • I do not want that STDIN and STDOUT is piped to the spawned process, therefore I want to setup an explicit additional pipe which uses its own filedesrciptors. The main process still needs to read from the original STDIN. So somehow I want to pass filedescriptors to process.new or process.run.

This is the case with above code. STDIN is still the original in the block.

You may need to spawn a fiber and communicate to it through a Channel if you want to read from the process and STDIN at the same time rather than on a back and forth kind of basis.

I got my shell wrapper working by using socat or python to achieve a sort of PTY session. Is there a way in crystal to fake/spawn a PTY ? This wrapper can be used to talk to any process STDIN which is normally used in a terminal session.

  • I always wanted to have a sort of mediation script on STDIN which allows me to add additional help when commands are typed in, e.g. open a browser help window or search an API for code completion or code examples while typing.
def run_cmd(cmd, mystdin)
spawn do 
  Process.run(cmd, shell: true, output: STDOUT, input: mystdin) 
   end
 end

#Make the shell believe that input comes from a PTY (pseudo terminal)  
#cmd ="python -c 'import pty; pty.spawn(\"/bin/bash\")'"
cmd ="socat exec:'bash -i',pty,stderr,setsid,sigint,sane -"
reader, writer = IO.pipe
handle_stdin(STDIN,writer)
run_cmd(cmd, reader)
writer << "date\n"     #write something to the shells STDIN

#handle stdin char by char - this allows command completion with TAB
def handle_stdin(myin,writer)
spawn do 
  while (char = STDIN.raw &.read_char) != '\u{3}'    #ctrl-c
  #p! char  
    print " #Return" if char == '\r' 
    writer << char
  end
  puts "You have pressed crtl-c"
  exit
 end
end

sleep

I’m not aware of any standard library or shard for dealing with pty’s, which means the way is to figure out how to do it in C, then write a C binding doing the same thing. Then ideally building a nice abstraction on it and publishing it as a shard so others don’t have to do it again :)

Here is how Ruby 2.7 does it in the ruby stdlib:
https://www.rubydoc.info/stdlib/pty/PTY.spawn
https://ruby-doc.org/stdlib-2.7.0.rc2/libdoc/pty/rdoc/PTY.html
The C code (its about 700 lines):

Back to the original question, which I think has 2 parts:

  1. Why are input and output nil in Process?
    Looking at the Crystal code for Process, these are only set if the input/output arguments are passed as Process::Redirect::Pipe.

  2. Why doesn’t your code work?
    The problem is that you are only using one pipe. Pipes are unidrectional, unlike sockets. A process writes into one end and another reads from the other end. If you want to pass commands to bash, you need one pipe to do that, and if you want to read its output, you need a second pipe. This variation of your original code seems to work:

cmd = "bash"
reader1,writer1 = IO.pipe
reader2,writer2 = IO.pipe
p1 = Process.new(cmd, shell: false, input: reader1, output: writer2, error: Process::Redirect::Inherit)
reader1.close
writer2.close
pp p1
sleep 2
writer1.puts "pwd"
puts reader2.gets
sleep 2
writer1.puts "ls -l"
puts reader2.gets
puts reader2.gets
puts reader2.gets
sleep

This also makes sure that bash’s STDERR is not closed so you can see any errors. What would be even neater is to let Process.new create the pipes for you by setting them to Process::Redirect::Pipe, then you can use p1.input & p1.output.

1 Like