This commit is contained in:
Connor Peet
2022-10-14 15:39:10 -07:00
parent 84a929b8b7
commit 84439a87f6
15 changed files with 110 additions and 166 deletions

View File

@@ -1,123 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use clap::Parser;
use cli::{
commands::{args, tunnels, CommandContext},
constants, log as own_log,
state::LauncherPaths,
};
use opentelemetry::sdk::trace::TracerProvider as SdkTracerProvider;
use opentelemetry::trace::TracerProvider;
use log::{Level, Metadata, Record};
#[derive(Parser, Debug)]
#[clap(
long_about = None,
name = "Visual Studio Code Tunnels CLI",
version = match constants::VSCODE_CLI_VERSION { Some(v) => v, None => "dev" },
)]
pub struct TunnelCli {
#[clap(flatten, next_help_heading = Some("GLOBAL OPTIONS"))]
pub global_options: args::GlobalOptions,
#[clap(flatten, next_help_heading = Some("TUNNEL OPTIONS"))]
pub tunnel_options: args::TunnelArgs,
}
/// Entrypoint for a standalone "code-tunnel" subcommand. This is a temporary
/// artifact until we're ready to do swap to the full "code" CLI, and most
/// code in here is duplicated from `src/bin/code/main.rs`
#[tokio::main]
async fn main() -> Result<(), std::convert::Infallible> {
let parsed = TunnelCli::parse();
let context = CommandContext {
http: reqwest::Client::new(),
paths: LauncherPaths::new(&parsed.global_options.cli_data_dir).unwrap(),
log: own_log::Logger::new(
SdkTracerProvider::builder().build().tracer("codecli"),
if parsed.global_options.verbose {
own_log::Level::Trace
} else {
parsed.global_options.log.unwrap_or(own_log::Level::Info)
},
),
args: args::CliCore {
global_options: parsed.global_options,
subcommand: Some(args::Commands::Tunnel(parsed.tunnel_options.clone())),
..Default::default()
},
};
log::set_logger(Box::leak(Box::new(RustyLogger(context.log.clone()))))
.map(|()| log::set_max_level(log::LevelFilter::Debug))
.expect("expected to make logger");
let result = match parsed.tunnel_options.subcommand {
Some(args::TunnelSubcommand::Prune) => tunnels::prune(context).await,
Some(args::TunnelSubcommand::Unregister) => tunnels::unregister(context).await,
Some(args::TunnelSubcommand::Rename(rename_args)) => {
tunnels::rename(context, rename_args).await
}
Some(args::TunnelSubcommand::User(user_command)) => {
tunnels::user(context, user_command).await
}
Some(args::TunnelSubcommand::Service(service_args)) => {
tunnels::service(context, service_args).await
}
None => tunnels::serve(context, parsed.tunnel_options.serve_args).await,
};
match result {
Err(e) => print_and_exit(e),
Ok(code) => std::process::exit(code),
}
}
fn print_and_exit<E>(err: E) -> !
where
E: std::fmt::Display,
{
own_log::emit(own_log::Level::Error, "", &format!("{}", err));
std::process::exit(1);
}
/// Logger that uses the common rust "log" crate and directs back to one of
/// our managed loggers.
struct RustyLogger(own_log::Logger);
impl log::Log for RustyLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Debug
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
// exclude noisy log modules:
let src = match record.module_path() {
Some("russh::cipher") => return,
Some("russh::negotiation") => return,
Some(s) => s,
None => "<unknown>",
};
self.0.emit(
match record.level() {
log::Level::Debug => own_log::Level::Debug,
log::Level::Error => own_log::Level::Error,
log::Level::Info => own_log::Level::Info,
log::Level::Trace => own_log::Level::Trace,
log::Level::Warn => own_log::Level::Warn,
},
&format!("[{}] {}", src, record.args()),
);
}
fn flush(&self) {}
}

View File

