first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+6 -7
View File
@@ -1,16 +1,15 @@
use std::{iter, sync::Arc};
use std::iter;
use std::sync::Arc;
use anyhow::Result;
use channel_versions::{Changelog, ChannelVersions};
use rand::{distributions::Alphanumeric, thread_rng, Rng as _};
use crate::{
channel::{Channel, ChannelState},
server::server_api::ServerApi,
};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng as _};
use super::channel_versions::fetch_channel_versions;
use super::release_assets_directory_url;
use crate::channel::{Channel, ChannelState};
use crate::server::server_api::ServerApi;
pub async fn get_current_changelog(server_api: Arc<ServerApi>) -> Result<Option<Changelog>> {
let rand: String = {
+6 -6
View File
@@ -1,13 +1,13 @@
use std::{env, fs::read_to_string, sync::Arc};
use std::env;
use std::fs::read_to_string;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use channel_versions::ChannelVersions;
use crate::{
channel::{Channel, ChannelState},
report_error,
server::server_api::{ServerApi, FETCH_CHANNEL_VERSIONS_TIMEOUT},
};
use crate::channel::{Channel, ChannelState};
use crate::report_error;
use crate::server::server_api::{ServerApi, FETCH_CHANNEL_VERSIONS_TIMEOUT};
// Fetches channel versions asynchronously from the Warp server. If the Warp server request fails,
// then fetches from GCP JSON storage as a fallback.
+21 -10
View File
@@ -8,11 +8,9 @@ use galaxy_terminal::shell::ShellType;
use galaxyui::ViewContext;
use instant::Duration;
use super::{release_assets_directory_url, DownloadReady, ReadyForRelaunch};
use crate::workspace::Workspace;
use super::release_assets_directory_url;
use super::{DownloadReady, ReadyForRelaunch};
lazy_static::lazy_static! {
/// Stores the path to the current executable.
///
@@ -161,18 +159,14 @@ mod appimage {
}
mod package_manager {
use galaxyui::{
elements::{Container, FormattedTextElement, HighlightedHyperlink},
Element, SingletonEntity as _,
};
use markdown_parser::{
FormattedText, FormattedTextFragment, FormattedTextHeader, FormattedTextLine,
};
use galaxyui::elements::{Container, FormattedTextElement, HighlightedHyperlink};
use galaxyui::{Element, SingletonEntity as _};
use crate::appearance::Appearance;
use super::*;
pub struct AutoupdateContextBlock {
package_manager: PackageManager,
hyperlink: HighlightedHyperlink,
@@ -626,6 +620,23 @@ fn is_pacman_signing_key_installed() -> bool {
return false;
};
// After parsing the pub: line, also check validity field (index 1 = validity)
let fields: Vec<&str> = stdout
.lines()
.find(|line| line.starts_with("pub:"))
.map(|line| line.split(':').collect())
.unwrap_or_default();
// Field index 1 = validity: 'f' (full), 'u' (ultimate) are valid;
// 'e' (expired), 'r' (revoked), '-', 'q' = invalid
let validity = fields
.get(1)
.and_then(|field| field.chars().next())
.unwrap_or('\0');
if !matches!(validity, 'f' | 'u') {
return false; // Force key reconfiguration
}
// Parse the expiry timestamp from the pub: line (field 7, 1-indexed).
let Some(expiry_field) = stdout
.lines()
@@ -671,5 +682,5 @@ fn repo_name(channel: Channel) -> String {
}
#[cfg(test)]
#[path = "linux_test.rs"]
#[path = "linux_tests.rs"]
mod tests;
+19 -23
View File
@@ -1,35 +1,31 @@
#![allow(deprecated)]
use command::{blocking, r#async::Command};
use futures::{StreamExt, TryStreamExt as _};
use futures_lite::future;
use galaxy_core::safe_error;
use instant::Instant;
use std::{
env,
ffi::{CString, OsString},
fs,
os::unix::{ffi::OsStrExt as _, fs::MetadataExt, io::AsRawFd as _},
path::{Path, PathBuf},
str,
time::Duration,
};
use std::ffi::{CString, OsString};
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd as _;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{env, fs, str};
use anyhow::{anyhow, bail, ensure, Context, Result};
use channel_versions::VersionInfo;
use command::blocking;
use command::r#async::Command;
use futures::{StreamExt, TryStreamExt as _};
use futures_lite::future;
use instant::Instant;
use nix::errno::Errno;
use nix::unistd::{fchown, getgid, getuid};
use galaxy_core::macos::get_bundle_path;
use galaxy_core::safe_error;
use galaxyui::{AppContext, ModelContext, SingletonEntity};
use nix::unistd::{fchown, getgid};
use nix::{errno::Errno, unistd::getuid};
use crate::{
appearance::AppearanceManager,
autoupdate::{AutoupdateStage, AutoupdateState},
channel::{Channel, ChannelState},
safe_info,
};
use super::{release_assets_directory_url, DownloadReady};
use crate::appearance::AppearanceManager;
use crate::autoupdate::{AutoupdateStage, AutoupdateState};
use crate::channel::{Channel, ChannelState};
use crate::safe_info;
// Relative path to the directory containing old executables from before an autoupdate.
//
+80 -16
View File
@@ -8,15 +8,10 @@ mod mac;
#[cfg(windows)]
mod windows;
use crate::features::FeatureFlag;
use crate::send_telemetry_sync_from_app_ctx;
use crate::server::server_api::ServerApi;
use crate::server::telemetry::TelemetryEvent;
use crate::workspace::Workspace;
use crate::{
channel::Channel, report_if_error, send_telemetry_from_ctx, server::datetime_ext::DateTimeExt,
ChannelState,
};
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use ::channel_versions::{ParsedVersion, VersionInfo};
use anyhow::{anyhow, Context as _, Result};
use chrono::{DateTime, FixedOffset, NaiveDate};
@@ -31,12 +26,19 @@ use galaxyui::{
};
use galaxyui::{Entity, ModelContext, SingletonEntity, ViewContext};
use rand::Rng as _;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use galaxyui::accessibility::{AccessibilityContent, WarpA11yRole};
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity, ViewContext};
pub use self::changelog::get_current_changelog;
use self::channel_versions::fetch_channel_versions;
use crate::channel::Channel;
use crate::features::FeatureFlag;
use crate::server::server_api::ServerApi;
use crate::server::telemetry::TelemetryEvent;
use crate::workspace::Workspace;
use crate::{
report_if_error, send_telemetry_from_ctx, send_telemetry_sync_from_app_ctx, ChannelState,
};
/// A successfully downloaded and unpacked target update.
#[derive(Clone, Debug)]
@@ -113,6 +115,10 @@ pub struct AutoupdateState {
/// but to queue them instead.
request_queue: VecDeque<RequestType>,
server_api: Arc<ServerApi>,
/// Whether the polling loop has been explicitly started. Requests are silently queued but not
/// executed until `start_polling` is called. This ensures no version-check requests are made
/// before onboarding completes.
polling_started: bool,
}
impl AutoupdateState {
@@ -123,6 +129,7 @@ impl AutoupdateState {
stage: AutoupdateStage::default(),
downloaded_update: None,
request_queue: VecDeque::new(),
polling_started: false,
}
}
@@ -130,6 +137,32 @@ impl AutoupdateState {
ctx.add_singleton_model(move |_ctx| Self::new(server_api));
}
/// Start the autoupdate polling loop. Idempotent: subsequent calls are no-ops.
///
/// Must be called explicitly once onboarding (if any) has completed. For returning users
/// who bypass onboarding, this should be called during app startup.
pub fn start_polling(&mut self, ctx: &mut ModelContext<Self>) {
if self.polling_started {
return;
}
if FeatureFlag::Autoupdate.is_enabled() && AppExecutionMode::as_ref(ctx).can_autoupdate() {
log::info!("Starting autoupdate polling loop");
self.polling_started = true;
// Initiate the polling loop.
self.poll_for_update(ctx);
// Queue a possible update check when the app gets activated, i.e. focused.
let state_handle = WindowManager::handle(ctx);
ctx.subscribe_to_model(&state_handle, |me, _, event, ctx| {
let windowing::StateEvent::ValueChanged { current, previous } = event;
if previous.stage == ApplicationStage::Inactive
&& current.stage == ApplicationStage::Active
{
me.enqueue_request(RequestType::DailyCheck, ctx);
}
});
}
}
/// Check if any requests are pending. If there are and we're ready to submit a new request,
/// run it.
fn try_execute_request(&mut self, ctx: &mut ModelContext<Self>) {
@@ -140,12 +173,43 @@ impl AutoupdateState {
/// Check if there are any requests in the queue. Return the next one, but only if there isn't
/// already a request in-flight.
fn get_next_request(&mut self, _ctx: &mut ModelContext<Self>) -> Option<RequestType> {
fn get_next_request(&mut self, ctx: &mut ModelContext<Self>) -> Option<RequestType> {
// WASM cannot apply updates, so no request type should ever contact the server.
if cfg!(target_family = "wasm") {
return None;
}
// Don't execute any requests until polling has been explicitly started (i.e. onboarding
// has completed). Requests enqueued before that point are silently deferred.
if !self.polling_started {
return None;
}
if !self.should_start_update_check() {
return None;
}
while let Some(request) = self.request_queue.pop_front() {
match request {
RequestType::ManualCheck => return Some(request),
RequestType::Poll | RequestType::DailyCheck => {
if AppExecutionMode::as_ref(ctx).can_autoupdate() {
return Some(request);
}
}
}
}
None
}
/// After queueing the request, immediately try executing it.
fn enqueue_request(&mut self, request_type: RequestType, ctx: &mut ModelContext<Self>) {
// WASM cannot execute any update requests; skip enqueuing entirely so the
// queue never grows with work that can never be consumed.
if cfg!(target_family = "wasm") {
return;
}
self.request_queue.push_back(request_type);
self.try_execute_request(ctx);
}
@@ -190,7 +254,7 @@ impl AutoupdateState {
/// The caller is responsible for checking that we _should_ check for an update. Generally, the
/// only caller should be [`Self::try_execute_request`].
fn check_for_update(&mut self, request_type: RequestType, ctx: &mut ModelContext<Self>) {
let current_date = DateTime::now().date_naive();
let current_date = chrono::Local::now().date_naive();
let is_daily = self.should_make_daily_request(
request_type,
&current_date,
@@ -335,7 +399,7 @@ impl AutoupdateState {
ctx: &mut ModelContext<AutoupdateState>,
) {
if is_daily && version.is_ok() {
self.last_successful_daily_update_check = Some(DateTime::now());
self.last_successful_daily_update_check = Some(chrono::Local::now().fixed_offset());
}
// If one update was already applied, we cannot apply another.
@@ -1101,5 +1165,5 @@ fn release_assets_directory_url(channel: Channel, version: &str) -> String {
}
#[cfg(test)]
#[path = "mod_test.rs"]
#[path = "mod_tests.rs"]
mod tests;
@@ -1,16 +1,11 @@
use chrono::{Local, TimeZone};
use galaxy_core::execution_mode::{AppExecutionMode, ExecutionMode};
use galaxyui::{App, ModelHandle, ReadModel, UpdateModel};
use crate::{
auth::{AuthManager, AuthStateProvider},
server::{
server_api::ServerApiProvider, telemetry::context_provider::AppTelemetryContextProvider,
},
};
use galaxy_core::execution_mode::{AppExecutionMode, ExecutionMode};
use super::*;
use crate::auth::{AuthManager, AuthStateProvider};
use crate::server::server_api::ServerApiProvider;
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
fn initialize_app(app: &mut App) -> ModelHandle<AutoupdateState> {
let server_api_provider = app.add_singleton_model(|_| ServerApiProvider::new_for_test());
@@ -32,6 +27,8 @@ fn test_queueing_behavior() {
let autoupdate_state = initialize_app(&mut app);
app.update_model(&autoupdate_state, |autoupdate, ctx| {
// Simulate post-onboarding state so the polling_started gate doesn't interfere.
autoupdate.polling_started = true;
assert_eq!(autoupdate.get_next_request(ctx), None);
autoupdate.request_queue.push_back(RequestType::DailyCheck);
assert_eq!(autoupdate.request_queue.len(), 1);
@@ -76,6 +73,8 @@ fn test_queue_behavior_sdk_mode() {
let autoupdate_state = initialize_app(&mut app);
app.update_model(&autoupdate_state, |autoupdate, ctx| {
// Set polling_started so the onboarding gate doesn't mask SDK-mode filtering logic.
autoupdate.polling_started = true;
assert_eq!(autoupdate.get_next_request(ctx), None);
autoupdate.request_queue.push_back(RequestType::DailyCheck);
assert_eq!(autoupdate.request_queue.len(), 1);
@@ -122,6 +121,9 @@ fn test_cli_sdk_mode_prevents_autoupdate_polling() {
let autoupdate_state = initialize_app(&mut app);
app.update_model(&autoupdate_state, |autoupdate, ctx| {
// Set polling_started so the onboarding gate doesn't interfere with
// SDK-mode filtering, which is what this test exercises.
autoupdate.polling_started = true;
// Simulate what the autoupdate poll loop would do: call poll_for_update.
// In SDK mode, the Poll request enqueued by poll_for_update must be discarded
// immediately without initiating a check (stage stays NoUpdateAvailable).
+84 -22
View File
@@ -1,20 +1,21 @@
use crate::server::telemetry::TelemetryEvent;
use anyhow::anyhow;
use anyhow::{bail, Result};
use std::fs::File;
use std::io::Write as _;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::{fs, io};
use anyhow::{anyhow, bail, Result};
use channel_versions::VersionInfo;
use command::blocking::Command;
use galaxy_core::channel::{Channel, ChannelState};
use galaxyui::AppContext;
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 super::{release_assets_directory_url, DownloadReady};
use crate::server::telemetry::TelemetryEvent;
use crate::util::windows::install_dir;
lazy_static! {
@@ -44,7 +45,9 @@ pub(super) async fn download_update_and_cleanup(
.rand_bytes(0)
.suffix(&format!("{}-{}", version_info.version, installer_file_name))
.make(|path| {
already_exists = path.is_file();
// Treat a 0-byte file as missing.
let non_empty = path.metadata().map(|m| m.len() > 0).unwrap_or(false);
already_exists = non_empty;
if already_exists {
File::open(path)
} else {
@@ -77,6 +80,47 @@ fn autoupdate_log_file() -> Result<PathBuf> {
galaxy_logging::log_directory().map(|dir| dir.join(UPDATE_LOG_FILENAME))
}
fn parse_exit_code_after_marker(contents_lowercase: &[u8], failed_marker: &[u8]) -> Option<i32> {
const EXIT_CODE_MARKER: &[u8] = b"exit code: ";
let failed_pos = memchr::memmem::find(contents_lowercase, failed_marker)?;
let after_failed = &contents_lowercase[failed_pos..];
let marker_pos = memchr::memmem::find(after_failed, EXIT_CODE_MARKER)?;
let after_marker = &after_failed[marker_pos + EXIT_CODE_MARKER.len()..];
let sign_len = if after_marker.first() == Some(&b'-') {
1
} else {
0
};
let digit_len = after_marker[sign_len..]
.iter()
.take_while(|b| b.is_ascii_digit())
.count();
if digit_len == 0 {
return None;
}
std::str::from_utf8(&after_marker[..sign_len + digit_len])
.ok()?
.parse()
.ok()
}
/// Parses the taskkill exit code from an Inno Setup log containing a
/// "force-kill failed for" line. Returns `None` if no such line is found or
/// the exit code cannot be parsed.
fn parse_forcekill_exit_code(contents_lowercase: &[u8]) -> Option<i32> {
const FAILED_MARKER: &[u8] = b"force-kill failed for";
parse_exit_code_after_marker(contents_lowercase, FAILED_MARKER)
}
/// Parses the PowerShell exit code from an Inno Setup log containing a
/// "minidump-server cleanup failed" line. Returns `None` if no such line is
/// found or the exit code cannot be parsed.
fn parse_minidump_cleanup_exit_code(contents_lowercase: &[u8]) -> Option<i32> {
const FAILED_MARKER: &[u8] = b"minidump-server cleanup failed";
parse_exit_code_after_marker(contents_lowercase, FAILED_MARKER)
}
/// 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.
@@ -134,10 +178,24 @@ pub(super) fn check_and_report_update_errors(ctx: &mut AppContext) {
}
// 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);
// Exit code 128 means "no matching process found" — the process was already
// gone when taskkill ran — so suppress that harmless race condition.
if let Some(exit_code) = parse_forcekill_exit_code(&contents_lowercase) {
if exit_code != 128 {
crate::send_telemetry_sync_from_app_ctx!(
TelemetryEvent::AutoupdateForcekillFailed { exit_code },
ctx
);
}
}
// Fired when the PowerShell cleanup of the orphaned minidump server process
// returned a non-zero exit code.
if let Some(exit_code) = parse_minidump_cleanup_exit_code(&contents_lowercase) {
crate::send_telemetry_sync_from_app_ctx!(
TelemetryEvent::AutoupdateMinidumpCleanupFailed { exit_code },
ctx
);
}
#[cfg(feature = "crash_reporting")]
@@ -152,6 +210,10 @@ pub(super) fn check_and_report_update_errors(ctx: &mut AppContext) {
// 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",
// Bundled skill files whose names contain "error" appear in "Dest filename:" log lines
// and produce false positives.
b"error-codes.md",
b"error-recovery.md",
];
let mut error_count = memchr::memmem::find_iter(&contents_lowercase, b"error").count();
@@ -189,7 +251,11 @@ pub(super) fn check_and_report_update_errors(ctx: &mut AppContext) {
pub(super) fn relaunch() -> Result<()> {
let install_dir = install_dir()?;
let Some(installer_path) = INSTALLER_PATH.lock().take() else {
let Some(installer_path) = INSTALLER_PATH
.lock()
.as_ref()
.map(|path| path.to_path_buf())
else {
bail!("No installer path");
};
@@ -225,14 +291,6 @@ pub(super) fn relaunch() -> Result<()> {
])
.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(())
}
@@ -262,3 +320,7 @@ fn app_name_prefix(channel: Channel) -> &'static str {
Channel::Oss => "galaxy-ai-oss",
}
}
#[cfg(test)]
#[path = "windows_tests.rs"]
mod tests;
+72
View File
@@ -0,0 +1,72 @@
use super::{parse_forcekill_exit_code, parse_minidump_cleanup_exit_code};
fn log(line: &str) -> Vec<u8> {
line.to_ascii_lowercase().into_bytes()
}
#[test]
fn parses_typical_failure() {
// Typical Inno Setup log line for a real taskkill failure (e.g. access denied).
let contents = log("force-kill failed for dev.exe (exit code: 1)");
assert_eq!(parse_forcekill_exit_code(&contents), Some(1));
}
#[test]
fn parses_exit_code_128() {
// Exit code 128 = "no matching process" — the harmless race condition.
let contents = log("force-kill failed for dev.exe (exit code: 128)");
assert_eq!(parse_forcekill_exit_code(&contents), Some(128));
}
#[test]
fn parses_exit_code_embedded_in_multiline_log() {
// The pattern appears after several unrelated log lines.
let contents = log(
"[2024-01-01 00:00:00] Warp mutex still held after timeout; force-killing remaining processes.\n\
[2024-01-01 00:00:01] force-kill failed for warp.exe (exit code: 5)\n\
[2024-01-01 00:00:02] Installation complete.",
);
assert_eq!(parse_forcekill_exit_code(&contents), Some(5));
}
#[test]
fn returns_none_when_no_forcekill_line() {
// Log contains no force-kill attempt at all.
let contents = log("warp mutex still held after timeout; proceeding.");
assert_eq!(parse_forcekill_exit_code(&contents), None);
}
#[test]
fn returns_none_when_forcekill_marker_present_but_no_exit_code() {
// Malformed log line — marker present but no "exit code:" substring.
let contents = log("force-kill failed for dev.exe");
assert_eq!(parse_forcekill_exit_code(&contents), None);
}
#[test]
fn returns_none_when_exit_code_has_no_digits() {
// Malformed log line — marker present but no parseable integer.
let contents = log("force-kill failed for dev.exe (exit code: -)");
assert_eq!(parse_forcekill_exit_code(&contents), None);
}
#[test]
fn parses_signed_minidump_cleanup_exit_code() {
// PowerShell can report signed HRESULT values for cleanup failures.
let contents = log("minidump-server cleanup failed (exit code: -2147024891)");
assert_eq!(
parse_minidump_cleanup_exit_code(&contents),
Some(-2147024891)
);
}
#[test]
fn parses_unsigned_minidump_cleanup_exit_code() {
let contents = log("minidump-server cleanup failed (exit code: 5)");
assert_eq!(parse_minidump_cleanup_exit_code(&contents), Some(5));
}
#[test]
fn returns_none_for_empty_log() {
assert_eq!(parse_forcekill_exit_code(b""), None);
assert_eq!(parse_minidump_cleanup_exit_code(b""), None);
}