1544 lines · 50.1 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, 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};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 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 {        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(Clone, Debug, PartialEq, Eq)]pub enum VersionCheckType {    Sha(AppCommitSha),    Semantic(Version),} #[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: VersionCheckType },    Installing { version: VersionCheckType },    Updated { version: VersionCheckType },    Errored { error: Arc<anyhow::Error> },} impl PartialEq for AutoUpdateStatus {    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,} #[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 Drop for MacOsUnmounter<'_> {    fn drop(&mut self) {        let mount_path = mem::take(&mut self.mount_path);        self.background_executor            .spawn(async move {                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);                    }                }            })            .detach();    }} #[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"))]struct InstallerDir(tempfile::TempDir); #[cfg(not(target_os = "windows"))]impl InstallerDir {    async fn new() -> Result<Self> {        Ok(Self(            tempfile::Builder::new()                .prefix("zed-auto-update")                .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,        }    }     pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {        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();            }             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 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();         self.pending_poll = Some(cx.spawn(async move |this, cx| {            let result = Self::update(this.upgrade()?, cx).await;            this.update(cx, |this, cx| {                this.pending_poll = None;                if let Err(error) = result {                    let is_missing_dependency =                        error.downcast_ref::<MissingDependencyError>().is_some();                    this.status = match check_type {                        UpdateCheckType::Automatic if is_missing_dependency => {                            log::warn!("auto-update: {}", error);                            AutoUpdateStatus::Errored {                                error: Arc::new(error),                            }                        }                        // Be quiet if the check was automated (e.g. when offline)                        UpdateCheckType::Automatic => {                            log::info!("auto-update check failed: error:{:?}", error);                            AutoUpdateStatus::Idle                        }                        UpdateCheckType::Manual => {                            log::error!("auto-update failed: error:{:?}", error);                            AutoUpdateStatus::Errored {                                error: Arc::new(error),                            }                        }                    };                     cx.notify();                }            })            .ok()        }));    }     pub fn current_version(&self) -> Version {        self.current_version.clone()    }     pub fn status(&self) -> AutoUpdateStatus {        self.status.clone()    }     pub fn dismiss(&mut self, cx: &mut Context<Self>) -> bool {        if let AutoUpdateStatus::Idle = self.status {            return false;        }        self.status = AutoUpdateStatus::Idle;        cx.notify();        true    } 

Lines 1–500 of 1544.