1462 lines · 46.9 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use client::Client;use db::kvp::KEY_VALUE_STORE;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";const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);const REMOTE_SERVER_CACHE_LIMIT: usize = 5; 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 current_version = &auto_updater.current_version;            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 {                    this.status = match check_type {                        // 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    }     // If you are packaging Zed and need to override the place it downloads SSH remotes from,    // you can override this function. You should also update get_remote_server_release_url to return    // Ok(None).    pub async fn download_remote_server_release(        release_channel: ReleaseChannel,        version: Option<Version>,        os: &str,        arch: &str,        set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,        cx: &mut AsyncApp,    ) -> Result<PathBuf> {        let this = cx.update(|cx| {            cx.default_global::<GlobalAutoUpdate>()                .0                .clone()                .context("auto-update not initialized")        })?;         set_status("Fetching remote server release", cx);        let release = Self::get_release_asset(            &this,            release_channel,            version,            "zed-remote-server",            os,            arch,            cx,        )        .await?;         let servers_dir = paths::remote_servers_dir();        let channel_dir = servers_dir.join(release_channel.dev_name());        let platform_dir = channel_dir.join(format!("{}-{}", os, arch));        let version_path = platform_dir.join(format!("{}.gz", release.version));        smol::fs::create_dir_all(&platform_dir).await.ok();         let client = this.read_with(cx, |this, _| this.client.http_client());         if smol::fs::metadata(&version_path).await.is_err() {            log::info!(                "downloading zed-remote-server {os} {arch} version {}",                release.version            );            set_status("Downloading remote server", cx);            download_remote_server_binary(&version_path, release, client).await?;        }         if let Err(error) =            cleanup_remote_server_cache(&platform_dir, &version_path, REMOTE_SERVER_CACHE_LIMIT)                .await        {            log::warn!(                "Failed to clean up remote server cache in {:?}: {error:#}",                platform_dir            );        }         Ok(version_path)    }     pub async fn get_remote_server_release_url(        channel: ReleaseChannel,        version: Option<Version>,        os: &str,        arch: &str,

Lines 1–500 of 1462.