mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-09-09 14:17:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e9a66cbc8 | ||
|
|
1d9c59b43b | ||
|
|
7039bf7e98 | ||
|
|
cc73c3f9b4 | ||
|
|
a68d80415b | ||
|
|
cce22cc3b7 |
Generated
+1
-1
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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, ¤t, &release.version)?;
|
spawn_update_helper(&staging, ¤t, &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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user