1use super::paths::{InstalledServer, ServerPaths};
6use crate::async_pipe::get_socket_name;
7use crate::constants::{
8 APPLICATION_NAME, EDITOR_WEB_URL, QUALITYLESS_PRODUCT_NAME, QUALITYLESS_SERVER_NAME,
9};
10use crate::download_cache::DownloadCache;
11use crate::options::{Quality, TelemetryLevel};
12use crate::state::LauncherPaths;
13use crate::tunnels::paths::{get_server_folder_name, SERVER_FOLDER_NAME};
14use crate::update_service::{
15 unzip_downloaded_release, Platform, Release, TargetKind, UpdateService,
16};
17use crate::util::command::{
18 capture_command, capture_command_and_check_status, check_output_status, kill_tree,
19 new_script_command,
20};
21use crate::util::errors::{wrap, AnyError, CodeError, ExtensionInstallFailed, WrappedError};
22use crate::util::http::{self, BoxedHttp};
23use crate::util::io::SilentCopyProgress;
24use crate::util::machine::process_exists;
25use crate::util::prereqs::skip_requirements_check;
26use crate::{debug, info, log, spanf, trace, warning};
27use lazy_static::lazy_static;
28use opentelemetry::KeyValue;
29use regex::Regex;
30use serde::Deserialize;
31use std::fs;
32use std::fs::File;
33use std::io::Write;
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36use std::time::Duration;
37use tokio::fs::remove_file;
38use tokio::io::{AsyncBufReadExt, BufReader};
39use tokio::process::{Child, Command};
40use tokio::sync::oneshot::Receiver;
41use tokio::time::{interval, timeout};
42
43lazy_static! {
44 static ref LISTENING_PORT_RE: Regex =
45 Regex::new(r"Extension host agent listening on (.+)").unwrap();
46 static ref WEB_UI_RE: Regex = Regex::new(r"Web UI available at (.+)").unwrap();
47}
48
49#[derive(Clone, Debug, Default)]
50pub struct CodeServerArgs {
51 pub host: Option<String>,
52 pub port: Option<u16>,
53 pub socket_path: Option<String>,
54
55 pub telemetry_level: Option<TelemetryLevel>,
57 pub log: Option<log::Level>,
58 pub accept_server_license_terms: bool,
59 pub verbose: bool,
60 pub install_extensions: Vec<String>,
62 pub uninstall_extensions: Vec<String>,
63 pub update_extensions: bool,
64 pub list_extensions: bool,
65 pub show_versions: bool,
66 pub category: Option<String>,
67 pub pre_release: bool,
68 pub force: bool,
69 pub start_server: bool,
70 pub connection_token: Option<String>,
72 pub connection_token_file: Option<String>,
73 pub without_connection_token: bool,
74}
75
76impl CodeServerArgs {
77 pub fn log_level(&self) -> log::Level {
78 if self.verbose {
79 log::Level::Trace
80 } else {
81 self.log.unwrap_or(log::Level::Info)
82 }
83 }
84
85 pub fn telemetry_disabled(&self) -> bool {
86 self.telemetry_level == Some(TelemetryLevel::Off)
87 }
88
89 pub fn command_arguments(&self) -> Vec<String> {
90 let mut args = Vec::new();
91 if let Some(i) = &self.socket_path {
92 args.push(format!("--socket-path={}", i));
93 } else {
94 if let Some(i) = &self.host {
95 args.push(format!("--host={}", i));
96 }
97 if let Some(i) = &self.port {
98 args.push(format!("--port={}", i));
99 }
100 }
101
102 if let Some(i) = &self.connection_token {
103 args.push(format!("--connection-token={}", i));
104 }
105 if let Some(i) = &self.connection_token_file {
106 args.push(format!("--connection-token-file={}", i));
107 }
108 if self.without_connection_token {
109 args.push(String::from("--without-connection-token"));
110 }
111 if self.accept_server_license_terms {
112 args.push(String::from("--accept-server-license-terms"));
113 }
114 if let Some(i) = self.telemetry_level {
115 args.push(format!("--telemetry-level={}", i));
116 }
117 if let Some(i) = self.log {
118 args.push(format!("--log={}", i));
119 }
120
121 for extension in &self.install_extensions {
122 args.push(format!("--install-extension={}", extension));
123 }
124 if !&self.install_extensions.is_empty() {
125 if self.pre_release {
126 args.push(String::from("--pre-release"));
127 }
128 if self.force {
129 args.push(String::from("--force"));
130 }
131 }
132 for extension in &self.uninstall_extensions {
133 args.push(format!("--uninstall-extension={}", extension));
134 }
135 if self.update_extensions {
136 args.push(String::from("--update-extensions"));
137 }
138 if self.list_extensions {
139 args.push(String::from("--list-extensions"));
140 if self.show_versions {
141 args.push(String::from("--show-versions"));
142 }
143 if let Some(i) = &self.category {
144 args.push(format!("--category={}", i));
145 }
146 }
147 if self.start_server {
148 args.push(String::from("--start-server"));
149 }
150 args
151 }
152}
153
154pub struct ServerParamsRaw {
158 pub commit_id: Option<String>,
159 pub quality: Quality,
160 pub code_server_args: CodeServerArgs,
161 pub headless: bool,
162 pub platform: Platform,
163}
164
165pub struct ResolvedServerParams {
167 pub release: Release,
168 pub code_server_args: CodeServerArgs,
169}
170
171impl ResolvedServerParams {
172 fn as_installed_server(&self) -> InstalledServer {
173 InstalledServer {
174 commit: self.release.commit.clone(),
175 quality: self.release.quality,
176 headless: self.release.target == TargetKind::Server,
177 }
178 }
179}
180
181impl ServerParamsRaw {
182 pub async fn resolve(
183 self,
184 log: &log::Logger,
185 http: BoxedHttp,
186 ) -> Result<ResolvedServerParams, AnyError> {
187 Ok(ResolvedServerParams {
188 release: self.get_or_fetch_commit_id(log, http).await?,
189 code_server_args: self.code_server_args,
190 })
191 }
192
193 async fn get_or_fetch_commit_id(
194 &self,
195 log: &log::Logger,
196 http: BoxedHttp,
197 ) -> Result<Release, AnyError> {
198 let target = match self.headless {
199 true => TargetKind::Server,
200 false => TargetKind::Web,
201 };
202
203 if let Some(c) = &self.commit_id {
204 return Ok(Release {
205 commit: c.clone(),
206 quality: self.quality,
207 target,
208 name: String::new(),
209 platform: self.platform,
210 });
211 }
212
213 UpdateService::new(log.clone(), http)
214 .get_latest_commit(self.platform, target, self.quality)
215 .await
216 }
217}
218
219#[derive(Deserialize)]
220#[serde(rename_all = "camelCase")]
221#[allow(dead_code)]
222struct UpdateServerVersion {
223 pub name: String,
224 pub version: String,
225 pub product_version: String,
226 pub timestamp: i64,
227}
228
229#[derive(Clone)]
231pub struct SocketCodeServer {
232 pub commit_id: String,
233 pub socket: PathBuf,
234 pub origin: Arc<CodeServerOrigin>,
235}
236
237#[derive(Clone)]
239pub struct PortCodeServer {
240 pub commit_id: String,
241 pub port: u16,
242 pub origin: Arc<CodeServerOrigin>,
243}
244
245pub enum AnyCodeServer {
247 Socket(SocketCodeServer),
248 Port(PortCodeServer),
249}
250
251pub enum CodeServerOrigin {
252 New(Box<Child>),
254 Existing(u32),
256}
257
258impl CodeServerOrigin {
259 pub async fn wait_for_exit(&mut self) {
260 match self {
261 CodeServerOrigin::New(child) => {
262 child.wait().await.ok();
263 }
264 CodeServerOrigin::Existing(pid) => {
265 let mut interval = interval(Duration::from_secs(30));
266 while process_exists(*pid) {
267 interval.tick().await;
268 }
269 }
270 }
271 }
272
273 pub async fn kill(&mut self) {
274 match self {
275 CodeServerOrigin::New(child) => {
276 child.kill().await.ok();
277 }
278 CodeServerOrigin::Existing(pid) => {
279 kill_tree(*pid).await.ok();
280 }
281 }
282 }
283}
284
285async fn do_extension_install_on_running_server(
287 start_script_path: &Path,
288 extensions: &[String],
289 log: &log::Logger,
290) -> Result<(), AnyError> {
291 if extensions.is_empty() {
292 return Ok(());
293 }
294
295 debug!(log, "Installing extensions...");
296 let command = format!(
297 "{} {}",
298 start_script_path.display(),
299 extensions
300 .iter()
301 .map(|s| get_extensions_flag(s))
302 .collect::<Vec<String>>()
303 .join(" ")
304 );
305
306 let result = capture_command("bash", &["-c", &command]).await?;
307 if !result.status.success() {
308 Err(AnyError::from(ExtensionInstallFailed(
309 String::from_utf8_lossy(&result.stderr).to_string(),
310 )))
311 } else {
312 Ok(())
313 }
314}
315
316pub struct ServerBuilder<'a> {
317 logger: &'a log::Logger,
318 server_params: &'a ResolvedServerParams,
319 launcher_paths: &'a LauncherPaths,
320 server_paths: ServerPaths,
321 http: BoxedHttp,
322}
323
324impl<'a> ServerBuilder<'a> {
325 pub fn new(
326 logger: &'a log::Logger,
327 server_params: &'a ResolvedServerParams,
328 launcher_paths: &'a LauncherPaths,
329 http: BoxedHttp,
330 ) -> Self {
331 Self {
332 logger,
333 server_params,
334 launcher_paths,
335 server_paths: server_params
336 .as_installed_server()
337 .server_paths(launcher_paths),
338 http,
339 }
340 }
341
342 pub async fn get_running(&self) -> Result<Option<AnyCodeServer>, AnyError> {
344 info!(
345 self.logger,
346 "Checking {} and {} for a running server...",
347 self.server_paths.logfile.display(),
348 self.server_paths.pidfile.display()
349 );
350
351 let pid = match self.server_paths.get_running_pid() {
352 Some(pid) => pid,
353 None => return Ok(None),
354 };
355 info!(self.logger, "Found running server (pid={})", pid);
356 if !Path::new(&self.server_paths.logfile).exists() {
357 warning!(self.logger, "{} Server is running but its logfile is missing. Don't delete the {} Server manually, run the command '{} prune'.", QUALITYLESS_PRODUCT_NAME, QUALITYLESS_PRODUCT_NAME, APPLICATION_NAME);
358 return Ok(None);
359 }
360
361 do_extension_install_on_running_server(
362 &self.server_paths.executable,
363 &self.server_params.code_server_args.install_extensions,
364 self.logger,
365 )
366 .await?;
367
368 let origin = Arc::new(CodeServerOrigin::Existing(pid));
369 let contents = fs::read_to_string(&self.server_paths.logfile)
370 .expect("Something went wrong reading log file");
371
372 if let Some(port) = parse_port_from(&contents) {
373 Ok(Some(AnyCodeServer::Port(PortCodeServer {
374 commit_id: self.server_params.release.commit.to_owned(),
375 port,
376 origin,
377 })))
378 } else if let Some(socket) = parse_socket_from(&contents) {
379 Ok(Some(AnyCodeServer::Socket(SocketCodeServer {
380 commit_id: self.server_params.release.commit.to_owned(),
381 socket,
382 origin,
383 })))
384 } else {
385 Ok(None)
386 }
387 }
388
389 pub async fn setup(&self) -> Result<(), AnyError> {
391 debug!(
392 self.logger,
393 "Installing and setting up {}...", QUALITYLESS_SERVER_NAME
394 );
395
396 let update_service = UpdateService::new(self.logger.clone(), self.http.clone());
397 let name = get_server_folder_name(
398 self.server_params.release.quality,
399 &self.server_params.release.commit,
400 );
401
402 self.launcher_paths
403 .server_cache
404 .create(name, |target_dir| async move {
405 let tmpdir =
406 tempfile::tempdir().map_err(|e| wrap(e, "error creating temp download dir"))?;
407
408 let response = update_service
409 .get_download_stream(&self.server_params.release)
410 .await?;
411 let archive_path = tmpdir.path().join(response.url_path_basename().unwrap());
412
413 info!(
414 self.logger,
415 "Downloading {} server -> {}",
416 QUALITYLESS_PRODUCT_NAME,
417 archive_path.display()
418 );
419
420 http::download_into_file(
421 &archive_path,
422 self.logger.get_download_logger("server download progress:"),
423 response,
424 )
425 .await?;
426
427 let server_dir = target_dir.join(SERVER_FOLDER_NAME);
428 unzip_downloaded_release(&archive_path, &server_dir, SilentCopyProgress())?;
429
430 if !skip_requirements_check().await {
431 let output = capture_command_and_check_status(
432 server_dir
433 .join("bin")
434 .join(self.server_params.release.quality.server_entrypoint()),
435 &["--version"],
436 )
437 .await
438 .map_err(|e| wrap(e, "error checking server integrity"))?;
439
440 trace!(
441 self.logger,
442 "Server integrity verified, version: {}",
443 String::from_utf8_lossy(&output.stdout).replace('\n', " / ")
444 );
445 } else {
446 info!(self.logger, "Skipping server integrity check");
447 }
448
449 Ok(())
450 })
451 .await?;
452
453 debug!(self.logger, "Server setup complete");
454
455 Ok(())
456 }
457
458 pub async fn listen_on_port(&self, port: u16) -> Result<PortCodeServer, AnyError> {
459 let mut cmd = self.get_base_command();
460 cmd.arg("--start-server")
461 .arg("--enable-remote-auto-shutdown")
462 .arg(format!("--port={}", port));
463
464 let child = self.spawn_server_process(cmd)?;
465 let log_file = self.get_logfile()?;
466 let plog = self.logger.prefixed(&log::new_code_server_prefix());
467
468 let (mut origin, listen_rx) =
469 monitor_server::<PortMatcher, u16>(child, Some(log_file), plog, false);
470
471 let port = match timeout(Duration::from_secs(8), listen_rx).await {
472 Err(e) => {
473 origin.kill().await;
474 Err(wrap(e, "timed out looking for port"))
475 }
476 Ok(Err(e)) => {
477 origin.kill().await;
478 Err(wrap(e, "server exited without writing port"))
479 }
480 Ok(Ok(p)) => Ok(p),
481 }?;
482
483 info!(self.logger, "Server started");
484
485 Ok(PortCodeServer {
486 commit_id: self.server_params.release.commit.to_owned(),
487 port,
488 origin: Arc::new(origin),
489 })
490 }
491
492 pub async fn install_extensions(&self) -> Result<(), AnyError> {
494 let mut cmd = self.get_base_command();
496 let cmd_str = || {
497 self.server_params
498 .code_server_args
499 .command_arguments()
500 .join(" ")
501 };
502
503 let r = cmd.output().await.map_err(|e| CodeError::CommandFailed {
504 command: cmd_str(),
505 code: -1,
506 output: e.to_string(),
507 })?;
508
509 check_output_status(r, cmd_str)?;
510
511 Ok(())
512 }
513
514 pub async fn listen_on_default_socket(&self) -> Result<SocketCodeServer, AnyError> {
515 let requested_file = get_socket_name();
516 self.listen_on_socket(&requested_file).await
517 }
518
519 pub async fn listen_on_socket(&self, socket: &Path) -> Result<SocketCodeServer, AnyError> {
520 Ok(spanf!(
521 self.logger,
522 self.logger.span("server.start").with_attributes(vec! {
523 KeyValue::new("commit_id", self.server_params.release.commit.to_string()),
524 KeyValue::new("quality", format!("{}", self.server_params.release.quality)),
525 }),
526 self._listen_on_socket(socket)
527 )?)
528 }
529
530 async fn _listen_on_socket(&self, socket: &Path) -> Result<SocketCodeServer, AnyError> {
531 remove_file(&socket).await.ok(); let mut cmd = self.get_base_command();
534 cmd.arg("--start-server")
535 .arg("--enable-remote-auto-shutdown")
536 .arg(format!("--socket-path={}", socket.display()));
537
538 let child = self.spawn_server_process(cmd)?;
539 let log_file = self.get_logfile()?;
540 let plog = self.logger.prefixed(&log::new_code_server_prefix());
541
542 let (mut origin, listen_rx) =
543 monitor_server::<SocketMatcher, PathBuf>(child, Some(log_file), plog, false);
544
545 let socket = match timeout(Duration::from_secs(30), listen_rx).await {
546 Err(e) => {
547 origin.kill().await;
548 Err(wrap(e, "timed out looking for socket"))
549 }
550 Ok(Err(e)) => {
551 origin.kill().await;
552 Err(wrap(e, "server exited without writing socket"))
553 }
554 Ok(Ok(socket)) => Ok(socket),
555 }?;
556
557 info!(self.logger, "Server started");
558
559 Ok(SocketCodeServer {
560 commit_id: self.server_params.release.commit.to_owned(),
561 socket,
562 origin: Arc::new(origin),
563 })
564 }
565
566 pub async fn start_opaque_with_args<M, R>(
569 &self,
570 args: &[String],
571 ) -> Result<(CodeServerOrigin, Receiver<R>), AnyError>
572 where
573 M: ServerOutputMatcher<R>,
574 R: 'static + Send + std::fmt::Debug,
575 {
576 let mut cmd = self.get_base_command();
577 cmd.args(args);
578
579 let child = self.spawn_server_process(cmd)?;
580 let plog = self.logger.prefixed(&log::new_code_server_prefix());
581
582 Ok(monitor_server::<M, R>(child, None, plog, true))
583 }
584
585 fn spawn_server_process(&self, mut cmd: Command) -> Result<Child, AnyError> {
586 info!(self.logger, "Starting server...");
587
588 debug!(self.logger, "Starting server with command... {:?}", cmd);
589
590 #[cfg(target_os = "windows")]
597 let cmd = cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
598
599 let child = cmd
600 .stderr(std::process::Stdio::piped())
601 .stdout(std::process::Stdio::piped())
602 .spawn()
603 .map_err(|e| wrap(e, "error spawning server"))?;
604
605 self.server_paths
606 .write_pid(child.id().expect("expected server to have pid"))?;
607
608 Ok(child)
609 }
610
611 fn get_logfile(&self) -> Result<File, WrappedError> {
612 File::create(&self.server_paths.logfile).map_err(|e| {
613 wrap(
614 e,
615 format!(
616 "error creating log file {}",
617 self.server_paths.logfile.display()
618 ),
619 )
620 })
621 }
622
623 fn get_base_command(&self) -> Command {
624 let mut cmd = new_script_command(&self.server_paths.executable);
625 cmd.stdin(std::process::Stdio::null())
626 .args(self.server_params.code_server_args.command_arguments());
627 cmd
628 }
629}
630
631fn monitor_server<M, R>(
632 mut child: Child,
633 log_file: Option<File>,
634 plog: log::Logger,
635 write_directly: bool,
636) -> (CodeServerOrigin, Receiver<R>)
637where
638 M: ServerOutputMatcher<R>,
639 R: 'static + Send + std::fmt::Debug,
640{
641 let stdout = child
642 .stdout
643 .take()
644 .expect("child did not have a handle to stdout");
645
646 let stderr = child
647 .stderr
648 .take()
649 .expect("child did not have a handle to stdout");
650
651 let (listen_tx, listen_rx) = tokio::sync::oneshot::channel();
652
653 tokio::spawn(async move {
656 let mut stdout_reader = BufReader::new(stdout).lines();
657 let mut stderr_reader = BufReader::new(stderr).lines();
658 let write_line = |line: &str| -> std::io::Result<()> {
659 if let Some(mut f) = log_file.as_ref() {
660 f.write_all(line.as_bytes())?;
661 f.write_all(&[b'\n'])?;
662 }
663 if write_directly {
664 println!("{}", line);
665 } else {
666 trace!(plog, line);
667 }
668 Ok(())
669 };
670
671 loop {
672 let line = tokio::select! {
673 l = stderr_reader.next_line() => l,
674 l = stdout_reader.next_line() => l,
675 };
676
677 match line {
678 Err(e) => {
679 trace!(plog, "error reading from stdout/stderr: {}", e);
680 return;
681 }
682 Ok(None) => break,
683 Ok(Some(l)) => {
684 write_line(&l).ok();
685
686 if let Some(listen_on) = M::match_line(&l) {
687 trace!(plog, "parsed location: {:?}", listen_on);
688 listen_tx.send(listen_on).ok();
689 break;
690 }
691 }
692 }
693 }
694
695 loop {
696 let line = tokio::select! {
697 l = stderr_reader.next_line() => l,
698 l = stdout_reader.next_line() => l,
699 };
700
701 match line {
702 Err(e) => {
703 trace!(plog, "error reading from stdout/stderr: {}", e);
704 break;
705 }
706 Ok(None) => break,
707 Ok(Some(l)) => {
708 write_line(&l).ok();
709 }
710 }
711 }
712 });
713
714 let origin = CodeServerOrigin::New(Box::new(child));
715 (origin, listen_rx)
716}
717
718fn get_extensions_flag(extension_id: &str) -> String {
719 format!("--install-extension={}", extension_id)
720}
721
722pub trait ServerOutputMatcher<R>
725where
726 R: Send,
727{
728 fn match_line(line: &str) -> Option<R>;
729}
730
731struct SocketMatcher();
733
734impl ServerOutputMatcher<PathBuf> for SocketMatcher {
735 fn match_line(line: &str) -> Option<PathBuf> {
736 parse_socket_from(line)
737 }
738}
739
740pub struct PortMatcher();
742
743impl ServerOutputMatcher<u16> for PortMatcher {
744 fn match_line(line: &str) -> Option<u16> {
745 parse_port_from(line)
746 }
747}
748
749pub struct WebUiMatcher();
751
752impl ServerOutputMatcher<reqwest::Url> for WebUiMatcher {
753 fn match_line(line: &str) -> Option<reqwest::Url> {
754 WEB_UI_RE.captures(line).and_then(|cap| {
755 cap.get(1)
756 .and_then(|uri| reqwest::Url::parse(uri.as_str()).ok())
757 })
758 }
759}
760
761pub struct NoOpMatcher();
763
764impl ServerOutputMatcher<()> for NoOpMatcher {
765 fn match_line(_: &str) -> Option<()> {
766 Some(())
767 }
768}
769
770fn parse_socket_from(text: &str) -> Option<PathBuf> {
771 LISTENING_PORT_RE
772 .captures(text)
773 .and_then(|cap| cap.get(1).map(|path| PathBuf::from(path.as_str())))
774}
775
776fn parse_port_from(text: &str) -> Option<u16> {
777 LISTENING_PORT_RE.captures(text).and_then(|cap| {
778 cap.get(1)
779 .and_then(|path| path.as_str().parse::<u16>().ok())
780 })
781}
782
783pub fn print_listening(log: &log::Logger, tunnel_name: &str) {
784 debug!(
785 log,
786 "{} is listening for incoming connections", QUALITYLESS_SERVER_NAME
787 );
788
789 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(""));
790 let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(""));
791
792 let dir = if home_dir == current_dir {
793 PathBuf::from("")
794 } else {
795 current_dir
796 };
797
798 let base_web_url = match EDITOR_WEB_URL {
799 Some(u) => u,
800 None => return,
801 };
802
803 let mut addr = url::Url::parse(base_web_url).unwrap();
804 {
805 let mut ps = addr.path_segments_mut().unwrap();
806 ps.push("tunnel");
807 ps.push(tunnel_name);
808 for segment in &dir {
809 let as_str = segment.to_string_lossy();
810 if !(as_str.len() == 1 && as_str.starts_with(std::path::MAIN_SEPARATOR)) {
811 ps.push(as_str.as_ref());
812 }
813 }
814 }
815
816 let message = &format!("\nOpen this link in your browser {}\n", addr);
817 log.result(message);
818}
819
820pub async fn download_cli_into_cache(
821 cache: &DownloadCache,
822 release: &Release,
823 update_service: &UpdateService,
824) -> Result<PathBuf, AnyError> {
825 let cache_name = format!(
826 "{}-{}-{}",
827 release.quality, release.commit, release.platform
828 );
829 let cli_dir = cache
830 .create(&cache_name, |target_dir| async move {
831 let tmpdir =
832 tempfile::tempdir().map_err(|e| wrap(e, "error creating temp download dir"))?;
833 let response = update_service.get_download_stream(release).await?;
834
835 let name = response.url_path_basename().unwrap();
836 let archive_path = tmpdir.path().join(name);
837 http::download_into_file(&archive_path, SilentCopyProgress(), response).await?;
838 unzip_downloaded_release(&archive_path, &target_dir, SilentCopyProgress())?;
839 Ok(())
840 })
841 .await?;
842
843 let cli = std::fs::read_dir(cli_dir)
844 .map_err(|_| CodeError::CorruptDownload("could not read cli folder contents"))?
845 .next();
846
847 match cli {
848 Some(Ok(cli)) => Ok(cli.path()),
849 _ => {
850 let _ = cache.delete(&cache_name);
851 Err(CodeError::CorruptDownload("cli directory is empty").into())
852 }
853 }
854}