mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-09-10 04:19:35 +00:00
Both the in-app restart and the auto-update install previously shelled out to cmd.exe so the new instance could wait for the old one to release the singleton mutex and the locked exe file. On some Windows configurations the `start ""` inside `cmd /c ...` can flash a console window despite CREATE_NO_WINDOW + DETACHED_PROCESS flags. The replacement spawns the child binary directly via CreateProcessW; since the main exe is built with windows_subsystem = "windows", no console is ever allocated. - New `src/update/handoff.rs` exposes `spawn_detached`, `wait_for_parent_exit`, and `cleanup_stale_old_exes`. - New CLI flags `--wait-pid <pid>` and `--updated-to <version>` parsed early in `main`; the child waits up to 5s on the parent PID via OpenProcess+WaitForSingleObject before falling through to a 3s mutex-acquisition retry. - `restart_app` and `install::begin` both spawn detached children using the new helper. - Update install now uses MoveFileExW twice (rename running exe sideways, then move staged exe into place with MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED so portable installs on non-system drives still work). Rollback restores the backup if either the swap OR the post-swap detached spawn fails, and a MessageBoxW modal surfaces the backup path if the rollback itself fails. - First launch after an auto-update shows a blue-info tray balloon "Updated to vX.Y.Z" via a new `tray::notify_info` (the existing `tray::notify` is split into `notify_warning` + `notify_info` sharing a `notify_inner`). - Startup sweeps stale `<exe>.old.<pid>` siblings left by past in-place updates. - Three new `LocaleStrings` fields translated across all 8 supported locales (en/nl/es/fr/de/ja/ko/zh-TW).
134 lines
4.3 KiB
Rust
134 lines
4.3 KiB
Rust
// Native Win32 process + file handoff primitives used by the in-app
|
|
// restart path and the auto-update install path. The main binary uses
|
|
// `windows_subsystem = "windows"`, so spawning the child directly via
|
|
// `CreateProcessW` allocates no console — nothing can flash.
|
|
|
|
use std::ffi::OsString;
|
|
use std::io;
|
|
use std::os::windows::ffi::OsStrExt;
|
|
use std::path::Path;
|
|
|
|
use windows::core::PCWSTR;
|
|
use windows::Win32::Foundation::{CloseHandle, FALSE, HANDLE, WAIT_OBJECT_0};
|
|
use windows::Win32::System::Threading::{
|
|
CreateProcessW, OpenProcess, WaitForSingleObject, CREATE_NEW_PROCESS_GROUP,
|
|
CREATE_NO_WINDOW, DETACHED_PROCESS, PROCESS_INFORMATION, PROCESS_SYNCHRONIZE,
|
|
STARTUPINFOW,
|
|
};
|
|
|
|
/// Spawn `exe` with the supplied args as a detached, console-less child.
|
|
///
|
|
/// Caller is fire-and-forget: the child's handles are closed immediately
|
|
/// so no zombie wait is required.
|
|
pub fn spawn_detached(exe: &Path, args: &[OsString]) -> io::Result<()> {
|
|
let mut cmdline = build_command_line(exe, args);
|
|
|
|
let si = STARTUPINFOW {
|
|
cb: std::mem::size_of::<STARTUPINFOW>() as u32,
|
|
..Default::default()
|
|
};
|
|
let mut pi = PROCESS_INFORMATION::default();
|
|
|
|
let flags = CREATE_NO_WINDOW | DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
|
|
let ok = unsafe {
|
|
CreateProcessW(
|
|
PCWSTR::null(),
|
|
windows::core::PWSTR(cmdline.as_mut_ptr()),
|
|
None,
|
|
None,
|
|
FALSE,
|
|
flags,
|
|
None,
|
|
PCWSTR::null(),
|
|
&si,
|
|
&mut pi,
|
|
)
|
|
};
|
|
|
|
if ok.is_err() {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
|
|
unsafe {
|
|
if !pi.hThread.is_invalid() {
|
|
let _ = CloseHandle(pi.hThread);
|
|
}
|
|
if !pi.hProcess.is_invalid() {
|
|
let _ = CloseHandle(pi.hProcess);
|
|
}
|
|
}
|
|
// Suppress the unused-variable warning until si.lpReserved fields ever matter.
|
|
let _ = &si;
|
|
Ok(())
|
|
}
|
|
|
|
/// Wait up to `timeout_ms` for `pid` to exit. Silent on any failure —
|
|
/// caller treats this as a best-effort barrier before acquiring the
|
|
/// singleton mutex.
|
|
pub fn wait_for_parent_exit(pid: u32, timeout_ms: u32) {
|
|
let handle: HANDLE = match unsafe { OpenProcess(PROCESS_SYNCHRONIZE, FALSE, pid) } {
|
|
Ok(h) if !h.is_invalid() => h,
|
|
_ => return,
|
|
};
|
|
unsafe {
|
|
let res = WaitForSingleObject(handle, timeout_ms);
|
|
if res != WAIT_OBJECT_0 {
|
|
log::debug!("wait_for_parent_exit pid={pid} timeout/err res={:?}", res.0);
|
|
}
|
|
let _ = CloseHandle(handle);
|
|
}
|
|
}
|
|
|
|
/// Remove leftover `<exe>.old.<pid>` siblings from previous in-place updates.
|
|
/// Filled in by phase 4; stubbed here so phase 1 can wire the call sites.
|
|
pub fn cleanup_stale_old_exes(current_exe: &Path) {
|
|
let Some(dir) = current_exe.parent() else {
|
|
return;
|
|
};
|
|
let Some(stem) = current_exe.file_name() else {
|
|
return;
|
|
};
|
|
let prefix = format!("{}.old.", stem.to_string_lossy());
|
|
let entries = match std::fs::read_dir(dir) {
|
|
Ok(e) => e,
|
|
Err(_) => return,
|
|
};
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name();
|
|
if name.to_string_lossy().starts_with(&prefix) {
|
|
if let Err(e) = std::fs::remove_file(entry.path()) {
|
|
log::debug!(
|
|
"cleanup_stale_old_exes: remove {:?} failed: {e}",
|
|
entry.path()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_command_line(exe: &Path, args: &[OsString]) -> Vec<u16> {
|
|
// CreateProcessW parses argv[0] from a quoted exe path. We wrap the
|
|
// exe in `"…"` and join args separated by spaces. Args are quoted
|
|
// only when they contain whitespace; our callers pass simple tokens
|
|
// (--wait-pid <number>, --updated-to <version>) so naive quoting is
|
|
// sufficient.
|
|
let mut line = String::new();
|
|
line.push('"');
|
|
line.push_str(&exe.to_string_lossy());
|
|
line.push('"');
|
|
for a in args {
|
|
line.push(' ');
|
|
let s = a.to_string_lossy();
|
|
if s.chars().any(|c| c.is_whitespace()) {
|
|
line.push('"');
|
|
line.push_str(&s);
|
|
line.push('"');
|
|
} else {
|
|
line.push_str(&s);
|
|
}
|
|
}
|
|
let mut wide: Vec<u16> = std::ffi::OsString::from(line).encode_wide().collect();
|
|
wide.push(0);
|
|
wide
|
|
}
|