Compare commits

...
6 Commits
4 changed files with 58 additions and 44 deletions
Generated
+1 -1
View File
@@ -59,7 +59,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "claude-code-usage-bubble" name = "claude-code-usage-bubble"
version = "0.3.8" version = "0.3.12"
dependencies = [ dependencies = [
"dirs", "dirs",
"embed-resource", "embed-resource",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "claude-code-usage-bubble" name = "claude-code-usage-bubble"
version = "0.3.8" version = "0.3.12"
edition = "2021" edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
description = "Floating bubble showing Claude Code and Codex usage on Windows" description = "Floating bubble showing Claude Code and Codex usage on Windows"
+2 -1
View File
@@ -22,6 +22,7 @@ use windows::Win32::System::Threading::{
/// so no zombie wait is required. /// so no zombie wait is required.
pub fn spawn_detached(exe: &Path, args: &[OsString]) -> io::Result<()> { pub fn spawn_detached(exe: &Path, args: &[OsString]) -> io::Result<()> {
let mut cmdline = build_command_line(exe, args); let mut cmdline = build_command_line(exe, args);
let exe_w: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
let si = STARTUPINFOW { let si = STARTUPINFOW {
cb: std::mem::size_of::<STARTUPINFOW>() as u32, cb: std::mem::size_of::<STARTUPINFOW>() as u32,
@@ -32,7 +33,7 @@ pub fn spawn_detached(exe: &Path, args: &[OsString]) -> io::Result<()> {
let flags = CREATE_NO_WINDOW | DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP; let flags = CREATE_NO_WINDOW | DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
let ok = unsafe { let ok = unsafe {
CreateProcessW( CreateProcessW(
PCWSTR::null(), PCWSTR::from_raw(exe_w.as_ptr()),
windows::core::PWSTR(cmdline.as_mut_ptr()), windows::core::PWSTR(cmdline.as_mut_ptr()),
None, None,
None, None,
+54 -41
View File
@@ -1,10 +1,9 @@
// Download a release asset and swap it in via native Win32 calls. // Download a release asset and swap it in via a detached helper process.
// //
// After writing the new .exe to a staging path and verifying its // The running process cannot reliably replace its own mapped image on
// SHA-256, we `MoveFileExW` the running exe sideways (so Windows // Windows. Instead, it writes the new .exe to a staging path, starts that
// releases the file lock on our own image), then `MoveFileExW` the // staged binary with `--apply-update`, and then exits. The helper waits for
// staged exe into place, then spawn the new binary detached via // the parent process to exit before replacing the install target.
// `handoff::spawn_detached`. No shell, no console allocation.
use std::ffi::OsString; use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -13,7 +12,7 @@ use sha2::{Digest, Sha256};
use windows::core::PCWSTR; use windows::core::PCWSTR;
use windows::Win32::Storage::FileSystem::{ use windows::Win32::Storage::FileSystem::{
MoveFileExW, MOVEFILE_COPY_ALLOWED, MOVEFILE_REPLACE_EXISTING, MOVE_FILE_FLAGS, MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVE_FILE_FLAGS,
}; };
use windows::Win32::System::Threading::GetCurrentProcessId; use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::UI::WindowsAndMessaging::{ use windows::Win32::UI::WindowsAndMessaging::{
@@ -36,19 +35,37 @@ pub fn begin(http: &Client, release: &super::Release) -> Result<(), super::Error
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
download(http, &release.asset_url, &staging, release.asset_sha256.as_ref())?; download(http, &release.asset_url, &staging, release.asset_sha256.as_ref())?;
swap_and_spawn(&staging, &current, &release.version)?; spawn_update_helper(&staging, &current, &release.version)?;
Ok(()) Ok(())
} }
/// CLI entry-point compatibility for `--apply-update <target> <source> <pid>`. /// CLI entry point for `--apply-update <target> <source> <parent-pid> <version>`.
/// The native handoff already does the swap-and-restart; if this binary /// Runs from the staged new binary, waits for the old UI process to exit,
/// is invoked with the legacy flag (e.g. from an older release's helper) /// replaces the installed exe, and starts the installed copy.
/// just exit cleanly so the upgrade still completes.
pub fn run_cli(args: &[String]) -> Option<i32> { pub fn run_cli(args: &[String]) -> Option<i32> {
if args.len() >= 2 && args[1] == "--apply-update" { if args.get(1).map(String::as_str) != Some("--apply-update") {
Some(0) return None;
} else { }
None let Some(target) = args.get(2).map(PathBuf::from) else {
return Some(2);
};
let Some(source) = args.get(3).map(PathBuf::from) else {
return Some(2);
};
let Some(parent_pid) = args.get(4).and_then(|s| s.parse::<u32>().ok()) else {
return Some(2);
};
let Some(version) = args.get(5).cloned() else {
return Some(2);
};
super::handoff::wait_for_parent_exit(parent_pid, 15_000);
match replace_from_helper(&source, &target, &version) {
Ok(()) => Some(0),
Err(e) => {
log::error!("apply-update failed: {e}");
Some(1)
}
} }
} }
@@ -101,25 +118,29 @@ fn reject_unsafe_path(p: &Path) -> Result<(), super::Error> {
Ok(()) Ok(())
} }
fn swap_and_spawn( fn spawn_update_helper(
source: &Path, staging: &Path,
target: &Path, target: &Path,
version: &super::release::Version, version: &super::release::Version,
) -> Result<(), super::Error> { ) -> Result<(), super::Error> {
let pid = unsafe { GetCurrentProcessId() };
let version_str = format!("{}.{}.{}", version.major, version.minor, version.patch);
let args = vec![
OsString::from("--apply-update"),
target.as_os_str().to_os_string(),
staging.as_os_str().to_os_string(),
OsString::from(pid.to_string()),
OsString::from(version_str),
];
super::handoff::spawn_detached(staging, &args).map_err(super::Error::Io)
}
fn replace_from_helper(source: &Path, target: &Path, version: &str) -> Result<(), super::Error> {
let backup = backup_path(target); let backup = backup_path(target);
// Step 1: rename running exe sideways. Windows allows renaming a // Parent has exited, so the install target is no longer mapped.
// file even while its image is mapped into memory; this releases
// the lock on the original `target` path. Same directory by
// construction, so plain MoveFileExW with no flags is sufficient.
move_file(target, &backup, MOVE_FILE_FLAGS(0))?; move_file(target, &backup, MOVE_FILE_FLAGS(0))?;
// Step 2: move staged exe into place. Staging lives under if let Err(copy_err) = std::fs::copy(source, target) {
// %LOCALAPPDATA%, target lives wherever the user installed —
// COPY_ALLOWED lets MoveFileExW fall back to copy+delete when
// the two paths cross volumes (portable installs on D:/E:/etc.).
let step2_flags = MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED;
if let Err(swap_err) = move_file(source, target, step2_flags) {
// Best-effort revert. Same volume, no COPY_ALLOWED needed.
if let Err(revert_err) = move_file(&backup, target, MOVEFILE_REPLACE_EXISTING) { if let Err(revert_err) = move_file(&backup, target, MOVEFILE_REPLACE_EXISTING) {
log::error!("rollback also failed: {revert_err}; surfacing modal"); log::error!("rollback also failed: {revert_err}; surfacing modal");
let target_name = target let target_name = target
@@ -128,27 +149,19 @@ fn swap_and_spawn(
.unwrap_or_else(|| "claude-code-usage-bubble.exe".to_string()); .unwrap_or_else(|| "claude-code-usage-bubble.exe".to_string());
surface_rollback_failure(&backup, &target_name); surface_rollback_failure(&backup, &target_name);
} }
return Err(swap_err); return Err(super::Error::Io(copy_err));
} }
// Step 3: spawn the new exe detached with --wait-pid + --updated-to. let args = vec![OsString::from("--updated-to"), OsString::from(version)];
let pid = unsafe { GetCurrentProcessId() };
let version_str = format!("{}.{}.{}", version.major, version.minor, version.patch);
let args = vec![
OsString::from("--wait-pid"),
OsString::from(pid.to_string()),
OsString::from("--updated-to"),
OsString::from(version_str),
];
if let Err(spawn_err) = super::handoff::spawn_detached(target, &args) { if let Err(spawn_err) = super::handoff::spawn_detached(target, &args) {
// New binary is on disk but won't auto-launch. Roll back so
// the user's next "Restart" stays on the known-good version.
log::error!("spawn_detached failed after swap: {spawn_err}; attempting revert"); log::error!("spawn_detached failed after swap: {spawn_err}; attempting revert");
let _ = std::fs::remove_file(target);
if let Err(revert_err) = move_file(&backup, target, MOVEFILE_REPLACE_EXISTING) { if let Err(revert_err) = move_file(&backup, target, MOVEFILE_REPLACE_EXISTING) {
log::error!("post-spawn revert failed: {revert_err}"); log::error!("post-spawn revert failed: {revert_err}");
} }
return Err(super::Error::Io(spawn_err)); return Err(super::Error::Io(spawn_err));
} }
Ok(()) Ok(())
} }