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}