usi/process/
engine.rs

1use std::collections::HashMap;
2use std::ffi::OsStr;
3use std::io::BufReader;
4use std::path::Path;
5use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
6use std::thread;
7
8use super::reader::{EngineCommandReader, EngineOutput};
9use super::writer::GuiCommandWriter;
10use crate::error::Error;
11use crate::protocol::*;
12
13/// Represents a metadata returned from a USI engine.
14#[derive(Clone, Debug, Default)]
15pub struct EngineInfo {
16    name: String,
17    options: HashMap<String, String>,
18}
19
20impl EngineInfo {
21    /// Returns an engine name.
22    pub fn name(&self) -> &str {
23        &self.name
24    }
25
26    /// Returns available engine options.
27    pub fn options(&self) -> &HashMap<String, String> {
28        &self.options
29    }
30}
31
32/// `UsiEngineHandler` provides a type-safe interface to the USI engine process.
33///
34/// # Examples
35/// ```no_run
36/// use usi::{BestMoveParams, Error, EngineCommand, GuiCommand, UsiEngineHandler};
37///
38/// let mut handler = UsiEngineHandler::spawn("/path/to/usi_engine", "/path/to/working_dir").unwrap();
39///
40/// // Get the USI engine information.
41/// let info = handler.get_info().unwrap();
42/// assert_eq!("engine name", info.name());
43///
44/// // Set options and prepare the engine.
45/// handler.send_command(&GuiCommand::SetOption("USI_Ponder".to_string(), Some("true".to_string()))).unwrap();
46/// handler.prepare().unwrap();
47/// handler.send_command(&GuiCommand::UsiNewGame).unwrap();
48///
49/// // Start listening to the engine output.
50/// // You can pass the closure which will be called
51/// //   everytime new command is received from the engine.
52/// handler.listen(move |output| -> Result<(), Error> {
53///     match output.response() {
54///         Some(EngineCommand::BestMove(BestMoveParams::MakeMove(
55///                      ref best_move_sfen,
56///                      ref ponder_move,
57///                 ))) => {
58///                     assert_eq!("5g5f", best_move_sfen);
59///                 }
60///         _ => {}
61///     }
62///     Ok(())
63/// }).unwrap();
64/// handler.send_command(&GuiCommand::Usi).unwrap();
65/// ```
66#[derive(Debug)]
67pub struct UsiEngineHandler {
68    process: Child,
69    reader: Option<EngineCommandReader<BufReader<ChildStdout>>>,
70    writer: GuiCommandWriter<ChildStdin>,
71}
72
73impl Drop for UsiEngineHandler {
74    fn drop(&mut self) {
75        self.kill().unwrap();
76    }
77}
78impl UsiEngineHandler {
79    /// Spanws a new process of the specific USI engine.
80    pub fn spawn<P: AsRef<OsStr>, Q: AsRef<Path>>(
81        engine_path: P,
82        working_dir: Q,
83    ) -> Result<Self, Error> {
84        let mut process = Command::new(engine_path)
85            .current_dir(working_dir)
86            .stdin(Stdio::piped())
87            .stdout(Stdio::piped())
88            .spawn()?;
89
90        let stdin = process.stdin.take().unwrap();
91        let stdout = process.stdout.take().unwrap();
92
93        Ok(UsiEngineHandler {
94            process,
95            reader: Some(EngineCommandReader::new(BufReader::new(stdout))),
96            writer: GuiCommandWriter::new(stdin),
97        })
98    }
99
100    /// Request metadata such as a name and available options.
101    /// Internally `get_info()` sends `usi` command and
102    /// records `id` and `option` commands until `usiok` is received.
103    /// Returns `Error::IllegalOperation` when called after `listen` method.
104    pub fn get_info(&mut self) -> Result<EngineInfo, Error> {
105        let reader = match &mut self.reader {
106            Some(r) => Ok(r),
107            None => Err(Error::IllegalOperation),
108        }?;
109
110        let mut info = EngineInfo::default();
111        self.writer.send(&GuiCommand::Usi)?;
112
113        loop {
114            let output = reader.next_command()?;
115            match output.response() {
116                Some(EngineCommand::Id(IdParams::Name(name))) => {
117                    info.name = name.to_string();
118                }
119                Some(EngineCommand::Option(OptionParams {
120                    ref name,
121                    ref value,
122                })) => {
123                    info.options.insert(
124                        name.to_string(),
125                        match value {
126                            OptionKind::Check { default: Some(f) } => {
127                                if *f { "true" } else { "false" }.to_string()
128                            }
129                            OptionKind::Spin {
130                                default: Some(n), ..
131                            } => n.to_string(),
132                            OptionKind::Combo {
133                                default: Some(s), ..
134                            } => s.to_string(),
135                            OptionKind::Button { default: Some(s) } => s.to_string(),
136                            OptionKind::String { default: Some(s) } => s.to_string(),
137                            OptionKind::Filename { default: Some(s) } => s.to_string(),
138                            _ => String::new(),
139                        },
140                    );
141                }
142                Some(EngineCommand::UsiOk) => break,
143                _ => {}
144            }
145        }
146
147        Ok(info)
148    }
149
150    /// Prepare the engine to be ready to start a new game.
151    /// Internally, `prepare()` sends `isready` command and waits until `readyok` is received.
152    /// Returns `Error::IllegalOperation` when called after `listen` method.
153    pub fn prepare(&mut self) -> Result<(), Error> {
154        let reader = match &mut self.reader {
155            Some(r) => Ok(r),
156            None => Err(Error::IllegalOperation),
157        }?;
158
159        self.writer.send(&GuiCommand::IsReady)?;
160        loop {
161            let output = reader.next_command()?;
162
163            if let Some(EngineCommand::ReadyOk) = output.response() {
164                break;
165            }
166        }
167
168        Ok(())
169    }
170    /// Sends a command to the engine.
171    pub fn send_command(&mut self, command: &GuiCommand) -> Result<(), Error> {
172        self.writer.send(command)
173    }
174
175    /// Terminates the engine.
176    pub fn kill(&mut self) -> Result<(), Error> {
177        self.writer.send(&GuiCommand::Quit)?;
178        self.process.kill()?;
179        Ok(())
180    }
181
182    /// Spanws a new thread to monitor outputs from the engine.
183    /// `hook` will be called for each USI command received.
184    /// `prepare` method can only be called before `listen` method.
185    pub fn listen<F, E>(&mut self, mut hook: F) -> Result<(), Error>
186    where
187        F: FnMut(&EngineOutput) -> Result<(), E> + Send + 'static,
188        E: std::error::Error + Send + Sync + 'static,
189    {
190        let mut reader = self.reader.take().ok_or(Error::IllegalOperation)?;
191
192        thread::spawn(move || -> Result<(), Error> {
193            loop {
194                match reader.next_command() {
195                    Ok(output) => {
196                        if let Err(e) = hook(&output) {
197                            return Err(Error::HandlerError(Box::new(e)));
198                        }
199                    }
200                    Err(Error::IllegalSyntax) => {
201                        // Ignore illegal commands.
202                        continue;
203                    }
204                    Err(err) => {
205                        return Err(err);
206                    }
207                }
208            }
209        });
210
211        Ok(())
212    }
213}