1846 lines · 59.6 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use client::Client;use db::kvp::KeyValueStore;use futures_lite::StreamExt;use gpui::{    App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, TaskExt,    Window, actions,};use http_client::{HttpClient, HttpClientWithUrl};use paths::remote_servers_dir;use release_channel::{AppCommitSha, ReleaseChannel};use semver::Version;use serde::{Deserialize, Serialize};use settings::{RegisterSetting, Settings, SettingsStore};use smol::fs::File;use smol::{    fs,    io::{AsyncReadExt, AsyncWriteExt},};use std::mem;use std::{    env::{        self,        consts::{ARCH, OS},    },    ffi::OsStr,    ffi::OsString,    path::{Path, PathBuf},    sync::Arc,    time::{Duration, SystemTime},};use util::command::new_command;use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; #[derive(Debug)]struct MissingDependencyError(String); impl std::fmt::Display for MissingDependencyError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.0)    }} impl std::error::Error for MissingDependencyError {}const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);const NIGHTLY_POLL_INTERVAL: Duration = Duration::from_secs(15 * 60);const REMOTE_SERVER_CACHE_LIMIT: usize = 5; #[cfg(target_os = "linux")]fn linux_rsync_install_hint() -> &'static str {    let os_release = match std::fs::read_to_string("/etc/os-release") {        Ok(os_release) => os_release,        Err(_) => return "Please install rsync using your package manager",    };     let mut distribution_ids = Vec::new();    for line in os_release.lines() {        let trimmed = line.trim();        if let Some(value) = trimmed.strip_prefix("ID=") {            distribution_ids.push(value.trim_matches('"').to_ascii_lowercase());        } else if let Some(value) = trimmed.strip_prefix("ID_LIKE=") {            for id in value.trim_matches('"').split_whitespace() {                distribution_ids.push(id.to_ascii_lowercase());            }        }    }     let package_manager_hint = if distribution_ids        .iter()        .any(|distribution_id| distribution_id == "arch")    {        Some("Install it with: sudo pacman -S rsync")    } else if distribution_ids        .iter()        .any(|distribution_id| distribution_id == "debian" || distribution_id == "ubuntu")    {        Some("Install it with: sudo apt install rsync")    } else if distribution_ids.iter().any(|distribution_id| {        distribution_id == "fedora"            || distribution_id == "rhel"            || distribution_id == "centos"            || distribution_id == "rocky"            || distribution_id == "almalinux"    }) {        Some("Install it with: sudo dnf install rsync")    } else if distribution_ids        .iter()        .any(|distribution_id| distribution_id == "nixos")    {        Some("Install pkgs.rsync from nixpkgs")    } else {        None    };     package_manager_hint.unwrap_or("Please install rsync using your package manager")} actions!(    auto_update,    [        /// Checks for available updates.        Check,        /// Dismisses the update error message.        DismissMessage,        /// Opens the release notes for the current version in a browser.        ViewReleaseNotes,    ]); #[derive(Serialize, Debug)]pub struct AssetQuery<'a> {    asset: &'a str,    os: &'a str,    arch: &'a str,    metrics_id: Option<&'a str>,    system_id: Option<&'a str>,    is_staff: Option<bool>,} #[derive(Clone, Debug)]pub enum AutoUpdateStatus {    Idle,    Checking,    Downloading {        version: Version,        /// Download progress as a fraction in the range `0.0..=1.0`, or `None`        /// when the total download size is not yet known.        progress: Option<f32>,    },    Installing {        version: Version,    },    Updated {        version: Version,    },    Errored {        error: Arc<anyhow::Error>,    },} impl PartialEq for AutoUpdateStatus {    // `progress` is deliberately not compared: two `Downloading` statuses for    // the same version are equal regardless of how far the download is.    fn eq(&self, other: &Self) -> bool {        match (self, other) {            (AutoUpdateStatus::Idle, AutoUpdateStatus::Idle) => true,            (AutoUpdateStatus::Checking, AutoUpdateStatus::Checking) => true,            (                AutoUpdateStatus::Downloading { version: v1, .. },                AutoUpdateStatus::Downloading { version: v2, .. },            ) => v1 == v2,            (                AutoUpdateStatus::Installing { version: v1 },                AutoUpdateStatus::Installing { version: v2 },            ) => v1 == v2,            (                AutoUpdateStatus::Updated { version: v1 },                AutoUpdateStatus::Updated { version: v2 },            ) => v1 == v2,            (AutoUpdateStatus::Errored { error: e1 }, AutoUpdateStatus::Errored { error: e2 }) => {                e1.to_string() == e2.to_string()            }            _ => false,        }    }} impl AutoUpdateStatus {    pub fn is_updated(&self) -> bool {        matches!(self, Self::Updated { .. })    }} pub struct AutoUpdater {    status: AutoUpdateStatus,    current_version: Version,    client: Arc<Client>,    pending_poll: Option<Task<Option<()>>>,    quit_subscription: Option<gpui::Subscription>,    update_check_type: UpdateCheckType,    dismissed_status: Option<AutoUpdateStatus>,} #[derive(Deserialize, Serialize, Clone, Debug)]pub struct ReleaseAsset {    pub version: String,    pub url: String,} struct MacOsUnmounter<'a> {    mount_path: PathBuf,    background_executor: &'a BackgroundExecutor,} impl MacOsUnmounter<'_> {    /// Unmounts the disk image and waits for completion. This must happen    /// before the `InstallerDir` is dropped: deleting the temp dir while the    /// image is still mounted inside it fails silently and leaks the    /// directory (and the downloaded DMG) in the system temp dir.    async fn unmount(mut self) {        let mount_path = mem::take(&mut self.mount_path);        unmount_disk_image(&mount_path).await;    }} impl Drop for MacOsUnmounter<'_> {    fn drop(&mut self) {        let mount_path = mem::take(&mut self.mount_path);        // Safety net for early exits and cancellation; the happy path calls        // `unmount`, which leaves the path empty.        if mount_path.as_os_str().is_empty() {            return;        }        self.background_executor            .spawn(async move { unmount_disk_image(&mount_path).await })            .detach();    }} async fn unmount_disk_image(mount_path: &Path) {    let unmount_output = new_command("hdiutil")        .args(["detach", "-force"])        .arg(mount_path)        .output()        .await;    match unmount_output {        Ok(output) if output.status.success() => {            log::info!("Successfully unmounted the disk image");        }        Ok(output) => {            log::error!(                "Failed to unmount disk image: {:?}",                String::from_utf8_lossy(&output.stderr)            );        }        Err(error) => {            log::error!("Error while trying to unmount disk image: {:?}", error);        }    }} #[derive(Clone, Copy, Debug, RegisterSetting)]struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates.////// Default: trueimpl Settings for AutoUpdateSetting {    fn from_settings(content: &settings::SettingsContent) -> Self {        Self(content.auto_update.unwrap())    }} #[derive(Default)]struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>); impl Global for GlobalAutoUpdate {} pub fn init(client: Arc<Client>, cx: &mut App) {    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {        workspace.register_action(|_, action, window, cx| check(action, window, cx));         workspace.register_action(|_, action, _, cx| {            view_release_notes(action, cx);        });    })    .detach();     let version = release_channel::AppVersion::global(cx);    let auto_updater = cx.new(|cx| {        let updater = AutoUpdater::new(version, client, cx);         let poll_for_updates = ReleaseChannel::try_global(cx)            .map(|channel| channel.poll_for_updates())            .unwrap_or(false);         if option_env!("ZED_UPDATE_EXPLANATION").is_none()            && env::var("ZED_UPDATE_EXPLANATION").is_err()            && poll_for_updates        {            let mut update_subscription = AutoUpdateSetting::get_global(cx)                .0                .then(|| updater.start_polling(cx));             cx.observe_global::<SettingsStore>(move |updater: &mut AutoUpdater, cx| {                if AutoUpdateSetting::get_global(cx).0 {                    if update_subscription.is_none() {                        update_subscription = Some(updater.start_polling(cx))                    }                } else {                    update_subscription.take();                }            })            .detach();        }         updater    });    cx.set_global(GlobalAutoUpdate(Some(auto_updater)));} pub fn check(_: &Check, window: &mut Window, cx: &mut App) {    if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION")        .map(ToOwned::to_owned)        .or_else(|| env::var("ZED_UPDATE_EXPLANATION").ok())    {        drop(window.prompt(            gpui::PromptLevel::Info,            "Zed was installed via a package manager.",            Some(&message),            &["OK"],            cx,        ));        return;    }     if !ReleaseChannel::try_global(cx)        .map(|channel| channel.poll_for_updates())        .unwrap_or(false)    {        return;    }     if let Some(updater) = AutoUpdater::get(cx) {        updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx));    } else {        drop(window.prompt(            gpui::PromptLevel::Info,            "Could not check for updates",            Some("Auto-updates disabled for non-bundled app."),            &["OK"],            cx,        ));    }} pub fn release_notes_url(cx: &mut App) -> Option<String> {    let release_channel = ReleaseChannel::try_global(cx)?;    let url = match release_channel {        ReleaseChannel::Stable | ReleaseChannel::Preview => {            let auto_updater = AutoUpdater::get(cx)?;            let auto_updater = auto_updater.read(cx);            let mut current_version = auto_updater.current_version.clone();            current_version.pre = semver::Prerelease::EMPTY;            current_version.build = semver::BuildMetadata::EMPTY;            let release_channel = release_channel.dev_name();            let path = format!("/releases/{release_channel}/{current_version}");            auto_updater.client.http_client().build_url(&path)        }        ReleaseChannel::Nightly => {            "https://github.com/zed-industries/zed/commits/nightly/".to_string()        }        ReleaseChannel::Dev => "https://github.com/zed-industries/zed/commits/main/".to_string(),    };    Some(url)} pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {    let url = release_notes_url(cx)?;    cx.open_url(&url);    None} #[cfg(not(target_os = "windows"))]const INSTALLER_DIR_PREFIX: &str = "zed-auto-update"; #[cfg(not(target_os = "windows"))]struct InstallerDir(tempfile::TempDir); #[cfg(not(target_os = "windows"))]impl InstallerDir {    async fn new() -> Result<Self> {        Ok(Self(            tempfile::Builder::new()                .prefix(INSTALLER_DIR_PREFIX)                .tempdir()?,        ))    }     fn path(&self) -> &Path {        self.0.path()    }} #[cfg(target_os = "windows")]struct InstallerDir(PathBuf); #[cfg(target_os = "windows")]impl InstallerDir {    async fn new() -> Result<Self> {        let installer_dir = std::env::current_exe()?            .parent()            .context("No parent dir for Zed.exe")?            .join("updates");        if smol::fs::metadata(&installer_dir).await.is_ok() {            smol::fs::remove_dir_all(&installer_dir).await?;        }        smol::fs::create_dir(&installer_dir).await?;        Ok(Self(installer_dir))    }     fn path(&self) -> &Path {        self.0.as_path()    }} #[derive(Clone, Copy, Debug, PartialEq)]pub enum UpdateCheckType {    Automatic,    Manual,} impl UpdateCheckType {    pub fn is_manual(self) -> bool {        self == Self::Manual    }} impl AutoUpdater {    pub fn get(cx: &mut App) -> Option<Entity<Self>> {        cx.default_global::<GlobalAutoUpdate>().0.clone()    }     fn new(current_version: Version, client: Arc<Client>, cx: &mut Context<Self>) -> Self {        // On windows, executable files cannot be overwritten while they are        // running, so we must wait to overwrite the application until quitting        // or restarting. When quitting the app, we spawn the auto update helper        // to finish the auto update process after Zed exits. When restarting        // the app after an update, we use `set_restart_path` to run the auto        // update helper instead of the app, so that it can overwrite the app        // and then spawn the new binary.        #[cfg(target_os = "windows")]        let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));        #[cfg(not(target_os = "windows"))]        let quit_subscription = None;         cx.on_app_restart(|this, _| {            this.quit_subscription.take();        })        .detach();         Self {            status: AutoUpdateStatus::Idle,            current_version,            client,            pending_poll: None,            quit_subscription,            update_check_type: UpdateCheckType::Automatic,            dismissed_status: None,        }    }     pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {        let poll_interval =            ReleaseChannel::try_global(cx).map_or(POLL_INTERVAL, |channel| match channel {                ReleaseChannel::Nightly => NIGHTLY_POLL_INTERVAL,                _ => POLL_INTERVAL,            });         cx.spawn(async move |this, cx| {            if cfg!(target_os = "windows") {                use util::ResultExt;                 cleanup_windows()                    .await                    .context("failed to cleanup old directories")                    .log_err();            }             #[cfg(all(not(target_os = "windows"), not(test)))]            cx.background_spawn(cleanup_stale_installer_dirs()).detach();             loop {                this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;                cx.background_executor().timer(poll_interval).await;            }        })    }     pub fn update_check_type(&self) -> UpdateCheckType {        self.update_check_type    }     pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {        if check_type.is_manual() {            self.dismissed_status = None;        }        if self.pending_poll.is_some() {            if self.update_check_type == UpdateCheckType::Automatic {                self.update_check_type = check_type;                cx.notify();            }            return;        }        self.update_check_type = check_type;         cx.notify(); 

Lines 1–500 of 1846.