1use crate::error::AppError;
10use serde_json::{Value, json};
11use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWrite, AsyncWriteExt};
12
13pub const PROTOCOL: u32 = 4;
14const VERSION: &str = env!("CARGO_PKG_VERSION");
15const BUILD: &str = env!("TEAMS_BUILD");
18const MAX_LINE: usize = super::MAX_READ + 64 * 1024;
19
20async fn read_line(reader: &mut (impl AsyncBufRead + Unpin)) -> Result<Option<String>, AppError> {
22 let mut line = Vec::new();
23 loop {
24 let buffer = reader.fill_buf().await?;
25 if buffer.is_empty() {
26 return Ok((!line.is_empty()).then(|| String::from_utf8_lossy(&line).into_owned()));
27 }
28 let (chunk, done) = match buffer.iter().position(|&b| b == b'\n') {
29 Some(end) => (&buffer[..end], Some(end + 1)),
30 None => (buffer, None),
31 };
32 if line.len() + chunk.len() > MAX_LINE {
33 return Err(AppError::Desktop(
34 "desktop helper message is too large".into(),
35 ));
36 }
37 line.extend_from_slice(chunk);
38 let consumed = done.unwrap_or(chunk.len());
39 reader.consume(consumed);
40 if done.is_some() {
41 return Ok(Some(String::from_utf8_lossy(&line).into_owned()));
42 }
43 }
44}
45
46async fn write_line(writer: &mut (impl AsyncWrite + Unpin), value: &Value) -> Result<(), AppError> {
47 let mut line = value.to_string();
48 line.push('\n');
49 writer.write_all(line.as_bytes()).await?;
50 writer.flush().await?;
51 Ok(())
52}
53
54#[cfg(any(windows, test))]
56pub async fn serve<S: super::session::Verifier>(
57 mut reader: impl AsyncBufRead + Unpin,
58 mut writer: impl AsyncWrite + Unpin,
59 session: &mut super::session::Session<S>,
60) -> Result<(), AppError> {
61 while let Some(line) = read_line(&mut reader).await? {
62 let request: Value = serde_json::from_str(&line).unwrap_or_default();
63 let result = match request["op"].as_str() {
64 Some("hello") => Ok(json!({"version": VERSION, "protocol": PROTOCOL, "build": BUILD})),
65 Some("probe") => match request["port"].as_u64().and_then(|p| u16::try_from(p).ok()) {
66 Some(port) => session.probe(port).await.map(|metadata| json!(metadata)),
67 None => Err(AppError::InvalidInput("invalid desktop port".into())),
68 },
69 Some("read") => session.read(&request["request"]).await,
70 Some("people") => session.people(&request["request"]).await,
71 Some("send") => session.send(&request["request"]).await,
72 Some("send_check") => {
73 match request["port"].as_u64().and_then(|p| u16::try_from(p).ok()) {
74 Some(port) => {
75 match crate::config::DesktopAccount::from_request(&request["account_type"])
76 {
77 Ok(account) => session.send_check(port, account).await,
78 Err(error) => Err(error),
79 }
80 }
81 None => Err(AppError::InvalidInput("invalid desktop port".into())),
82 }
83 }
84 _ => Err(AppError::InvalidInput(
85 "unknown desktop helper request".into(),
86 )),
87 };
88 let reply = match result {
89 Ok(value) => json!({"id": request["id"], "ok": value}),
90 Err(error) => json!({"id": request["id"], "error": error.to_string()}),
91 };
92 write_line(&mut writer, &reply).await?;
93 }
94 Ok(())
95}
96
97#[cfg(not(windows))]
98pub use client::HelperClient;
99
100#[cfg(not(windows))]
101mod client {
102 use super::*;
103 use std::{
104 path::{Path, PathBuf},
105 process::Stdio,
106 time::Duration,
107 };
108 use tokio::{
109 io::BufReader,
110 process::{Child, ChildStdin, ChildStdout, Command},
111 };
112
113 const START_TIMEOUT: Duration = Duration::from_secs(20);
114 const CALL_TIMEOUT: Duration = Duration::from_secs(45);
115
116 enum Failure {
117 Refused(String),
119 Transport,
121 }
122
123 struct Running {
124 _child: Child,
126 stdin: ChildStdin,
127 stdout: BufReader<ChildStdout>,
128 next_id: u64,
129 }
130
131 impl Running {
132 async fn call(&mut self, mut request: Value) -> Result<Value, Failure> {
133 self.next_id += 1;
134 request["id"] = json!(self.next_id);
135 let exchange = async {
136 write_line(&mut self.stdin, &request).await.ok()?;
137 let line = read_line(&mut self.stdout).await.ok()??;
138 serde_json::from_str::<Value>(&line).ok()
139 };
140 let reply = tokio::time::timeout(CALL_TIMEOUT, exchange)
141 .await
142 .ok()
143 .flatten()
144 .ok_or(Failure::Transport)?;
145 if reply["id"] != self.next_id {
146 return Err(Failure::Transport);
147 }
148 match (reply.get("ok"), reply["error"].as_str()) {
149 (Some(ok), _) => Ok(ok.clone()),
150 (None, Some(message)) => Err(Failure::Refused(message.into())),
151 _ => Err(Failure::Transport),
152 }
153 }
154 }
155
156 #[derive(Default)]
158 pub struct HelperClient {
159 running: Option<Running>,
160 executable: Option<PathBuf>,
162 }
163
164 impl HelperClient {
165 pub async fn call(&mut self, request: Value) -> Result<Value, AppError> {
166 if let Some(running) = &mut self.running {
167 match running.call(request.clone()).await {
168 Ok(value) => return Ok(value),
169 Err(Failure::Refused(message)) => return Err(AppError::Desktop(message)),
170 Err(Failure::Transport) => self.running = None,
171 }
172 }
173 match self.started().await?.call(request).await {
174 Ok(value) => Ok(value),
175 Err(Failure::Refused(message)) => Err(AppError::Desktop(message)),
176 Err(Failure::Transport) => {
177 self.running = None;
178 Err(AppError::Desktop(
179 "the Windows desktop helper stopped unexpectedly".into(),
180 ))
181 }
182 }
183 }
184
185 pub async fn call_once(&mut self, request: Value) -> Result<Value, AppError> {
188 match self.started().await?.call(request).await {
189 Ok(value) => Ok(value),
190 Err(Failure::Refused(message)) => Err(AppError::Desktop(message)),
191 Err(Failure::Transport) => {
192 self.running = None;
193 Ok(json!({"error": "unconfirmed"}))
194 }
195 }
196 }
197
198 async fn started(&mut self) -> Result<&mut Running, AppError> {
200 let running = match self.running.take() {
201 Some(running) => running,
202 None => {
203 let executable = match &self.executable {
204 Some(executable) => executable.clone(),
205 None => super::super::install::ensure().await?,
206 };
207 let running = start(&executable).await?;
208 self.executable = Some(executable);
209 running
210 }
211 };
212 Ok(self.running.insert(running))
213 }
214 }
215
216 fn mismatch(hello: &Value) -> Option<String> {
218 (hello["protocol"] != PROTOCOL).then(|| {
219 let theirs = describe(
220 hello["version"].as_str(),
221 hello["build"].as_str(),
222 hello["protocol"].as_u64(),
223 );
224 let ours = describe(Some(VERSION), Some(BUILD), Some(PROTOCOL.into()));
225 format!("is {theirs}, but this CLI is {ours}")
226 })
227 }
228
229 fn describe(version: Option<&str>, build: Option<&str>, protocol: Option<u64>) -> String {
231 let mut text = format!("teams-cli {}", version.unwrap_or("of an unknown version"));
232 if let Some(build) = build.filter(|b| !b.is_empty()) {
233 text.push_str(&format!(" ({build})"));
234 }
235 match protocol {
236 Some(protocol) => text.push_str(&format!(" speaking protocol {protocol}")),
237 None => text.push_str(" without a protocol number"),
238 }
239 text
240 }
241
242 async fn start(executable: &Path) -> Result<Running, AppError> {
243 let mut child = Command::new(executable)
244 .arg("desktop-helper")
245 .stdin(Stdio::piped())
246 .stdout(Stdio::piped())
247 .stderr(Stdio::null())
248 .kill_on_drop(true)
249 .spawn()
250 .map_err(|_| {
251 AppError::Desktop(format!(
252 "could not start the Windows desktop helper at {}; check that WSL can run Windows executables",
253 executable.display()
254 ))
255 })?;
256 let (Some(stdin), Some(stdout)) = (child.stdin.take(), child.stdout.take()) else {
257 return Err(AppError::Desktop(
258 "desktop helper pipes are unavailable".into(),
259 ));
260 };
261 let mut running = Running {
262 _child: child,
263 stdin,
264 stdout: BufReader::new(stdout),
265 next_id: 0,
266 };
267 let hello = tokio::time::timeout(START_TIMEOUT, running.call(json!({"op": "hello"})))
268 .await
269 .ok()
270 .and_then(Result::ok);
271 let problem = match &hello {
272 None => Some("did not answer".to_owned()),
273 Some(hello) => mismatch(hello),
274 };
275 if let Some(problem) = problem {
276 return Err(AppError::Desktop(format!(
277 "the Windows desktop helper at {} {problem}; {}",
278 executable.display(),
279 super::super::install::remedy()
280 )));
281 }
282 Ok(running)
283 }
284
285 #[cfg(all(test, unix))]
286 mod tests {
287 use super::*;
288 use std::os::unix::fs::PermissionsExt;
289
290 fn fake_helper(dir: &std::path::Path, version: &str, protocol: u32, lives: u32) -> PathBuf {
293 let path = dir.join("teams.exe");
294 let script = format!(
295 r#"#!/bin/sh
296[ "$1" = desktop-helper ] || exit 2
297n=0
298while read -r line; do
299 id=$(printf '%s' "$line" | sed 's/.*"id":\([0-9]*\).*/\1/')
300 case "$line" in
301 *'"op":"hello"'*) echo "{{\"id\":$id,\"ok\":{{\"version\":\"{version}\",\"protocol\":{protocol},\"build\":\"0123abcd\"}}}}" ;;
302 *'"op":"fail"'*) echo "{{\"id\":$id,\"error\":\"refused\"}}" ;;
303 *) n=$((n+1)); echo "{{\"id\":$id,\"ok\":$n}}"; [ $n -ge {lives} ] && exit 0 ;;
304 esac
305done
306"#
307 );
308 std::fs::write(&path, script).unwrap();
309 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
310 while matches!(
314 std::process::Command::new(&path).status(),
315 Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy
316 ) {
317 std::thread::sleep(Duration::from_millis(5));
318 }
319 path
320 }
321
322 #[tokio::test]
323 async fn handshakes_and_refuses_other_protocols_only() {
324 let dir = tempfile::tempdir().unwrap();
325 let mut running = start(&fake_helper(dir.path(), VERSION, PROTOCOL, 9))
326 .await
327 .unwrap();
328 assert_eq!(running.call(json!({"op": "x"})).await.ok(), Some(json!(1)));
329 assert_eq!(running.call(json!({"op": "x"})).await.ok(), Some(json!(2)));
330 assert!(matches!(
331 running.call(json!({"op": "fail"})).await,
332 Err(Failure::Refused(m)) if m == "refused"
333 ));
334 let older = tempfile::tempdir().unwrap();
336 assert!(
337 start(&fake_helper(older.path(), "0.0.1", PROTOCOL, 9))
338 .await
339 .is_ok()
340 );
341 let other = tempfile::tempdir().unwrap();
342 let error = start(&fake_helper(other.path(), "0.0.1", 1, 9))
343 .await
344 .err()
345 .unwrap()
346 .to_string();
347 assert!(
348 error
349 .contains("is teams-cli 0.0.1 (0123abcd) speaking protocol 1, but this CLI is"),
350 "{error}"
351 );
352 assert!(
353 error.contains(&format!("speaking protocol {PROTOCOL};")),
354 "{error}"
355 );
356 }
357
358 #[tokio::test]
359 async fn a_dead_helper_is_replaced_once() {
360 let dir = tempfile::tempdir().unwrap();
361 let mut client = HelperClient {
362 running: None,
363 executable: Some(fake_helper(dir.path(), VERSION, PROTOCOL, 1)),
364 };
365 for _ in 0..3 {
367 assert_eq!(client.call(json!({"op": "x"})).await.unwrap(), json!(1));
368 }
369 assert!(matches!(
370 client.call(json!({"op": "fail"})).await,
371 Err(AppError::Desktop(m)) if m == "refused"
372 ));
373 }
374
375 #[tokio::test]
376 async fn a_lost_answer_to_a_single_call_is_unconfirmed() {
377 let dir = tempfile::tempdir().unwrap();
378 let mut client = HelperClient {
380 running: None,
381 executable: Some(fake_helper(dir.path(), VERSION, PROTOCOL, 1)),
382 };
383 assert_eq!(
384 client.call_once(json!({"op": "x"})).await.unwrap(),
385 json!(1)
386 );
387 assert_eq!(
388 client.call_once(json!({"op": "x"})).await.unwrap(),
389 json!({"error": "unconfirmed"})
390 );
391 assert_eq!(
393 client.call_once(json!({"op": "x"})).await.unwrap(),
394 json!(1)
395 );
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use crate::desktop::{owner::tests::FakeSystem, session::Session};
404 use tokio::io::{BufReader, duplex};
405
406 #[tokio::test]
407 async fn serves_hello_probe_errors_and_stops_at_end_of_input() {
408 let (client, server) = duplex(1 << 20);
409 let (server_read, server_write) = tokio::io::split(server);
410 let task = tokio::spawn(async move {
411 let mut session = Session::new(FakeSystem {
412 listeners: vec![],
413 ..FakeSystem::teams()
414 });
415 serve(BufReader::new(server_read), server_write, &mut session).await
416 });
417 let (client_read, mut client_write) = tokio::io::split(client);
418 let mut client_read = BufReader::new(client_read);
419 let mut call = async |request: Value| {
420 write_line(&mut client_write, &request).await.unwrap();
421 serde_json::from_str::<Value>(&read_line(&mut client_read).await.unwrap().unwrap())
422 .unwrap()
423 };
424 let hello = call(json!({"id": 1, "op": "hello"})).await;
425 assert_eq!(hello["id"], 1);
426 assert_eq!(
427 hello["ok"],
428 json!({"version": VERSION, "protocol": PROTOCOL, "build": BUILD})
429 );
430 let probe = call(json!({"id": 2, "op": "probe", "port": 9222})).await;
431 assert_eq!(probe["ok"]["status"], "no_listener");
432 let bad = call(json!({"id": 3, "op": "read", "request": {"port": 9222}})).await;
433 assert!(bad["error"].is_string() && bad.get("ok").is_none());
434 let send = call(json!({"id": 4, "op": "send", "request": {"port": 9222}})).await;
435 assert!(send["error"].is_string() && send.get("ok").is_none());
436 let check = call(json!({"id": 5, "op": "send_check", "port": 9222})).await;
437 assert_eq!(check["ok"]["error"], "no_listener");
438 let unknown = call(json!({"id": 6, "op": "launch"})).await;
439 assert!(unknown["error"].is_string());
440 let invalid_account =
441 call(json!({"id": 7, "op": "send_check", "port": 9222, "account_type": "all"})).await;
442 assert!(
443 invalid_account["error"]
444 .as_str()
445 .unwrap()
446 .contains("account type")
447 );
448 drop((client_read, client_write));
450 task.await.unwrap().unwrap();
451 }
452
453 #[tokio::test]
454 async fn oversized_lines_are_refused() {
455 let data = vec![b'x'; MAX_LINE + 1];
456 let mut reader = BufReader::new(&data[..]);
457 assert!(read_line(&mut reader).await.is_err());
458 let mut reader = BufReader::new(&b"a\nb"[..]);
459 assert_eq!(read_line(&mut reader).await.unwrap().as_deref(), Some("a"));
460 assert_eq!(read_line(&mut reader).await.unwrap().as_deref(), Some("b"));
461 assert_eq!(read_line(&mut reader).await.unwrap(), None);
462 }
463}