Pasupathi Rajamanickam
Pasupathi Rajamanickam

Reputation: 2052

Communicate with process send key in subprocess linux

I have one sh file, I need to install it in target linux box. So I'm in the process of writing automatic installation for the sh file which required lot of input from user. Example, first thing I made ./file.sh it will show a big paragaraph and ask user to press Enter. I'm stuck in this place. How to send key data to the sub process. Here is what I've tried.

import subprocess

def runProcess(exe):
    global p
    p = subprocess.Popen(exe, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    while(True):
      retcode = p.poll() #returns None while subprocess is running
      line = p.stdout.readline()
      yield line
      if(retcode is not None):
        break

for line in runProcess('./file.sh'.split()):
    if '[Enter]' in line:
        print line + 'got it'
        p.communicate('\r')

Correct me if my understanding is wrong, pardon me if it is duplicate.

Upvotes: 0

Views: 2946

Answers (2)

ShadowRanger
ShadowRanger

Reputation: 155684

If you need to send a bunch of newlines and nothing else, you need to:

  1. Make sure the stdin for the Popen is a pipe
  2. Send the newlines without causing a deadlock

Your current code does neither. Something that might work (assuming they're not using APIs that require direct interaction in a tty, rather than just reading stdin):

import subprocess
import threading

def feednewlines(f):
    try:
        # Write as many newlines as it will take
        while True:
            f.write(b'\n')  # Write newline, not carriage return
            f.flush()       # Flush to ensure it's sent as quickly as possible
    except OSError:
        return   # Done when pipe closed/process exited

def runProcess(exe):
    global p
    # Get stdin as pipe too
    p = subprocess.Popen(exe, stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    # Use thread to just feed as many newlines as needed to stdin of subprocess
    feeder = threading.Thread(target=feednewlines, args=(p.stdin,))
    feeder.daemon = True
    feeder.start()

    # No need to poll, just read until it closes stdout or exits
    for line in p.stdout:
        yield line
    p.stdin.close()  # Stop feeding (causes thread to error and exit)
    p.wait()         # Cleanup process

# Iterate output, and echo when [Enter] seen
for line in runProcess('./file.sh'.split()):
    if '[Enter]' in line:
        print line + 'got it'

For the case where you need to customize the responses, you're going to need to add communication between parent and feeder thread, which makes this uglier, and it only works if the child process is properly flushing its output when it prompts you, even when not connected to a terminal. You might do something like this to define a global queue:

import queue   # Queue on Python 2

feederqueue = queue.Queue()

then change the feeder function to:

def feednewlines(f):
    try:
        while True:
            f.write(feederqueue.get())
            f.flush()
    except OSError:
        return

and change the global code lower down to:

for line in runProcess('./file.sh'.split()):
    if '[Enter]' in line:
        print line + 'got it'
        feederqueue.put(b'\n')
    elif 'THING THAT REQUIRES YOU TO TYPE FOO' in line:
        feederqueue.put(b'foo\n')

etc.

Upvotes: 2

tdelaney
tdelaney

Reputation: 77407

Command line programs run differently when they are run in a terminal verses when they are run in the background. If the program is attached to a terminal, they run in an interactive line mode expecting user interaction. If stdin is a file or a pipe, they run in block mode where writes are delayed until a certain block size is buffered. Your program will never see the [Enter] prompt because it uses pipes and the data is still in the subprocesses output buffer.

The python pexpect module solves this problem by emulating a terminal and allowing you to interact with the program with a series of "expect" statements.

Suppose we want to run a test program

#!/usr/bin/env python3
data = input('[Enter]')
print(data)

its pretty boring. It prompts for data, prints it, then exits. We can run it with pexpect

#!/usr/bin/env python3

import pexpect

# run the program
p = pexpect.spawn('./test.py')
# we don't need to see our input to the program echoed back
p.setecho(False)
# read lines until the desired program output is seen
p.expect(r'\[Enter\]')
# send some data to the program
p.sendline('inner data')
# wait for it to exit
p.expect(pexpect.EOF)
# show everything since the previous expect
print(p.before)

print('outer done')

Upvotes: 1

Related Questions