Detailed changes
@@ -724,6 +724,30 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "auto_update2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "db2",
+ "gpui2",
+ "isahc",
+ "lazy_static",
+ "log",
+ "menu2",
+ "project2",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smol",
+ "tempdir",
+ "theme2",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -11543,6 +11567,7 @@ dependencies = [
"async-recursion 0.3.2",
"async-tar",
"async-trait",
+ "auto_update2",
"backtrace",
"call2",
"chrono",
@@ -6,6 +6,7 @@ members = [
"crates/audio",
"crates/audio2",
"crates/auto_update",
+ "crates/auto_update2",
"crates/breadcrumbs",
"crates/call",
"crates/call2",
@@ -0,0 +1,29 @@
+[package]
+name = "auto_update2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/auto_update.rs"
+doctest = false
+
+[dependencies]
+db = { package = "db2", path = "../db2" }
+client = { package = "client2", path = "../client2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+anyhow.workspace = true
+isahc.workspace = true
+lazy_static.workspace = true
+log.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+smol.workspace = true
+tempdir.workspace = true
@@ -0,0 +1,388 @@
+mod update_notification;
+
+use anyhow::{anyhow, Context, Result};
+use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use db::kvp::KEY_VALUE_STORE;
+use db::RELEASE_CHANNEL;
+use gpui::{
+ actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
+ ViewContext, VisualContext,
+};
+use isahc::AsyncBody;
+use serde::Deserialize;
+use serde_derive::Serialize;
+use smol::io::AsyncReadExt;
+
+use settings::{Settings, SettingsStore};
+use smol::{fs::File, process::Command};
+use std::{ffi::OsString, sync::Arc, time::Duration};
+use update_notification::UpdateNotification;
+use util::channel::{AppCommitSha, ReleaseChannel};
+use util::http::HttpClient;
+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!(Check, DismissErrorMessage, ViewReleaseNotes);
+
+#[derive(Serialize)]
+struct UpdateRequestBody {
+ installation_id: Option<Arc<str>>,
+ release_channel: Option<&'static str>,
+ telemetry: bool,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum AutoUpdateStatus {
+ Idle,
+ Checking,
+ Downloading,
+ Installing,
+ Updated,
+ Errored,
+}
+
+pub struct AutoUpdater {
+ status: AutoUpdateStatus,
+ current_version: SemanticVersion,
+ http_client: Arc<dyn HttpClient>,
+ pending_poll: Option<Task<Option<()>>>,
+ server_url: String,
+}
+
+#[derive(Deserialize)]
+struct JsonRelease {
+ version: String,
+ url: String,
+}
+
+struct AutoUpdateSetting(bool);
+
+impl Settings for AutoUpdateSetting {
+ const KEY: Option<&'static str> = Some("auto_update");
+
+ type FileContent = Option<bool>;
+
+ fn load(
+ default_value: &Option<bool>,
+ user_values: &[&Option<bool>],
+ _: &mut AppContext,
+ ) -> Result<Self> {
+ Ok(Self(
+ Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
+ ))
+ }
+}
+
+pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
+ AutoUpdateSetting::register(cx);
+
+ if let Some(version) = *ZED_APP_VERSION {
+ let auto_updater = cx.build_model(|cx| {
+ let updater = AutoUpdater::new(version, http_client, server_url);
+
+ 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(Some(auto_updater));
+ //todo!(action)
+ // cx.add_global_action(check);
+ // cx.add_global_action(view_release_notes);
+ // cx.add_action(UpdateNotification::dismiss);
+ }
+}
+
+pub fn check(_: &Check, cx: &mut AppContext) {
+ if let Some(updater) = AutoUpdater::get(cx) {
+ updater.update(cx, |updater, cx| updater.poll(cx));
+ }
+}
+
+fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
+ if let Some(auto_updater) = AutoUpdater::get(cx) {
+ let auto_updater = auto_updater.read(cx);
+ let server_url = &auto_updater.server_url;
+ let current_version = auto_updater.current_version;
+ if cx.has_global::<ReleaseChannel>() {
+ match cx.global::<ReleaseChannel>() {
+ ReleaseChannel::Dev => {}
+ ReleaseChannel::Nightly => {}
+ ReleaseChannel::Preview => {
+ cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
+ }
+ ReleaseChannel::Stable => {
+ cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
+ }
+ }
+ }
+ }
+}
+
+pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
+ let updater = AutoUpdater::get(cx)?;
+ let version = updater.read(cx).current_version;
+ let should_show_notification = updater.read(cx).should_show_update_notification(cx);
+
+ cx.spawn(|workspace, mut cx| async move {
+ let should_show_notification = should_show_notification.await?;
+ if should_show_notification {
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.show_notification(0, cx, |cx| {
+ cx.build_view(|_| UpdateNotification::new(version))
+ });
+ updater
+ .read(cx)
+ .set_should_show_update_notification(false, cx)
+ .detach_and_log_err(cx);
+ })?;
+ }
+ anyhow::Ok(())
+ })
+ .detach();
+
+ None
+}
+
+impl AutoUpdater {
+ pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
+ cx.default_global::<Option<Model<Self>>>().clone()
+ }
+
+ fn new(
+ current_version: SemanticVersion,
+ http_client: Arc<dyn HttpClient>,
+ server_url: String,
+ ) -> Self {
+ Self {
+ status: AutoUpdateStatus::Idle,
+ current_version,
+ http_client,
+ server_url,
+ 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 == AutoUpdateStatus::Updated {
+ return;
+ }
+
+ self.status = AutoUpdateStatus::Checking;
+ 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 status(&self) -> AutoUpdateStatus {
+ self.status
+ }
+
+ pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
+ self.status = AutoUpdateStatus::Idle;
+ cx.notify();
+ }
+
+ async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
+ let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
+ (
+ this.http_client.clone(),
+ this.server_url.clone(),
+ this.current_version,
+ )
+ })?;
+
+ let mut url_string = format!(
+ "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
+ );
+ cx.update(|cx| {
+ if cx.has_global::<ReleaseChannel>() {
+ if let Some(param) = cx.global::<ReleaseChannel>().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
+ .context("error reading release")?;
+ let release: JsonRelease =
+ serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
+
+ let should_download = match *RELEASE_CHANNEL {
+ ReleaseChannel::Nightly => cx
+ .try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
+ .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 = tempdir::TempDir::new("zed-auto-update")?;
+ let dmg_path = temp_dir.path().join("Zed.dmg");
+ let mount_path = temp_dir.path().join("Zed");
+ let running_app_path = ZED_APP_PATH
+ .clone()
+ .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
+ let running_app_filename = running_app_path
+ .file_name()
+ .ok_or_else(|| anyhow!("invalid running app path"))?;
+ let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
+ mounted_app_path.push("/");
+
+ let mut dmg_file = File::create(&dmg_path).await?;
+
+ let (installation_id, release_channel, telemetry) = cx.update(|cx| {
+ let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
+ let release_channel = cx
+ .has_global::<ReleaseChannel>()
+ .then(|| cx.global::<ReleaseChannel>().display_name());
+ let telemetry = TelemetrySettings::get_global(cx).metrics;
+
+ (installation_id, release_channel, telemetry)
+ })?;
+
+ let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
+ installation_id,
+ release_channel,
+ telemetry,
+ })?);
+
+ let mut response = client.get(&release.url, request_body, true).await?;
+ smol::io::copy(response.body_mut(), &mut dmg_file).await?;
+ log::info!("downloaded update. path:{:?}", dmg_path);
+
+ this.update(&mut cx, |this, cx| {
+ this.status = AutoUpdateStatus::Installing;
+ cx.notify();
+ })?;
+
+ let output = Command::new("hdiutil")
+ .args(&["attach", "-nobrowse"])
+ .arg(&dmg_path)
+ .arg("-mountroot")
+ .arg(&temp_dir.path())
+ .output()
+ .await?;
+ if !output.status.success() {
+ Err(anyhow!(
+ "failed to mount: {:?}",
+ String::from_utf8_lossy(&output.stderr)
+ ))?;
+ }
+
+ let output = Command::new("rsync")
+ .args(&["-av", "--delete"])
+ .arg(&mounted_app_path)
+ .arg(&running_app_path)
+ .output()
+ .await?;
+ if !output.status.success() {
+ Err(anyhow!(
+ "failed to copy app: {:?}",
+ String::from_utf8_lossy(&output.stderr)
+ ))?;
+ }
+
+ let output = Command::new("hdiutil")
+ .args(&["detach"])
+ .arg(&mount_path)
+ .output()
+ .await?;
+ if !output.status.success() {
+ Err(anyhow!(
+ "failed to unmount: {:?}",
+ String::from_utf8_lossy(&output.stderr)
+ ))?;
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.set_should_show_update_notification(true, cx)
+ .detach_and_log_err(cx);
+ this.status = AutoUpdateStatus::Updated;
+ cx.notify();
+ })?;
+ Ok(())
+ }
+
+ fn set_should_show_update_notification(
+ &self,
+ should_show: bool,
+ cx: &AppContext,
+ ) -> Task<Result<()>> {
+ cx.background_executor().spawn(async move {
+ if should_show {
+ KEY_VALUE_STORE
+ .write_kvp(
+ SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
+ "".to_string(),
+ )
+ .await?;
+ } else {
+ KEY_VALUE_STORE
+ .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
+ .await?;
+ }
+ Ok(())
+ })
+ }
+
+ fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
+ cx.background_executor().spawn(async move {
+ Ok(KEY_VALUE_STORE
+ .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
+ .is_some())
+ })
+ }
+}
@@ -0,0 +1,87 @@
+use gpui::{div, Div, EventEmitter, ParentComponent, Render, SemanticVersion, ViewContext};
+use menu::Cancel;
+use workspace::notifications::NotificationEvent;
+
+pub struct UpdateNotification {
+ _version: SemanticVersion,
+}
+
+impl EventEmitter<NotificationEvent> for UpdateNotification {}
+
+impl Render for UpdateNotification {
+ type Element = Div<Self>;
+
+ fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+ div().child("Updated zed!")
+ // let theme = theme::current(cx).clone();
+ // let theme = &theme.update_notification;
+
+ // let app_name = cx.global::<ReleaseChannel>().display_name();
+
+ // MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
+ // Flex::column()
+ // .with_child(
+ // Flex::row()
+ // .with_child(
+ // Text::new(
+ // format!("Updated to {app_name} {}", self.version),
+ // theme.message.text.clone(),
+ // )
+ // .contained()
+ // .with_style(theme.message.container)
+ // .aligned()
+ // .top()
+ // .left()
+ // .flex(1., true),
+ // )
+ // .with_child(
+ // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
+ // let style = theme.dismiss_button.style_for(state);
+ // Svg::new("icons/x.svg")
+ // .with_color(style.color)
+ // .constrained()
+ // .with_width(style.icon_width)
+ // .aligned()
+ // .contained()
+ // .with_style(style.container)
+ // .constrained()
+ // .with_width(style.button_width)
+ // .with_height(style.button_width)
+ // })
+ // .with_padding(Padding::uniform(5.))
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.dismiss(&Default::default(), cx)
+ // })
+ // .aligned()
+ // .constrained()
+ // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
+ // .aligned()
+ // .top()
+ // .flex_float(),
+ // ),
+ // )
+ // .with_child({
+ // let style = theme.action_message.style_for(state);
+ // Text::new("View the release notes", style.text.clone())
+ // .contained()
+ // .with_style(style.container)
+ // })
+ // .contained()
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, |_, _, cx| {
+ // crate::view_release_notes(&Default::default(), cx)
+ // })
+ // .into_any_named("update notification")
+ }
+}
+
+impl UpdateNotification {
+ pub fn new(version: SemanticVersion) -> Self {
+ Self { _version: version }
+ }
+
+ pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ cx.emit(NotificationEvent::Dismiss);
+ }
+}
@@ -492,6 +492,10 @@ impl AppContext {
self.platform.open_url(url);
}
+ pub fn app_path(&self) -> Result<PathBuf> {
+ self.platform.app_path()
+ }
+
pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
self.platform.path_for_auxiliary_executable(name)
}
@@ -15,6 +15,8 @@ pub enum NotificationEvent {
pub trait Notification: EventEmitter<NotificationEvent> + Render {}
+impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
+
pub trait NotificationHandle: Send {
fn id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
@@ -164,7 +166,7 @@ impl Workspace {
}
pub mod simple_message_notification {
- use super::{Notification, NotificationEvent};
+ use super::NotificationEvent;
use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
use serde::Deserialize;
use std::{borrow::Cow, sync::Arc};
@@ -359,7 +361,6 @@ pub mod simple_message_notification {
// }
impl EventEmitter<NotificationEvent> for MessageNotification {}
- impl Notification for MessageNotification {}
}
pub trait NotifyResultExt {
@@ -18,7 +18,7 @@ path = "src/main.rs"
ai = { package = "ai2", path = "../ai2"}
# audio = { path = "../audio" }
# activity_indicator = { path = "../activity_indicator" }
-# auto_update = { path = "../auto_update" }
+auto_update = { package = "auto_update2", path = "../auto_update2" }
# breadcrumbs = { path = "../breadcrumbs" }
call = { package = "call2", path = "../call2" }
# channel = { path = "../channel" }
@@ -186,7 +186,7 @@ fn main() {
cx.set_global(Arc::downgrade(&app_state));
// audio::init(Assets, cx);
- // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
+ auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
// recent_projects::init(cx);
@@ -162,7 +162,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// status_bar.add_right_item(cursor_position, cx);
});
- // auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
+ auto_update::notify_of_any_new_update(cx);
// vim::observe_keystrokes(cx);