2

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?

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Wow, thanks for this example! I think, this helps me a lot.
Is it possible to terminate and restart a bot, if it does not respond in a given time window?
Sure, you have the child pid available to you, so just use os.kill(bot2._pid, signal.SIGTERM), you'll need to add import signal to the top. Then just create a new bot instance.
2

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.

3 Comments

It could easily be a problem even at low speeds unless you're extremely careful. All it takes is you sending more data than the pipe can hold while the child process is emitting output larger than its pipe can hold. If this happens even once (or you try to read more from the bot's output than is available), both processes will block. The only way this is remotely safe without careful use of the threading or select module is if there is a predictable correspondence between inputs and outputs (say, lines of data), both sides flush predictably, and all messages are < PIPE_BUF in size.
On my operating system, PIPE_BUF is the limit for atomic writes. The size of a pipe buffer is PIPE_SIZE which is 16 kiB.
I chose PIPE_BUF over PIPE_SIZE because there are a number of ways to race a write/read where a read only pulls part of what it's looking for and assumes it's done for now; PIPE_BUF is being conservative, but PIPE_SIZE would usually be okay.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.