/// <summary> /// Send a command to the chess engine. Commands are serialized on another thread /// </summary> /// <param name="commandString">command to send</param> /// <param name="expectedResponse">response we expect to get. If this is /// an empty string, then sync up after the command with the engine</param> void IChessEngine.SendCommandAsync(string commandString, string expectedResponse) { // Under lock - update the queue of commands to execute and signal // our running worker thread (or create it the first time) lock (queue_lock) { CommandExecutionParameters cep = new CommandExecutionParameters(commandString, expectedResponse); Debug.WriteLine(String.Format("Enqueueing command pair (\"{0}\", \"{1}\")", commandString, expectedResponse)); commandQueue.Enqueue(cep); // Signal the worker thread newCommandAddedToQueue.Set(); } }
/// <summary> /// Worker Thread Method responsible for processing commands to the chess engine. /// When first launched, the thread will attempt to launch the engine process, /// then enter a wait state until work is sent (added to the queue). /// /// When signalled for work, the thread will take the next queueded item (if any) /// and send the command to the engine, and wait for the response triggered in /// the stdout callback OnDataReceieved. This will repeat so long as items /// are in the queue. /// /// If a command has no response (e.g. 'position' uci command) a pair of /// "IsReady"/"ReadyOk" command/response is sent to sync up. /// /// This thread exists for the lifetime of the owning ChessGame object /// </summary> public void CommandQueueExecutionMethod() { // The loader and path are injected, so the UCIChessEngine object doesn't // need to care how the interface was created/obtained/loaded from disk engineProcess = engineProcessLoader.Start(fullPathToEngine, OnDataReceived); AutoResetEvent[] workerEvents = { newCommandAddedToQueue, shutdownCommandQueue }; int waitResult = WaitHandle.WaitTimeout; bool exitThread = false; bool moreWork = false; while (!exitThread) { lock (queue_lock) { moreWork = (commandQueue.Count() > 0); } if (!moreWork) { waitResult = WaitHandle.WaitAny(workerEvents); } else { waitResult = 0; // Process more items } if (waitResult == 1) // shutdownCommandQueue { exitThread = true; } else { // Set to an empty one since it's a value type...but we only care // if we actually find one. string dummyCommand = "{CommandQueueExecutionMethod}"; CommandExecutionParameters cep = new CommandExecutionParameters(dummyCommand, ""); // There should be new work to do lock (queue_lock) { if (commandQueue.Count() > 0) { // Do not remove the item yet - wait until it's processed cep = commandQueue.Peek(); } } if ((String.Compare(cep.Command, dummyCommand) != 0) && (engineProcess.InputStream != null)) { // Found something - write it to the process, Debug.WriteLine(String.Concat("=>Engine: ", cep.Command)); engineProcess.InputStream.WriteLine(cep.Command); // If this is true, the above command has no response, and we'd wait here forever if (cep.NeedsIsReadySync) { StreamWriter writer = engineProcess.InputStream; UciIsReadyCommand isReadyCommand = new UciIsReadyCommand(ref writer); isReadyCommand.Execute(this); } // Wait on the exit event as well, so we can bail as fast as needed AutoResetEvent[] commandEvents = { uciCommandFinished, shutdownCommandQueue }; int commandWait = WaitHandle.WaitAny(commandEvents); if (commandWait == 1) { exitThread = true; } } } } }
/// <summary> /// Event handler for process stdout. This is where we parse responses from the chess engine /// </summary> /// <param name="sender">ignored</param> /// <param name="e">The string sent to stdout</param> public void OnDataReceived(object sender, DataReceivedEventArgs e) { // This level gets all responses from the engine, thinking lines, options, debug, etc // However if we get a null event, filter it out since there is nothing we can do if (e.Data == null) { return; } string data = e.Data; SendVerboseEvent(data); // let the verbose handler get the empty lines if (data == String.Empty) { return; // don't bother, nothing to do } // Spike 1.1 if (String.Compare(data, "Error: Fatal no best move") == 0) { // Convert to something approaching a real protocol response data = "bestmove (none)"; SendVerboseEvent(String.Format("Fixed formatting to \"{0}\"", data)); } // MadChess else if (String.Compare(data, "bestmove Null") == 0) { data = "bestmove (none)"; SendVerboseEvent(String.Format("Fixed formatting to \"{0}\"", data)); } // Set to an empty one since it's a value type...but we only care // if we actually find one. string dummyCommand = "{OnDataReceived}"; CommandExecutionParameters cep = new CommandExecutionParameters("", ""); // Don't need a lock here, just need to know if it's > 0, and only this // method Dequeues if (commandQueue.Count() > 0) { cep = commandQueue.Peek(); } bool foundCommand = (String.Compare(dummyCommand, cep.Command) != 0); bool dequeue = false; if (foundCommand) { // First check if we need to wait on a ReadyOK response, this happens // when there is no expected response when queued if (cep.NeedsIsReadySync) { if (String.Compare(data, ReadyOk) == 0) { dequeue = true; // This dequeues the original command commandResponse = data; // nothing was actually queued for the "IsReady" write } } else if (data.StartsWith(cep.Expected)) { dequeue = true; // Mark command for dequeue and commandResponse = data; // save the response // If we're asking for a move - then save the response we care about // the SAN for the move - it comes right after "bestmove" // If no move (e.g. mate) will return 'bestmove (none)' if (data.StartsWith(BestMoveResponse)) { string[] parts = data.Split(' '); bestMove = parts[1]; } } } // Remove item if processed and notify listeners if (dequeue) { lock (queue_lock) { commandQueue.Dequeue(); } SendReceivedEvent(CommandResponse); uciCommandFinished.Set(); } }