Skip to content

Commit

Permalink
Fixed race condition during UCI engine startup
Browse files Browse the repository at this point in the history
Lines received during engine startup sometimes needed to be processed before
the Engine object was fully initialized.
  • Loading branch information
niklasf committed Mar 8, 2015
1 parent 7177894 commit 3efd4de
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Up for the next release
* The result of the `stop` and `go` UCI commands are now named tuples (instead
of just normal tuples).

* Fixed race condition during UCI engine startup. Lines received during engine
startup sometimes needed to be processed before the Engine object was fully
initialized.

New in v0.7.0
-------------

Expand Down
43 changes: 28 additions & 15 deletions chess/uci.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,13 +549,14 @@ def result(self, timeout=None):


class MockProcess(object):
def __init__(self, engine):
self.engine = engine

def __init__(self):
self._expectations = collections.deque()
self._is_dead = threading.Event()
self._std_streams_closed = False

def spawn(self, engine):
self.engine = engine

def expect(self, expectation, responses=()):
self._expectations.append((expectation, responses))

Expand Down Expand Up @@ -603,12 +604,15 @@ def __repr__(self):


class PopenProcess(object):
def __init__(self, engine, command):
self.engine = engine
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=1, universal_newlines=True)
def __init__(self, command):
self.command = command

self._receiving_thread = threading.Thread(target=self._receiving_thread_target)
self._receiving_thread.daemon = True

def spawn(self, engine):
self.engine = engine
self.process = subprocess.Popen(self.command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=1, universal_newlines=True)
self._receiving_thread.start()

def _receiving_thread_target(self):
Expand Down Expand Up @@ -651,16 +655,21 @@ def __repr__(self):


class SpurProcess(object):
def __init__(self, engine, shell, command):
self._stdout_buffer = []
def __init__(self, shell, command):
self.shell = shell
self.command = command

self.engine = engine
self.process = shell.spawn(command, store_pid=True, allow_error=True, stdout=self)
self._stdout_buffer = []

self._result = None

self._waiting_thread = threading.Thread(target=self._waiting_thread_target)
self._waiting_thread.daemon = True

def spawn(self, engine):
self.engine = engine

self.process = self.shell.spawn(self.command, store_pid=True, allow_error=True, stdout=self)
self._waiting_thread.start()

def write(self, byte):
Expand Down Expand Up @@ -705,8 +714,8 @@ def __repr__(self):


class Engine(object):
def __init__(self, process_cls, args):
self.process = process_cls(self, *args)
def __init__(self, process):
self.process = process

self.name = None
self.author = None
Expand All @@ -722,13 +731,15 @@ def __init__(self, process_cls, args):
self.queue = queue.Queue()
self.stdin_thread = threading.Thread(target=self._stdin_thread_target)
self.stdin_thread.daemon = True
self.stdin_thread.start()

self.return_code = None
self.terminated = threading.Event()

self.info_handlers = []

self.process.spawn(self)
self.stdin_thread.start()

def send_line(self, line):
LOGGER.debug("%s << %s", self.process, line)
return self.process.send_line(line)
Expand Down Expand Up @@ -1257,7 +1268,8 @@ def popen_engine(command, engine_cls=Engine):
The input and input streams will be linebuffered and able both Windows
and Unix newlines.
"""
return engine_cls(PopenProcess, (command, ))
process = PopenProcess(command)
return engine_cls(process)


def spur_spawn_engine(shell, command, engine_cls=Engine):
Expand All @@ -1271,4 +1283,5 @@ def spur_spawn_engine(shell, command, engine_cls=Engine):
.. _Spur: https://pypi.python.org/pypi/spur
"""
return engine_cls(SpurProcess, (shell, command))
process = SpurProcess(shell, command)
return engine_cls(process)
2 changes: 1 addition & 1 deletion test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1250,7 +1250,7 @@ def test_async_terminate(self):
class UciEngineTestCase(unittest.TestCase):

def setUp(self):
self.engine = chess.uci.Engine(chess.uci.MockProcess, ())
self.engine = chess.uci.Engine(chess.uci.MockProcess())
self.mock = self.engine.process

self.mock.expect("uci", ("uciok", ))
Expand Down

0 comments on commit 3efd4de

Please sign in to comment.