first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
¤t_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).
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user