@@ -9,7 +9,6 @@ pub const CONTROL_PORT: u16 = 31545;
pub const PROTOCOL_VERSION: u32 = 1;
pub const VSCODE_CLI_VERSION: Option<&'static str> = option_env!("VSCODE_CLI_VERSION");
pub const VSCODE_CLI_ASSET_NAME: Option<&'static str> = option_env!("VSCODE_CLI_ASSET_NAME");
pub const VSCODE_CLI_AI_KEY: Option<&'static str> = option_env!("VSCODE_CLI_AI_KEY");
pub const VSCODE_CLI_AI_ENDPOINT: Option<&'static str> = option_env!("VSCODE_CLI_AI_ENDPOINT");
pub const VSCODE_CLI_QUALITY: Option<&'static str> = option_env!("VSCODE_CLI_QUALITY");

View File

@@ -3,20 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
use std::{
fs::{rename, set_permissions},
path::Path,
};
use std::{fs, path::Path, process::Command};
use tempfile::tempdir;
use crate::{
constants::{VSCODE_CLI_COMMIT, VSCODE_CLI_QUALITY},
options::Quality,
update_service::{Platform, Release, TargetKind, UpdateService},
update_service::{unzip_downloaded_release, Platform, Release, TargetKind, UpdateService},
util::{
errors::{wrap, AnyError, UpdatesNotConfigured},
errors::{wrap, AnyError, CorruptDownload, UpdatesNotConfigured},
http,
io::ReportCopyProgress,
io::{ReportCopyProgress, SilentCopyProgress},
},
};
@@ -66,32 +63,80 @@ impl<'a> SelfUpdate<'a> {
release: &Release,
progress: impl ReportCopyProgress,
) -> Result<(), AnyError> {
// 1. Download the archive into a temporary directory
let tempdir = tempdir().map_err(|e| wrap(e, "Failed to create temp dir"))?;
let archive_path = tempdir.path().join("archive");
let stream = self.update_service.get_download_stream(release).await?;
http::download_into_file(&archive_path, progress, stream).await?;
// 2. Unzip the archive and get the binary
let target_path =
std::env::current_exe().map_err(|e| wrap(e, "could not get current exe"))?;
let staging_path = target_path.with_extension(".update");
let archive_contents_path = tempdir.path().join("content");
// unzipping the single binary is pretty small and fast--don't bother with passing progress
unzip_downloaded_release(&archive_path, &archive_contents_path, SilentCopyProgress())?;
copy_updated_cli_to_path(&archive_contents_path, &staging_path)?;
http::download_into_file(&staging_path, progress, stream).await?;
// 3. Copy file metadata, make sure the new binary is executable\
copy_file_metadata(&target_path, &staging_path)
.map_err(|e| wrap(e, "failed to set file permissions"))?;
validate_cli_is_good(&staging_path)?;
// Try to rename the old CLI to a tempdir, where it can get cleaned up by the
// Try to rename the old CLI to the tempdir, where it can get cleaned up by the
// OS later. However, this can fail if the tempdir is on a different drive
// than the installation dir. In this case just rename it to ".old".
let disposal_dir = tempdir().map_err(|e| wrap(e, "Failed to create disposal dir"))?;
if rename(&target_path, &disposal_dir.path().join("old-code-cli")).is_err() {
rename(&target_path, &target_path.with_extension(".old"))
if fs::rename(&target_path, &tempdir.path().join("old-code-cli")).is_err() {
fs::rename(&target_path, &target_path.with_extension(".old"))
.map_err(|e| wrap(e, "failed to rename old CLI"))?;
}
rename(&staging_path, &target_path)
fs::rename(&staging_path, &target_path)
.map_err(|e| wrap(e, "failed to rename newly installed CLI"))?;
Ok(())
}
}
fn validate_cli_is_good(exe_path: &Path) -> Result<(), AnyError> {
let o = Command::new(exe_path)
.args(["--version"])
.output()
.map_err(|e| CorruptDownload(format!("could not execute new binary, aborting: {}", e)))?;
if !o.status.success() {
let msg = format!(
"could not execute new binary, aborting. Stdout:\r\n\r\n{}\r\n\r\nStderr:\r\n\r\n{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr),
);
return Err(CorruptDownload(msg).into());
}
Ok(())
}
fn copy_updated_cli_to_path(unzipped_content: &Path, staging_path: &Path) -> Result<(), AnyError> {
let unzipped_files = fs::read_dir(unzipped_content)
.map_err(|e| wrap(e, "could not read update contents"))?
.collect::<Vec<_>>();
if unzipped_files.len() != 1 {
let msg = format!(
"expected exactly one file in update, got {}",
unzipped_files.len()
);
return Err(CorruptDownload(msg).into());
}
let archive_file = unzipped_files[0]
.as_ref()
.map_err(|e| wrap(e, "error listing update files"))?;
fs::copy(&archive_file.path(), staging_path)
.map_err(|e| wrap(e, "error copying to staging file"))?;
Ok(())
}
#[cfg(target_os = "windows")]
fn copy_file_metadata(from: &Path, to: &Path) -> Result<(), std::io::Error> {
let permissions = from.metadata()?.permissions();
@@ -105,7 +150,7 @@ fn copy_file_metadata(from: &Path, to: &Path) -> Result<(), std::io::Error> {
use std::os::unix::fs::MetadataExt;
let metadata = from.metadata()?;
set_permissions(&to, metadata.permissions())?;
fs::set_permissions(&to, metadata.permissions())?;
// based on coreutils' chown https://github.com/uutils/coreutils/blob/72b4629916abe0852ad27286f4e307fbca546b6e/src/chown/chown.rs#L266-L281
let s = std::ffi::CString::new(to.as_os_str().as_bytes()).unwrap();

View File

@@ -220,6 +220,7 @@ pub enum Platform {
DarwinARM64,
WindowsX64,
WindowsX86,
WindowsARM64
}
impl Platform {
@@ -232,6 +233,7 @@ impl Platform {
Platform::DarwinARM64 => Some("darwin-arm64".to_owned()),
Platform::WindowsX64 => Some("win32-x64-archive".to_owned()),
Platform::WindowsX86 => Some("win32-archive".to_owned()),
Platform::WindowsARM64 => Some("win32-arm64-archive".to_owned()),
_ => None,
}
}
@@ -246,6 +248,7 @@ impl Platform {
Platform::DarwinARM64 => "server-darwin-arm64",
Platform::WindowsX64 => "server-win32-x64",
Platform::WindowsX86 => "server-win32",
Platform::WindowsARM64 => "server-win32-arm64",
}
.to_owned()
}
@@ -253,14 +256,15 @@ impl Platform {
pub fn cli(&self) -> String {
match self {
Platform::LinuxAlpineARM64 => "cli-alpine-arm64",
Platform::LinuxAlpineX64 => "cli-linux-alpine",
Platform::LinuxAlpineX64 => "cli-alpine-x64",
Platform::LinuxX64 => "cli-linux-x64",
Platform::LinuxARM64 => "cli-linux-arm64",
Platform::LinuxARM32 => "cli-linux-armhf",
Platform::DarwinX64 => "cli-darwin-x64",
Platform::DarwinARM64 => "cli-darwin-arm64",
Platform::WindowsARM64 => "cli-win32-arm64",
Platform::WindowsX64 => "cli-win32-x64",
Platform::WindowsX86 => "cli-win32-x84",
Platform::WindowsX86 => "cli-win32",
}
.to_owned()
}

View File

@@ -362,6 +362,15 @@ impl std::fmt::Display for WindowsNeedsElevation {
}
}
#[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 VS Code CLI: {}", self.0)
}
}
// 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"
@@ -423,7 +432,8 @@ makeAnyError!(
ServerHasClosed,
ServiceAlreadyRegistered,
WindowsNeedsElevation,
UpdatesNotConfigured
UpdatesNotConfigured,
CorruptDownload
);
impl From<reqwest::Error> for AnyError {