Merge vscode source through release 1.79.2 (#23482)

* log when an editor action doesn't run because of enablement

* notebooks create/dispose editors. this means controllers must be created eagerly (😢) and that notebooks need a custom way of plugging comparision keys for session. works unless creating another session for the same cell of a duplicated editor

* Set offSide to sql lang configuration to true (#183461)

* Fixes #181764 (#183550)

* fix typo

* Always scroll down and focus the input (#183557)

* Fixes #180386 (#183561)

* cli: ensure ordering of rpc server messages (#183558)

* cli: ensure ordering of rpc server messages

Sending lots of messages to a stream would block them around the async
tokio mutex, which is "fair" so doesn't preserve ordering. Instead, use
the write_loop approach I introduced to the server_multiplexer for the
same reason some time ago.

* fix clippy

* update for May endgame

* testing: allow invalidateTestResults to take an array (#183569)

* Document `ShareProvider` API proposal (#183568)

* Document `ShareProvider` API proposal

* Remove mention of VS Code from JSDoc

* Add support for rendering svg and md in welcome message (#183580)

* Remove toggle setting more eagerly (#183584)

* rm message abt macOS

* Change text (#183589)

* Change text

* Accidentally changed the wrong file

* cli: improve output for code tunnel status (#183571)

* testing: allow invalidateTestResults to take an array

* cli: improve output for code tunnel status

Fixes #183570

* [json/css/html] update services (#183595)

* Add experimental setting to enable this dialog

* Fix exporting chat model to JSON before it is initialized (#183597)

* minimum scrolling to reveal the next cell on shift+enter (#183600)

do minimum scrolling to reveal the next cell on Execute cell and select next

* Fixing Jupyter notebook issue 13263 (#183527)

fix for the issue, still need to understand why there is strange focusing

* Tweak proposed API JSDoc (#183590)

* Tweak proposed API JSDoc

* workbench -> workspace

* fix ? operator

* Use active editor and show progress when sharing (#183603)

Use active editor and show progress

* use scroll setting variable correctly

* Schedule welcome widget to show once between typing. (#183606)

* Schedule dialog to show once between typing

* Don't re-render if already displayed once

* Add F10 keybinding for debugger step, even on Web. (#183510)

Fixes #181792.
Previously, for Web the keyboard shortcut was Alt-F10, because it was
believed that F10 could not be bound on browsers. This turned out to be
incorrect, so we make the shortcut consistent (F10) with desktop VSCode
which is also what many other debuggers use.
We keep Alt-F10 on web as a secondary keybinding to keep the experience
some web users may have gotten used to by now.

* Also pass process.env

* Restore missing chat clear commands (#183651)

* chore: update electron@22.5.4 (#183716)

* Show remote indicator in web when remoteAuthority is set (#183728)

* feat: .vuerc as json file (#153017)

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>

* Delete --compatibility=1.63 code from the server (#183738)

* Copy vscode.dev link to tunnel generates an invalid link when an untitled workspace is open (#183739)

* Recent devcontainer display string corrupted on Get Started page (#183740)

* Improve "next codeblock" navigation (#183744)

* Improve "next codeblock" navigation
Operate on the current focused response, or the last one, and scroll to the selected item

* Normalize command title

* Git - run git status if similarityThreshold changes (#183762)

* fix aria-label issue in kb editor

fixes A11y_GradeB_VSCode_Keyboard shortcut reads words together - Blind: Arrow key navigation to row Find the binding keys and  "when" cell data are read together resulting in a word " CTRL + FeditorFocus  instead of CTRL + F editorFocus" #182490

* Status - fix compact padding (#183768)

* Remove angle brackets from VB brackets (#183782)

Fixes #183359

* Update language config schema with more details about brackets. (#183779)

* fix comment (#183812)

* Support for `Notebook` CodeAction Kind (#183457)

* nb kind support -- wip

* allow notebook codeactions around single cell edit check

* move notebook code action type out of editor

---------

Co-authored-by: rebornix <penn.lv@gmail.com>

* cli: fix connection default being applied (#183827)

* cli: bump to openssl 1.1.1u (#183828)

* Implement "delete" action for chat history (#183609)

* Use desired file name when generating new md pasted file paths (#183861)

Fixes #183851

* Default to filename for markdown new file if empty (#183864)

Fixes #183848

* Fix small typo (#183865)

Fixes #183819

* Noop when moving a symbol into the file it is already in (#183866)

Fixes #183793

* Adjust codeAction validation to account for notebook kind (#183859)

* Make JS/TS `go to configuration` commands work on non-`file:` file systems (#183688)

Make `go to project` commands work on non-`file:` file systems

Fixes #183685

* Can't do regex search after opening notebook (#183884)

Fixes #183858

* Default to current dir for `move to file` select (#183875)

Fixes #183870

`showOpenDialog` seems to ignore `defaultUri` if the file doesn't exist

* Use `<...>` style markdown links when needed (#183876)

Fixes #183849

* Remove check for context keys

* Update xterm package

* Enable updating a chat model without triggering incremental typing (#183894)

* Enable chat "move" commands on empty sessions (#183895)

* Enable chat "move" commands on empty sessions
and also imported sessions

* Fix command name

* Fix some chat keybindings on windows (#183896)

* "Revert File" on inactive editors are ignored (fix #177557) (#183903)

* Empty reason while switching profile (fix #183775) (#183904)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4278 (#183910)

* fix https://github.com/microsoft/vscode/issues/183770 (#183914)

* code --status displays a lot of errors before actual status output (fix #183787) (#183915)

* joh/icy manatee (#183917)

* Use idle value for widget of interactive editor controller

https://github.com/microsoft/vscode/issues/183820

* also make preview editors idle values

https://github.com/microsoft/vscode/issues/183820

* Fix #183777 (#183929)

* Fix #182309 (#183925)

* Tree checkbox item -> items (#183931)

Fixes #183826

* Fixes #183909 (#183940)

* Fix #183837 (#183943)

fix #183837

* Git - fix #183941 (#183944)

* Update xterm.css

Fixes #181242

* chore: add @ulugbekna and @aiday-mar to my-endgame notebook (#183946)

* Revert "When snippet mode is active, make `Tab` not accept suggestion but advance placeholder"

This reverts commit 50a80cdb61511343996ff1d41d0b676c3d329f48.

* revert not focusing completion list when quick suggest happens during snippet

* change `snippetsPreventQuickSuggestions` default to false

* Fix #181446 (#183956)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4298 (#183957)

* fix: remove extraneous incorrect context keys (#183959)

These were actually getting added in getTestItemContextOverlay, and the test ID was using the extended ID which extensions do not know about.

Fixes #183612

* Fixes https://github.com/microsoft/monaco-editor/issues/3920 (#183960)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4324 (#183961)

* fix #183030

* fix #180826 (#183962)

* make message more generic for interactive editor help

* .

* fix #183968

* Keep codeblock toolbar visible when focused

* Fix when clause on "Run in terminal" command

* add important info to help menu

* fix #183970

* Set `isRefactoring` for all TS refactoring edits (#183982)

* consolidate

* Disable move to file in TS versions < 5.2 (#183992)

There are still a few key bugs with refactoring. We will  ship this as a preview for TS 5.2+ instead of for 5.1

* Polish query accepting (#183995)

We shouldn't send the same request to Copilot if the query hasn't changed. So if the query is the same, we short circut.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4286

Also, when we open in chat, we should use the last accepted query, not what's in the input box.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4280

* Allow widget to have focus (#184000)

So that selecting non-code text works.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4294

* Fix microsoft/vscode-internalbacklog#4257. Mitigate zindex for zone widgets. (#184001)

* Change welcome dialog contribution to Eventually

* Misc fixes

* Workspace folder picker entry descriptions are suboptimal for some filesystems (fix #183418) (#184018)

* cli - ignore std error unless verbose (#183787) (#184031)

* joh/inquisitive meerkat (#184034)

* only stash sessions that are none empty

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* only unstash a session once - unless new exchanges are made,

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* account for all exchange types

* Improve declared components (#184039)

* make sure to read setting (#184040)

d'oh, related to https://github.com/microsoft/vscode/issues/173387#issuecomment-1571696644

* [html] update service (#184049)

[html] update service. FIxes #181176

* reset context keys on reset/hide (#184042)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4330

* use `Lazy`, not `IdleValue` for the IE widget held by the eager controller (#184048)

https://github.com/microsoft/vscode/issues/183820

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4333 (#184067)

* use undo-loop instead of undo-edit when discarding chat session (#184063)

* use undo-loop instead of undo-edit when discarding chat session

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4118

* fix tests, wait for correct state

* Add logging to node download (#184070)

Add logging to node download. For #182951

* re-enable default zone widget revealing when showing (#184072)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4332, also fixes https://github.com/microsoft/vscode-internalbacklog/issues/3784

* fix #178202

* Allow APIs in stable (#184062)

* Fix microsoft/vscode-internalbacklog#4206. Override List view whitespace css for monaco editor (#184087)

* Fix JSDoc grammatical error (#184090)

* Pick up TS 5.1.3 (#184091)

Fixes #182931

* Misc fixes

* update distro (#184097)

* chore: update electron@22.5.5 (#184116)

* Extension host veto is registered multiple times on restart (fix #183778) (#184127)

Extension host veto is registered multiple times on restart (#183778)

* Do not auto start the local web worker extension host (#184137)

* Allow embedders to intercept trustedTypes.createPolicy calls (#184136)

Allow embedders to intercept trustedTypes.createPolicy calls (#184100)

* fix: reading from console output for --status on windows and linux (#184138)

fix: reading from console output for --status on windows and linux (#184118)

* Misc fixes

* code --status displays a lot of errors before actual status output (fix #183787) (#184200)

fix 183787

* (cherry-pick to 1.79 from main) Handle galleryExtension failure in featuredExtensionService (#184205)

Handle galleryExtension failure in featuredExtensionService (#184198)

Handle galleryExtension failure

* Fix #184183. Multiple output height updates are skipped. (#184188)

* Post merge init fixes

* Misc build issues

* disable toggle inline diff of `alt` down

https://github.com/microsoft/vscode-internalbacklog/issues/4342

* Take into account already activated extensions when computing running locations (#184303)

Take into account already activated extensions when computing running locations (fixes #184180)

* Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (#184310)

Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (fixes #182217)

* WIP

* rm fish auto injection

* More breaks

* Fix Port Attributes constructor (#184412)

* WIP

* WIP

* Allow extensions to get at the exports of other extensions during resolving (#184487)

Allow extensions to get at the exports of other extensions during resolving (fixes #184472)

* do not auto finish session when inline chat widgets have focus

re https://github.com/microsoft/vscode-internalbacklog/issues/4354

* fix compile errors caused by new base method

* WIP

* WIP

* WIP

* WIP

* Build errors

* unc - fix path traversal bypass

* Bump version

* cherry-pick prod changes from main

* Disable sandbox

* Build break from merge

* bump version

* Merge pull request #184739 from max06/max06/issue184659

Restore ShellIntegration for fish (#184659)

* Git - only add --find-renames if the value is not the default one (#185053)

Git - only add --find-renames if the value is not the default one (#184992)

* Cherry-pick: Revert changes to render featured extensions when available (#184747)

Revert changes to render featured extensions when available.  (#184573)

* Lower timeouts for experimentation and gallery service

* Revert changes to render extensions when available

* Add audio cues

* fix: disable app sandbox when --no-sandbox is present (#184913)

* fix: disable app sandbox when --no-sandbox is present (#184897)

* fix: loading minimist in packaged builds

* Runtime errors

* UNC allow list checks cannot be disabled in extension host (fix #184989) (#185085)

* UNC allow list checks cannot be disabled in extension host (#184989)

* Update src/vs/base/node/unc.js

Co-authored-by: Robo <hop2deep@gmail.com>

---------

Co-authored-by: Robo <hop2deep@gmail.com>

* Add notebook extension

* Fix mangling issues

* Fix mangling issues

* npm install

* npm install

* Issues blocking bundle

* Fix build folder compile errors

* Fix windows bundle build

* Linting fixes

* Fix sqllint issues

* Update yarn.lock files

* Fix unit tests

* Fix a couple breaks from test fixes

* Bump distro

* redo the checkbox style

* Update linux build container dockerfile

* Bump build image tag

* Bump native watch dog package

* Bump node-pty

* Bump distro

* Fix documnetation error

* Update distro

* redo the button styles

* Update datasource TS

* Add missing yarn.lock files

* Windows setup fix

* Turn off extension unit tests while investigating

* color box style

* Remove appx

* Turn off test log upload

* update dropdownlist style

* fix universal app build error (#23488)

* Skip flaky bufferContext vscode test

---------

Co-authored-by: Johannes <johannes.rieken@gmail.com>
Co-authored-by: Henning Dieterichs <hdieterichs@microsoft.com>
Co-authored-by: Julien Richard <jairbubbles@hotmail.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Megan Rogge <merogge@microsoft.com>
Co-authored-by: meganrogge <megan.rogge@microsoft.com>
Co-authored-by: Rob Lourens <roblourens@gmail.com>
Co-authored-by: Connor Peet <connor@peet.io>
Co-authored-by: Joyce Er <joyce.er@microsoft.com>
Co-authored-by: Bhavya U <bhavyau@microsoft.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
Co-authored-by: Aaron Munger <aamunger@microsoft.com>
Co-authored-by: Aiday Marlen Kyzy <amarlenkyzy@microsoft.com>
Co-authored-by: rebornix <penn.lv@gmail.com>
Co-authored-by: Ole <oler@google.com>
Co-authored-by: Jean Pierre <jeanp413@hotmail.com>
Co-authored-by: Robo <hop2deep@gmail.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com>
Co-authored-by: Ulugbek Abdullaev <ulugbekna@gmail.com>
Co-authored-by: Alex Ross <alros@microsoft.com>
Co-authored-by: Michael Lively <milively@microsoft.com>
Co-authored-by: Matt Bierner <matb@microsoft.com>
Co-authored-by: Andrea Mah <31675041+andreamah@users.noreply.github.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Sandeep Somavarapu <sasomava@microsoft.com>
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Co-authored-by: Tyler James Leonhardt <me@tylerleonhardt.com>
Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
Co-authored-by: Joao Moreno <Joao.Moreno@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Karl Burtram
2023-06-27 15:26:51 -07:00
committed by GitHub
parent 7975fda6dd
commit 01e66ab3e6
4335 changed files with 252586 additions and 164604 deletions

61
cli/src/util/app_lock.rs Normal file
View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
#[cfg(windows)]
use std::{io, ptr};
#[cfg(windows)]
use winapi::{
shared::winerror::ERROR_ALREADY_EXISTS,
um::{handleapi::CloseHandle, synchapi::CreateMutexA, winnt::HANDLE},
};
use super::errors::CodeError;
pub struct AppMutex {
#[cfg(windows)]
handle: HANDLE,
}
#[cfg(windows)] // handle is thread-safe, mark it so with this
unsafe impl Send for AppMutex {}
impl AppMutex {
#[cfg(unix)]
pub fn new(_name: &str) -> Result<Self, CodeError> {
Ok(Self {})
}
#[cfg(windows)]
pub fn new(name: &str) -> Result<Self, CodeError> {
use std::ffi::CString;
let cname = CString::new(name).unwrap();
let handle = unsafe { CreateMutexA(ptr::null_mut(), 0, cname.as_ptr() as _) };
if !handle.is_null() {
return Ok(Self { handle });
}
let err = io::Error::last_os_error();
let raw = err.raw_os_error();
// docs report it should return ERROR_IO_PENDING, but in my testing it actually
// returns ERROR_LOCK_VIOLATION. Or maybe winapi is wrong?
if raw == Some(ERROR_ALREADY_EXISTS as i32) {
return Err(CodeError::AppAlreadyLocked(name.to_string()));
}
Err(CodeError::AppLockFailed(err))
}
}
impl Drop for AppMutex {
fn drop(&mut self) {
#[cfg(windows)]
unsafe {
CloseHandle(self.handle)
};
}
}

119
cli/src/util/command.rs Normal file
View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use super::errors::CodeError;
use std::{
borrow::Cow,
ffi::OsStr,
process::{Output, Stdio},
};
use tokio::process::Command;
pub async fn capture_command_and_check_status(
command_str: impl AsRef<OsStr>,
args: &[impl AsRef<OsStr>],
) -> Result<std::process::Output, CodeError> {
let output = capture_command(&command_str, args).await?;
check_output_status(output, || {
format!(
"{} {}",
command_str.as_ref().to_string_lossy(),
args.iter()
.map(|a| a.as_ref().to_string_lossy())
.collect::<Vec<Cow<'_, str>>>()
.join(" ")
)
})
}
pub fn check_output_status(
output: Output,
cmd_str: impl FnOnce() -> String,
) -> Result<std::process::Output, CodeError> {
if !output.status.success() {
return Err(CodeError::CommandFailed {
command: cmd_str(),
code: output.status.code().unwrap_or(-1),
output: String::from_utf8_lossy(if output.stderr.is_empty() {
&output.stdout
} else {
&output.stderr
})
.into(),
});
}
Ok(output)
}
pub async fn capture_command<A, I, S>(
command_str: A,
args: I,
) -> Result<std::process::Output, CodeError>
where
A: AsRef<OsStr>,
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new(&command_str)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.output()
.await
.map_err(|e| CodeError::CommandFailed {
command: command_str.as_ref().to_string_lossy().to_string(),
code: -1,
output: e.to_string(),
})
}
/// Kills and processes and all of its children.
#[cfg(target_os = "windows")]
pub async fn kill_tree(process_id: u32) -> Result<(), CodeError> {
capture_command("taskkill", &["/t", "/pid", &process_id.to_string()]).await?;
Ok(())
}
/// Kills and processes and all of its children.
#[cfg(not(target_os = "windows"))]
pub async fn kill_tree(process_id: u32) -> Result<(), CodeError> {
use futures::future::join_all;
use tokio::io::{AsyncBufReadExt, BufReader};
async fn kill_single_pid(process_id_str: String) {
capture_command("kill", &[&process_id_str]).await.ok();
}
// Rusty version of https://github.com/microsoft/vscode-js-debug/blob/main/src/targets/node/terminateProcess.sh
let parent_id = process_id.to_string();
let mut prgrep_cmd = Command::new("pgrep")
.arg("-P")
.arg(&parent_id)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()
.map_err(|e| CodeError::CommandFailed {
command: format!("pgrep -P {}", parent_id),
code: -1,
output: e.to_string(),
})?;
let mut kill_futures = vec![tokio::spawn(
async move { kill_single_pid(parent_id).await },
)];
if let Some(stdout) = prgrep_cmd.stdout.take() {
let mut reader = BufReader::new(stdout).lines();
while let Some(line) = reader.next_line().await.unwrap_or(None) {
kill_futures.push(tokio::spawn(async move { kill_single_pid(line).await }))
}
}
join_all(kill_futures).await;
prgrep_cmd.kill().await.ok();
Ok(())
}

532
cli/src/util/errors.rs Normal file
View File

@@ -0,0 +1,532 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use crate::{
constants::{APPLICATION_NAME, CONTROL_PORT, DOCUMENTATION_URL, QUALITYLESS_PRODUCT_NAME},
rpc::ResponseError,
};
use std::fmt::Display;
use thiserror::Error;
// Wraps another error with additional info.
#[derive(Debug, Clone)]
pub struct WrappedError {
message: String,
original: String,
}
impl std::fmt::Display for WrappedError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}: {}", self.message, self.original)
}
}
impl std::error::Error for WrappedError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl WrappedError {
// fn new(original: Box<dyn std::error::Error>, message: String) -> WrappedError {
// WrappedError { message, original }
// }
}
impl From<reqwest::Error> for WrappedError {
fn from(e: reqwest::Error) -> WrappedError {
WrappedError {
message: format!(
"error requesting {}",
e.url().map_or("<unknown>", |u| u.as_str())
),
original: format!("{}", e),
}
}
}
pub fn wrapdbg<T, S>(original: T, message: S) -> WrappedError
where
T: std::fmt::Debug,
S: Into<String>,
{
WrappedError {
message: message.into(),
original: format!("{:?}", original),
}
}
pub fn wrap<T, S>(original: T, message: S) -> WrappedError
where
T: Display,
S: Into<String>,
{
WrappedError {
message: message.into(),
original: format!("{}", original),
}
}
// Error generated by an unsuccessful HTTP response
#[derive(Debug)]
pub struct StatusError {
pub url: String,
pub status_code: u16,
pub body: String,
}
impl std::fmt::Display for StatusError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"error requesting {}: {} {}",
self.url, self.status_code, self.body
)
}
}
impl StatusError {
pub async fn from_res(res: reqwest::Response) -> Result<StatusError, AnyError> {
let status_code = res.status().as_u16();
let url = res.url().to_string();
let body = res.text().await.map_err(|e| {
wrap(
e,
format!(
"failed to read response body on {} code from {}",
status_code, url
),
)
})?;
Ok(StatusError {
url,
status_code,
body,
})
}
}
// When the user has not consented to the licensing terms in using the Launcher
#[derive(Debug)]
pub struct MissingLegalConsent(pub String);
impl std::fmt::Display for MissingLegalConsent {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
// When the provided connection token doesn't match the one used to set up the original VS Code Server
// This is most likely due to a new user joining.
#[derive(Debug)]
pub struct MismatchConnectionToken(pub String);
impl std::fmt::Display for MismatchConnectionToken {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
// When the VS Code server has an unrecognized extension (rather than zip or gz)
#[derive(Debug)]
pub struct InvalidServerExtensionError(pub String);
impl std::fmt::Display for InvalidServerExtensionError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "invalid server extension '{}'", self.0)
}
}
// When the tunnel fails to open
#[derive(Debug, Clone)]
pub struct DevTunnelError(pub String);
impl std::fmt::Display for DevTunnelError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "could not open tunnel: {}", self.0)
}
}
impl std::error::Error for DevTunnelError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
// When the server was downloaded, but the entrypoint scripts don't exist.
#[derive(Debug)]
pub struct MissingEntrypointError();
impl std::fmt::Display for MissingEntrypointError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Missing entrypoints in server download. Most likely this is a corrupted download. Please retry")
}
}
#[derive(Debug)]
pub struct SetupError(pub String);
impl std::fmt::Display for SetupError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}\n\nMore info at {}/remote/linux",
DOCUMENTATION_URL.unwrap_or("<docs>"),
self.0
)
}
}
#[derive(Debug)]
pub struct NoHomeForLauncherError();
impl std::fmt::Display for NoHomeForLauncherError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"No $HOME variable was found in your environment. Either set it, or specify a `--data-dir` manually when invoking the launcher.",
)
}
}
#[derive(Debug)]
pub struct InvalidTunnelName(pub String);
impl std::fmt::Display for InvalidTunnelName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
#[derive(Debug)]
pub struct TunnelCreationFailed(pub String, pub String);
impl std::fmt::Display for TunnelCreationFailed {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Could not create tunnel with name: {}\nReason: {}",
&self.0, &self.1
)
}
}
#[derive(Debug)]
pub struct TunnelHostFailed(pub String);
impl std::fmt::Display for TunnelHostFailed {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
#[derive(Debug)]
pub struct ExtensionInstallFailed(pub String);
impl std::fmt::Display for ExtensionInstallFailed {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Extension install failed: {}", &self.0)
}
}
#[derive(Debug)]
pub struct MismatchedLaunchModeError();
impl std::fmt::Display for MismatchedLaunchModeError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "A server is already running, but it was not launched in the same listening mode (port vs. socket) as this request")
}
}
#[derive(Debug)]
pub struct NoAttachedServerError();
impl std::fmt::Display for NoAttachedServerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "No server is running")
}
}
#[derive(Debug)]
pub struct RefreshTokenNotAvailableError();
impl std::fmt::Display for RefreshTokenNotAvailableError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Refresh token not available, authentication is required")
}
}
#[derive(Debug)]
pub struct NoInstallInUserProvidedPath(pub String);
impl std::fmt::Display for NoInstallInUserProvidedPath {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"No {} installation could be found in {}. You can run `{} --use-quality=stable` to switch to the latest stable version of {}.",
QUALITYLESS_PRODUCT_NAME,
self.0,
APPLICATION_NAME,
QUALITYLESS_PRODUCT_NAME
)
}
}
#[derive(Debug)]
pub struct InvalidRequestedVersion();
impl std::fmt::Display for InvalidRequestedVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"The reqested version is invalid, expected one of 'stable', 'insiders', version number (x.y.z), or absolute path.",
)
}
}
#[derive(Debug)]
pub struct UserCancelledInstallation();
impl std::fmt::Display for UserCancelledInstallation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Installation aborted.")
}
}
#[derive(Debug)]
pub struct CannotForwardControlPort();
impl std::fmt::Display for CannotForwardControlPort {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Cannot forward or unforward port {}.", CONTROL_PORT)
}
}
#[derive(Debug)]
pub struct ServerHasClosed();
impl std::fmt::Display for ServerHasClosed {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Request cancelled because the server has closed")
}
}
#[derive(Debug)]
pub struct UpdatesNotConfigured(pub String);
impl UpdatesNotConfigured {
pub fn no_url() -> Self {
UpdatesNotConfigured("no service url".to_owned())
}
}
impl std::fmt::Display for UpdatesNotConfigured {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Update service is not configured: {}", self.0)
}
}
#[derive(Debug)]
pub struct ServiceAlreadyRegistered();
impl std::fmt::Display for ServiceAlreadyRegistered {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Already registered the service. Run `{} tunnel service uninstall` to unregister it first", APPLICATION_NAME)
}
}
#[derive(Debug)]
pub struct WindowsNeedsElevation(pub String);
impl std::fmt::Display for WindowsNeedsElevation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "{}", self.0)?;
writeln!(f)?;
writeln!(f, "You may need to run this command as an administrator:")?;
writeln!(f, " 1. Open the start menu and search for Powershell")?;
writeln!(f, " 2. Right click and 'Run as administrator'")?;
if let Ok(exe) = std::env::current_exe() {
writeln!(
f,
" 3. Run &'{}' '{}'",
exe.display(),
std::env::args().skip(1).collect::<Vec<_>>().join("' '")
)
} else {
writeln!(f, " 3. Run the same command again",)
}
}
}
#[derive(Debug)]
pub struct InvalidRpcDataError(pub String);
impl std::fmt::Display for InvalidRpcDataError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "parse error: {}", self.0)
}
}
#[derive(Debug)]
pub struct CorruptDownload(pub String);
impl std::fmt::Display for CorruptDownload {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Error updating the {} CLI: {}",
QUALITYLESS_PRODUCT_NAME, self.0
)
}
}
#[derive(Debug)]
pub struct MissingHomeDirectory();
impl std::fmt::Display for MissingHomeDirectory {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Could not find your home directory. Please ensure this command is running in the context of an normal user.")
}
}
#[derive(Debug)]
pub struct OAuthError {
pub error: String,
pub error_description: Option<String>,
}
impl std::fmt::Display for OAuthError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Error getting authorization: {} {}",
self.error,
self.error_description.as_deref().unwrap_or("")
)
}
}
// Makes an "AnyError" enum that contains any of the given errors, in the form
// `enum AnyError { FooError(FooError) }` (when given `makeAnyError!(FooError)`).
// Useful to easily deal with application error types without making tons of "From"
// clauses.
macro_rules! makeAnyError {
($($e:ident),*) => {
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
pub enum AnyError {
$($e($e),)*
}
impl std::fmt::Display for AnyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
$(AnyError::$e(ref e) => e.fmt(f),)*
}
}
}
impl std::error::Error for AnyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
$(impl From<$e> for AnyError {
fn from(e: $e) -> AnyError {
AnyError::$e(e)
}
})*
};
}
/// Internal errors in the VS Code CLI.
/// Note: other error should be migrated to this type gradually
#[derive(Error, Debug)]
pub enum CodeError {
#[error("could not connect to socket/pipe: {0:?}")]
AsyncPipeFailed(std::io::Error),
#[error("could not listen on socket/pipe: {0:?}")]
AsyncPipeListenerFailed(std::io::Error),
#[error("could not create singleton lock file: {0:?}")]
SingletonLockfileOpenFailed(std::io::Error),
#[error("could not read singleton lock file: {0:?}")]
SingletonLockfileReadFailed(rmp_serde::decode::Error),
#[error("the process holding the singleton lock file (pid={0}) exited")]
SingletonLockedProcessExited(u32),
#[error("no tunnel process is currently running")]
NoRunningTunnel,
#[error("rpc call failed: {0:?}")]
TunnelRpcCallFailed(ResponseError),
#[cfg(windows)]
#[error("the windows app lock {0} already exists")]
AppAlreadyLocked(String),
#[cfg(windows)]
#[error("could not get windows app lock: {0:?}")]
AppLockFailed(std::io::Error),
#[error("failed to run command \"{command}\" (code {code}): {output}")]
CommandFailed {
command: String,
code: i32,
output: String,
},
#[error("platform not currently supported: {0}")]
UnsupportedPlatform(String),
#[error("This machine not meet {name}'s prerequisites, expected either...: {bullets}")]
PrerequisitesFailed { name: &'static str, bullets: String },
#[error("failed to spawn process: {0:?}")]
ProcessSpawnFailed(std::io::Error),
#[error("failed to handshake spawned process: {0:?}")]
ProcessSpawnHandshakeFailed(std::io::Error),
#[error("download appears corrupted, please retry ({0})")]
CorruptDownload(&'static str),
#[error("port forwarding is not available in this context")]
PortForwardingNotAvailable,
#[error("'auth' call required")]
ServerAuthRequired,
#[error("challenge not yet issued")]
AuthChallengeNotIssued,
#[error("unauthorized client refused")]
AuthMismatch,
}
makeAnyError!(
MissingLegalConsent,
MismatchConnectionToken,
DevTunnelError,
StatusError,
WrappedError,
InvalidServerExtensionError,
MissingEntrypointError,
SetupError,
NoHomeForLauncherError,
TunnelCreationFailed,
TunnelHostFailed,
InvalidTunnelName,
ExtensionInstallFailed,
MismatchedLaunchModeError,
NoAttachedServerError,
RefreshTokenNotAvailableError,
NoInstallInUserProvidedPath,
UserCancelledInstallation,
InvalidRequestedVersion,
CannotForwardControlPort,
ServerHasClosed,
ServiceAlreadyRegistered,
WindowsNeedsElevation,
UpdatesNotConfigured,
CorruptDownload,
MissingHomeDirectory,
OAuthError,
InvalidRpcDataError,
CodeError
);
impl From<reqwest::Error> for AnyError {
fn from(e: reqwest::Error) -> AnyError {
AnyError::WrappedError(WrappedError::from(e))
}
}

125
cli/src/util/file_lock.rs Normal file
View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use crate::util::errors::CodeError;
use std::{fs::File, io};
pub struct FileLock {
file: File,
#[cfg(windows)]
overlapped: winapi::um::minwinbase::OVERLAPPED,
}
#[cfg(windows)] // overlapped is thread-safe, mark it so with this
unsafe impl Send for FileLock {}
pub enum Lock {
Acquired(FileLock),
AlreadyLocked(File),
}
/// Number of locked bytes in the file. On Windows, locking prevents reads,
/// but consumers of the lock may still want to read what the locking file
/// as written. Thus, only PREFIX_LOCKED_BYTES are locked, and any globally-
/// readable content should be written after the prefix.
#[cfg(windows)]
pub const PREFIX_LOCKED_BYTES: usize = 1;
#[cfg(unix)]
pub const PREFIX_LOCKED_BYTES: usize = 0;
impl FileLock {
#[cfg(windows)]
pub fn acquire(file: File) -> Result<Lock, CodeError> {
use std::os::windows::prelude::AsRawHandle;
use winapi::{
shared::winerror::{ERROR_IO_PENDING, ERROR_LOCK_VIOLATION},
um::{
fileapi::LockFileEx,
minwinbase::{LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY},
},
};
let handle = file.as_raw_handle();
let (overlapped, ok) = unsafe {
let mut overlapped = std::mem::zeroed();
let ok = LockFileEx(
handle,
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
PREFIX_LOCKED_BYTES as u32,
0,
&mut overlapped,
);
(overlapped, ok)
};
if ok != 0 {
return Ok(Lock::Acquired(Self { file, overlapped }));
}
let err = io::Error::last_os_error();
let raw = err.raw_os_error();
// docs report it should return ERROR_IO_PENDING, but in my testing it actually
// returns ERROR_LOCK_VIOLATION. Or maybe winapi is wrong?
if raw == Some(ERROR_IO_PENDING as i32) || raw == Some(ERROR_LOCK_VIOLATION as i32) {
return Ok(Lock::AlreadyLocked(file));
}
Err(CodeError::SingletonLockfileOpenFailed(err))
}
#[cfg(unix)]
pub fn acquire(file: File) -> Result<Lock, CodeError> {
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let res = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if res == 0 {
return Ok(Lock::Acquired(Self { file }));
}
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
return Ok(Lock::AlreadyLocked(file));
}
Err(CodeError::SingletonLockfileOpenFailed(err))
}
pub fn file(&self) -> &File {
&self.file
}
pub fn file_mut(&mut self) -> &mut File {
&mut self.file
}
}
impl Drop for FileLock {
#[cfg(windows)]
fn drop(&mut self) {
use std::os::windows::prelude::AsRawHandle;
use winapi::um::fileapi::UnlockFileEx;
unsafe {
UnlockFileEx(
self.file.as_raw_handle(),
0,
u32::MAX,
u32::MAX,
&mut self.overlapped,
)
};
}
#[cfg(unix)]
fn drop(&mut self) {
use std::os::unix::io::AsRawFd;
unsafe { libc::flock(self.file.as_raw_fd(), libc::LOCK_UN) };
}
}

376
cli/src/util/http.rs Normal file
View File

@@ -0,0 +1,376 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use crate::{
constants::get_default_user_agent,
log,
util::errors::{self, WrappedError},
};
use async_trait::async_trait;
use core::panic;
use futures::stream::TryStreamExt;
use hyper::{
header::{HeaderName, CONTENT_LENGTH},
http::HeaderValue,
HeaderMap, StatusCode,
};
use serde::de::DeserializeOwned;
use std::{io, pin::Pin, str::FromStr, sync::Arc, task::Poll};
use tokio::{
fs,
io::{AsyncRead, AsyncReadExt},
sync::mpsc,
};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use super::{
errors::{wrap, AnyError, StatusError},
io::{copy_async_progress, ReadBuffer, ReportCopyProgress},
};
pub async fn download_into_file<T>(
filename: &std::path::Path,
progress: T,
mut res: SimpleResponse,
) -> Result<fs::File, WrappedError>
where
T: ReportCopyProgress,
{
let mut file = fs::File::create(filename)
.await
.map_err(|e| errors::wrap(e, "failed to create file"))?;
let content_length = res
.headers
.get(CONTENT_LENGTH)
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
copy_async_progress(progress, &mut res.read, &mut file, content_length)
.await
.map_err(|e| errors::wrap(e, "failed to download file"))?;
Ok(file)
}
pub struct SimpleResponse {
pub status_code: StatusCode,
pub headers: HeaderMap,
pub read: Pin<Box<dyn Send + AsyncRead + 'static>>,
pub url: Option<url::Url>,
}
impl SimpleResponse {
pub fn url_path_basename(&self) -> Option<String> {
self.url.as_ref().and_then(|u| {
u.path_segments()
.and_then(|s| s.last().map(|s| s.to_owned()))
})
}
}
impl SimpleResponse {
pub fn generic_error(url: &str) -> Self {
let (_, rx) = mpsc::unbounded_channel();
SimpleResponse {
url: url::Url::parse(url).ok(),
status_code: StatusCode::INTERNAL_SERVER_ERROR,
headers: HeaderMap::new(),
read: Box::pin(DelegatedReader::new(rx)),
}
}
/// Converts the response into a StatusError
pub async fn into_err(mut self) -> StatusError {
let mut body = String::new();
self.read.read_to_string(&mut body).await.ok();
StatusError {
url: self
.url
.map(|u| u.to_string())
.unwrap_or_else(|| "<invalid url>".to_owned()),
status_code: self.status_code.as_u16(),
body,
}
}
/// Deserializes the response body as JSON
pub async fn json<T: DeserializeOwned>(&mut self) -> Result<T, AnyError> {
let mut buf = vec![];
// ideally serde would deserialize a stream, but it does not appear that
// is supported. reqwest itself reads and decodes separately like we do here:
self.read
.read_to_end(&mut buf)
.await
.map_err(|e| wrap(e, "error reading response"))?;
let t = serde_json::from_slice(&buf)
.map_err(|e| wrap(e, format!("error decoding json from {:?}", self.url)))?;
Ok(t)
}
}
/// *Very* simple HTTP implementation. In most cases, this will just delegate to
/// the request library on the server (i.e. `reqwest`) but it can also be used
/// to make update/download requests on the client rather than the server,
/// similar to SSH's `remote.SSH.localServerDownload` setting.
#[async_trait]
pub trait SimpleHttp {
async fn make_request(
&self,
method: &'static str,
url: String,
) -> Result<SimpleResponse, AnyError>;
}
pub type BoxedHttp = Arc<dyn SimpleHttp + Send + Sync + 'static>;
// Implementation of SimpleHttp that uses a reqwest client.
#[derive(Clone)]
pub struct ReqwestSimpleHttp {
client: reqwest::Client,
}
impl ReqwestSimpleHttp {
pub fn new() -> Self {
Self {
client: reqwest::ClientBuilder::new()
.user_agent(get_default_user_agent())
.build()
.unwrap(),
}
}
pub fn with_client(client: reqwest::Client) -> Self {
Self { client }
}
}
impl Default for ReqwestSimpleHttp {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SimpleHttp for ReqwestSimpleHttp {
async fn make_request(
&self,
method: &'static str,
url: String,
) -> Result<SimpleResponse, AnyError> {
let res = self
.client
.request(reqwest::Method::try_from(method).unwrap(), &url)
.send()
.await?;
Ok(SimpleResponse {
status_code: res.status(),
headers: res.headers().clone(),
url: Some(res.url().clone()),
read: Box::pin(
res.bytes_stream()
.map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e))
.into_async_read()
.compat(),
),
})
}
}
enum DelegatedHttpEvent {
InitResponse {
status_code: u16,
headers: Vec<(String, String)>,
},
Body(Vec<u8>),
End,
}
// Handle for a delegated request that allows manually issuing and response.
pub struct DelegatedHttpRequest {
pub method: &'static str,
pub url: String,
ch: mpsc::UnboundedSender<DelegatedHttpEvent>,
}
impl DelegatedHttpRequest {
pub fn initial_response(&self, status_code: u16, headers: Vec<(String, String)>) {
self.ch
.send(DelegatedHttpEvent::InitResponse {
status_code,
headers,
})
.ok();
}
pub fn body(&self, chunk: Vec<u8>) {
self.ch.send(DelegatedHttpEvent::Body(chunk)).ok();
}
pub fn end(self) {}
}
impl Drop for DelegatedHttpRequest {
fn drop(&mut self) {
self.ch.send(DelegatedHttpEvent::End).ok();
}
}
/// Implementation of SimpleHttp that allows manually controlling responses.
#[derive(Clone)]
pub struct DelegatedSimpleHttp {
start_request: mpsc::Sender<DelegatedHttpRequest>,
log: log::Logger,
}
impl DelegatedSimpleHttp {
pub fn new(log: log::Logger) -> (Self, mpsc::Receiver<DelegatedHttpRequest>) {
let (tx, rx) = mpsc::channel(4);
(
DelegatedSimpleHttp {
log,
start_request: tx,
},
rx,
)
}
}
#[async_trait]
impl SimpleHttp for DelegatedSimpleHttp {
async fn make_request(
&self,
method: &'static str,
url: String,
) -> Result<SimpleResponse, AnyError> {
trace!(self.log, "making delegated request to {}", url);
let (tx, mut rx) = mpsc::unbounded_channel();
let sent = self
.start_request
.send(DelegatedHttpRequest {
method,
url: url.clone(),
ch: tx,
})
.await;
if sent.is_err() {
return Ok(SimpleResponse::generic_error(&url)); // sender shut down
}
match rx.recv().await {
Some(DelegatedHttpEvent::InitResponse {
status_code,
headers,
}) => {
trace!(
self.log,
"delegated request to {} resulted in status = {}",
url,
status_code
);
let mut headers_map = HeaderMap::with_capacity(headers.len());
for (k, v) in &headers {
if let (Ok(key), Ok(value)) = (
HeaderName::from_str(&k.to_lowercase()),
HeaderValue::from_str(v),
) {
headers_map.insert(key, value);
}
}
Ok(SimpleResponse {
url: url::Url::parse(&url).ok(),
status_code: StatusCode::from_u16(status_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
headers: headers_map,
read: Box::pin(DelegatedReader::new(rx)),
})
}
Some(DelegatedHttpEvent::End) => Ok(SimpleResponse::generic_error(&url)),
Some(_) => panic!("expected initresponse as first message from delegated http"),
None => Ok(SimpleResponse::generic_error(&url)), // sender shut down
}
}
}
struct DelegatedReader {
receiver: mpsc::UnboundedReceiver<DelegatedHttpEvent>,
readbuf: ReadBuffer,
}
impl DelegatedReader {
pub fn new(rx: mpsc::UnboundedReceiver<DelegatedHttpEvent>) -> Self {
DelegatedReader {
readbuf: ReadBuffer::default(),
receiver: rx,
}
}
}
impl AsyncRead for DelegatedReader {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
if let Some((v, s)) = self.readbuf.take_data() {
return self.readbuf.put_data(buf, v, s);
}
match self.receiver.poll_recv(cx) {
Poll::Ready(Some(DelegatedHttpEvent::Body(msg))) => self.readbuf.put_data(buf, msg, 0),
Poll::Ready(Some(_)) => Poll::Ready(Ok(())), // EOF
Poll::Ready(None) => {
Poll::Ready(Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF")))
}
Poll::Pending => Poll::Pending,
}
}
}
/// Simple http implementation that falls back to delegated http if
/// making a direct reqwest fails.
pub struct FallbackSimpleHttp {
native: ReqwestSimpleHttp,
delegated: DelegatedSimpleHttp,
}
impl FallbackSimpleHttp {
pub fn new(native: ReqwestSimpleHttp, delegated: DelegatedSimpleHttp) -> Self {
FallbackSimpleHttp { native, delegated }
}
pub fn native(&self) -> ReqwestSimpleHttp {
self.native.clone()
}
pub fn delegated(&self) -> DelegatedSimpleHttp {
self.delegated.clone()
}
}
#[async_trait]
impl SimpleHttp for FallbackSimpleHttp {
async fn make_request(
&self,
method: &'static str,
url: String,
) -> Result<SimpleResponse, AnyError> {
let r1 = self.native.make_request(method, url.clone()).await;
if let Ok(res) = r1 {
if !res.status_code.is_server_error() {
return Ok(res);
}
}
self.delegated.make_request(method, url).await
}
}

69
cli/src/util/input.rs Normal file
View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use crate::util::errors::wrap;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use indicatif::ProgressBar;
use std::fmt::Display;
use super::{errors::WrappedError, io::ReportCopyProgress};
/// Wrapper around indicatif::ProgressBar that implements ReportCopyProgress.
pub struct ProgressBarReporter {
bar: ProgressBar,
has_set_total: bool,
}
impl From<ProgressBar> for ProgressBarReporter {
fn from(bar: ProgressBar) -> Self {
ProgressBarReporter {
bar,
has_set_total: false,
}
}
}
impl ReportCopyProgress for ProgressBarReporter {
fn report_progress(&mut self, bytes_so_far: u64, total_bytes: u64) {
if !self.has_set_total {
self.bar.set_length(total_bytes);
}
if bytes_so_far == total_bytes {
self.bar.finish_and_clear();
} else {
self.bar.set_position(bytes_so_far);
}
}
}
pub fn prompt_yn(text: &str) -> Result<bool, WrappedError> {
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(text)
.default(true)
.interact()
.map_err(|e| wrap(e, "Failed to read confirm input"))
}
pub fn prompt_options<T>(text: impl Into<String>, options: &[T]) -> Result<T, WrappedError>
where
T: Display + Copy,
{
let chosen = Select::with_theme(&ColorfulTheme::default())
.with_prompt(text)
.items(options)
.default(0)
.interact()
.map_err(|e| wrap(e, "Failed to read select input"))?;
Ok(options[chosen])
}
pub fn prompt_placeholder(question: &str, placeholder: &str) -> Result<String, WrappedError> {
Input::with_theme(&ColorfulTheme::default())
.with_prompt(question)
.default(placeholder.to_string())
.interact_text()
.map_err(|e| wrap(e, "Failed to read confirm input"))
}

355
cli/src/util/io.rs Normal file
View File

@@ -0,0 +1,355 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use std::{
fs::File,
io::{self, BufRead, Seek},
task::Poll,
time::Duration,
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
sync::mpsc,
time::sleep,
};
use super::ring_buffer::RingBuffer;
pub trait ReportCopyProgress {
fn report_progress(&mut self, bytes_so_far: u64, total_bytes: u64);
}
/// Type that doesn't emit anything for download progress.
pub struct SilentCopyProgress();
impl ReportCopyProgress for SilentCopyProgress {
fn report_progress(&mut self, _bytes_so_far: u64, _total_bytes: u64) {}
}
/// Copies from the reader to the writer, reporting progress to the provided
/// reporter every so often.
pub async fn copy_async_progress<T, R, W>(
mut reporter: T,
reader: &mut R,
writer: &mut W,
total_bytes: u64,
) -> io::Result<u64>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
T: ReportCopyProgress,
{
let mut buf = vec![0; 8 * 1024];
let mut bytes_so_far = 0;
let mut bytes_last_reported = 0;
let report_granularity = std::cmp::min(total_bytes / 10, 2 * 1024 * 1024);
reporter.report_progress(0, total_bytes);
loop {
let read_buf = match reader.read(&mut buf).await {
Ok(0) => break,
Ok(n) => &buf[..n],
Err(e) => return Err(e),
};
writer.write_all(read_buf).await?;
bytes_so_far += read_buf.len() as u64;
if bytes_so_far - bytes_last_reported > report_granularity {
bytes_last_reported = bytes_so_far;
reporter.report_progress(bytes_so_far, total_bytes);
}
}
reporter.report_progress(bytes_so_far, total_bytes);
Ok(bytes_so_far)
}
/// Helper used when converting Future interfaces to poll-based interfaces.
/// Stores excess data that can be reused on future polls.
#[derive(Default)]
pub(crate) struct ReadBuffer(Option<(Vec<u8>, usize)>);
impl ReadBuffer {
/// Removes any data stored in the read buffer
pub fn take_data(&mut self) -> Option<(Vec<u8>, usize)> {
self.0.take()
}
/// Writes as many bytes as possible to the readbuf, stashing any extra.
pub fn put_data(
&mut self,
target: &mut tokio::io::ReadBuf<'_>,
bytes: Vec<u8>,
start: usize,
) -> Poll<std::io::Result<()>> {
if bytes.is_empty() {
self.0 = None;
// should not return Ok(), since if nothing is written to the target
// it signals EOF. Instead wait for more data from the source.
return Poll::Pending;
}
if target.remaining() >= bytes.len() - start {
target.put_slice(&bytes[start..]);
self.0 = None;
} else {
let end = start + target.remaining();
target.put_slice(&bytes[start..end]);
self.0 = Some((bytes, end));
}
Poll::Ready(Ok(()))
}
}
#[derive(Debug)]
pub enum TailEvent {
/// A new line was read from the file. The line includes its trailing newline character.
Line(String),
/// The file appears to have been rewritten (size shrunk)
Reset,
/// An error was encountered with the file.
Err(io::Error),
}
/// Simple, naive implementation of `tail -f -n <n> <path>`. Uses polling, so
/// it's not the fastest, but simple and working for easy cases.
pub fn tailf(file: File, n: usize) -> mpsc::UnboundedReceiver<TailEvent> {
let (tx, rx) = mpsc::unbounded_channel();
let mut last_len = match file.metadata() {
Ok(m) => m.len(),
Err(e) => {
tx.send(TailEvent::Err(e)).ok();
return rx;
}
};
let mut reader = io::BufReader::new(file);
let mut pos = 0;
// Read the initial "n" lines back from the request. initial_lines
// is a small ring buffer.
let mut initial_lines = RingBuffer::new(n);
loop {
let mut line = String::new();
let bytes_read = match reader.read_line(&mut line) {
Ok(0) => break,
Ok(n) => n,
Err(e) => {
tx.send(TailEvent::Err(e)).ok();
return rx;
}
};
if !line.ends_with('\n') {
// EOF
break;
}
pos += bytes_read as u64;
initial_lines.push(line);
}
for line in initial_lines.into_iter() {
tx.send(TailEvent::Line(line)).ok();
}
// now spawn the poll process to keep reading new lines
tokio::spawn(async move {
let poll_interval = Duration::from_millis(500);
loop {
tokio::select! {
_ = sleep(poll_interval) => {},
_ = tx.closed() => return
}
match reader.get_ref().metadata() {
Err(e) => {
tx.send(TailEvent::Err(e)).ok();
return;
}
Ok(m) => {
if m.len() == last_len {
continue;
}
if m.len() < last_len {
tx.send(TailEvent::Reset).ok();
pos = 0;
}
last_len = m.len();
}
}
if let Err(e) = reader.seek(io::SeekFrom::Start(pos)) {
tx.send(TailEvent::Err(e)).ok();
return;
}
loop {
let mut line = String::new();
let n = match reader.read_line(&mut line) {
Ok(0) => break,
Ok(n) => n,
Err(e) => {
tx.send(TailEvent::Err(e)).ok();
return;
}
};
if n == 0 || !line.ends_with('\n') {
break;
}
pos += n as u64;
if tx.send(TailEvent::Line(line)).is_err() {
return;
}
}
}
});
rx
}
#[cfg(test)]
mod tests {
use rand::Rng;
use std::{fs::OpenOptions, io::Write};
use super::*;
#[tokio::test]
async fn test_tailf_empty() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("tmp");
let read_file = OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(&file_path)
.unwrap();
let mut rx = tailf(read_file, 32);
assert!(rx.try_recv().is_err());
let mut append_file = OpenOptions::new()
.write(true)
.append(true)
.open(&file_path)
.unwrap();
writeln!(&mut append_file, "some line").unwrap();
let recv = rx.recv().await;
if let Some(TailEvent::Line(l)) = recv {
assert_eq!("some line\n".to_string(), l);
} else {
unreachable!("expect a line event, got {:?}", recv)
}
write!(&mut append_file, "partial ").unwrap();
writeln!(&mut append_file, "line").unwrap();
let recv = rx.recv().await;
if let Some(TailEvent::Line(l)) = recv {
assert_eq!("partial line\n".to_string(), l);
} else {
unreachable!("expect a line event, got {:?}", recv)
}
}
#[tokio::test]
async fn test_tailf_resets() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("tmp");
let mut read_file = OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(&file_path)
.unwrap();
writeln!(&mut read_file, "some existing content").unwrap();
let mut rx = tailf(read_file, 0);
assert!(rx.try_recv().is_err());
let mut append_file = File::create(&file_path).unwrap(); // truncates
writeln!(&mut append_file, "some line").unwrap();
let recv = rx.recv().await;
if let Some(TailEvent::Reset) = recv {
// ok
} else {
unreachable!("expect a reset event, got {:?}", recv)
}
let recv = rx.recv().await;
if let Some(TailEvent::Line(l)) = recv {
assert_eq!("some line\n".to_string(), l);
} else {
unreachable!("expect a line event, got {:?}", recv)
}
}
#[tokio::test]
async fn test_tailf_with_data() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("tmp");
let mut read_file = OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(&file_path)
.unwrap();
let mut rng = rand::thread_rng();
let mut written = vec![];
let base_line = "Elit ipsum cillum ex cillum. Adipisicing consequat cupidatat do proident ut in sunt Lorem ipsum tempor. Eiusmod ipsum Lorem labore exercitation sunt pariatur excepteur fugiat cillum velit cillum enim. Nisi Lorem cupidatat ad enim velit officia eiusmod esse tempor aliquip. Deserunt pariatur tempor in duis culpa esse sit nulla irure ullamco ipsum voluptate non laboris. Occaecat officia nulla officia mollit do aliquip reprehenderit ad incididunt.";
for i in 0..100 {
let line = format!("{}: {}", i, &base_line[..rng.gen_range(0..base_line.len())]);
writeln!(&mut read_file, "{}", line).unwrap();
written.push(line);
}
write!(&mut read_file, "partial line").unwrap();
read_file.seek(io::SeekFrom::Start(0)).unwrap();
let last_n = 32;
let mut rx = tailf(read_file, last_n);
for i in 0..last_n {
let recv = rx.try_recv().unwrap();
if let TailEvent::Line(l) = recv {
let mut expected = written[written.len() - last_n + i].to_string();
expected.push('\n');
assert_eq!(expected, l);
} else {
unreachable!("expect a line event, got {:?}", recv)
}
}
assert!(rx.try_recv().is_err());
let mut append_file = OpenOptions::new()
.write(true)
.append(true)
.open(&file_path)
.unwrap();
writeln!(append_file, " is now complete").unwrap();
let recv = rx.recv().await;
if let Some(TailEvent::Line(l)) = recv {
assert_eq!("partial line is now complete\n".to_string(), l);
} else {
unreachable!("expect a line event, got {:?}", recv)
}
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use std::{env, io};
/// Gets whether the current CLI seems like it's running in integrated mode,
/// by looking at the location of the exe and known VS Code files.
pub fn is_integrated_cli() -> io::Result<bool> {
let exe = env::current_exe()?;
let parent = match exe.parent() {
Some(parent) if parent.file_name().and_then(|n| n.to_str()) == Some("bin") => parent,
_ => return Ok(false),
};
let parent = match parent.parent() {
Some(p) => p,
None => return Ok(false),
};
let expected_file = if cfg!(target_os = "macos") {
"node_modules.asar"
} else {
"resources.pak"
};
Ok(parent.join(expected_file).exists())
}

61
cli/src/util/machine.rs Normal file
View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use std::{path::Path, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
pub fn process_at_path_exists(pid: u32, name: &Path) -> bool {
let mut sys = System::new();
let pid = Pid::from_u32(pid);
if !sys.refresh_process(pid) {
return false;
}
let name_str = format!("{}", name.display());
if let Some(process) = sys.process(pid) {
for cmd in process.cmd() {
if cmd.contains(&name_str) {
return true;
}
}
}
false
}
pub fn process_exists(pid: u32) -> bool {
let mut sys = System::new();
sys.refresh_process(Pid::from_u32(pid))
}
pub async fn wait_until_process_exits(pid: Pid, poll_ms: u64) {
let mut s = System::new();
let duration = Duration::from_millis(poll_ms);
while s.refresh_process(pid) {
tokio::time::sleep(duration).await;
}
}
pub fn find_running_process(name: &Path) -> Option<u32> {
let mut sys = System::new();
sys.refresh_processes();
let name_str = format!("{}", name.display());
for (pid, process) in sys.processes() {
for cmd in process.cmd() {
if cmd.contains(&name_str) {
return Some(pid.as_u32());
}
}
}
None
}
pub async fn wait_until_exe_deleted(current_exe: &Path, poll_ms: u64) {
let duration = Duration::from_millis(poll_ms);
while current_exe.exists() {
tokio::time::sleep(duration).await;
}
}

39
cli/src/util/os.rs Normal file
View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
#[cfg(windows)]
pub fn os_release() -> Result<String, std::io::Error> {
// The windows API *had* nice GetVersionEx/A APIs, but these were deprecated
// in Winodws 8 and there's no newer win API to get version numbers. So
// instead read the registry.
use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey};
let key = RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")?;
let major: u32 = key.get_value("CurrentMajorVersionNumber")?;
let minor: u32 = key.get_value("CurrentMinorVersionNumber")?;
let build: String = key.get_value("CurrentBuild")?;
Ok(format!("{}.{}.{}", major, minor, build))
}
#[cfg(unix)]
pub fn os_release() -> Result<String, std::io::Error> {
use std::{ffi::CStr, mem};
unsafe {
let mut ret = mem::MaybeUninit::zeroed();
if libc::uname(ret.as_mut_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
let ret = ret.assume_init();
let c_str: &CStr = CStr::from_ptr(ret.release.as_ptr());
Ok(c_str.to_string_lossy().into_owned())
}
}

349
cli/src/util/prereqs.rs Normal file
View File

@@ -0,0 +1,349 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use std::cmp::Ordering;
use super::command::capture_command;
use crate::constants::QUALITYLESS_SERVER_NAME;
use crate::update_service::Platform;
use lazy_static::lazy_static;
use regex::bytes::Regex as BinRegex;
use regex::Regex;
use tokio::fs;
use super::errors::CodeError;
lazy_static! {
static ref LDCONFIG_STDC_RE: Regex = Regex::new(r"libstdc\+\+.* => (.+)").unwrap();
static ref LDD_VERSION_RE: BinRegex = BinRegex::new(r"^ldd.*(.+)\.(.+)\s").unwrap();
static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap();
static ref LIBSTD_CXX_VERSION_RE: BinRegex =
BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap();
static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 18);
static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0);
}
const NIXOS_TEST_PATH: &str = "/etc/NIXOS";
pub struct PreReqChecker {}
impl Default for PreReqChecker {
fn default() -> Self {
Self::new()
}
}
impl PreReqChecker {
pub fn new() -> PreReqChecker {
PreReqChecker {}
}
#[cfg(not(target_os = "linux"))]
pub async fn verify(&self) -> Result<Platform, CodeError> {
Platform::env_default().ok_or_else(|| {
CodeError::UnsupportedPlatform(format!(
"{} {}",
std::env::consts::OS,
std::env::consts::ARCH
))
})
}
#[cfg(target_os = "linux")]
pub async fn verify(&self) -> Result<Platform, CodeError> {
let (is_nixos, gnu_a, gnu_b, or_musl) = tokio::join!(
check_is_nixos(),
check_glibc_version(),
check_glibcxx_version(),
check_musl_interpreter()
);
if (gnu_a.is_ok() && gnu_b.is_ok()) || is_nixos {
return Ok(if cfg!(target_arch = "x86_64") {
Platform::LinuxX64
} else if cfg!(target_arch = "arm") {
Platform::LinuxARM32
} else {
Platform::LinuxARM64
});
}
if or_musl.is_ok() {
return Ok(if cfg!(target_arch = "x86_64") {
Platform::LinuxAlpineX64
} else {
Platform::LinuxAlpineARM64
});
}
let mut errors: Vec<String> = vec![];
if let Err(e) = gnu_a {
errors.push(e);
} else if let Err(e) = gnu_b {
errors.push(e);
}
if let Err(e) = or_musl {
errors.push(e);
}
let bullets = errors
.iter()
.map(|e| format!(" - {}", e))
.collect::<Vec<String>>()
.join("\n");
Err(CodeError::PrerequisitesFailed {
bullets,
name: QUALITYLESS_SERVER_NAME,
})
}
}
#[allow(dead_code)]
async fn check_musl_interpreter() -> Result<(), String> {
const MUSL_PATH: &str = if cfg!(target_arch = "aarch64") {
"/lib/ld-musl-aarch64.so.1"
} else {
"/lib/ld-musl-x86_64.so.1"
};
if fs::metadata(MUSL_PATH).await.is_err() {
return Err(format!(
"find {}, which is required to run the {} in musl environments",
MUSL_PATH, QUALITYLESS_SERVER_NAME
));
}
Ok(())
}
#[allow(dead_code)]
async fn check_glibc_version() -> Result<(), String> {
#[cfg(target_env = "gnu")]
let version = {
let v = unsafe { libc::gnu_get_libc_version() };
let v = unsafe { std::ffi::CStr::from_ptr(v) };
let v = v.to_str().unwrap();
extract_generic_version(v)
};
#[cfg(not(target_env = "gnu"))]
let version = {
capture_command("ldd", ["--version"])
.await
.ok()
.and_then(|o| extract_ldd_version(&o.stdout))
};
if let Some(v) = version {
return if v >= *MIN_LDD_VERSION {
Ok(())
} else {
Err(format!(
"find GLIBC >= 2.17 (but found {} instead) for GNU environments",
v
))
};
}
Ok(())
}
/// Check for nixos to avoid mandating glibc versions. See:
/// https://github.com/microsoft/vscode-remote-release/issues/7129
#[allow(dead_code)]
async fn check_is_nixos() -> bool {
fs::metadata(NIXOS_TEST_PATH).await.is_ok()
}
#[allow(dead_code)]
async fn check_glibcxx_version() -> Result<(), String> {
let mut libstdc_path: Option<String> = None;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
const DEFAULT_LIB_PATH: &str = "/usr/lib64/libstdc++.so.6";
#[cfg(any(target_arch = "x86", target_arch = "arm"))]
const DEFAULT_LIB_PATH: &str = "/usr/lib/libstdc++.so.6";
const LDCONFIG_PATH: &str = "/sbin/ldconfig";
if fs::metadata(DEFAULT_LIB_PATH).await.is_ok() {
libstdc_path = Some(DEFAULT_LIB_PATH.to_owned());
} else if fs::metadata(LDCONFIG_PATH).await.is_ok() {
libstdc_path = capture_command(LDCONFIG_PATH, ["-p"])
.await
.ok()
.and_then(|o| extract_libstd_from_ldconfig(&o.stdout));
}
match libstdc_path {
Some(path) => match fs::read(&path).await {
Ok(contents) => check_for_sufficient_glibcxx_versions(contents),
Err(e) => Err(format!(
"validate GLIBCXX version for GNU environments, but could not: {}",
e
)),
},
None => Err("find libstdc++.so or ldconfig for GNU environments".to_owned()),
}
}
#[allow(dead_code)]
fn check_for_sufficient_glibcxx_versions(contents: Vec<u8>) -> Result<(), String> {
let all_versions: Vec<SimpleSemver> = LIBSTD_CXX_VERSION_RE
.captures_iter(&contents)
.map(|m| SimpleSemver {
major: m.get(1).map_or(0, |s| u32_from_bytes(s.as_bytes())),
minor: m.get(2).map_or(0, |s| u32_from_bytes(s.as_bytes())),
patch: m.get(3).map_or(0, |s| u32_from_bytes(s.as_bytes())),
})
.collect();
if !all_versions.iter().any(|v| &*MIN_CXX_VERSION >= v) {
return Err(format!(
"find GLIBCXX >= 3.4.18 (but found {} instead) for GNU environments",
all_versions
.iter()
.map(String::from)
.collect::<Vec<String>>()
.join(", ")
));
}
Ok(())
}
#[allow(dead_code)]
fn extract_ldd_version(output: &[u8]) -> Option<SimpleSemver> {
LDD_VERSION_RE.captures(output).map(|m| SimpleSemver {
major: m.get(1).map_or(0, |s| u32_from_bytes(s.as_bytes())),
minor: m.get(2).map_or(0, |s| u32_from_bytes(s.as_bytes())),
patch: 0,
})
}
#[allow(dead_code)]
fn extract_generic_version(output: &str) -> Option<SimpleSemver> {
GENERIC_VERSION_RE.captures(output).map(|m| SimpleSemver {
major: m.get(1).map_or(0, |s| s.as_str().parse().unwrap()),
minor: m.get(2).map_or(0, |s| s.as_str().parse().unwrap()),
patch: 0,
})
}
fn extract_libstd_from_ldconfig(output: &[u8]) -> Option<String> {
String::from_utf8_lossy(output)
.lines()
.find_map(|l| LDCONFIG_STDC_RE.captures(l))
.and_then(|cap| cap.get(1))
.map(|cap| cap.as_str().to_owned())
}
fn u32_from_bytes(b: &[u8]) -> u32 {
String::from_utf8_lossy(b).parse::<u32>().unwrap_or(0)
}
#[derive(Debug, Default, PartialEq, Eq)]
struct SimpleSemver {
major: u32,
minor: u32,
patch: u32,
}
impl PartialOrd for SimpleSemver {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SimpleSemver {
fn cmp(&self, other: &Self) -> Ordering {
let major = self.major.cmp(&other.major);
if major != Ordering::Equal {
return major;
}
let minor = self.minor.cmp(&other.minor);
if minor != Ordering::Equal {
return minor;
}
self.patch.cmp(&other.patch)
}
}
impl From<&SimpleSemver> for String {
fn from(s: &SimpleSemver) -> Self {
format!("v{}.{}.{}", s.major, s.minor, s.patch)
}
}
impl std::fmt::Display for SimpleSemver {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", String::from(self))
}
}
#[allow(dead_code)]
impl SimpleSemver {
fn new(major: u32, minor: u32, patch: u32) -> SimpleSemver {
SimpleSemver {
major,
minor,
patch,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_libstd_from_ldconfig() {
let actual = "
libstoken.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstoken.so.1
libstemmer.so.0d (libc6,x86-64) => /lib/x86_64-linux-gnu/libstemmer.so.0d
libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6
libstartup-notification-1.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstartup-notification-1.so.0
libssl3.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libssl3.so
".to_owned().into_bytes();
assert_eq!(
extract_libstd_from_ldconfig(&actual),
Some("/lib/x86_64-linux-gnu/libstdc++.so.6".to_owned()),
);
assert_eq!(
extract_libstd_from_ldconfig(&"nothing here!".to_owned().into_bytes()),
None,
);
}
#[test]
fn test_gte() {
assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(1, 2, 3));
assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(0, 10, 10));
assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(1, 1, 10));
assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(1, 2, 10));
assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(1, 3, 1));
assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(2, 2, 1));
}
#[test]
fn check_for_sufficient_glibcxx_versions() {
let actual = "ldd (Ubuntu GLIBC 2.31-0ubuntu9.7) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper."
.to_owned()
.into_bytes();
assert_eq!(
extract_ldd_version(&actual),
Some(SimpleSemver::new(2, 31, 0)),
);
}
}

142
cli/src/util/ring_buffer.rs Normal file
View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
pub struct RingBuffer<T> {
data: Vec<T>,
i: usize,
}
impl<T> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
Self {
data: Vec::with_capacity(capacity),
i: 0,
}
}
pub fn capacity(&self) -> usize {
self.data.capacity()
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_full(&self) -> bool {
self.data.len() == self.data.capacity()
}
pub fn is_empty(&self) -> bool {
self.data.len() == 0
}
pub fn push(&mut self, value: T) {
if self.data.len() == self.data.capacity() {
self.data[self.i] = value;
} else {
self.data.push(value);
}
self.i = (self.i + 1) % self.data.capacity();
}
pub fn iter(&self) -> RingBufferIter<'_, T> {
RingBufferIter {
index: 0,
buffer: self,
}
}
}
impl<T: Default> IntoIterator for RingBuffer<T> {
type Item = T;
type IntoIter = OwnedRingBufferIter<T>;
fn into_iter(self) -> OwnedRingBufferIter<T>
where
T: Default,
{
OwnedRingBufferIter {
index: 0,
buffer: self,
}
}
}
pub struct OwnedRingBufferIter<T: Default> {
buffer: RingBuffer<T>,
index: usize,
}
impl<T: Default> Iterator for OwnedRingBufferIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
if self.index == self.buffer.len() {
return None;
}
let ii = (self.index + self.buffer.i) % self.buffer.len();
let item = std::mem::take(&mut self.buffer.data[ii]);
self.index += 1;
Some(item)
}
}
pub struct RingBufferIter<'a, T> {
buffer: &'a RingBuffer<T>,
index: usize,
}
impl<'a, T> Iterator for RingBufferIter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if self.index == self.buffer.len() {
return None;
}
let ii = (self.index + self.buffer.i) % self.buffer.len();
let item = &self.buffer.data[ii];
self.index += 1;
Some(item)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inserts() {
let mut rb = RingBuffer::new(3);
assert_eq!(rb.capacity(), 3);
assert!(!rb.is_full());
assert_eq!(rb.len(), 0);
assert_eq!(rb.iter().copied().count(), 0);
rb.push(1);
assert!(!rb.is_full());
assert_eq!(rb.len(), 1);
assert_eq!(rb.iter().copied().collect::<Vec<i32>>(), vec![1]);
rb.push(2);
assert!(!rb.is_full());
assert_eq!(rb.len(), 2);
assert_eq!(rb.iter().copied().collect::<Vec<i32>>(), vec![1, 2]);
rb.push(3);
assert!(rb.is_full());
assert_eq!(rb.len(), 3);
assert_eq!(rb.iter().copied().collect::<Vec<i32>>(), vec![1, 2, 3]);
rb.push(4);
assert!(rb.is_full());
assert_eq!(rb.len(), 3);
assert_eq!(rb.iter().copied().collect::<Vec<i32>>(), vec![2, 3, 4]);
assert_eq!(rb.into_iter().collect::<Vec<i32>>(), vec![2, 3, 4]);
}
}

221
cli/src/util/sync.rs Normal file
View File

@@ -0,0 +1,221 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use async_trait::async_trait;
use std::{marker::PhantomData, sync::Arc};
use tokio::sync::{
broadcast, mpsc,
watch::{self, error::RecvError},
};
#[derive(Clone)]
pub struct Barrier<T>(watch::Receiver<Option<T>>)
where
T: Clone;
impl<T> Barrier<T>
where
T: Clone,
{
/// Waits for the barrier to be closed, returning a value if one was sent.
pub async fn wait(&mut self) -> Result<T, RecvError> {
loop {
self.0.changed().await?;
if let Some(v) = self.0.borrow().clone() {
return Ok(v);
}
}
}
/// Gets whether the barrier is currently open
pub fn is_open(&self) -> bool {
self.0.borrow().is_some()
}
}
#[async_trait]
impl<T: Clone + Send + Sync> Receivable<T> for Barrier<T> {
async fn recv_msg(&mut self) -> Option<T> {
self.wait().await.ok()
}
}
#[derive(Clone)]
pub struct BarrierOpener<T: Clone>(Arc<watch::Sender<Option<T>>>);
impl<T: Clone> BarrierOpener<T> {
/// Opens the barrier.
pub fn open(&self, value: T) {
self.0.send_if_modified(|v| {
if v.is_none() {
*v = Some(value);
true
} else {
false
}
});
}
}
/// The Barrier is something that can be opened once from one side,
/// and is thereafter permanently closed. It can contain a value.
pub fn new_barrier<T>() -> (Barrier<T>, BarrierOpener<T>)
where
T: Copy,
{
let (closed_tx, closed_rx) = watch::channel(None);
(Barrier(closed_rx), BarrierOpener(Arc::new(closed_tx)))
}
/// Type that can receive messages in an async way.
#[async_trait]
pub trait Receivable<T> {
async fn recv_msg(&mut self) -> Option<T>;
}
// todo: ideally we would use an Arc in the broadcast::Receiver to avoid having
// to clone bytes everywhere, requires updating rpc consumers as well.
#[async_trait]
impl<T: Clone + Send> Receivable<T> for broadcast::Receiver<T> {
async fn recv_msg(&mut self) -> Option<T> {
loop {
match self.recv().await {
Ok(v) => return Some(v),
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => return None,
}
}
}
}
#[async_trait]
impl<T: Send> Receivable<T> for mpsc::UnboundedReceiver<T> {
async fn recv_msg(&mut self) -> Option<T> {
self.recv().await
}
}
#[async_trait]
impl<T: Send> Receivable<T> for () {
async fn recv_msg(&mut self) -> Option<T> {
futures::future::pending().await
}
}
pub struct ConcatReceivable<T: Send, A: Receivable<T>, B: Receivable<T>> {
left: Option<A>,
right: B,
_marker: PhantomData<T>,
}
impl<T: Send, A: Receivable<T>, B: Receivable<T>> ConcatReceivable<T, A, B> {
pub fn new(left: A, right: B) -> Self {
Self {
left: Some(left),
right,
_marker: PhantomData,
}
}
}
#[async_trait]
impl<T: Send, A: Send + Receivable<T>, B: Send + Receivable<T>> Receivable<T>
for ConcatReceivable<T, A, B>
{
async fn recv_msg(&mut self) -> Option<T> {
if let Some(left) = &mut self.left {
match left.recv_msg().await {
Some(v) => return Some(v),
None => {
self.left = None;
}
}
}
return self.right.recv_msg().await;
}
}
pub struct MergedReceivable<T: Send, A: Receivable<T>, B: Receivable<T>> {
left: Option<A>,
right: Option<B>,
_marker: PhantomData<T>,
}
impl<T: Send, A: Receivable<T>, B: Receivable<T>> MergedReceivable<T, A, B> {
pub fn new(left: A, right: B) -> Self {
Self {
left: Some(left),
right: Some(right),
_marker: PhantomData,
}
}
}
#[async_trait]
impl<T: Send, A: Send + Receivable<T>, B: Send + Receivable<T>> Receivable<T>
for MergedReceivable<T, A, B>
{
async fn recv_msg(&mut self) -> Option<T> {
loop {
match (&mut self.left, &mut self.right) {
(Some(left), Some(right)) => {
tokio::select! {
left = left.recv_msg() => match left {
Some(v) => return Some(v),
None => { self.left = None; continue; },
},
right = right.recv_msg() => match right {
Some(v) => return Some(v),
None => { self.right = None; continue; },
},
}
}
(Some(a), None) => break a.recv_msg().await,
(None, Some(b)) => break b.recv_msg().await,
(None, None) => break None,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_barrier_close_after_spawn() {
let (mut barrier, opener) = new_barrier::<u32>();
let (tx, rx) = tokio::sync::oneshot::channel::<u32>();
tokio::spawn(async move {
tx.send(barrier.wait().await.unwrap()).unwrap();
});
opener.open(42);
assert!(rx.await.unwrap() == 42);
}
#[tokio::test]
async fn test_barrier_close_before_spawn() {
let (barrier, opener) = new_barrier::<u32>();
let (tx1, rx1) = tokio::sync::oneshot::channel::<u32>();
let (tx2, rx2) = tokio::sync::oneshot::channel::<u32>();
opener.open(42);
let mut b1 = barrier.clone();
tokio::spawn(async move {
tx1.send(b1.wait().await.unwrap()).unwrap();
});
let mut b2 = barrier.clone();
tokio::spawn(async move {
tx2.send(b2.wait().await.unwrap()).unwrap();
});
assert!(rx1.await.unwrap() == 42);
assert!(rx2.await.unwrap() == 42);
}
}

105
cli/src/util/tar.rs Normal file
View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use crate::util::errors::{wrap, WrappedError};
use flate2::read::GzDecoder;
use std::fs;
use std::io::{Seek, SeekFrom};
use std::path::{Path, PathBuf};
use tar::Archive;
use super::io::ReportCopyProgress;
fn should_skip_first_segment(file: &fs::File) -> Result<bool, WrappedError> {
// unfortunately, we need to re-read the archive here since you cannot reuse
// `.entries()`. But this will generally only look at one or two files, so this
// should be acceptably speedy... If not, we could hardcode behavior for
// different types of archives.
let tar = GzDecoder::new(file);
let mut archive = Archive::new(tar);
let mut entries = archive
.entries()
.map_err(|e| wrap(e, "error opening archive"))?;
let first_name = {
let file = entries
.next()
.expect("expected not to have an empty archive")
.map_err(|e| wrap(e, "error reading entry file"))?;
let path = file.path().expect("expected to have path");
path.iter()
.next()
.expect("expected to have non-empty name")
.to_owned()
};
let mut had_multiple = false;
for file in entries.flatten() {
had_multiple = true;
if let Ok(name) = file.path() {
if name.iter().next() != Some(&first_name) {
return Ok(false);
}
}
}
Ok(had_multiple) // prefix removal is invalid if there's only a single file
}
pub fn decompress_tarball<T>(
path: &Path,
parent_path: &Path,
mut reporter: T,
) -> Result<(), WrappedError>
where
T: ReportCopyProgress,
{
let mut tar_gz = fs::File::open(path)
.map_err(|e| wrap(e, format!("error opening file {}", path.display())))?;
let skip_first = should_skip_first_segment(&tar_gz)?;
// reset since skip logic read the tar already:
tar_gz
.seek(SeekFrom::Start(0))
.map_err(|e| wrap(e, "error resetting seek position"))?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
let results = archive
.entries()
.map_err(|e| wrap(e, format!("error opening archive {}", path.display())))?
.filter_map(|e| e.ok())
.map(|mut entry| {
let entry_path = entry
.path()
.map_err(|e| wrap(e, "error reading entry path"))?;
let path = parent_path.join(if skip_first {
entry_path.iter().skip(1).collect::<PathBuf>()
} else {
entry_path.into_owned()
});
if let Some(p) = path.parent() {
fs::create_dir_all(p)
.map_err(|e| wrap(e, format!("could not create dir for {}", p.display())))?;
}
entry
.unpack(&path)
.map_err(|e| wrap(e, format!("error unpacking {}", path.display())))?;
Ok(path)
})
.collect::<Result<Vec<PathBuf>, WrappedError>>()?;
// Tarballs don't have a way to get the number of entries ahead of time
reporter.report_progress(results.len() as u64, results.len() as u64);
Ok(())
}

150
cli/src/util/zipper.rs Normal file
View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use super::errors::{wrap, WrappedError};
use super::io::ReportCopyProgress;
use std::fs::{self, File};
use std::io;
use std::path::Path;
use std::path::PathBuf;
use zip::read::ZipFile;
use zip::{self, ZipArchive};
// Borrowed and modified from https://github.com/zip-rs/zip/blob/master/examples/extract.rs
/// Returns whether all files in the archive start with the same path segment.
/// If so, it's an indication we should skip that segment when extracting.
fn should_skip_first_segment(archive: &mut ZipArchive<File>) -> bool {
let first_name = {
let file = archive
.by_index_raw(0)
.expect("expected not to have an empty archive");
let path = file
.enclosed_name()
.expect("expected to have path")
.iter()
.next()
.expect("expected to have non-empty name");
path.to_owned()
};
for i in 1..archive.len() {
if let Ok(file) = archive.by_index_raw(i) {
if let Some(name) = file.enclosed_name() {
if name.iter().next() != Some(&first_name) {
return false;
}
}
}
}
archive.len() > 1 // prefix removal is invalid if there's only a single file
}
pub fn unzip_file<T>(path: &Path, parent_path: &Path, mut reporter: T) -> Result<(), WrappedError>
where
T: ReportCopyProgress,
{
let file = fs::File::open(path)
.map_err(|e| wrap(e, format!("unable to open file {}", path.display())))?;
let mut archive = zip::ZipArchive::new(file)
.map_err(|e| wrap(e, format!("failed to open zip archive {}", path.display())))?;
let skip_segments_no = usize::from(should_skip_first_segment(&mut archive));
for i in 0..archive.len() {
reporter.report_progress(i as u64, archive.len() as u64);
let mut file = archive
.by_index(i)
.map_err(|e| wrap(e, format!("could not open zip entry {}", i)))?;
let outpath: PathBuf = match file.enclosed_name() {
Some(path) => {
let mut full_path = PathBuf::from(parent_path);
full_path.push(PathBuf::from_iter(path.iter().skip(skip_segments_no)));
full_path
}
None => continue,
};
if file.is_dir() || file.name().ends_with('/') {
fs::create_dir_all(&outpath)
.map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?;
apply_permissions(&file, &outpath)?;
continue;
}
if let Some(p) = outpath.parent() {
fs::create_dir_all(p)
.map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?;
}
#[cfg(unix)]
{
use libc::S_IFLNK;
use std::io::Read;
use std::os::unix::ffi::OsStringExt;
if matches!(file.unix_mode(), Some(mode) if mode & (S_IFLNK as u32) == (S_IFLNK as u32))
{
let mut link_to = Vec::new();
file.read_to_end(&mut link_to).map_err(|e| {
wrap(
e,
format!("could not read symlink linkpath {}", outpath.display()),
)
})?;
let link_path = PathBuf::from(std::ffi::OsString::from_vec(link_to));
std::os::unix::fs::symlink(link_path, &outpath).map_err(|e| {
wrap(e, format!("could not create symlink {}", outpath.display()))
})?;
continue;
}
}
let mut outfile = fs::File::create(&outpath).map_err(|e| {
wrap(
e,
format!(
"unable to open file to write {} (from {:?})",
outpath.display(),
file.enclosed_name().map(|p| p.to_string_lossy()),
),
)
})?;
io::copy(&mut file, &mut outfile)
.map_err(|e| wrap(e, format!("error copying file {}", outpath.display())))?;
apply_permissions(&file, &outpath)?;
}
reporter.report_progress(archive.len() as u64, archive.len() as u64);
Ok(())
}
#[cfg(unix)]
fn apply_permissions(file: &ZipFile, outpath: &Path) -> Result<(), WrappedError> {
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(outpath, fs::Permissions::from_mode(mode)).map_err(|e| {
wrap(
e,
format!("error setting permissions on {}", outpath.display()),
)
})?;
}
Ok(())
}
#[cfg(windows)]
fn apply_permissions(_file: &ZipFile, _outpath: &Path) -> Result<(), WrappedError> {
Ok(())
}