cli/commands/
serve_web.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 *--------------------------------------------------------------------------------------------*/
5
6use std::collections::HashMap;
7use std::convert::Infallible;
8use std::fs;
9use std::io::{Read, Write};
10use std::net::{IpAddr, Ipv4Addr, SocketAddr};
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Mutex};
13use std::time::{Duration, Instant};
14
15use const_format::concatcp;
16use hyper::service::{make_service_fn, service_fn};
17use hyper::{Body, Request, Response, Server};
18use tokio::io::{AsyncBufReadExt, BufReader};
19use tokio::pin;
20
21use crate::async_pipe::{
22	get_socket_name, get_socket_rw_stream, listen_socket_rw_stream, AsyncPipe,
23};
24use crate::constants::VSCODE_CLI_QUALITY;
25use crate::download_cache::DownloadCache;
26use crate::log;
27use crate::options::Quality;
28use crate::state::{LauncherPaths, PersistedState};
29use crate::tunnels::shutdown_signal::ShutdownRequest;
30use crate::update_service::{
31	unzip_downloaded_release, Platform, Release, TargetKind, UpdateService,
32};
33use crate::util::command::new_script_command;
34use crate::util::errors::AnyError;
35use crate::util::http::{self, ReqwestSimpleHttp};
36use crate::util::io::SilentCopyProgress;
37use crate::util::sync::{new_barrier, Barrier, BarrierOpener};
38use crate::{
39	tunnels::legal,
40	util::{errors::CodeError, prereqs::PreReqChecker},
41};
42
43use super::{args::ServeWebArgs, CommandContext};
44
45/// Length of a commit hash, for validation
46const COMMIT_HASH_LEN: usize = 40;
47/// Number of seconds where, if there's no connections to a VS Code server,
48/// the server is shut down.
49const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60;
50/// Number of seconds in which the server times out when there is a connection
51/// (should be large enough to basically never happen)
52const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12;
53/// How long to cache the "latest" version we get from the update service.
54const RELEASE_CACHE_SECS: u64 = 60 * 60;
55
56/// Number of bytes for the secret keys. See workbench.ts for their usage.
57const SECRET_KEY_BYTES: usize = 32;
58/// Path to mint the key combining server and client parts.
59const SECRET_KEY_MINT_PATH: &str = "/_vscode-cli/mint-key";
60/// Cookie set to the `SECRET_KEY_MINT_PATH`
61const PATH_COOKIE_NAME: &str = "vscode-secret-key-path";
62/// Cookie set to the `SECRET_KEY_MINT_PATH`
63const PATH_COOKIE_VALUE: &str = concatcp!(
64	PATH_COOKIE_NAME,
65	"=",
66	SECRET_KEY_MINT_PATH,
67	"; SameSite=Strict; Path=/"
68);
69/// HTTP-only cookie where the client's secret half is stored.
70const SECRET_KEY_COOKIE_NAME: &str = "vscode-cli-secret-half";
71
72/// Implements the vscode "server of servers". Clients who go to the URI get
73/// served the latest version of the VS Code server whenever they load the
74/// page. The VS Code server prefixes all assets and connections it loads with
75/// its version string, so existing clients can continue to get served even
76/// while new clients get new VS Code Server versions.
77pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result<i32, AnyError> {
78	legal::require_consent(&ctx.paths, args.accept_server_license_terms)?;
79
80	let platform: crate::update_service::Platform = PreReqChecker::new().verify().await?;
81	if !args.without_connection_token {
82		// Ensure there's a defined connection token, since if multiple server versions
83		// are excuted, they will need to have a single shared token.
84		let token_path = ctx.paths.root().join("serve-web-token");
85		let token = mint_connection_token(&token_path, args.connection_token.clone())
86			.map_err(CodeError::CouldNotCreateConnectionTokenFile)?;
87		args.connection_token = Some(token);
88		args.connection_token_file = Some(token_path.to_string_lossy().to_string());
89	}
90
91	let cm = ConnectionManager::new(&ctx, platform, args.clone());
92	let key = get_server_key_half(&ctx.paths);
93	let make_svc = move || {
94		let ctx = HandleContext {
95			cm: cm.clone(),
96			log: cm.log.clone(),
97			server_secret_key: key.clone(),
98		};
99		let service = service_fn(move |req| handle(ctx.clone(), req));
100		async move { Ok::<_, Infallible>(service) }
101	};
102
103	let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]);
104	let r = if let Some(s) = args.socket_path {
105		let s = PathBuf::from(&s);
106		let socket = listen_socket_rw_stream(&s).await?;
107		ctx.log
108			.result(format!("Web UI available on {}", s.display()));
109		let r = Server::builder(socket.into_pollable())
110			.serve(make_service_fn(|_| make_svc()))
111			.with_graceful_shutdown(async {
112				let _ = shutdown.wait().await;
113			})
114			.await;
115		let _ = std::fs::remove_file(&s); // cleanup
116		r
117	} else {
118		let addr: SocketAddr = match &args.host {
119			Some(h) => {
120				SocketAddr::new(h.parse().map_err(CodeError::InvalidHostAddress)?, args.port)
121			}
122			None => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port),
123		};
124		let builder = Server::try_bind(&addr).map_err(CodeError::CouldNotListenOnInterface)?;
125
126		let mut listening = format!("Web UI available at http://{}", addr);
127		if let Some(base) = args.server_base_path {
128			if !base.starts_with('/') {
129				listening.push('/');
130			}
131			listening.push_str(&base);
132		}
133		if let Some(ct) = args.connection_token {
134			listening.push_str(&format!("?tkn={}", ct));
135		}
136		ctx.log.result(listening);
137
138		builder
139			.serve(make_service_fn(|_| make_svc()))
140			.with_graceful_shutdown(async {
141				let _ = shutdown.wait().await;
142			})
143			.await
144	};
145
146	r.map_err(CodeError::CouldNotListenOnInterface)?;
147
148	Ok(0)
149}
150
151#[derive(Clone)]
152struct HandleContext {
153	cm: Arc<ConnectionManager>,
154	log: log::Logger,
155	server_secret_key: SecretKeyPart,
156}
157
158/// Handler function for an inbound request
159async fn handle(ctx: HandleContext, req: Request<Body>) -> Result<Response<Body>, Infallible> {
160	let client_key_half = get_client_key_half(&req);
161	let mut res = match req.uri().path() {
162		SECRET_KEY_MINT_PATH => handle_secret_mint(ctx, req),
163		_ => handle_proxied(ctx, req).await,
164	};
165
166	append_secret_headers(&mut res, &client_key_half);
167
168	Ok(res)
169}
170
171async fn handle_proxied(ctx: HandleContext, req: Request<Body>) -> Response<Body> {
172	let release = if let Some((r, _)) = get_release_from_path(req.uri().path(), ctx.cm.platform) {
173		r
174	} else {
175		match ctx.cm.get_latest_release().await {
176			Ok(r) => r,
177			Err(e) => {
178				error!(ctx.log, "error getting latest version: {}", e);
179				return response::code_err(e);
180			}
181		}
182	};
183
184	match ctx.cm.get_connection(release).await {
185		Ok(rw) => {
186			if req.headers().contains_key(hyper::header::UPGRADE) {
187				forward_ws_req_to_server(ctx.log.clone(), rw, req).await
188			} else {
189				forward_http_req_to_server(rw, req).await
190			}
191		}
192		Err(CodeError::ServerNotYetDownloaded) => response::wait_for_download(),
193		Err(e) => response::code_err(e),
194	}
195}
196
197fn handle_secret_mint(ctx: HandleContext, req: Request<Body>) -> Response<Body> {
198	use sha2::{Digest, Sha256};
199
200	let mut hasher = Sha256::new();
201	hasher.update(ctx.server_secret_key.0.as_ref());
202	hasher.update(get_client_key_half(&req).0.as_ref());
203	let hash = hasher.finalize();
204	let hash = hash[..SECRET_KEY_BYTES].to_vec();
205	response::secret_key(hash)
206}
207
208/// Appends headers to response to maintain the secret storage of the workbench:
209/// sets the `PATH_COOKIE_VALUE` so workbench.ts knows about the 'mint' endpoint,
210/// and maintains the http-only cookie the client will use for cookies.
211fn append_secret_headers(res: &mut Response<Body>, client_key_half: &SecretKeyPart) {
212	let headers = res.headers_mut();
213	headers.append(
214		hyper::header::SET_COOKIE,
215		PATH_COOKIE_VALUE.parse().unwrap(),
216	);
217	headers.append(
218		hyper::header::SET_COOKIE,
219		format!(
220			"{}={}; SameSite=Strict; HttpOnly; Max-Age=2592000; Path=/",
221			SECRET_KEY_COOKIE_NAME,
222			client_key_half.encode()
223		)
224		.parse()
225		.unwrap(),
226	);
227}
228
229/// Gets the release info from the VS Code path prefix, which is in the
230/// format `/<quality>-<commit>/...`
231fn get_release_from_path(path: &str, platform: Platform) -> Option<(Release, String)> {
232	if !path.starts_with('/') {
233		return None; // paths must start with '/'
234	}
235
236	let path = &path[1..];
237	let i = path.find('/').unwrap_or(path.len());
238	let quality_commit_sep = path.get(..i).and_then(|p| p.find('-'))?;
239
240	let (quality_commit, remaining) = path.split_at(i);
241	let (quality, commit) = quality_commit.split_at(quality_commit_sep);
242
243	if !is_commit_hash(commit) {
244		return None;
245	}
246
247	Some((
248		Release {
249			// remember to trim off the leading '/' which is now part of th quality
250			quality: Quality::try_from(quality).ok()?,
251			commit: commit.to_string(),
252			platform,
253			target: TargetKind::Web,
254			name: "".to_string(),
255		},
256		remaining.to_string(),
257	))
258}
259
260/// Proxies the standard HTTP request to the async pipe, returning the piped response
261async fn forward_http_req_to_server(
262	(rw, handle): (AsyncPipe, ConnectionHandle),
263	req: Request<Body>,
264) -> Response<Body> {
265	let (mut request_sender, connection) =
266		match hyper::client::conn::Builder::new().handshake(rw).await {
267			Ok(r) => r,
268			Err(e) => return response::connection_err(e),
269		};
270
271	tokio::spawn(connection);
272
273	let res = request_sender
274		.send_request(req)
275		.await
276		.unwrap_or_else(response::connection_err);
277
278	// technically, we should buffer the body into memory since it may not be
279	// read at this point, but because the keepalive time is very large
280	// there's not going to be responses that take hours to send and x
281	// cause us to kill the server before the response is sent
282	drop(handle);
283
284	res
285}
286
287/// Proxies the websocket request to the async pipe
288async fn forward_ws_req_to_server(
289	log: log::Logger,
290	(rw, handle): (AsyncPipe, ConnectionHandle),
291	mut req: Request<Body>,
292) -> Response<Body> {
293	// splicing of client and servers inspired by https://github.com/hyperium/hyper/blob/fece9f7f50431cf9533cfe7106b53a77b48db699/examples/upgrades.rs
294	let (mut request_sender, connection) =
295		match hyper::client::conn::Builder::new().handshake(rw).await {
296			Ok(r) => r,
297			Err(e) => return response::connection_err(e),
298		};
299
300	tokio::spawn(connection);
301
302	let mut proxied_req = Request::builder().uri(req.uri());
303	for (k, v) in req.headers() {
304		proxied_req = proxied_req.header(k, v);
305	}
306
307	let mut res = request_sender
308		.send_request(proxied_req.body(Body::empty()).unwrap())
309		.await
310		.unwrap_or_else(response::connection_err);
311
312	let mut proxied_res = Response::new(Body::empty());
313	*proxied_res.status_mut() = res.status();
314	for (k, v) in res.headers() {
315		proxied_res.headers_mut().insert(k, v.clone());
316	}
317
318	// only start upgrade at this point in case the server decides to deny socket
319	if res.status() == hyper::StatusCode::SWITCHING_PROTOCOLS {
320		tokio::spawn(async move {
321			let (s_req, s_res) =
322				tokio::join!(hyper::upgrade::on(&mut req), hyper::upgrade::on(&mut res));
323
324			match (s_req, s_res) {
325				(Err(e1), Err(e2)) => debug!(
326					log,
327					"client ({}) and server ({}) websocket upgrade failed", e1, e2
328				),
329				(Err(e1), _) => debug!(log, "client ({}) websocket upgrade failed", e1),
330				(_, Err(e2)) => debug!(log, "server ({}) websocket upgrade failed", e2),
331				(Ok(mut s_req), Ok(mut s_res)) => {
332					trace!(log, "websocket upgrade succeeded");
333					let r = tokio::io::copy_bidirectional(&mut s_req, &mut s_res).await;
334					trace!(log, "websocket closed (error: {:?})", r.err());
335				}
336			}
337
338			drop(handle);
339		});
340	}
341
342	proxied_res
343}
344
345/// Returns whether the string looks like a commit hash.
346fn is_commit_hash(s: &str) -> bool {
347	s.len() == COMMIT_HASH_LEN && s.chars().all(|c| c.is_ascii_hexdigit())
348}
349
350/// Gets a cookie from the request by name.
351fn extract_cookie(req: &Request<Body>, name: &str) -> Option<String> {
352	for h in req.headers().get_all(hyper::header::COOKIE) {
353		if let Ok(str) = h.to_str() {
354			for pair in str.split("; ") {
355				let i = match pair.find('=') {
356					Some(i) => i,
357					None => continue,
358				};
359
360				if &pair[..i] == name {
361					return Some(pair[i + 1..].to_string());
362				}
363			}
364		}
365	}
366
367	None
368}
369
370#[derive(Clone)]
371struct SecretKeyPart(Box<[u8; SECRET_KEY_BYTES]>);
372
373impl SecretKeyPart {
374	pub fn new() -> Self {
375		let key: [u8; SECRET_KEY_BYTES] = rand::random();
376		Self(Box::new(key))
377	}
378
379	pub fn decode(s: &str) -> Result<Self, base64::DecodeSliceError> {
380		use base64::{engine::general_purpose, Engine as _};
381		let mut key: [u8; SECRET_KEY_BYTES] = [0; SECRET_KEY_BYTES];
382		let v = general_purpose::URL_SAFE.decode(s)?;
383		if v.len() != SECRET_KEY_BYTES {
384			return Err(base64::DecodeSliceError::OutputSliceTooSmall);
385		}
386
387		key.copy_from_slice(&v);
388		Ok(Self(Box::new(key)))
389	}
390
391	pub fn encode(&self) -> String {
392		use base64::{engine::general_purpose, Engine as _};
393		general_purpose::URL_SAFE.encode(self.0.as_ref())
394	}
395}
396
397/// Gets the server's half of the secret key.
398fn get_server_key_half(paths: &LauncherPaths) -> SecretKeyPart {
399	let ps = PersistedState::new(paths.root().join("serve-web-key-half"));
400	let value: String = ps.load();
401	if let Ok(sk) = SecretKeyPart::decode(&value) {
402		return sk;
403	}
404
405	let key = SecretKeyPart::new();
406	let _ = ps.save(key.encode());
407	key
408}
409
410/// Gets the client's half of the secret key.
411fn get_client_key_half(req: &Request<Body>) -> SecretKeyPart {
412	if let Some(c) = extract_cookie(req, SECRET_KEY_COOKIE_NAME) {
413		if let Ok(sk) = SecretKeyPart::decode(&c) {
414			return sk;
415		}
416	}
417
418	SecretKeyPart::new()
419}
420
421/// Module holding original responses the CLI's server makes.
422mod response {
423	use const_format::concatcp;
424
425	use crate::constants::QUALITYLESS_SERVER_NAME;
426
427	use super::*;
428
429	pub fn connection_err(err: hyper::Error) -> Response<Body> {
430		Response::builder()
431			.status(503)
432			.body(Body::from(format!("Error connecting to server: {:?}", err)))
433			.unwrap()
434	}
435
436	pub fn code_err(err: CodeError) -> Response<Body> {
437		Response::builder()
438			.status(500)
439			.body(Body::from(format!("Error serving request: {}", err)))
440			.unwrap()
441	}
442
443	pub fn wait_for_download() -> Response<Body> {
444		Response::builder()
445			.status(202)
446			.header("Content-Type", "text/html") // todo: get latest
447			.body(Body::from(concatcp!("The latest version of the ", QUALITYLESS_SERVER_NAME, " is downloading, please wait a moment...<script>setTimeout(()=>location.reload(),1500)</script>", )))
448			.unwrap()
449	}
450
451	pub fn secret_key(hash: Vec<u8>) -> Response<Body> {
452		Response::builder()
453			.status(200)
454			.header("Content-Type", "application/octet-stream") // todo: get latest
455			.body(Body::from(hash))
456			.unwrap()
457	}
458}
459
460/// Handle returned when getting a stream to the server, used to refcount
461/// connections to a server so it can be disposed when there are no more clients.
462struct ConnectionHandle {
463	client_counter: Arc<tokio::sync::watch::Sender<usize>>,
464}
465
466impl ConnectionHandle {
467	pub fn new(client_counter: Arc<tokio::sync::watch::Sender<usize>>) -> Self {
468		client_counter.send_modify(|v| {
469			*v += 1;
470		});
471		Self { client_counter }
472	}
473}
474
475impl Drop for ConnectionHandle {
476	fn drop(&mut self) {
477		self.client_counter.send_modify(|v| {
478			*v -= 1;
479		});
480	}
481}
482
483type StartData = (PathBuf, Arc<tokio::sync::watch::Sender<usize>>);
484
485/// State stored in the ConnectionManager for each server version.
486struct VersionState {
487	downloaded: bool,
488	socket_path: Barrier<Result<StartData, String>>,
489}
490
491type ConnectionStateMap = Arc<Mutex<HashMap<(Quality, String), VersionState>>>;
492
493/// Manages the connections to running web UI instances. Multiple web servers
494/// can run concurrently, with routing based on the URL path.
495struct ConnectionManager {
496	pub platform: Platform,
497	pub log: log::Logger,
498	args: ServeWebArgs,
499	/// Cache where servers are stored
500	cache: DownloadCache,
501	/// Mapping of (Quality, Commit) to the state each server is in
502	state: ConnectionStateMap,
503	/// Update service instance
504	update_service: UpdateService,
505	/// Cache of the latest released version, storing the time we checked as well
506	latest_version: tokio::sync::Mutex<Option<(Instant, Release)>>,
507}
508
509fn key_for_release(release: &Release) -> (Quality, String) {
510	(release.quality, release.commit.clone())
511}
512
513impl ConnectionManager {
514	pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc<Self> {
515		Arc::new(Self {
516			platform,
517			args,
518			log: ctx.log.clone(),
519			cache: DownloadCache::new(ctx.paths.web_server_storage()),
520			update_service: UpdateService::new(
521				ctx.log.clone(),
522				Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())),
523			),
524			state: ConnectionStateMap::default(),
525			latest_version: tokio::sync::Mutex::default(),
526		})
527	}
528
529	/// Gets a connection to a server version
530	pub async fn get_connection(
531		&self,
532		release: Release,
533	) -> Result<(AsyncPipe, ConnectionHandle), CodeError> {
534		// todo@connor4312: there is likely some performance benefit to
535		// implementing a 'keepalive' for these connections.
536		let (path, counter) = self.get_version_data(release).await?;
537		let handle = ConnectionHandle::new(counter);
538		let rw = get_socket_rw_stream(&path).await?;
539		Ok((rw, handle))
540	}
541
542	/// Gets the latest release for the CLI quality, caching its result for some
543	/// time to allow for fast loads.
544	pub async fn get_latest_release(&self) -> Result<Release, CodeError> {
545		let mut latest = self.latest_version.lock().await;
546		let now = Instant::now();
547		if let Some((checked_at, release)) = &*latest {
548			if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) {
549				return Ok(release.clone());
550			}
551		}
552
553		let quality = VSCODE_CLI_QUALITY
554			.ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality"))
555			.and_then(|q| {
556				Quality::try_from(q).map_err(|_| CodeError::UpdatesNotConfigured("unknown quality"))
557			})?;
558
559		let release = self
560			.update_service
561			.get_latest_commit(self.platform, TargetKind::Web, quality)
562			.await
563			.map_err(|e| CodeError::UpdateCheckFailed(e.to_string()));
564
565		// If the update service is unavailable and we have stale data, use that
566		if let (Err(e), Some((_, previous))) = (&release, &*latest) {
567			warning!(self.log, "error getting latest release, using stale: {}", e);
568			return Ok(previous.clone());
569		}
570
571		let release = release?;
572		debug!(self.log, "refreshed latest release: {}", release);
573		*latest = Some((now, release.clone()));
574
575		Ok(release)
576	}
577
578	/// Gets the StartData for the a version of the VS Code server, triggering
579	/// download/start if necessary. It returns `CodeError::ServerNotYetDownloaded`
580	/// while the server is downloading, which is used to have a refresh loop on the page.
581	async fn get_version_data(&self, release: Release) -> Result<StartData, CodeError> {
582		self.get_version_data_inner(release)?
583			.wait()
584			.await
585			.unwrap()
586			.map_err(CodeError::ServerDownloadError)
587	}
588
589	fn get_version_data_inner(
590		&self,
591		release: Release,
592	) -> Result<Barrier<Result<StartData, String>>, CodeError> {
593		let mut state = self.state.lock().unwrap();
594		let key = key_for_release(&release);
595		if let Some(s) = state.get_mut(&key) {
596			if !s.downloaded {
597				if s.socket_path.is_open() {
598					s.downloaded = true;
599				} else {
600					return Err(CodeError::ServerNotYetDownloaded);
601				}
602			}
603
604			return Ok(s.socket_path.clone());
605		}
606
607		let (socket_path, opener) = new_barrier();
608		let state_map_dup = self.state.clone();
609		let args = StartArgs {
610			args: self.args.clone(),
611			log: self.log.clone(),
612			opener,
613			release,
614		};
615
616		if let Some(p) = self.cache.exists(&args.release.commit) {
617			state.insert(
618				key.clone(),
619				VersionState {
620					socket_path: socket_path.clone(),
621					downloaded: true,
622				},
623			);
624
625			tokio::spawn(async move {
626				Self::start_version(args, p).await;
627				state_map_dup.lock().unwrap().remove(&key);
628			});
629			Ok(socket_path)
630		} else {
631			state.insert(
632				key.clone(),
633				VersionState {
634					socket_path,
635					downloaded: false,
636				},
637			);
638			let update_service = self.update_service.clone();
639			let cache = self.cache.clone();
640			tokio::spawn(async move {
641				Self::download_version(args, update_service.clone(), cache.clone()).await;
642				state_map_dup.lock().unwrap().remove(&key);
643			});
644			Err(CodeError::ServerNotYetDownloaded)
645		}
646	}
647
648	/// Downloads a server version into the cache and starts it.
649	async fn download_version(
650		args: StartArgs,
651		update_service: UpdateService,
652		cache: DownloadCache,
653	) {
654		let release_for_fut = args.release.clone();
655		let log_for_fut = args.log.clone();
656		let dir_fut = cache.create(&args.release.commit, |target_dir| async move {
657			info!(log_for_fut, "Downloading server {}", release_for_fut.commit);
658			let tmpdir = tempfile::tempdir().unwrap();
659			let response = update_service.get_download_stream(&release_for_fut).await?;
660
661			let name = response.url_path_basename().unwrap();
662			let archive_path = tmpdir.path().join(name);
663			http::download_into_file(
664				&archive_path,
665				log_for_fut.get_download_logger("Downloading server:"),
666				response,
667			)
668			.await?;
669			unzip_downloaded_release(&archive_path, &target_dir, SilentCopyProgress())?;
670			Ok(())
671		});
672
673		match dir_fut.await {
674			Err(e) => args.opener.open(Err(e.to_string())),
675			Ok(dir) => Self::start_version(args, dir).await,
676		}
677	}
678
679	/// Starts a downloaded server that can be found in the given `path`.
680	async fn start_version(args: StartArgs, path: PathBuf) {
681		info!(args.log, "Starting server {}", args.release.commit);
682
683		let executable = path
684			.join("bin")
685			.join(args.release.quality.server_entrypoint());
686
687		dbg!(&executable);
688
689		let socket_path = get_socket_name();
690
691		dbg!(&socket_path);
692
693		let mut cmd = new_script_command(&executable);
694		cmd.stdin(std::process::Stdio::null());
695		cmd.stderr(std::process::Stdio::piped());
696		cmd.stdout(std::process::Stdio::piped());
697		cmd.arg("--socket-path");
698		cmd.arg(&socket_path);
699
700		// License agreement already checked by the `server_web` function.
701		cmd.args(["--accept-server-license-terms"]);
702
703		if let Some(a) = &args.args.server_base_path {
704			cmd.arg("--server-base-path");
705			cmd.arg(a);
706		}
707		if let Some(a) = &args.args.server_data_dir {
708			cmd.arg("--server-data-dir");
709			cmd.arg(a);
710		}
711		if let Some(a) = &args.args.user_data_dir {
712			cmd.arg("--user-data-dir");
713			cmd.arg(a);
714		}
715		if let Some(a) = &args.args.extensions_dir {
716			cmd.arg("--extensions-dir");
717			cmd.arg(a);
718		}
719		if args.args.without_connection_token {
720			cmd.arg("--without-connection-token");
721		}
722		// Note: intentional that we don't pass --connection-token here, we always
723		// convert it into the file variant.
724		if let Some(ct) = &args.args.connection_token_file {
725			cmd.arg("--connection-token-file");
726			cmd.arg(ct);
727		}
728
729		// removed, otherwise the workbench will not be usable when running the CLI from sources.
730		cmd.env_remove("VSCODE_DEV");
731
732		let mut child = match cmd.spawn() {
733			Ok(c) => c,
734			Err(e) => {
735				args.opener.open(Err(e.to_string()));
736				return;
737			}
738		};
739
740		let (mut stdout, mut stderr) = (
741			BufReader::new(child.stdout.take().unwrap()).lines(),
742			BufReader::new(child.stderr.take().unwrap()).lines(),
743		);
744
745		// wrapped option to prove that we only use this once in the loop
746		let (counter_tx, mut counter_rx) = tokio::sync::watch::channel(0);
747		let mut opener = Some((args.opener, socket_path, Arc::new(counter_tx)));
748		let commit_prefix = &args.release.commit[..7];
749		let kill_timer = tokio::time::sleep(Duration::from_secs(SERVER_IDLE_TIMEOUT_SECS));
750		pin!(kill_timer);
751
752		loop {
753			tokio::select! {
754				Ok(Some(l)) = stdout.next_line() => {
755					info!(args.log, "[{} stdout]: {}", commit_prefix, l);
756
757					if l.contains("Server bound to") {
758						if let Some((opener, path, counter_tx)) = opener.take() {
759							opener.open(Ok((path, counter_tx)));
760						}
761					}
762				}
763				Ok(Some(l)) = stderr.next_line() => {
764					info!(args.log, "[{} stderr]: {}", commit_prefix, l);
765				},
766				n = counter_rx.changed() => {
767					kill_timer.as_mut().reset(match n {
768						// err means that the record was dropped
769						Err(_) => tokio::time::Instant::now(),
770						Ok(_) => {
771							if *counter_rx.borrow() == 0 {
772								tokio::time::Instant::now() + Duration::from_secs(SERVER_IDLE_TIMEOUT_SECS)
773							} else {
774								tokio::time::Instant::now() + Duration::from_secs(SERVER_ACTIVE_TIMEOUT_SECS)
775							}
776						}
777					});
778				}
779				_ = &mut kill_timer => {
780					info!(args.log, "[{} process]: idle timeout reached, ending", commit_prefix);
781					let _ = child.kill().await;
782					break;
783				}
784				e = child.wait() => {
785					info!(args.log, "[{} process]: exited: {:?}", commit_prefix, e);
786					break;
787				}
788			}
789		}
790	}
791}
792
793struct StartArgs {
794	log: log::Logger,
795	args: ServeWebArgs,
796	release: Release,
797	opener: BarrierOpener<Result<StartData, String>>,
798}
799
800fn mint_connection_token(path: &Path, prefer_token: Option<String>) -> std::io::Result<String> {
801	#[cfg(not(windows))]
802	use std::os::unix::fs::OpenOptionsExt;
803
804	let mut f = fs::OpenOptions::new();
805	f.create(true);
806	f.write(true);
807	f.read(true);
808	#[cfg(not(windows))]
809	f.mode(0o600);
810	let mut f = f.open(path)?;
811
812	if prefer_token.is_none() {
813		let mut t = String::new();
814		f.read_to_string(&mut t)?;
815		let t = t.trim();
816		if !t.is_empty() {
817			return Ok(t.to_string());
818		}
819	}
820
821	f.set_len(0)?;
822	let prefer_token = prefer_token.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
823	f.write_all(prefer_token.as_bytes())?;
824	Ok(prefer_token)
825}