743 lines · 22.5 KB · Rust
RawDownload
use anyhow::{anyhow, Context, Result};use client::{Client, TelemetrySettings};use db::kvp::KEY_VALUE_STORE;use db::RELEASE_CHANNEL;use gpui::{    actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,    SemanticVersion, Task, WindowContext,};use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};use paths::remote_servers_dir;use release_channel::{AppCommitSha, ReleaseChannel};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{Settings, SettingsSources, SettingsStore};use smol::{fs, io::AsyncReadExt};use smol::{fs::File, process::Command};use std::{    env::{        self,        consts::{ARCH, OS},    },    ffi::OsString,    path::{Path, PathBuf},    sync::Arc,    time::Duration,};use which::which;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); actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]); #[derive(Serialize)]struct UpdateRequestBody {    installation_id: Option<Arc<str>>,    release_channel: Option<&'static str>,    telemetry: bool,    is_staff: Option<bool>,    destination: &'static str,} #[derive(Clone, PartialEq, Eq)]pub enum AutoUpdateStatus {    Idle,    Checking,    Downloading,    Installing,    Updated { binary_path: PathBuf },    Errored,} impl AutoUpdateStatus {    pub fn is_updated(&self) -> bool {        matches!(self, Self::Updated { .. })    }} pub struct AutoUpdater {    status: AutoUpdateStatus,    current_version: SemanticVersion,    http_client: Arc<HttpClientWithUrl>,    pending_poll: Option<Task<Option<()>>>,} #[derive(Deserialize)]pub struct JsonRelease {    pub version: String,    pub url: String,} struct MacOsUnmounter {    mount_path: PathBuf,} impl Drop for MacOsUnmounter {    fn drop(&mut self) {        let unmount_output = std::process::Command::new("hdiutil")            .args(["detach", "-force"])            .arg(&self.mount_path)            .output();         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);            }        }    }} struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates.////// Default: true#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]#[serde(transparent)]struct AutoUpdateSettingContent(bool); impl Settings for AutoUpdateSetting {    const KEY: Option<&'static str> = Some("auto_update");     type FileContent = Option<AutoUpdateSettingContent>;     fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {        let auto_update = [sources.server, sources.release_channel, sources.user]            .into_iter()            .find_map(|value| value.copied().flatten())            .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);         Ok(Self(auto_update.0))    }} #[derive(Default)]struct GlobalAutoUpdate(Option<Model<AutoUpdater>>); impl Global for GlobalAutoUpdate {} pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {    AutoUpdateSetting::register(cx);     cx.observe_new_views(|workspace: &mut Workspace, _cx| {        workspace.register_action(|_, action: &Check, cx| check(action, cx));         workspace.register_action(|_, action, cx| {            view_release_notes(action, cx);        });    })    .detach();     let version = release_channel::AppVersion::global(cx);    let auto_updater = cx.new_model(|cx| {        let updater = AutoUpdater::new(version, http_client);         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, 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, cx: &mut WindowContext) {    if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {        drop(cx.prompt(            gpui::PromptLevel::Info,            "Zed was installed via a package manager.",            Some(message),            &["Ok"],        ));        return;    }     if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {        drop(cx.prompt(            gpui::PromptLevel::Info,            "Zed was installed via a package manager.",            Some(&message),            &["Ok"],        ));        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(cx));    } else {        drop(cx.prompt(            gpui::PromptLevel::Info,            "Could not check for updates",            Some("Auto-updates disabled for non-bundled app."),            &["Ok"],        ));    }} pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> {    let auto_updater = AutoUpdater::get(cx)?;    let release_channel = ReleaseChannel::try_global(cx)?;     match release_channel {        ReleaseChannel::Stable | ReleaseChannel::Preview => {            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}");            let url = &auto_updater.http_client.build_url(&path);            cx.open_url(url);        }        ReleaseChannel::Nightly => {            cx.open_url("https://github.com/zed-industries/zed/commits/nightly/");        }        ReleaseChannel::Dev => {            cx.open_url("https://github.com/zed-industries/zed/commits/main/");        }    }    None} impl AutoUpdater {    pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {        cx.default_global::<GlobalAutoUpdate>().0.clone()    }     fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {        Self {            status: AutoUpdateStatus::Idle,            current_version,            http_client,            pending_poll: None,        }    }     pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {        cx.spawn(|this, mut cx| async move {            loop {                this.update(&mut cx, |this, cx| this.poll(cx))?;                cx.background_executor().timer(POLL_INTERVAL).await;            }        })    }     pub fn poll(&mut self, cx: &mut ModelContext<Self>) {        if self.pending_poll.is_some() || self.status.is_updated() {            return;        }         cx.notify();         self.pending_poll = Some(cx.spawn(|this, mut cx| async move {            let result = Self::update(this.upgrade()?, cx.clone()).await;            this.update(&mut cx, |this, cx| {                this.pending_poll = None;                if let Err(error) = result {                    log::error!("auto-update failed: error:{:?}", error);                    this.status = AutoUpdateStatus::Errored;                    cx.notify();                }            })            .ok()        }));    }     pub fn current_version(&self) -> SemanticVersion {        self.current_version    }     pub fn status(&self) -> AutoUpdateStatus {        self.status.clone()    }     pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {        self.status = AutoUpdateStatus::Idle;        cx.notify();    }     // 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(        os: &str,        arch: &str,        release_channel: ReleaseChannel,        version: Option<SemanticVersion>,        cx: &mut AsyncAppContext,    ) -> Result<PathBuf> {        let this = cx.update(|cx| {            cx.default_global::<GlobalAutoUpdate>()                .0                .clone()                .ok_or_else(|| anyhow!("auto-update not initialized"))        })??;         let release = Self::get_release(            &this,            "zed-remote-server",            os,            arch,            version,            Some(release_channel),            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.http_client.clone())?;         if smol::fs::metadata(&version_path).await.is_err() {            log::info!(                "downloading zed-remote-server {os} {arch} version {}",                release.version            );            download_remote_server_binary(&version_path, release, client, cx).await?;        }         Ok(version_path)    }     pub async fn get_remote_server_release_url(        os: &str,        arch: &str,        release_channel: ReleaseChannel,        version: Option<SemanticVersion>,        cx: &mut AsyncAppContext,    ) -> Result<Option<(String, String)>> {        let this = cx.update(|cx| {            cx.default_global::<GlobalAutoUpdate>()                .0                .clone()                .ok_or_else(|| anyhow!("auto-update not initialized"))        })??;         let release = Self::get_release(            &this,            "zed-remote-server",            os,            arch,            version,            Some(release_channel),            cx,        )        .await?;         let update_request_body = build_remote_server_update_request_body(cx)?;        let body = serde_json::to_string(&update_request_body)?;         Ok(Some((release.url, body)))    }     async fn get_release(        this: &Model<Self>,        asset: &str,        os: &str,        arch: &str,        version: Option<SemanticVersion>,        release_channel: Option<ReleaseChannel>,        cx: &mut AsyncAppContext,    ) -> Result<JsonRelease> {        let client = this.read_with(cx, |this, _| this.http_client.clone())?;         if let Some(version) = version {            let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");             let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);             Ok(JsonRelease {                version: version.to_string(),                url: client.build_url(&url),            })        } else {            let mut url_string = client.build_url(&format!(                "/api/releases/latest?asset={}&os={}&arch={}",                asset, os, arch            ));            if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {                url_string += "&";                url_string += param;            }             let mut response = client.get(&url_string, Default::default(), true).await?;            let mut body = Vec::new();            response.body_mut().read_to_end(&mut body).await?;             if !response.status().is_success() {                return Err(anyhow!(                    "failed to fetch release: {:?}",                    String::from_utf8_lossy(&body),                ));            }             serde_json::from_slice(body.as_slice()).with_context(|| {                format!(                    "error deserializing release {:?}",                    String::from_utf8_lossy(&body),                )            })        }    }     async fn get_latest_release(        this: &Model<Self>,        asset: &str,        os: &str,        arch: &str,        release_channel: Option<ReleaseChannel>,        cx: &mut AsyncAppContext,    ) -> Result<JsonRelease> {        Self::get_release(this, asset, os, arch, None, release_channel, cx).await    }     async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {        let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {            this.status = AutoUpdateStatus::Checking;            cx.notify();            (                this.http_client.clone(),                this.current_version,                ReleaseChannel::try_global(cx),            )        })?;         let release =            Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;         let should_download = match *RELEASE_CHANNEL {            ReleaseChannel::Nightly => cx                .update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))                .ok()                .flatten()                .unwrap_or(true),            _ => release.version.parse::<SemanticVersion>()? > current_version,        };         if !should_download {            this.update(&mut cx, |this, cx| {                this.status = AutoUpdateStatus::Idle;                cx.notify();            })?;            return Ok(());        }         this.update(&mut cx, |this, cx| {            this.status = AutoUpdateStatus::Downloading;            cx.notify();        })?;         let temp_dir = tempfile::Builder::new()            .prefix("zed-auto-update")            .tempdir()?;         let filename = match OS {            "macos" => Ok("Zed.dmg"),            "linux" => Ok("zed.tar.gz"),            _ => Err(anyhow!("not supported: {:?}", OS)),        }?;         anyhow::ensure!(            which("rsync").is_ok(),            "Aborting. Could not find rsync which is required for auto-updates."        );         let downloaded_asset = temp_dir.path().join(filename);        download_release(&downloaded_asset, release, client, &cx).await?;         this.update(&mut cx, |this, cx| {            this.status = AutoUpdateStatus::Installing;            cx.notify();        })?;         let binary_path = match OS {            "macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,            "linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,            _ => Err(anyhow!("not supported: {:?}", OS)),        }?;         this.update(&mut cx, |this, cx| {            this.set_should_show_update_notification(true, cx)

Lines 1–500 of 743.