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