aexl
aexl

Reputation: 125

Python: Communicate with always running subprocesses

I have the following situation. I have a game and two bots, who play this game against each other. All the communication is done via stdin and stdout. Now I want to write an engine, which lets the bots play against each other and decides, which bot won the game. To do this, I need to run the bots from my engine and communicate with them through stdin and stdout.

So, to start a bot, I have done the following:

from subprocess import Popen, PIPE, STDOUT
bot = Popen(['/path/to/bot1'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)

Now I want to transmit the game state to the bot and request a move from my bot. So first, I want to write some lines of text to this process, and after that my engine should wait some time and read the result from the process. Note, that the bot is always running and does not terminate itself.

How can this be achieved?

Upvotes: 2

Views: 114

Answers (2)

Aldehir
Aldehir

Reputation: 2053

Unfortunately, it may not be as easy as reading from bot.stdout and writing to bot.stdin. Reason being, some programs do not flush to STDOUT until the program is done executing if it's a child process. The way to get around this is to spawn it under a pseudo-terminal. Here's an example using the pty module:

bot1.py

#!/usr/bin/env python

name = raw_input("What is your name?\n")
print "Hello {}, my name is Bot1".format(name)
raw_input() # Bot2: Hello Bot1

bot2.py

#!/usr/bin/env python

raw_input() # Bot1: What is your name?
print "Bot2"
greeting = raw_input() # Bot1: Hello Bot2, my name is Bot1
print "Hello {}".format(greeting.split()[-1])

run.py

#!/usr/bin/env python
import os
import pty
import termios

class Bot:

  def __init__(self, name, program):
    self.name = name

    # Fork a process in a pseudo-terminal
    self._pid, self._fd = pty.fork()

    if self._pid == 0:
      # Child process, replace with program
      os.execvp(program, [program])

    # Turn off echoing for child file descriptor, otherwise it will spew the
    # child's output to the parent's STDOUT.
    attribs = termios.tcgetattr(self._fd)
    attribs[3] = attribs[3] & ~termios.ECHO
    termios.tcsetattr(self._fd, termios.TCSANOW, attribs)

  def speak(self, bot):
    output = self.read()
    bot.write(output)

    print '{} -> {}: {}'.format(self.name, bot.name, output)

  def read(self):
    return os.read(self._fd, 1024).strip()

  def write(self, line):
    os.write(self._fd, line + "\n")

if __name__ == '__main__':

  bot1 = Bot('Bot1', './bot1.py')
  bot2 = Bot('Bot2', './bot2.py')

  # Bot1 -> Bot2: What is your name?
  bot1.speak(bot2)

  # Bot2 -> Bot1: Bot2
  bot2.speak(bot1)

  # Bot1 -> Bot2: Hello Bot2, my name is Bot1
  bot1.speak(bot2)

  # Bot2 -> Bot1: Hello Bot1
  bot2.speak(bot1)

output

$ ./run.py 
Bot1 -> Bot2: What is your name?
Bot2 -> Bot1: Bot2
Bot1 -> Bot2: Hello Bot2, my name is Bot1
Bot2 -> Bot1: Hello Bot1

It's a simple example, but hopefully it's enough to get you started.

Upvotes: 3

Roland Smith
Roland Smith

Reputation: 43495

Since you use PIPE, the Popen object bot contains three attributes (stdin, stdout amd stderr) that are streams.

So you can just use bot.stdout.read() and bot.stdin.write(). as Aldehir mentions, you might have to flush the streams that you write to, so that bot actually receives something.

The reason that the documentation advises to use communicate (which is totally unsuitable in this case) is that the OS pipe buffers might fill up if the subprocess or you don't read data fast/often enough, causing a deadlock.

Unless you are sending large quantities of data at high speed I don't thing this will be a problem.

Upvotes: 2

Related Questions