cli/tunnels/
code_server.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5use 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	// common argument
56	pub telemetry_level: Option<TelemetryLevel>,
57	pub log: Option<log::Level>,
58	pub accept_server_license_terms: bool,
59	pub verbose: bool,
60	// extension management
61	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	// connection tokens
71	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
154/// Base server params that can be `resolve()`d to a `ResolvedServerParams`.
155/// Doing so fetches additional information like a commit ID if previously
156/// unspecified.
157pub 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
165/// Server params that can be used to start a VS Code server.
166pub 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/// Code server listening on a port address.
230#[derive(Clone)]
231pub struct SocketCodeServer {
232	pub commit_id: String,
233	pub socket: PathBuf,
234	pub origin: Arc<CodeServerOrigin>,
235}
236
237/// Code server listening on a socket address.
238#[derive(Clone)]
239pub struct PortCodeServer {
240	pub commit_id: String,
241	pub port: u16,
242	pub origin: Arc<CodeServerOrigin>,
243}
244
245/// A server listening on any address/location.
246pub enum AnyCodeServer {
247	Socket(SocketCodeServer),
248	Port(PortCodeServer),
249}
250
251pub enum CodeServerOrigin {
252	/// A new code server, that opens the barrier when it exits.
253	New(Box<Child>),
254	/// An existing code server with a PID.
255	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
285/// Ensures the given list of extensions are installed on the running server.
286async 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	/// Gets any already-running server from this directory.
343	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	/// Ensures the server is set up in the configured directory.
390	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	/// Runs the command that just installs extensions and exits.
493	pub async fn install_extensions(&self) -> Result<(), AnyError> {
494		// cmd already has --install-extensions from base
495		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(); // ignore any error if it doesn't exist
532
533		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	/// Starts with a given opaque set of args. Does not set up any port or
567	/// socket, but does return one if present, in the form of a channel.
568	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		// On Windows spawning a code-server binary will run cmd.exe /c C:\path\to\code-server.cmd...
591		// This spawns a cmd.exe window for the user, which if they close will kill the code-server process
592		// and disconnect the tunnel. To prevent this, pass the CREATE_NO_WINDOW flag to the Command
593		// only on Windows.
594		// Original issue: https://github.com/microsoft/vscode/issues/184058
595		// Partial fix: https://github.com/microsoft/vscode/pull/184621
596		#[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	// Handle stderr and stdout in a separate task. Initially scan lines looking
654	// for the listening port. Afterwards, just scan and write out to the file.
655	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
722/// A type that can be used to scan stdout from the VS Code server. Returns
723/// some other type that, in turn, is returned from starting the server.
724pub trait ServerOutputMatcher<R>
725where
726	R: Send,
727{
728	fn match_line(line: &str) -> Option<R>;
729}
730
731/// Parses a line like "Extension host agent listening on /tmp/foo.sock"
732struct SocketMatcher();
733
734impl ServerOutputMatcher<PathBuf> for SocketMatcher {
735	fn match_line(line: &str) -> Option<PathBuf> {
736		parse_socket_from(line)
737	}
738}
739
740/// Parses a line like "Extension host agent listening on 9000"
741pub struct PortMatcher();
742
743impl ServerOutputMatcher<u16> for PortMatcher {
744	fn match_line(line: &str) -> Option<u16> {
745		parse_port_from(line)
746	}
747}
748
749/// Parses a line like "Web UI available at http://localhost:9000/?tkn=..."
750pub 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
761/// Does not do any parsing and just immediately returns an empty result.
762pub 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}