1use std::io;
12use std::path::Path;
13use std::process::{Child, Command, ExitStatus, Stdio};
14use std::sync::mpsc::{Receiver, SyncSender};
15use std::time::{Duration, Instant};
16
17use crate::client::{Client, ClientEvent};
18use crate::frame::{self, FrameDecoder};
19use crate::message::{
20 AckOutcome, AdmittedAction, ClientCapabilities, ClientInfo, ClientMessage, EffectOutcome,
21 EffectRequest, ProtocolError, Refusal, ServerMessage, ShutdownReason, ViewSnapshot,
22 PROTOCOL_VERSION,
23};
24use crate::{ClientError, MAX_PENDING_REQUESTS};
25
26pub const DEFAULT_BUDGET: Duration = Duration::from_secs(120);
29
30#[derive(Debug, thiserror::Error)]
33pub enum DriverError {
34 #[error("io: {0}")]
35 Io(#[from] io::Error),
36 #[error("refused: {0}")]
37 Refused(Refusal),
38 #[error("protocol error: {0}")]
39 Protocol(ProtocolError),
40 #[error("the backend closed ({reason:?}) while waiting for {waiting}")]
41 BackendClosed {
42 reason: Option<ShutdownReason>,
43 waiting: &'static str,
44 },
45 #[error("timed out waiting for {waiting} (last applied generation {last_generation}, state {last_state})")]
46 Timeout {
47 waiting: &'static str,
48 last_generation: u64,
49 last_state: String,
52 },
53 #[error("client state: {0}")]
54 Client(#[from] ClientError),
55 #[error("the backend sent undecodable bytes: {0}")]
56 Transport(String),
57}
58
59enum Transport {
61 Message(ServerMessage),
62 Failed(String),
63 Closed,
64}
65
66enum Pumped {
69 Ack(u64, AckOutcome),
70 Error(ProtocolError),
71 Closed(ShutdownReason),
72}
73
74pub struct Driver {
76 child: Child,
77 stdin: io::BufWriter<std::process::ChildStdin>,
78 rx: Receiver<Transport>,
79 stderr: std::sync::Arc<std::sync::Mutex<String>>,
80 client: Client,
81 seq: u64,
82 budget: Duration,
83 closed: Option<ShutdownReason>,
84 effects: Vec<(u64, EffectRequest)>,
85}
86
87impl Driver {
88 pub fn spawn(
93 backend: &Path,
94 cwd: &Path,
95 env: &[(&str, std::ffi::OsString)],
96 ) -> Result<Self, DriverError> {
97 Self::spawn_args(backend, &["--ui-stdio"], cwd, env)
98 }
99
100 pub fn spawn_args(
102 backend: &Path,
103 args: &[&str],
104 cwd: &Path,
105 env: &[(&str, std::ffi::OsString)],
106 ) -> Result<Self, DriverError> {
107 let mut command = Command::new(backend);
108 command
109 .args(args)
110 .current_dir(cwd)
111 .stdin(Stdio::piped())
112 .stdout(Stdio::piped())
113 .stderr(Stdio::piped());
114 for (key, value) in env {
115 command.env(key, value);
116 }
117 let mut child = command.spawn()?;
118 let stdin = child
119 .stdin
120 .take()
121 .ok_or_else(|| io::Error::other("backend stdin was not piped"))?;
122 let stdout = child
123 .stdout
124 .take()
125 .ok_or_else(|| io::Error::other("backend stdout was not piped"))?;
126 let mut stderr_pipe = child
127 .stderr
128 .take()
129 .ok_or_else(|| io::Error::other("backend stderr was not piped"))?;
130
131 let (tx, rx) = std::sync::mpsc::sync_channel(MAX_PENDING_REQUESTS);
132 std::thread::spawn(move || read_backend(stdout, tx));
133 let stderr = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
134 std::thread::spawn({
135 let captured = stderr.clone();
136 move || {
137 let mut chunk = [0u8; 4096];
141 loop {
142 match io::Read::read(&mut stderr_pipe, &mut chunk) {
143 Ok(0) | Err(_) => return,
144 Ok(n) => {
145 if let Ok(mut captured) = captured.lock() {
146 const CAP: usize = 64 * 1024;
147 let room = CAP.saturating_sub(captured.len());
148 let take = n.min(room);
149 captured.push_str(&String::from_utf8_lossy(&chunk[..take]));
150 }
151 }
152 }
153 }
154 }
155 });
156
157 let mut driver = Self {
159 child,
160 stdin: io::BufWriter::new(stdin),
161 rx,
162 stderr,
163 client: Client::new(&crate::message::BackendInfo {
164 name: String::new(),
165 version: String::new(),
166 build: None,
167 incarnation: 0,
168 }),
169 seq: 0,
170 budget: DEFAULT_BUDGET,
171 closed: None,
172 effects: Vec::new(),
173 };
174 driver.handshake()?;
175 Ok(driver)
176 }
177
178 fn handshake(&mut self) -> Result<(), DriverError> {
179 self.send(&ClientMessage::Hello {
180 protocol: PROTOCOL_VERSION,
181 client: ClientInfo {
182 name: "strop-ui-protocol-driver".into(),
183 version: env!("CARGO_PKG_VERSION").into(),
184 },
185 capabilities: ClientCapabilities {
186 clipboard_write: true,
187 },
188 })?;
189 let deadline = self.deadline();
190 while self.client.view().is_none() {
193 if let Some(pumped) = self.pump("the handshake", deadline)? {
194 match pumped {
195 Pumped::Error(error) => return Err(DriverError::Protocol(error)),
196 Pumped::Closed(reason) => {
197 return Err(DriverError::BackendClosed {
198 reason: Some(reason),
199 waiting: "the handshake",
200 })
201 }
202 Pumped::Ack(..) => {}
203 }
204 }
205 }
206 Ok(())
207 }
208
209 pub fn set_budget(&mut self, budget: Duration) {
211 self.budget = budget;
212 }
213
214 pub fn client(&self) -> &Client {
216 &self.client
217 }
218
219 pub fn effects(&self) -> &[(u64, EffectRequest)] {
221 &self.effects
222 }
223
224 pub fn stderr(&self) -> String {
226 self.stderr
227 .lock()
228 .map(|captured| captured.clone())
229 .unwrap_or_else(|_| String::new())
230 }
231
232 pub fn act(&mut self, actions: Vec<AdmittedAction>) -> Result<u64, DriverError> {
238 let base = self.client.base_stamp()?;
239 let seq = self.next_seq();
240 self.send(&ClientMessage::Act { seq, base, actions })?;
241 let deadline = self.deadline();
242 let generation = match self.wait_ack(seq, "the action acknowledgement", deadline)? {
243 AckOutcome::Applied { generation, .. } => generation,
244 AckOutcome::Refused { refusal } => return Err(DriverError::Refused(refusal)),
245 };
246 self.wait_generation_atleast(generation, "the action's publication", deadline)?;
247 Ok(generation)
248 }
249
250 pub fn act_keys(&mut self, keys: &str) -> Result<u64, DriverError> {
254 let actions: Vec<AdmittedAction> = strop_core::frontend_input::notation::parse(keys)
255 .map(|key| AdmittedAction::Input(strop_core::frontend_input::Input::Key(key)))
256 .collect();
257 self.act(actions)
258 }
259
260 pub fn act_text(&mut self, text: &str) -> Result<u64, DriverError> {
262 self.act(vec![AdmittedAction::Input(
263 strop_core::frontend_input::Input::Text(text.to_string()),
264 )])
265 }
266
267 pub fn act_paste(&mut self, text: &str) -> Result<u64, DriverError> {
269 self.act(vec![AdmittedAction::Paste(text.to_string())])
270 }
271
272 pub fn viewport(&mut self, columns: u16, rows: u16) -> Result<u64, DriverError> {
275 let seq = self.next_seq();
276 self.send(&ClientMessage::Viewport { seq, columns, rows })?;
277 let deadline = self.deadline();
278 let generation = match self.wait_ack(seq, "the viewport acknowledgement", deadline)? {
279 AckOutcome::Applied { generation, .. } => generation,
280 AckOutcome::Refused { refusal } => return Err(DriverError::Refused(refusal)),
281 };
282 self.wait_generation_atleast(generation, "the viewport's publication", deadline)?;
283 Ok(generation)
284 }
285
286 pub fn resync(&mut self) -> Result<u64, DriverError> {
289 let seq = self.next_seq();
290 self.send(&ClientMessage::Resync { seq })?;
291 let deadline = self.deadline();
292 let generation = match self.wait_ack(seq, "the resync acknowledgement", deadline)? {
293 AckOutcome::Applied { generation, .. } => generation,
294 AckOutcome::Refused { refusal } => return Err(DriverError::Refused(refusal)),
295 };
296 loop {
297 if self.client.poisoned().is_none() && self.client.generation() >= generation {
298 return Ok(self.client.generation());
299 }
300 if let Some(pumped) = self.pump("the resync snapshot", deadline)? {
301 match pumped {
302 Pumped::Error(error) => return Err(DriverError::Protocol(error)),
303 Pumped::Closed(reason) => {
304 return Err(DriverError::BackendClosed {
305 reason: Some(reason),
306 waiting: "the resync snapshot",
307 })
308 }
309 Pumped::Ack(..) => {}
310 }
311 }
312 }
313 }
314
315 pub fn wait_generation(&mut self, generation: u64) -> Result<u64, DriverError> {
317 let deadline = self.deadline();
318 self.wait_generation_atleast(generation, "a view generation", deadline)?;
319 Ok(self.client.generation())
320 }
321
322 pub fn wait_view(
325 &mut self,
326 waiting: &'static str,
327 predicate: impl Fn(&ViewSnapshot) -> bool,
328 ) -> Result<u64, DriverError> {
329 let deadline = self.deadline();
330 loop {
331 if let Some(view) = self.client.view() {
332 if predicate(view) {
333 return Ok(view.generation);
334 }
335 }
336 if let Some(pumped) = self.pump(waiting, deadline)? {
337 match pumped {
338 Pumped::Error(error) => return Err(DriverError::Protocol(error)),
339 Pumped::Closed(reason) => {
340 return Err(DriverError::BackendClosed {
341 reason: Some(reason),
342 waiting,
343 })
344 }
345 Pumped::Ack(..) => {}
346 }
347 }
348 }
349 }
350
351 pub fn shutdown(mut self) -> Result<ExitStatus, DriverError> {
354 let seq = self.next_seq();
355 let deadline = self.deadline();
356 self.send(&ClientMessage::Shutdown { seq })?;
357 loop {
358 match self.pump("bye", deadline)? {
359 Some(Pumped::Closed(_)) => break,
360 Some(Pumped::Error(error)) => return Err(DriverError::Protocol(error)),
361 Some(Pumped::Ack(..)) | None => {}
362 }
363 if self.closed.is_some() {
364 break;
365 }
366 }
367 self.reap(deadline)
368 }
369
370 pub fn wait_exit(mut self) -> Result<ExitStatus, DriverError> {
373 let deadline = self.deadline();
374 while self.closed.is_none() {
375 let _ = self.pump("backend exit", deadline)?;
376 }
377 self.reap(deadline)
378 }
379
380 fn reap(&mut self, deadline: Instant) -> Result<ExitStatus, DriverError> {
381 loop {
382 if let Some(status) = self.child.try_wait()? {
383 return Ok(status);
384 }
385 if Instant::now() >= deadline {
386 let _ = self.child.kill();
387 return Err(self.timeout("backend exit"));
388 }
389 std::thread::sleep(Duration::from_millis(5));
391 }
392 }
393
394 fn timeout(&self, waiting: &'static str) -> DriverError {
395 DriverError::Timeout {
396 waiting,
397 last_generation: self.client.generation(),
398 last_state: self
399 .client
400 .view()
401 .map(|view| view.state.to_string())
402 .unwrap_or_else(|| "<no view>".into()),
403 }
404 }
405
406 fn next_seq(&mut self) -> u64 {
407 self.seq += 1;
408 self.seq
409 }
410
411 fn deadline(&self) -> Instant {
412 Instant::now() + self.budget
413 }
414
415 fn send(&mut self, message: &ClientMessage) -> Result<(), DriverError> {
416 frame::write_message(&mut self.stdin, message).map_err(DriverError::Io)
417 }
418
419 fn wait_ack(
420 &mut self,
421 seq: u64,
422 waiting: &'static str,
423 deadline: Instant,
424 ) -> Result<AckOutcome, DriverError> {
425 loop {
426 match self.pump(waiting, deadline)? {
427 Some(Pumped::Ack(seen, outcome)) if seen == seq => return Ok(outcome),
428 Some(Pumped::Error(error)) => return Err(DriverError::Protocol(error)),
429 Some(Pumped::Closed(reason)) => {
430 return Err(DriverError::BackendClosed {
431 reason: Some(reason),
432 waiting,
433 })
434 }
435 _ => {}
436 }
437 }
438 }
439
440 fn wait_generation_atleast(
441 &mut self,
442 generation: u64,
443 waiting: &'static str,
444 deadline: Instant,
445 ) -> Result<(), DriverError> {
446 while self.client.generation() < generation {
447 if let Some(pumped) = self.pump(waiting, deadline)? {
448 match pumped {
449 Pumped::Error(error) => return Err(DriverError::Protocol(error)),
450 Pumped::Closed(reason) => {
451 return Err(DriverError::BackendClosed {
452 reason: Some(reason),
453 waiting,
454 })
455 }
456 Pumped::Ack(..) => {}
457 }
458 }
459 }
460 Ok(())
461 }
462
463 fn pump(
467 &mut self,
468 waiting: &'static str,
469 deadline: Instant,
470 ) -> Result<Option<Pumped>, DriverError> {
471 let remaining = deadline.saturating_duration_since(Instant::now());
472 if remaining.is_zero() {
473 return Err(self.timeout(waiting));
474 }
475 let transport = match self.rx.recv_timeout(remaining) {
476 Ok(transport) => transport,
477 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => return Err(self.timeout(waiting)),
478 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
479 self.client.link_lost();
480 return Err(DriverError::BackendClosed {
481 reason: self.closed,
482 waiting,
483 });
484 }
485 };
486 let message = match transport {
487 Transport::Message(message) => message,
488 Transport::Failed(error) => {
489 self.client.link_lost();
490 return Err(DriverError::Transport(error));
491 }
492 Transport::Closed => {
493 self.client.link_lost();
494 return Err(DriverError::BackendClosed {
495 reason: self.closed,
496 waiting,
497 });
498 }
499 };
500 if let ServerMessage::Bye { reason } = &message {
501 self.closed = Some(*reason);
502 self.client.apply(&message);
505 return Ok(Some(Pumped::Closed(*reason)));
506 }
507 if let ServerMessage::Error { error, .. } = &message {
508 return Ok(Some(Pumped::Error(error.clone())));
509 }
510 if let ServerMessage::Ack { seq, outcome } = &message {
511 return Ok(Some(Pumped::Ack(*seq, outcome.clone())));
512 }
513 for event in self.client.apply(&message) {
514 if let ClientEvent::Effect { id, effect } = event {
515 self.effects.push((id, effect));
516 self.send(&ClientMessage::EffectResult {
517 id,
518 outcome: EffectOutcome::Applied,
519 })?;
520 }
521 }
522 Ok(None)
523 }
524}
525
526impl Drop for Driver {
527 fn drop(&mut self) {
528 if self.closed.is_none() {
529 let _ = self.child.kill();
530 }
531 let _ = self.child.wait();
532 }
533}
534
535fn read_backend(mut stdout: std::process::ChildStdout, tx: SyncSender<Transport>) {
538 let mut decoder = FrameDecoder::new();
539 let mut chunk = [0u8; 8192];
540 loop {
541 match io::Read::read(&mut stdout, &mut chunk) {
542 Ok(0) | Err(_) => {
543 let _ = tx.send(if decoder.is_empty() {
546 Transport::Closed
547 } else {
548 Transport::Failed("stream ended mid-frame".into())
549 });
550 return;
551 }
552 Ok(n) => {
553 if let Err(error) = decoder.accept(&chunk[..n]) {
554 let _ = tx.send(Transport::Failed(error.to_string()));
555 return;
556 }
557 loop {
558 match decoder.next_frame() {
559 Ok(Some(body)) => match serde_json::from_slice::<ServerMessage>(&body) {
560 Ok(message) => {
561 if tx.send(Transport::Message(message)).is_err() {
562 return;
563 }
564 }
565 Err(error) => {
566 let _ = tx.send(Transport::Failed(format!(
567 "undecodable server message: {error}"
568 )));
569 return;
570 }
571 },
572 Ok(None) => break,
573 Err(error) => {
574 let _ = tx.send(Transport::Failed(error.to_string()));
575 return;
576 }
577 }
578 }
579 }
580 }
581 }
582}