cli: implement better self-updating

- Start separating a "standalone" CLI. This is a little awkward with clap-
  derive, but I got it working. Detection of whether the CLI _is_
  standalone is still todo.
- Remove the old ad-hoc update code for code-server, and use the update
  service instead.
- Fix some of the "permission denied" errors people got while updating
  before. We need to rename the old running binary, not just overwrite it.
This commit is contained in:
Connor Peet 2022-09-23 17:44:22 -07:00
parent a9bcb15b75
commit 07453efc00
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
22 changed files with 381 additions and 102 deletions

View file

@ -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": [

View file

@ -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

View file

@ -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 }}

View file

@ -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

View file

@ -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 }

View file

@ -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()

View file

@ -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<Item = impl Into<std::ffi::OsString>>,
) -> Option<Cli> {
) -> Option<CliCore> {
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()
})

View file

@ -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::<Vec<_>>();
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,
},
},
};

View file

@ -8,5 +8,6 @@ mod output;
pub mod args;
pub mod tunnels;
pub mod update;
pub mod version;
pub use context::CommandContext;

View file

@ -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<String>,
@ -42,7 +49,36 @@ pub struct Cli {
pub subcommand: Option<Commands>,
}
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<StandaloneCommands>,
}
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<String> {
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)]

View file

@ -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,
}

View file

@ -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<ExistingTunnelArgs> for Option<dev_tunnels::ExistingTunnel> {
}
struct TunnelServiceContainer {
args: Cli,
args: CliCore,
}
impl TunnelServiceContainer {
fn new(args: Cli) -> Self {
fn new(args: CliCore) -> Self {
Self { args }
}
}

View file

@ -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<i32, AnyError> {
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(&current_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(&current_version, ProgressBarReporter::from(pb))
.await?;
ctx.log
.result(format!("Successfully updated to {}", current_version));
Ok(0)
}

View file

@ -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");

View file

@ -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

View file

@ -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;

View file

@ -209,9 +209,9 @@ impl Logger {
}
}
pub fn result(&self, message: &str) {
pub fn result(&self, message: impl AsRef<str>) {
for sink in &self.sink {
sink.write_result(message);
sink.write_result(message.as_ref());
}
}

117
cli/src/self_update.rs Normal file
View file

@ -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<Self, AnyError> {
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<Release, AnyError> {
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(())
}

View file

@ -187,6 +187,7 @@ impl ServerParamsRaw {
commit: c.clone(),
quality: self.quality,
target,
name: String::new(),
platform: self.platform,
});
}

View file

@ -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<UpdateResult, AnyError> {
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, &current_exe)
.do_update(&latest_release, SilentCopyProgress())
.await?;
Ok(UpdateResult {

View file

@ -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<Release, AnyError> {
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<Release, AnyError> {
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<reqwest::Response, AnyError> {
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())
}

View file

@ -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)]