Files
galaxy/app/src/autoupdate/windows.rs
T
Ryan Ward 11152f2f40 Galaxy v1.0.0: Remove warp-channel-config, rebrand bins/icons/settings, remove teams from Drive
- Remove warp-channel-config dependency; all bin targets hardcode ChannelConfig inline
- Rename bin targets: galaxy-oss, galaxy-local, galaxy-stable, galaxy-dev, galaxy-preview
- Rename config dir from .galaxy-ai to .galaxy
- Add Galaxy app icon (512x512 rounded PNG + SVG logo)
- Replace settings gear icon with Stars (sparkle) icon
- Remove lightbulb/resource center button from header toolbar
- Add Galaxy logo SVG to Settings > About page
- Rename Galaxify -> Galaxyize across all UI strings
- Remove Create Team / Join Team sections from Galaxy Drive
- Fix missing => in OpenRichInput match arms
- Clean up unused imports and warnings
- Update Cargo.toml bundle metadata with Samsung branding
- Re-enable code review, project explorer, global search in settings
2026-05-13 01:29:25 -05:00

265 lines
9.6 KiB
Rust

use crate::server::telemetry::TelemetryEvent;
use anyhow::anyhow;
use anyhow::{bail, Result};
use channel_versions::VersionInfo;
use command::blocking::Command;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::fs::File;
use std::path::PathBuf;
use std::sync::Arc;
use std::{fs, io};
use std::{io::Write as _, time::Duration};
use tempfile::TempPath;
use galaxy_core::channel::{Channel, ChannelState};
use galaxyui::AppContext;
use super::{release_assets_directory_url, DownloadReady};
use crate::util::windows::install_dir;
lazy_static! {
/// The path to the temporary file that stores the installer for the new update.
static ref INSTALLER_PATH: Arc<Mutex<Option<TempPath>>> = Default::default();
}
/// Download the Inno Setup install wizard, the same one users run on the first Warp install, and
/// place it into the "data dir".
pub(super) async fn download_update_and_cleanup(
version_info: &VersionInfo,
_update_id: &str,
client: &http_client::Client,
) -> Result<DownloadReady> {
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(600);
let installer_file_name = installer_file_name()?;
let url = format!(
"{}/{}",
release_assets_directory_url(ChannelState::channel(), &version_info.version),
installer_file_name
);
// Create a temporary file that we'll write the download into.
let mut already_exists = false;
let mut new_installer = tempfile::Builder::new()
.rand_bytes(0)
.suffix(&format!("{}-{}", version_info.version, installer_file_name))
.make(|path| {
already_exists = path.is_file();
if already_exists {
File::open(path)
} else {
File::create(path)
}
})?;
if !already_exists {
log::info!("Downloading {url} to {}...", new_installer.path().display());
let response = client
.get(&url)
.timeout(DOWNLOAD_TIMEOUT)
.send()
.await?
.error_for_status()?;
new_installer
.as_file_mut()
.write_all(&response.bytes().await?)?;
}
*INSTALLER_PATH.lock() = Some(new_installer.into_temp_path());
Ok(DownloadReady::Yes)
}
const UPDATE_LOG_FILENAME: &str = "warp_update.log";
fn autoupdate_log_file() -> Result<PathBuf> {
galaxy_logging::log_directory().map(|dir| dir.join(UPDATE_LOG_FILENAME))
}
/// Checks the autoupdate log file from a previous update attempt.
/// Sends telemetry for specific known issues, and sends a Sentry event if errors are found.
/// The log file is renamed after processing to avoid duplicate reports on subsequent launches.
pub(super) fn check_and_report_update_errors(ctx: &mut AppContext) {
let log_path = match autoupdate_log_file() {
Ok(path) => path,
Err(e) => {
log::warn!("Failed to determine autoupdate log file path: {e:#}");
return;
}
};
// Inno Setup logs use the system's active codepage (often Windows-1252), not UTF-8.
// We read as raw bytes to avoid silently skipping non-UTF-8 log files.
let contents = match fs::read(&log_path) {
Ok(contents) => contents,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
log::info!("No autoupdate logs found");
return;
}
Err(e) => {
log::warn!("Failed to read autoupdate log file: {e:#}");
return;
}
};
let contents_lowercase = contents.to_ascii_lowercase();
let has_unable_to_close = memchr::memmem::find(
&contents_lowercase,
b"setup was unable to automatically close all applications",
)
.is_some();
if has_unable_to_close {
crate::send_telemetry_sync_from_app_ctx!(
TelemetryEvent::AutoupdateUnableToCloseApplications,
ctx
);
}
let has_file_in_use = memchr::memmem::find(
&contents_lowercase,
b"the process cannot access the file because it is being used by another process",
)
.is_some();
if has_file_in_use {
crate::send_telemetry_sync_from_app_ctx!(TelemetryEvent::AutoupdateFileInUse, ctx);
}
// Fired when the mutex polling loop timed out and a force-kill was attempted.
let has_mutex_timeout =
memchr::memmem::find(&contents_lowercase, b"warp mutex still held after timeout").is_some();
if has_mutex_timeout {
crate::send_telemetry_sync_from_app_ctx!(TelemetryEvent::AutoupdateMutexTimeout, ctx);
}
// Fired when taskkill returned non-zero after the mutex timeout.
let has_forcekill_failed =
memchr::memmem::find(&contents_lowercase, b"force-kill failed for").is_some();
if has_forcekill_failed {
crate::send_telemetry_sync_from_app_ctx!(TelemetryEvent::AutoupdateForcekillFailed, ctx);
}
#[cfg(feature = "crash_reporting")]
{
use sentry::protocol::{Attachment, AttachmentType};
// Patterns for known benign errors that should not trigger Sentry reporting.
const IGNOREABLE_ERRORS: &[&[u8]] = &[
// User running out of disk space is not an error we need concern ourselves with.
// This message occurs after "An error occurred while trying to copy a file:"
b"there is not enough space on the disk",
// Recent Inno Setup versions try to enable a security feature which is unavailable on
// Windows 10 versions prior to 22H2 and this call fails. The failure is benign.
b"setprocessmitigationpolicy failed with error code 87",
];
let mut error_count = memchr::memmem::find_iter(&contents_lowercase, b"error").count();
for pattern in IGNOREABLE_ERRORS {
let ignoreable_count = memchr::memmem::find_iter(&contents_lowercase, pattern).count();
error_count = error_count.saturating_sub(ignoreable_count);
}
if error_count > 0 {
log::warn!("Autoupdate log file contains errors; reporting to Sentry");
let attachment = Attachment {
buffer: contents,
filename: UPDATE_LOG_FILENAME.to_string(),
ty: Some(AttachmentType::Attachment),
..Default::default()
};
sentry::with_scope(
|scope| {
scope.add_attachment(attachment);
},
|| sentry::capture_message("Windows auto-update error", sentry::Level::Error),
);
}
}
// Rename the log file to avoid duplicate reports on subsequent launches.
// We keep the file around so the user can still view it or attach it to a GitHub issue.
let reported_path = log_path.with_extension("log.reported");
if let Err(e) = fs::rename(&log_path, &reported_path) {
log::warn!("Failed to rename autoupdate log file after reporting: {e:#}");
}
}
pub(super) fn relaunch() -> Result<()> {
let install_dir = install_dir()?;
let Some(installer_path) = INSTALLER_PATH.lock().take() else {
bail!("No installer path");
};
let log_arg = match autoupdate_log_file() {
Ok(dir) => format!("/LOG={}", dir.display()),
Err(e) => {
log::warn!("Failed to determine location for autoupdate logs: {e:#}");
"/LOG".to_string()
}
};
// The Inno Setup install wizard will run without user input. It will re-launch Warp after
// installing the update files.
// https://jrsoftware.org/ishelp/index.php?topic=setupcmdline
Command::new(&installer_path)
.args([
// Skip asking the user to confirm.
"/SP-",
// Do not prompt the user for anything. Note that we do not use "VERYSILENT" so that a
// progress bar is still shown. This is useful since the update process may take a few
// seconds.
"/SILENT",
// Do not provide a cancel button on the progress bar page.
"/NOCANCEL",
// Indicate that restarting Windows is not necessary.
"/NORESTART",
&log_arg,
"/update=1",
// Do not forcibly kill Warp via RestartManager. The installer will wait for
// Warp to exit naturally by polling the single-instance mutex instead.
"/NOCLOSEAPPLICATIONS",
&format!("/DIR={}", install_dir.display()),
])
.spawn()?;
// DEV ONLY: Sleep after spawning the installer so this process is still alive
// when Inno Setup tries to overwrite files. This reliably reproduces the
// auto-update race condition (APP-3702) for testing.
if matches!(ChannelState::channel(), Channel::Dev) {
log::info!("DEV: Sleeping 10s after spawning installer to reproduce update race");
std::thread::sleep(Duration::from_secs(10));
}
Ok(())
}
fn installer_file_name() -> Result<String> {
let app_name_prefix = app_name_prefix(ChannelState::channel());
// For example, on arm64 this is WarpSetup-arm64.exe and on x64 this is
// WarpSetup.exe.
if cfg!(target_arch = "aarch64") {
Ok(format!("{app_name_prefix}Setup-arm64.exe"))
} else if cfg!(target_arch = "x86_64") {
Ok(format!("{app_name_prefix}Setup.exe"))
} else {
Err(anyhow!(
"Could not construct setup file name for unsupported architecture"
))
}
}
fn app_name_prefix(channel: Channel) -> &'static str {
match channel {
Channel::Stable => "Galaxy",
Channel::Preview => "GalaxyPreview",
Channel::Local => "galaxy-ai",
Channel::Integration => "integration",
Channel::Dev => "GalaxyDev",
Channel::Oss => "galaxy-ai-oss",
}
}