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