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