windows: Fix auto update failure when launching from the cli (#34303)
Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
658d56bd72
commit
32975c4208
13 changed files with 250 additions and 131 deletions
|
@ -716,18 +716,10 @@ impl ActivityIndicator {
|
||||||
})),
|
})),
|
||||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||||
}),
|
}),
|
||||||
AutoUpdateStatus::Updated {
|
AutoUpdateStatus::Updated { version } => Some(Content {
|
||||||
binary_path,
|
|
||||||
version,
|
|
||||||
} => Some(Content {
|
|
||||||
icon: None,
|
icon: None,
|
||||||
message: "Click to restart and update Zed".to_string(),
|
message: "Click to restart and update Zed".to_string(),
|
||||||
on_click: Some(Arc::new({
|
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
|
||||||
let reload = workspace::Reload {
|
|
||||||
binary_path: Some(binary_path.clone()),
|
|
||||||
};
|
|
||||||
move |_, _, cx| workspace::reload(&reload, cx)
|
|
||||||
})),
|
|
||||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||||
}),
|
}),
|
||||||
AutoUpdateStatus::Errored => Some(Content {
|
AutoUpdateStatus::Errored => Some(Content {
|
||||||
|
|
|
@ -59,16 +59,9 @@ pub enum VersionCheckType {
|
||||||
pub enum AutoUpdateStatus {
|
pub enum AutoUpdateStatus {
|
||||||
Idle,
|
Idle,
|
||||||
Checking,
|
Checking,
|
||||||
Downloading {
|
Downloading { version: VersionCheckType },
|
||||||
version: VersionCheckType,
|
Installing { version: VersionCheckType },
|
||||||
},
|
Updated { version: VersionCheckType },
|
||||||
Installing {
|
|
||||||
version: VersionCheckType,
|
|
||||||
},
|
|
||||||
Updated {
|
|
||||||
binary_path: PathBuf,
|
|
||||||
version: VersionCheckType,
|
|
||||||
},
|
|
||||||
Errored,
|
Errored,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +76,7 @@ pub struct AutoUpdater {
|
||||||
current_version: SemanticVersion,
|
current_version: SemanticVersion,
|
||||||
http_client: Arc<HttpClientWithUrl>,
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
pending_poll: Option<Task<Option<()>>>,
|
pending_poll: Option<Task<Option<()>>>,
|
||||||
|
quit_subscription: Option<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
@ -164,7 +158,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||||
AutoUpdateSetting::register(cx);
|
AutoUpdateSetting::register(cx);
|
||||||
|
|
||||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||||
workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx));
|
workspace.register_action(|_, action, window, cx| check(action, window, cx));
|
||||||
|
|
||||||
workspace.register_action(|_, action, _, cx| {
|
workspace.register_action(|_, action, _, cx| {
|
||||||
view_release_notes(action, cx);
|
view_release_notes(action, cx);
|
||||||
|
@ -174,7 +168,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||||
|
|
||||||
let version = release_channel::AppVersion::global(cx);
|
let version = release_channel::AppVersion::global(cx);
|
||||||
let auto_updater = cx.new(|cx| {
|
let auto_updater = cx.new(|cx| {
|
||||||
let updater = AutoUpdater::new(version, http_client);
|
let updater = AutoUpdater::new(version, http_client, cx);
|
||||||
|
|
||||||
let poll_for_updates = ReleaseChannel::try_global(cx)
|
let poll_for_updates = ReleaseChannel::try_global(cx)
|
||||||
.map(|channel| channel.poll_for_updates())
|
.map(|channel| channel.poll_for_updates())
|
||||||
|
@ -321,12 +315,34 @@ impl AutoUpdater {
|
||||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
|
fn new(
|
||||||
|
current_version: SemanticVersion,
|
||||||
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
|
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.
|
||||||
|
let quit_subscription = Some(cx.on_app_quit(|_, _| async move {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
finalize_auto_update_on_quit();
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.on_app_restart(|this, _| {
|
||||||
|
this.quit_subscription.take();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
status: AutoUpdateStatus::Idle,
|
status: AutoUpdateStatus::Idle,
|
||||||
current_version,
|
current_version,
|
||||||
http_client,
|
http_client,
|
||||||
pending_poll: None,
|
pending_poll: None,
|
||||||
|
quit_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,6 +552,8 @@ impl AutoUpdater {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
Self::check_dependencies()?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.status = AutoUpdateStatus::Checking;
|
this.status = AutoUpdateStatus::Checking;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -582,13 +600,15 @@ impl AutoUpdater {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
|
let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?;
|
||||||
|
if let Some(new_binary_path) = new_binary_path {
|
||||||
|
cx.update(|cx| cx.set_restart_path(new_binary_path))?;
|
||||||
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.set_should_show_update_notification(true, cx)
|
this.set_should_show_update_notification(true, cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
this.status = AutoUpdateStatus::Updated {
|
this.status = AutoUpdateStatus::Updated {
|
||||||
binary_path,
|
|
||||||
version: newer_version,
|
version: newer_version,
|
||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -639,6 +659,15 @@ impl AutoUpdater {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_dependencies() -> Result<()> {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
anyhow::ensure!(
|
||||||
|
which::which("rsync").is_ok(),
|
||||||
|
"Aborting. Could not find rsync which is required for auto-updates."
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
|
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
|
||||||
let filename = match OS {
|
let filename = match OS {
|
||||||
"macos" => anyhow::Ok("Zed.dmg"),
|
"macos" => anyhow::Ok("Zed.dmg"),
|
||||||
|
@ -647,20 +676,14 @@ impl AutoUpdater {
|
||||||
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
anyhow::ensure!(
|
|
||||||
which::which("rsync").is_ok(),
|
|
||||||
"Aborting. Could not find rsync which is required for auto-updates."
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(installer_dir.path().join(filename))
|
Ok(installer_dir.path().join(filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn binary_path(
|
async fn install_release(
|
||||||
installer_dir: InstallerDir,
|
installer_dir: InstallerDir,
|
||||||
target_path: PathBuf,
|
target_path: PathBuf,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<Option<PathBuf>> {
|
||||||
match OS {
|
match OS {
|
||||||
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
|
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
|
||||||
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
|
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
|
||||||
|
@ -801,7 +824,7 @@ async fn install_release_linux(
|
||||||
temp_dir: &InstallerDir,
|
temp_dir: &InstallerDir,
|
||||||
downloaded_tar_gz: PathBuf,
|
downloaded_tar_gz: PathBuf,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<Option<PathBuf>> {
|
||||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||||
|
@ -861,14 +884,14 @@ async fn install_release_linux(
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(to.join(expected_suffix))
|
Ok(Some(to.join(expected_suffix)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn install_release_macos(
|
async fn install_release_macos(
|
||||||
temp_dir: &InstallerDir,
|
temp_dir: &InstallerDir,
|
||||||
downloaded_dmg: PathBuf,
|
downloaded_dmg: PathBuf,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<Option<PathBuf>> {
|
||||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||||
let running_app_filename = running_app_path
|
let running_app_filename = running_app_path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -910,10 +933,10 @@ async fn install_release_macos(
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(running_app_path)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
|
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
|
||||||
let output = Command::new(downloaded_installer)
|
let output = Command::new(downloaded_installer)
|
||||||
.arg("/verysilent")
|
.arg("/verysilent")
|
||||||
.arg("/update=true")
|
.arg("/update=true")
|
||||||
|
@ -926,29 +949,36 @@ async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBu
|
||||||
"failed to start installer: {:?}",
|
"failed to start installer: {:?}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
Ok(std::env::current_exe()?)
|
// We return the path to the update helper program, because it will
|
||||||
|
// perform the final steps of the update process, copying the new binary,
|
||||||
|
// deleting the old one, and launching the new binary.
|
||||||
|
let helper_path = std::env::current_exe()?
|
||||||
|
.parent()
|
||||||
|
.context("No parent dir for Zed.exe")?
|
||||||
|
.join("tools\\auto_update_helper.exe");
|
||||||
|
Ok(Some(helper_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_pending_installation() -> bool {
|
pub fn finalize_auto_update_on_quit() {
|
||||||
let Some(installer_path) = std::env::current_exe()
|
let Some(installer_path) = std::env::current_exe()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|p| p.parent().map(|p| p.join("updates")))
|
.and_then(|p| p.parent().map(|p| p.join("updates")))
|
||||||
else {
|
else {
|
||||||
return false;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The installer will create a flag file after it finishes updating
|
// The installer will create a flag file after it finishes updating
|
||||||
let flag_file = installer_path.join("versions.txt");
|
let flag_file = installer_path.join("versions.txt");
|
||||||
if flag_file.exists() {
|
if flag_file.exists()
|
||||||
if let Some(helper) = installer_path
|
&& let Some(helper) = installer_path
|
||||||
.parent()
|
.parent()
|
||||||
.map(|p| p.join("tools\\auto_update_helper.exe"))
|
.map(|p| p.join("tools\\auto_update_helper.exe"))
|
||||||
{
|
{
|
||||||
let _ = std::process::Command::new(helper).spawn();
|
let mut command = std::process::Command::new(helper);
|
||||||
return true;
|
command.arg("--launch");
|
||||||
}
|
command.arg("false");
|
||||||
|
let _ = command.spawn();
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1002,7 +1032,6 @@ mod tests {
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
binary_path: PathBuf::new(),
|
|
||||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||||
};
|
};
|
||||||
let fetched_version = SemanticVersion::new(1, 0, 1);
|
let fetched_version = SemanticVersion::new(1, 0, 1);
|
||||||
|
@ -1024,7 +1053,6 @@ mod tests {
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
binary_path: PathBuf::new(),
|
|
||||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||||
};
|
};
|
||||||
let fetched_version = SemanticVersion::new(1, 0, 2);
|
let fetched_version = SemanticVersion::new(1, 0, 2);
|
||||||
|
@ -1090,7 +1118,6 @@ mod tests {
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
binary_path: PathBuf::new(),
|
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "b".to_string();
|
let fetched_sha = "b".to_string();
|
||||||
|
@ -1112,7 +1139,6 @@ mod tests {
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
binary_path: PathBuf::new(),
|
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "c".to_string();
|
let fetched_sha = "c".to_string();
|
||||||
|
@ -1160,7 +1186,6 @@ mod tests {
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
binary_path: PathBuf::new(),
|
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "b".to_string();
|
let fetched_sha = "b".to_string();
|
||||||
|
@ -1183,7 +1208,6 @@ mod tests {
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
binary_path: PathBuf::new(),
|
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "c".to_string();
|
let fetched_sha = "c".to_string();
|
||||||
|
|
|
@ -37,6 +37,11 @@ mod windows_impl {
|
||||||
pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1;
|
pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1;
|
||||||
pub(crate) const WM_TERMINATE: u32 = WM_USER + 2;
|
pub(crate) const WM_TERMINATE: u32 = WM_USER + 2;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Args {
|
||||||
|
launch: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn run() -> Result<()> {
|
pub(crate) fn run() -> Result<()> {
|
||||||
let helper_dir = std::env::current_exe()?
|
let helper_dir = std::env::current_exe()?
|
||||||
.parent()
|
.parent()
|
||||||
|
@ -51,8 +56,9 @@ mod windows_impl {
|
||||||
log::info!("======= Starting Zed update =======");
|
log::info!("======= Starting Zed update =======");
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
let hwnd = create_dialog_window(rx)?.0 as isize;
|
let hwnd = create_dialog_window(rx)?.0 as isize;
|
||||||
|
let args = parse_args();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = perform_update(app_dir.as_path(), Some(hwnd));
|
let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch.unwrap_or(true));
|
||||||
tx.send(result).ok();
|
tx.send(result).ok();
|
||||||
unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok();
|
unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok();
|
||||||
});
|
});
|
||||||
|
@ -77,6 +83,41 @@ mod windows_impl {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> Args {
|
||||||
|
let mut result = Args { launch: None };
|
||||||
|
if let Some(candidate) = std::env::args().nth(1) {
|
||||||
|
parse_single_arg(&candidate, &mut result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_single_arg(arg: &str, result: &mut Args) {
|
||||||
|
let Some((key, value)) = arg.strip_prefix("--").and_then(|arg| arg.split_once('=')) else {
|
||||||
|
log::error!(
|
||||||
|
"Invalid argument format: '{}'. Expected format: --key=value",
|
||||||
|
arg
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"launch" => parse_launch_arg(value, &mut result.launch),
|
||||||
|
_ => log::error!("Unknown argument: --{}", key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_launch_arg(value: &str, arg: &mut Option<bool>) {
|
||||||
|
match value {
|
||||||
|
"true" => *arg = Some(true),
|
||||||
|
"false" => *arg = Some(false),
|
||||||
|
_ => log::error!(
|
||||||
|
"Invalid value for --launch: '{}'. Expected 'true' or 'false'",
|
||||||
|
value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn show_error(mut content: String) {
|
pub(crate) fn show_error(mut content: String) {
|
||||||
if content.len() > 600 {
|
if content.len() > 600 {
|
||||||
content.truncate(600);
|
content.truncate(600);
|
||||||
|
@ -91,4 +132,47 @@ mod windows_impl {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::windows_impl::{Args, parse_launch_arg, parse_single_arg};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_launch_arg() {
|
||||||
|
let mut arg = None;
|
||||||
|
parse_launch_arg("true", &mut arg);
|
||||||
|
assert_eq!(arg, Some(true));
|
||||||
|
|
||||||
|
let mut arg = None;
|
||||||
|
parse_launch_arg("false", &mut arg);
|
||||||
|
assert_eq!(arg, Some(false));
|
||||||
|
|
||||||
|
let mut arg = None;
|
||||||
|
parse_launch_arg("invalid", &mut arg);
|
||||||
|
assert_eq!(arg, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_single_arg() {
|
||||||
|
let mut args = Args { launch: None };
|
||||||
|
parse_single_arg("--launch=true", &mut args);
|
||||||
|
assert_eq!(args.launch, Some(true));
|
||||||
|
|
||||||
|
let mut args = Args { launch: None };
|
||||||
|
parse_single_arg("--launch=false", &mut args);
|
||||||
|
assert_eq!(args.launch, Some(false));
|
||||||
|
|
||||||
|
let mut args = Args { launch: None };
|
||||||
|
parse_single_arg("--launch=invalid", &mut args);
|
||||||
|
assert_eq!(args.launch, None);
|
||||||
|
|
||||||
|
let mut args = Args { launch: None };
|
||||||
|
parse_single_arg("--launch", &mut args);
|
||||||
|
assert_eq!(args.launch, None);
|
||||||
|
|
||||||
|
let mut args = Args { launch: None };
|
||||||
|
parse_single_arg("--unknown", &mut args);
|
||||||
|
assert_eq!(args.launch, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ pub(crate) fn create_dialog_window(receiver: Receiver<Result<()>>) -> Result<HWN
|
||||||
let hwnd = CreateWindowExW(
|
let hwnd = CreateWindowExW(
|
||||||
WS_EX_TOPMOST,
|
WS_EX_TOPMOST,
|
||||||
class_name,
|
class_name,
|
||||||
windows::core::w!("Zed Editor"),
|
windows::core::w!("Zed"),
|
||||||
WS_VISIBLE | WS_POPUP | WS_CAPTION,
|
WS_VISIBLE | WS_POPUP | WS_CAPTION,
|
||||||
rect.right / 2 - width / 2,
|
rect.right / 2 - width / 2,
|
||||||
rect.bottom / 2 - height / 2,
|
rect.bottom / 2 - height / 2,
|
||||||
|
@ -171,7 +171,7 @@ unsafe extern "system" fn wnd_proc(
|
||||||
&HSTRING::from(font_name),
|
&HSTRING::from(font_name),
|
||||||
);
|
);
|
||||||
let temp = SelectObject(hdc, font.into());
|
let temp = SelectObject(hdc, font.into());
|
||||||
let string = HSTRING::from("Zed Editor is updating...");
|
let string = HSTRING::from("Updating Zed...");
|
||||||
return_if_failed!(TextOutW(hdc, 20, 15, &string).ok());
|
return_if_failed!(TextOutW(hdc, 20, 15, &string).ok());
|
||||||
return_if_failed!(DeleteObject(temp).ok());
|
return_if_failed!(DeleteObject(temp).ok());
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ pub(crate) const JOBS: [Job; 2] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()> {
|
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
|
||||||
let hwnd = hwnd.map(|ptr| HWND(ptr as _));
|
let hwnd = hwnd.map(|ptr| HWND(ptr as _));
|
||||||
|
|
||||||
for job in JOBS.iter() {
|
for job in JOBS.iter() {
|
||||||
|
@ -145,9 +145,11 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = std::process::Command::new(app_dir.join("Zed.exe"))
|
if launch {
|
||||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
let _ = std::process::Command::new(app_dir.join("Zed.exe"))
|
||||||
.spawn();
|
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
log::info!("Update completed successfully");
|
log::info!("Update completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -159,11 +161,11 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_perform_update() {
|
fn test_perform_update() {
|
||||||
let app_dir = std::path::Path::new("C:/");
|
let app_dir = std::path::Path::new("C:/");
|
||||||
assert!(perform_update(app_dir, None).is_ok());
|
assert!(perform_update(app_dir, None, false).is_ok());
|
||||||
|
|
||||||
// Simulate a timeout
|
// Simulate a timeout
|
||||||
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
|
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
|
||||||
let ret = perform_update(app_dir, None);
|
let ret = perform_update(app_dir, None, false);
|
||||||
assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
|
assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22456,7 +22456,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.update(|_, cx| {
|
cx.update(|_, cx| {
|
||||||
workspace::reload(&workspace::Reload::default(), cx);
|
workspace::reload(cx);
|
||||||
});
|
});
|
||||||
assert_language_servers_count(
|
assert_language_servers_count(
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -277,6 +277,8 @@ pub struct App {
|
||||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||||
|
pub(crate) restart_observers: SubscriberSet<(), Handler>,
|
||||||
|
pub(crate) restart_path: Option<PathBuf>,
|
||||||
pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>,
|
pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>,
|
||||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||||
pub(crate) propagate_event: bool,
|
pub(crate) propagate_event: bool,
|
||||||
|
@ -349,6 +351,8 @@ impl App {
|
||||||
keyboard_layout_observers: SubscriberSet::new(),
|
keyboard_layout_observers: SubscriberSet::new(),
|
||||||
global_observers: SubscriberSet::new(),
|
global_observers: SubscriberSet::new(),
|
||||||
quit_observers: SubscriberSet::new(),
|
quit_observers: SubscriberSet::new(),
|
||||||
|
restart_observers: SubscriberSet::new(),
|
||||||
|
restart_path: None,
|
||||||
window_closed_observers: SubscriberSet::new(),
|
window_closed_observers: SubscriberSet::new(),
|
||||||
layout_id_buffer: Default::default(),
|
layout_id_buffer: Default::default(),
|
||||||
propagate_event: true,
|
propagate_event: true,
|
||||||
|
@ -832,8 +836,16 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restarts the application.
|
/// Restarts the application.
|
||||||
pub fn restart(&self, binary_path: Option<PathBuf>) {
|
pub fn restart(&mut self) {
|
||||||
self.platform.restart(binary_path)
|
self.restart_observers
|
||||||
|
.clone()
|
||||||
|
.retain(&(), |observer| observer(self));
|
||||||
|
self.platform.restart(self.restart_path.take())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the path to use when restarting the application.
|
||||||
|
pub fn set_restart_path(&mut self, path: PathBuf) {
|
||||||
|
self.restart_path = Some(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the HTTP client for the application.
|
/// Returns the HTTP client for the application.
|
||||||
|
@ -1466,6 +1478,21 @@ impl App {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the application is about to restart.
|
||||||
|
///
|
||||||
|
/// These callbacks are called before any `on_app_quit` callbacks.
|
||||||
|
pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription {
|
||||||
|
let (subscription, activate) = self.restart_observers.insert(
|
||||||
|
(),
|
||||||
|
Box::new(move |cx| {
|
||||||
|
on_restart(cx);
|
||||||
|
true
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
activate();
|
||||||
|
subscription
|
||||||
|
}
|
||||||
|
|
||||||
/// Register a callback to be invoked when a window is closed
|
/// Register a callback to be invoked when a window is closed
|
||||||
/// The window is no longer accessible at the point this callback is invoked.
|
/// The window is no longer accessible at the point this callback is invoked.
|
||||||
pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription {
|
pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription {
|
||||||
|
|
|
@ -164,6 +164,20 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||||
subscription
|
subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback to be invoked when the application is about to restart.
|
||||||
|
pub fn on_app_restart(
|
||||||
|
&self,
|
||||||
|
mut on_restart: impl FnMut(&mut T, &mut App) + 'static,
|
||||||
|
) -> Subscription
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let handle = self.weak_entity();
|
||||||
|
self.app.on_app_restart(move |cx| {
|
||||||
|
handle.update(cx, |entity, cx| on_restart(entity, cx)).ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Arrange for the given function to be invoked whenever the application is quit.
|
/// Arrange for the given function to be invoked whenever the application is quit.
|
||||||
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
|
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
|
||||||
pub fn on_app_quit<Fut>(
|
pub fn on_app_quit<Fut>(
|
||||||
|
@ -175,20 +189,15 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||||
T: 'static,
|
T: 'static,
|
||||||
{
|
{
|
||||||
let handle = self.weak_entity();
|
let handle = self.weak_entity();
|
||||||
let (subscription, activate) = self.app.quit_observers.insert(
|
self.app.on_app_quit(move |cx| {
|
||||||
(),
|
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
|
||||||
Box::new(move |cx| {
|
async move {
|
||||||
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
|
if let Some(future) = future {
|
||||||
async move {
|
future.await;
|
||||||
if let Some(future) = future {
|
|
||||||
future.await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.boxed_local()
|
}
|
||||||
}),
|
.boxed_local()
|
||||||
);
|
})
|
||||||
activate();
|
|
||||||
subscription
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tell GPUI that this entity has changed and observers of it should be notified.
|
/// Tell GPUI that this entity has changed and observers of it should be notified.
|
||||||
|
|
|
@ -370,9 +370,9 @@ impl Platform for WindowsPlatform {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restart(&self, _: Option<PathBuf>) {
|
fn restart(&self, binary_path: Option<PathBuf>) {
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
let Some(app_path) = self.app_path().log_err() else {
|
let Some(app_path) = binary_path.or(self.app_path().log_err()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let script = format!(
|
let script = format!(
|
||||||
|
|
|
@ -595,7 +595,7 @@ impl TitleBar {
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||||
if auto_updater.read(cx).status().is_updated() {
|
if auto_updater.read(cx).status().is_updated() {
|
||||||
workspace::reload(&Default::default(), cx);
|
workspace::reload(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,6 +224,8 @@ actions!(
|
||||||
ResetActiveDockSize,
|
ResetActiveDockSize,
|
||||||
/// Resets all open docks to their default sizes.
|
/// Resets all open docks to their default sizes.
|
||||||
ResetOpenDocksSize,
|
ResetOpenDocksSize,
|
||||||
|
/// Reloads the application
|
||||||
|
Reload,
|
||||||
/// Saves the current file with a new name.
|
/// Saves the current file with a new name.
|
||||||
SaveAs,
|
SaveAs,
|
||||||
/// Saves without formatting.
|
/// Saves without formatting.
|
||||||
|
@ -340,14 +342,6 @@ pub struct CloseInactiveTabsAndPanes {
|
||||||
#[action(namespace = workspace)]
|
#[action(namespace = workspace)]
|
||||||
pub struct SendKeystrokes(pub String);
|
pub struct SendKeystrokes(pub String);
|
||||||
|
|
||||||
/// Reloads the active item or workspace.
|
|
||||||
#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema, Action)]
|
|
||||||
#[action(namespace = workspace)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct Reload {
|
|
||||||
pub binary_path: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
project_symbols,
|
project_symbols,
|
||||||
[
|
[
|
||||||
|
@ -555,8 +549,8 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||||
toast_layer::init(cx);
|
toast_layer::init(cx);
|
||||||
history_manager::init(cx);
|
history_manager::init(cx);
|
||||||
|
|
||||||
cx.on_action(Workspace::close_global);
|
cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx));
|
||||||
cx.on_action(reload);
|
cx.on_action(|_: &Reload, cx| reload(cx));
|
||||||
|
|
||||||
cx.on_action({
|
cx.on_action({
|
||||||
let app_state = Arc::downgrade(&app_state);
|
let app_state = Arc::downgrade(&app_state);
|
||||||
|
@ -2184,7 +2178,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_global(_: &CloseWindow, cx: &mut App) {
|
pub fn close_global(cx: &mut App) {
|
||||||
cx.defer(|cx| {
|
cx.defer(|cx| {
|
||||||
cx.windows().iter().find(|window| {
|
cx.windows().iter().find(|window| {
|
||||||
window
|
window
|
||||||
|
@ -7642,7 +7636,7 @@ pub fn join_in_room_project(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reload(reload: &Reload, cx: &mut App) {
|
pub fn reload(cx: &mut App) {
|
||||||
let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
|
let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
|
||||||
let mut workspace_windows = cx
|
let mut workspace_windows = cx
|
||||||
.windows()
|
.windows()
|
||||||
|
@ -7669,7 +7663,6 @@ pub fn reload(reload: &Reload, cx: &mut App) {
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let binary_path = reload.binary_path.clone();
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
if let Some(prompt) = prompt {
|
if let Some(prompt) = prompt {
|
||||||
let answer = prompt.await?;
|
let answer = prompt.await?;
|
||||||
|
@ -7688,8 +7681,7 @@ pub fn reload(reload: &Reload, cx: &mut App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cx.update(|cx| cx.restart())
|
||||||
cx.update(|cx| cx.restart(binary_path))
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,16 +201,6 @@ pub fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is a pending installer
|
|
||||||
// If there is, run the installer and exit
|
|
||||||
// And we don't want to run the installer if we are not the first instance
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let is_first_instance = crate::zed::windows_only_instance::is_first_instance();
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if is_first_instance && auto_update::check_pending_installation() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.dump_all_actions {
|
if args.dump_all_actions {
|
||||||
dump_all_gpui_actions();
|
dump_all_gpui_actions();
|
||||||
return;
|
return;
|
||||||
|
@ -283,30 +273,27 @@ pub fn main() {
|
||||||
|
|
||||||
let (open_listener, mut open_rx) = OpenListener::new();
|
let (open_listener, mut open_rx) = OpenListener::new();
|
||||||
|
|
||||||
let failed_single_instance_check =
|
let failed_single_instance_check = if *db::ZED_STATELESS
|
||||||
if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
|
|| *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
|
||||||
false
|
{
|
||||||
} else {
|
false
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
} else {
|
||||||
{
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
|
{
|
||||||
}
|
crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
!crate::zed::windows_only_instance::handle_single_instance(
|
!crate::zed::windows_only_instance::handle_single_instance(open_listener.clone(), &args)
|
||||||
open_listener.clone(),
|
}
|
||||||
&args,
|
|
||||||
is_first_instance,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
use zed::mac_only_instance::*;
|
use zed::mac_only_instance::*;
|
||||||
ensure_only_instance() != IsOnlyInstance::Yes
|
ensure_only_instance() != IsOnlyInstance::Yes
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if failed_single_instance_check {
|
if failed_single_instance_check {
|
||||||
println!("zed is already running");
|
println!("zed is already running");
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -25,7 +25,8 @@ use windows::{
|
||||||
|
|
||||||
use crate::{Args, OpenListener, RawOpenRequest};
|
use crate::{Args, OpenListener, RawOpenRequest};
|
||||||
|
|
||||||
pub fn is_first_instance() -> bool {
|
#[inline]
|
||||||
|
fn is_first_instance() -> bool {
|
||||||
unsafe {
|
unsafe {
|
||||||
CreateMutexW(
|
CreateMutexW(
|
||||||
None,
|
None,
|
||||||
|
@ -37,7 +38,8 @@ pub fn is_first_instance() -> bool {
|
||||||
unsafe { GetLastError() != ERROR_ALREADY_EXISTS }
|
unsafe { GetLastError() != ERROR_ALREADY_EXISTS }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_single_instance(opener: OpenListener, args: &Args, is_first_instance: bool) -> bool {
|
pub fn handle_single_instance(opener: OpenListener, args: &Args) -> bool {
|
||||||
|
let is_first_instance = is_first_instance();
|
||||||
if is_first_instance {
|
if is_first_instance {
|
||||||
// We are the first instance, listen for messages sent from other instances
|
// We are the first instance, listen for messages sent from other instances
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue