1use 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
45const COMMIT_HASH_LEN: usize = 40;
47const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60;
50const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12;
53const RELEASE_CACHE_SECS: u64 = 60 * 60;
55
56const SECRET_KEY_BYTES: usize = 32;
58const SECRET_KEY_MINT_PATH: &str = "/_vscode-cli/mint-key";
60const PATH_COOKIE_NAME: &str = "vscode-secret-key-path";
62const PATH_COOKIE_VALUE: &str = concatcp!(
64 PATH_COOKIE_NAME,
65 "=",
66 SECRET_KEY_MINT_PATH,
67 "; SameSite=Strict; Path=/"
68);
69const SECRET_KEY_COOKIE_NAME: &str = "vscode-cli-secret-half";
71
72pub 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 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); 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
158async 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
208fn 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
229fn get_release_from_path(path: &str, platform: Platform) -> Option<(Release, String)> {
232 if !path.starts_with('/') {
233 return None; }
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 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
260async 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 drop(handle);
283
284 res
285}
286
287async fn forward_ws_req_to_server(
289 log: log::Logger,
290 (rw, handle): (AsyncPipe, ConnectionHandle),
291 mut req: Request<Body>,
292) -> Response<Body> {
293 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 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
345fn is_commit_hash(s: &str) -> bool {
347 s.len() == COMMIT_HASH_LEN && s.chars().all(|c| c.is_ascii_hexdigit())
348}
349
350fn 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
397fn 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
410fn 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
421mod 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") .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") .body(Body::from(hash))
456 .unwrap()
457 }
458}
459
460struct 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
485struct VersionState {
487 downloaded: bool,
488 socket_path: Barrier<Result<StartData, String>>,
489}
490
491type ConnectionStateMap = Arc<Mutex<HashMap<(Quality, String), VersionState>>>;
492
493struct ConnectionManager {
496 pub platform: Platform,
497 pub log: log::Logger,
498 args: ServeWebArgs,
499 cache: DownloadCache,
501 state: ConnectionStateMap,
503 update_service: UpdateService,
505 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 pub async fn get_connection(
531 &self,
532 release: Release,
533 ) -> Result<(AsyncPipe, ConnectionHandle), CodeError> {
534 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 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 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 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 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 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 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 if let Some(ct) = &args.args.connection_token_file {
725 cmd.arg("--connection-token-file");
726 cmd.arg(ct);
727 }
728
729 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 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(_) => 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}