diff --git a/.vscode/settings.json b/.vscode/settings.json index 33056829dde..5fa37b2d551 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -91,6 +91,9 @@ "editor.defaultFormatter": "rust-lang.rust-analyzer", "editor.formatOnSave": true, }, + "rust-analyzer.linkedProjects": [ + "cli/Cargo.toml" + ], "typescript.tsc.autoDetect": "off", "testing.autoRun.mode": "rerun", "conventionalCommits.scopes": [ diff --git a/build/azure-pipelines/cli/compile-linux.yml b/build/azure-pipelines/cli/compile-linux.yml index d17708ddca2..b84e76ea60a 100644 --- a/build/azure-pipelines/cli/compile-linux.yml +++ b/build/azure-pipelines/cli/compile-linux.yml @@ -7,6 +7,8 @@ parameters: default: './' - name: VSCODE_CLI_BINARY_NAME type: string + - name: VSCODE_QUALITY + type: string - name: channel type: string default: stable @@ -24,6 +26,11 @@ steps: targets: ${{ parameters.VSCODE_CLI_TARGETS }} channel: ${{ parameters.channel }} + - template: ./cli/prepare.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_IS_POSIX: true + - ${{ each target in parameters.VSCODE_CLI_TARGETS }}: - script: cargo build --release --target ${{ target.target }} --bin=${{ parameters.VSCODE_CLI_BINARY_NAME }} displayName: Compile ${{ target.artifact }} @@ -35,6 +42,8 @@ steps: VSCODE_CLI_ASSET_NAME: ${{ target.artifact }} VSCODE_CLI_AI_KEY: $(VSCODE_CLI_AI_KEY) VSCODE_CLI_AI_ENDPOINT: $(VSCODE_CLI_AI_ENDPOINT) + VSCODE_CLI_COMMIT: $(VSCODE_CLI_COMMIT) + VSCODE_QUALITY: $(VSCODE_QUALITY) CXX_aarch64-unknown-linux-musl: musl-g++ CC_aarch64-unknown-linux-musl: musl-gcc diff --git a/build/azure-pipelines/cli/compile-macos.yml b/build/azure-pipelines/cli/compile-macos.yml index 01395a88e7c..bb0353d514f 100644 --- a/build/azure-pipelines/cli/compile-macos.yml +++ b/build/azure-pipelines/cli/compile-macos.yml @@ -7,6 +7,8 @@ parameters: default: './' - name: VSCODE_CLI_BINARY_NAME type: string + - name: VSCODE_QUALITY + type: string - name: channel type: string default: stable @@ -17,6 +19,11 @@ steps: targets: ${{ parameters.VSCODE_CLI_TARGETS }} channel: ${{ parameters.channel }} + - template: ./cli/prepare.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_IS_POSIX: true + - ${{ each target in parameters.VSCODE_CLI_TARGETS }}: - script: cargo build --release --target ${{ target.target }} --bin=${{ parameters.VSCODE_CLI_BINARY_NAME }} displayName: Compile ${{ target.artifact }} @@ -28,6 +35,8 @@ steps: VSCODE_CLI_ASSET_NAME: ${{ target.artifact }} VSCODE_CLI_AI_KEY: $(VSCODE_CLI_AI_KEY) VSCODE_CLI_AI_ENDPOINT: $(VSCODE_CLI_AI_ENDPOINT) + VSCODE_CLI_COMMIT: $(VSCODE_CLI_COMMIT) + VSCODE_QUALITY: $(VSCODE_QUALITY) - publish: ${{ parameters.VSCODE_CLI_DIR }}/target/${{ target.target }}/release/${{ parameters.VSCODE_CLI_BINARY_NAME }} artifact: ${{ target.artifact }} diff --git a/build/azure-pipelines/cli/compile-windows.yml b/build/azure-pipelines/cli/compile-windows.yml index b024a281c69..f02a6c9d937 100644 --- a/build/azure-pipelines/cli/compile-windows.yml +++ b/build/azure-pipelines/cli/compile-windows.yml @@ -7,6 +7,8 @@ parameters: default: './' - name: VSCODE_CLI_BINARY_NAME type: string + - name: VSCODE_QUALITY + type: string - name: channel type: string default: stable @@ -17,6 +19,11 @@ steps: targets: ${{ parameters.VSCODE_CLI_TARGETS }} channel: ${{ parameters.channel }} + - template: ./cli/prepare.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_IS_POSIX: false + - ${{ each target in parameters.VSCODE_CLI_TARGETS }}: - script: cargo build --release --target ${{ target.target }} --bin=${{ parameters.VSCODE_CLI_BINARY_NAME }} displayName: Compile ${{ target.artifact }} @@ -28,6 +35,8 @@ steps: VSCODE_CLI_ASSET_NAME: ${{ target.artifact }} VSCODE_CLI_AI_KEY: $(VSCODE_CLI_AI_KEY) VSCODE_CLI_AI_ENDPOINT: $(VSCODE_CLI_AI_ENDPOINT) + VSCODE_CLI_COMMIT: $(VSCODE_CLI_COMMIT) + VSCODE_QUALITY: $(VSCODE_QUALITY) ${{ if eq(target, 'x86_64-pc-windows-msvc') }}: OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/deps/x64-windows-static-md/lib OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/deps/x64-windows-static-md/include diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index ff74581068b..970412d7869 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -189,14 +189,11 @@ stages: - job: LinuxX86 pool: vscode-1es-linux steps: - - template: ./cli/prepare.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_IS_POSIX: true - template: ./cli/compile-linux.yml parameters: VSCODE_CLI_DIR: $(Build.SourcesDirectory)/cli VSCODE_CLI_BINARY_NAME: ${{ variables.VSCODE_CLI_BINARY_NAME }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CLI_TARGETS: - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true) }}: - { target: x86_64-unknown-linux-musl, artifact: vscode_cli_alpine_x64_cli-unsigned } @@ -217,14 +214,11 @@ stages: sudo apt update -y sudo apt install -y build-essential pkg-config displayName: Install build dependencies - - template: ./cli/prepare.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_IS_POSIX: true - template: ./cli/compile-linux.yml parameters: VSCODE_CLI_DIR: $(Build.SourcesDirectory)/cli VSCODE_CLI_BINARY_NAME: ${{ variables.VSCODE_CLI_BINARY_NAME }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CLI_TARGETS: - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ALPINE_ARM64, true) }}: - { target: aarch64-unknown-linux-musl, artifact: vscode_cli_alpine_arm64_cli-unsigned } @@ -236,14 +230,11 @@ stages: pool: vmImage: macOS-latest steps: - - template: ./cli/prepare.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_IS_POSIX: true - template: ./cli/compile-macos.yml parameters: VSCODE_CLI_DIR: $(Build.SourcesDirectory)/cli VSCODE_CLI_BINARY_NAME: ${{ variables.VSCODE_CLI_BINARY_NAME }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CLI_TARGETS: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - { target: x86_64-apple-darwin, artifact: vscode_cli_darwin_x64_cli-unsigned } @@ -254,10 +245,6 @@ stages: - job: Windows pool: vscode-1es-windows steps: - - template: ./cli/prepare.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_IS_POSIX: false - template: ./cli/vcpkg-deps.yml parameters: targets: @@ -271,6 +258,7 @@ stages: parameters: VSCODE_CLI_DIR: $(Build.SourcesDirectory)/cli VSCODE_CLI_BINARY_NAME: ${{ variables.VSCODE_CLI_BINARY_NAME }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CLI_TARGETS: - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - { target: x86_64-pc-windows-msvc, artifact: vscode_cli_win32_x64_cli-unsigned } diff --git a/cli/src/bin/code-tunnel/main.rs b/cli/src/bin/code-tunnel/main.rs index 5e4a574be2d..4de8856f625 100644 --- a/cli/src/bin/code-tunnel/main.rs +++ b/cli/src/bin/code-tunnel/main.rs @@ -45,7 +45,7 @@ async fn main() -> Result<(), std::convert::Infallible> { parsed.global_options.log.unwrap_or(own_log::Level::Info) }, ), - args: args::Cli { + args: args::CliCore { global_options: parsed.global_options, subcommand: Some(args::Commands::Tunnel(parsed.tunnel_options.clone())), ..Default::default() diff --git a/cli/src/bin/code/legacy_args.rs b/cli/src/bin/code/legacy_args.rs index 54dbb9ac32d..361348d8373 100644 --- a/cli/src/bin/code/legacy_args.rs +++ b/cli/src/bin/code/legacy_args.rs @@ -6,15 +6,15 @@ use std::collections::HashMap; use cli::commands::args::{ - Cli, Commands, DesktopCodeOptions, ExtensionArgs, ExtensionSubcommand, InstallExtensionArgs, - ListExtensionArgs, UninstallExtensionArgs, + CliCore, Commands, DesktopCodeOptions, ExtensionArgs, ExtensionSubcommand, + InstallExtensionArgs, ListExtensionArgs, UninstallExtensionArgs, }; /// Tries to parse the argv using the legacy CLI interface, looking for its /// flags and generating a CLI with subcommands if those don't exist. pub fn try_parse_legacy( iter: impl IntoIterator>, -) -> Option { +) -> Option { let raw = clap_lex::RawArgs::new(iter); let mut cursor = raw.cursor(); raw.next(&mut cursor); // Skip the bin @@ -65,7 +65,7 @@ pub fn try_parse_legacy( // --status -> status if args.contains_key("list-extensions") { - Some(Cli { + Some(CliCore { subcommand: Some(Commands::Extension(ExtensionArgs { subcommand: ExtensionSubcommand::List(ListExtensionArgs { category: get_first_arg_value("category"), @@ -76,7 +76,7 @@ pub fn try_parse_legacy( ..Default::default() }) } else if let Some(exts) = args.remove("install-extension") { - Some(Cli { + Some(CliCore { subcommand: Some(Commands::Extension(ExtensionArgs { subcommand: ExtensionSubcommand::Install(InstallExtensionArgs { id_or_path: exts, @@ -88,7 +88,7 @@ pub fn try_parse_legacy( ..Default::default() }) } else if let Some(exts) = args.remove("uninstall-extension") { - Some(Cli { + Some(CliCore { subcommand: Some(Commands::Extension(ExtensionArgs { subcommand: ExtensionSubcommand::Uninstall(UninstallExtensionArgs { id: exts }), desktop_code_options, @@ -96,7 +96,7 @@ pub fn try_parse_legacy( ..Default::default() }) } else if args.contains_key("status") { - Some(Cli { + Some(CliCore { subcommand: Some(Commands::Status), ..Default::default() }) diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index b483ad17cb8..ca64b7478e9 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -8,7 +8,7 @@ use std::process::Command; use clap::Parser; use cli::{ - commands::{args, tunnels, version, CommandContext}, + commands::{args, tunnels, update, version, CommandContext}, desktop, log as own_log, state::LauncherPaths, update_service::UpdateService, @@ -26,68 +26,82 @@ use log::{Level, Metadata, Record}; #[tokio::main] async fn main() -> Result<(), std::convert::Infallible> { let raw_args = std::env::args_os().collect::>(); - let parsed = try_parse_legacy(&raw_args).unwrap_or_else(|| args::Cli::parse_from(&raw_args)); + // todo: only parse to the standalone CLI if not integrated + let parsed = try_parse_legacy(&raw_args) + .map(|core| args::AnyCli::Integrated(args::IntegratedCli { core })) + .unwrap_or_else(|| args::AnyCli::Standalone(args::StandaloneCli::parse_from(&raw_args))); + + let core = parsed.core(); let context = CommandContext { http: reqwest::Client::new(), - paths: LauncherPaths::new(&parsed.global_options.cli_data_dir).unwrap(), + paths: LauncherPaths::new(&core.global_options.cli_data_dir).unwrap(), log: own_log::Logger::new( SdkTracerProvider::builder().build().tracer("codecli"), - if parsed.global_options.verbose { + if core.global_options.verbose { own_log::Level::Trace } else { - parsed.global_options.log.unwrap_or(own_log::Level::Info) + core.global_options.log.unwrap_or(own_log::Level::Info) }, ), - args: parsed, + args: core.clone(), }; 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 context.args.subcommand.clone() { - None => { - let ca = context.args.get_base_code_args(); - start_code(context, ca).await - } - - Some(args::Commands::Extension(extension_args)) => { - let mut ca = context.args.get_base_code_args(); - extension_args.add_code_args(&mut ca); - start_code(context, ca).await - } - - Some(args::Commands::Status) => { - let mut ca = context.args.get_base_code_args(); - ca.push("--status".to_string()); - start_code(context, ca).await - } - - Some(args::Commands::Version(version_args)) => match version_args.subcommand { - args::VersionSubcommand::Use(use_version_args) => { - version::switch_to(context, use_version_args).await - } - args::VersionSubcommand::Uninstall(uninstall_version_args) => { - version::uninstall(context, uninstall_version_args).await - } - args::VersionSubcommand::List(list_version_args) => { - version::list(context, list_version_args).await - } + let result = match parsed { + args::AnyCli::Standalone(args::StandaloneCli { + subcommand: Some(cmd), + .. + }) => match cmd { + args::StandaloneCommands::Update(args) => update::update(context, args).await, }, + args::AnyCli::Standalone(args::StandaloneCli { core: c, .. }) + | args::AnyCli::Integrated(args::IntegratedCli { core: c, .. }) => match c.subcommand { + None => { + let ca = context.args.get_base_code_args(); + start_code(context, ca).await + } - Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.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::Commands::Extension(extension_args)) => { + let mut ca = context.args.get_base_code_args(); + extension_args.add_code_args(&mut ca); + start_code(context, ca).await } - Some(args::TunnelSubcommand::User(user_command)) => { - tunnels::user(context, user_command).await + + Some(args::Commands::Status) => { + let mut ca = context.args.get_base_code_args(); + ca.push("--status".to_string()); + start_code(context, ca).await } - Some(args::TunnelSubcommand::Service(service_args)) => { - tunnels::service(context, service_args).await - } - None => tunnels::serve(context, tunnel_args.serve_args).await, + + Some(args::Commands::Version(version_args)) => match version_args.subcommand { + args::VersionSubcommand::Use(use_version_args) => { + version::switch_to(context, use_version_args).await + } + args::VersionSubcommand::Uninstall(uninstall_version_args) => { + version::uninstall(context, uninstall_version_args).await + } + args::VersionSubcommand::List(list_version_args) => { + version::list(context, list_version_args).await + } + }, + + Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.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, tunnel_args.serve_args).await, + }, }, }; diff --git a/cli/src/commands.rs b/cli/src/commands.rs index ec4488e5f20..024c38e5c15 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -8,5 +8,6 @@ mod output; pub mod args; pub mod tunnels; +pub mod update; pub mod version; pub use context::CommandContext; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 8b4c1923048..b78dfdd35c1 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -24,7 +24,14 @@ const TEMPLATE: &str = " name = "Visual Studio Code CLI", version = match constants::VSCODE_CLI_VERSION { Some(v) => v, None => "dev" }, )] -pub struct Cli { +pub struct IntegratedCli { + #[clap(flatten)] + pub core: CliCore, +} + +/// Common CLI shared between intergated and standalone interfaces. +#[derive(Args, Debug, Default, Clone)] +pub struct CliCore { /// One or more files, folders, or URIs to open. #[clap(name = "paths")] pub open_paths: Vec, @@ -42,7 +49,36 @@ pub struct Cli { pub subcommand: Option, } -impl Cli { +#[derive(Parser, Debug, Default)] +#[clap( + help_template = TEMPLATE, + long_about = None, + name = "Visual Studio Code CLI", + version = match constants::VSCODE_CLI_VERSION { Some(v) => v, None => "dev" }, + )] +pub struct StandaloneCli { + #[clap(flatten)] + pub core: CliCore, + + #[clap(subcommand)] + pub subcommand: Option, +} + +pub enum AnyCli { + Integrated(IntegratedCli), + Standalone(StandaloneCli), +} + +impl AnyCli { + pub fn core(&self) -> &CliCore { + match self { + AnyCli::Integrated(cli) => &cli.core, + AnyCli::Standalone(cli) => &cli.core, + } + } +} + +impl CliCore { pub fn get_base_code_args(&self) -> Vec { let mut args = self.open_paths.clone(); self.editor_options.add_code_args(&mut args); @@ -52,8 +88,8 @@ impl Cli { } } -impl<'a> From<&'a Cli> for CodeServerArgs { - fn from(cli: &'a Cli) -> Self { +impl<'a> From<&'a CliCore> for CodeServerArgs { + fn from(cli: &'a CliCore) -> Self { let mut args = CodeServerArgs { log: cli.global_options.log, accept_server_license_terms: true, @@ -77,6 +113,19 @@ impl<'a> From<&'a Cli> for CodeServerArgs { } } +#[derive(Subcommand, Debug, Clone)] +pub enum StandaloneCommands { + /// Updates the VS Code CLI. + Update(StandaloneUpdateArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct StandaloneUpdateArgs { + /// Only check for updates, without actually updating the CLI. + #[clap(long)] + pub check: bool, +} + #[derive(Subcommand, Debug, Clone)] pub enum Commands { @@ -234,7 +283,7 @@ pub struct UninstallVersionArgs { pub name: String, } -#[derive(Args, Debug, Default)] +#[derive(Args, Debug, Default, Clone)] pub struct EditorOptions { /// Compare two files with each other. #[clap(short, long, value_names = &["file", "file"])] @@ -348,7 +397,7 @@ impl DesktopCodeOptions { } } -#[derive(Args, Debug, Default)] +#[derive(Args, Debug, Default, Clone)] pub struct GlobalOptions { /// Directory where CLI metadata, such as VS Code installations, should be stored. #[clap(long, env = "VSCODE_CLI_DATA_DIR", global = true)] @@ -389,7 +438,7 @@ impl GlobalOptions { } } -#[derive(Args, Debug, Default)] +#[derive(Args, Debug, Default, Clone)] pub struct EditorTroubleshooting { /// Run CPU profiler during startup. #[clap(long)] diff --git a/cli/src/commands/context.rs b/cli/src/commands/context.rs index 506ee0f57d0..630d0bb028b 100644 --- a/cli/src/commands/context.rs +++ b/cli/src/commands/context.rs @@ -5,11 +5,11 @@ use crate::{log, state::LauncherPaths}; -use super::args::Cli; +use super::args::CliCore; pub struct CommandContext { pub log: log::Logger, pub paths: LauncherPaths, - pub args: Cli, + pub args: CliCore, pub http: reqwest::Client, } diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 0f75688791b..658f3123b7a 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -10,7 +10,7 @@ use tokio::sync::oneshot; use super::{ args::{ - AuthProvider, Cli, ExistingTunnelArgs, TunnelRenameArgs, TunnelServeArgs, + AuthProvider, CliCore, ExistingTunnelArgs, TunnelRenameArgs, TunnelServeArgs, TunnelServiceSubCommands, TunnelUserSubCommands, }, CommandContext, @@ -57,11 +57,11 @@ impl From for Option { } struct TunnelServiceContainer { - args: Cli, + args: CliCore, } impl TunnelServiceContainer { - fn new(args: Cli) -> Self { + fn new(args: CliCore) -> Self { Self { args } } } diff --git a/cli/src/commands/update.rs b/cli/src/commands/update.rs new file mode 100644 index 00000000000..7e1db89964c --- /dev/null +++ b/cli/src/commands/update.rs @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use indicatif::ProgressBar; + +use crate::{ + self_update::SelfUpdate, + update_service::UpdateService, + util::{errors::AnyError, input::ProgressBarReporter}, +}; + +use super::{args::StandaloneUpdateArgs, CommandContext}; + +pub async fn update(ctx: CommandContext, args: StandaloneUpdateArgs) -> Result { + let update_service = UpdateService::new(ctx.log.clone(), ctx.http.clone()); + let update_service = SelfUpdate::new(&update_service)?; + + let current_version = update_service.get_current_release().await?; + if update_service.is_up_to_date_with(¤t_version) { + ctx.log.result(format!( + "VS Code is already to to date ({})", + current_version.commit + )); + return Ok(1); + } + + if args.check { + ctx.log + .result(format!("Update to {} is available", current_version)); + return Ok(0); + } + + let pb = ProgressBar::new(1); + pb.set_message("Downloading..."); + update_service + .do_update(¤t_version, ProgressBarReporter::from(pb)) + .await?; + ctx.log + .result(format!("Successfully updated to {}", current_version)); + + Ok(0) +} diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 6f357bc5d69..f89c34660a7 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -12,6 +12,8 @@ pub const VSCODE_CLI_VERSION: Option<&'static str> = option_env!("VSCODE_CLI_VER 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"); +pub const VSCODE_CLI_COMMIT: Option<&'static str> = option_env!("VSCODE_CLI_COMMIT"); pub const VSCODE_CLI_UPDATE_ENDPOINT: Option<&'static str> = option_env!("VSCODE_CLI_UPDATE_ENDPOINT"); diff --git a/cli/src/desktop/version_manager.rs b/cli/src/desktop/version_manager.rs index 7dc32bf7ded..c518dc38e3a 100644 --- a/cli/src/desktop/version_manager.rs +++ b/cli/src/desktop/version_manager.rs @@ -265,6 +265,7 @@ async fn get_release_for_request( platform, commit: commit.clone(), quality: *quality, + name: "".to_string(), target: TargetKind::Archive, }), RequestedVersion::Quality(quality) => update_service diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 98b8c4f9755..a1ebce4b0ef 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -14,6 +14,6 @@ pub mod desktop; pub mod options; pub mod state; pub mod tunnels; -pub mod update; +pub mod self_update; pub mod update_service; pub mod util; diff --git a/cli/src/log.rs b/cli/src/log.rs index d7a250b45ac..296ccc5a3ae 100644 --- a/cli/src/log.rs +++ b/cli/src/log.rs @@ -209,9 +209,9 @@ impl Logger { } } - pub fn result(&self, message: &str) { + pub fn result(&self, message: impl AsRef) { for sink in &self.sink { - sink.write_result(message); + sink.write_result(message.as_ref()); } } diff --git a/cli/src/self_update.rs b/cli/src/self_update.rs new file mode 100644 index 00000000000..275ebb12716 --- /dev/null +++ b/cli/src/self_update.rs @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::{fs::rename, path::Path}; +use tempfile::tempdir; + +use crate::{ + constants::{VSCODE_CLI_COMMIT, VSCODE_CLI_QUALITY}, + options::Quality, + update_service::{Platform, Release, TargetKind, UpdateService}, + util::{ + errors::{wrap, AnyError, UpdatesNotConfigured}, + http, + io::ReportCopyProgress, + }, +}; + +pub struct SelfUpdate<'a> { + commit: &'static str, + quality: Quality, + platform: Platform, + update_service: &'a UpdateService, +} + +impl<'a> SelfUpdate<'a> { + pub fn new(update_service: &'a UpdateService) -> Result { + let commit = VSCODE_CLI_COMMIT + .ok_or_else(|| UpdatesNotConfigured("unknown build commit".to_string()))?; + + let quality = VSCODE_CLI_QUALITY + .ok_or_else(|| UpdatesNotConfigured("no configured quality".to_string())) + .and_then(|q| Quality::try_from(q).map_err(UpdatesNotConfigured))?; + + let platform = Platform::env_default().ok_or_else(|| { + UpdatesNotConfigured("Unknown platform, please report this error".to_string()) + })?; + + Ok(Self { + commit, + quality, + platform, + update_service, + }) + } + + /// Gets the current release + pub async fn get_current_release(&self) -> Result { + self.update_service + .get_latest_commit(self.platform, TargetKind::Cli, self.quality) + .await + } + + /// Gets whether the given release is what this CLI is built against + pub fn is_up_to_date_with(&self, release: &Release) -> bool { + release.commit == self.commit + } + + /// Updates the CLI to the given release. + pub async fn do_update( + &self, + release: &Release, + progress: impl ReportCopyProgress, + ) -> Result<(), AnyError> { + let stream = self.update_service.get_download_stream(release).await?; + 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"); + + http::download_into_file(&staging_path, progress, stream).await?; + + copy_file_metadata(&target_path, &staging_path) + .map_err(|e| wrap(e, "failed to set file permissions"))?; + + // Try to rename the old CLI to a 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")) + .map_err(|e| wrap(e, "failed to rename old CLI"))?; + } + + rename(&staging_path, &target_path) + .map_err(|e| wrap(e, "failed to rename newly installed CLI"))?; + + Ok(()) + } +} + +#[cfg(target_os = "windows")] +fn copy_file_metadata(from: &Path, to: &Path) -> Result<(), std::io::Error> { + use std::fs::set_permissions; + + let permissions = from.metadata()?.permissions(); + set_permissions(&to, permissions)?; + Ok(()) +} + +#[cfg(not(target_os = "windows"))] +fn copy_file_metadata(from: &Path, to: &Path) -> Result<(), std::io::Error> { + use std::os::unix::ffi::OsStrExt; + use std::os::unix::fs::MetadataExt; + + let metadata = from.metadata()?; + 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(); + let ret = unsafe { libc::chown(s.as_ptr(), metadata.uid(), metadata.gid()) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) +} diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 989b6b5f40d..660c21c5c56 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -187,6 +187,7 @@ impl ServerParamsRaw { commit: c.clone(), quality: self.quality, target, + name: String::new(), platform: self.platform, }); } diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 6a021a72de3..6af9d806353 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ use crate::constants::{CONTROL_PORT, PROTOCOL_VERSION, VSCODE_CLI_VERSION}; use crate::log; +use crate::self_update::SelfUpdate; use crate::state::LauncherPaths; -use crate::update::Update; -use crate::update_service::Platform; +use crate::update_service::{Platform, UpdateService}; use crate::util::errors::{ wrap, AnyError, MismatchedLaunchModeError, NoAttachedServerError, ServerWriteError, }; +use crate::util::io::SilentCopyProgress; use crate::util::sync::{new_barrier, Barrier}; use opentelemetry::trace::SpanKind; use opentelemetry::KeyValue; @@ -617,13 +618,10 @@ async fn handle_update( ctx: &HandlerContext, params: &UpdateParams, ) -> Result { - let updater = Update::new(); - let latest_release = updater.get_latest_release().await?; - - let up_to_date = match VSCODE_CLI_VERSION { - Some(v) => v == latest_release.version, - None => true, - }; + let update_service = UpdateService::new(ctx.log.clone(), reqwest::Client::new()); + let updater = SelfUpdate::new(&update_service)?; + let latest_release = updater.get_current_release().await?; + let up_to_date = updater.is_up_to_date_with(&latest_release); if !params.do_update || up_to_date { return Ok(UpdateResult { @@ -632,12 +630,10 @@ async fn handle_update( }); } - info!(ctx.log, "Updating CLI from {}", latest_release.version); - - let current_exe = std::env::current_exe().map_err(|e| wrap(e, "could not get current exe"))?; + info!(ctx.log, "Updating CLI to {}", latest_release); updater - .switch_to_release(&latest_release, ¤t_exe) + .do_update(&latest_release, SilentCopyProgress()) .await?; Ok(UpdateResult { diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index f0d0acfeb6c..8bdf60138aa 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -26,15 +26,23 @@ pub struct UpdateService { /// Describes a specific release, can be created manually or returned from the update service. pub struct Release { + pub name: String, pub platform: Platform, pub target: TargetKind, pub quality: options::Quality, pub commit: String, } +impl std::fmt::Display for Release { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} (commit {})", self.name, self.commit) + } +} + #[derive(Deserialize)] struct UpdateServerVersion { pub version: String, + pub name: String, } fn quality_download_segment(quality: options::Quality) -> &'static str { @@ -57,7 +65,8 @@ impl UpdateService { quality: options::Quality, version: &str, ) -> Result { - let update_endpoint = VSCODE_CLI_UPDATE_ENDPOINT.ok_or(UpdatesNotConfigured())?; + let update_endpoint = + VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(UpdatesNotConfigured::no_url)?; let download_segment = target .download_segment(platform) .ok_or(UnsupportedPlatformError())?; @@ -86,6 +95,7 @@ impl UpdateService { target, platform, quality, + name: res.name, commit: res.version, }) } @@ -97,7 +107,8 @@ impl UpdateService { target: TargetKind, quality: options::Quality, ) -> Result { - let update_endpoint = VSCODE_CLI_UPDATE_ENDPOINT.ok_or(UpdatesNotConfigured())?; + let update_endpoint = + VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(UpdatesNotConfigured::no_url)?; let download_segment = target .download_segment(platform) .ok_or(UnsupportedPlatformError())?; @@ -125,6 +136,7 @@ impl UpdateService { target, platform, quality, + name: res.name, commit: res.version, }) } @@ -134,7 +146,8 @@ impl UpdateService { &self, release: &Release, ) -> Result { - let update_endpoint = VSCODE_CLI_UPDATE_ENDPOINT.ok_or(UpdatesNotConfigured())?; + let update_endpoint = + VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(UpdatesNotConfigured::no_url)?; let download_segment = release .target .download_segment(release.platform) @@ -182,6 +195,7 @@ pub enum TargetKind { Server, Archive, Web, + Cli, } impl TargetKind { @@ -190,6 +204,7 @@ impl TargetKind { TargetKind::Server => Some(platform.headless()), TargetKind::Archive => platform.archive(), TargetKind::Web => Some(platform.web()), + TargetKind::Cli => Some(platform.cli()), } } } @@ -235,6 +250,21 @@ impl Platform { .to_owned() } + pub fn cli(&self) -> String { + match self { + Platform::LinuxAlpineARM64 => "cli-alpine-arm64", + Platform::LinuxAlpineX64 => "cli-linux-alpine", + 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::WindowsX64 => "cli-win32-x64", + Platform::WindowsX86 => "cli-win32-x84", + } + .to_owned() + } + pub fn web(&self) -> String { format!("{}-web", self.headless()) } diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index e7abac0d224..b5308781259 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -317,11 +317,17 @@ impl std::fmt::Display for ServerHasClosed { } #[derive(Debug)] -pub struct UpdatesNotConfigured(); +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") + write!(f, "Update service is not configured: {}", self.0) } } #[derive(Debug)]