Skip to content

Commit f527e82

Browse files
committed
v1.2.6
- Added --diagnose parameter for troubleshooting startup issues
1 parent 110ae4a commit f527e82

File tree

6 files changed

+166
-13
lines changed

6 files changed

+166
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "claude-code-usage-monitor"
3-
version = "1.2.5"
3+
version = "1.2.6"
44
edition = "2021"
55
license = "MIT"
66
description = "Windows taskbar widget for monitoring Claude Code usage and rate limits"

src/diagnose.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::fs::{File, OpenOptions};
2+
use std::io::Write;
3+
use std::path::PathBuf;
4+
use std::sync::{Mutex, OnceLock};
5+
use std::time::{SystemTime, UNIX_EPOCH};
6+
7+
struct DiagnoseState {
8+
file: Mutex<File>,
9+
}
10+
11+
static DIAGNOSE_STATE: OnceLock<DiagnoseState> = OnceLock::new();
12+
13+
pub fn init() -> Result<PathBuf, String> {
14+
let path = std::env::temp_dir().join("claude-code-usage-monitor.log");
15+
let file = OpenOptions::new()
16+
.create(true)
17+
.write(true)
18+
.truncate(true)
19+
.open(&path)
20+
.map_err(|e| format!("Unable to open diagnostic log file {}: {e}", path.display()))?;
21+
22+
let _ = DIAGNOSE_STATE.set(DiagnoseState {
23+
file: Mutex::new(file),
24+
});
25+
26+
log("diagnostic logging enabled");
27+
Ok(path)
28+
}
29+
30+
pub fn is_enabled() -> bool {
31+
DIAGNOSE_STATE.get().is_some()
32+
}
33+
34+
pub fn log(message: impl AsRef<str>) {
35+
let Some(state) = DIAGNOSE_STATE.get() else {
36+
return;
37+
};
38+
39+
let timestamp = SystemTime::now()
40+
.duration_since(UNIX_EPOCH)
41+
.map(|duration| duration.as_secs())
42+
.unwrap_or(0);
43+
44+
if let Ok(mut file) = state.file.lock() {
45+
let _ = writeln!(file, "[{timestamp}] {}", message.as_ref());
46+
let _ = file.flush();
47+
}
48+
}
49+
50+
pub fn log_error(context: &str, error: impl std::fmt::Display) {
51+
log(format!("{context}: {error}"));
52+
}

src/main.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![windows_subsystem = "windows"]
22

3+
mod diagnose;
34
mod localization;
45
mod models;
56
mod native_interop;
@@ -10,8 +11,29 @@ mod window;
1011

1112
fn main() {
1213
let args: Vec<String> = std::env::args().collect();
14+
let diagnose_enabled = args.iter().any(|arg| arg == "--diagnose");
15+
if diagnose_enabled {
16+
match diagnose::init() {
17+
Ok(path) => diagnose::log(format!(
18+
"startup args={args:?} log_path={}",
19+
path.display()
20+
)),
21+
Err(error) => {
22+
// Logging may not be available yet, but keep startup behavior unchanged.
23+
let _ = error;
24+
}
25+
}
26+
}
27+
1328
if let Some(exit_code) = updater::handle_cli_mode(&args) {
29+
if diagnose_enabled {
30+
diagnose::log(format!("cli mode exited with code {exit_code}"));
31+
}
1432
std::process::exit(exit_code);
1533
}
34+
35+
if diagnose_enabled {
36+
diagnose::log("entering window::run");
37+
}
1638
window::run();
1739
}

src/poller.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
55
use serde::Deserialize;
66
use std::os::windows::process::CommandExt;
77

8+
use crate::diagnose;
89
use crate::localization::Strings;
910
use crate::models::{UsageData, UsageSection};
1011

@@ -36,18 +37,25 @@ struct UsageBucket {
3637
pub fn poll() -> Result<UsageData, PollError> {
3738
let mut creds = match read_credentials() {
3839
Some(c) => c,
39-
None => return Err(PollError::NoCredentials),
40+
None => {
41+
diagnose::log("poll failed: no Claude credentials found");
42+
return Err(PollError::NoCredentials);
43+
}
4044
};
4145

4246
if is_token_expired(creds.expires_at) {
4347
cli_refresh_token(&creds.source);
4448

4549
match read_credentials_from_source(&creds.source) {
4650
Some(refreshed) => creds = refreshed,
47-
None => return Err(PollError::NoCredentials),
51+
None => {
52+
diagnose::log("poll failed: credentials still unavailable after refresh attempt");
53+
return Err(PollError::NoCredentials);
54+
}
4855
}
4956

5057
if is_token_expired(creds.expires_at) {
58+
diagnose::log("poll failed: token is still expired after refresh attempt");
5159
return Err(PollError::TokenExpired);
5260
}
5361
}
@@ -67,6 +75,9 @@ fn cli_refresh_token(source: &CredentialSource) {
6775
fn cli_refresh_windows_token() {
6876
let claude_path = resolve_windows_claude_path();
6977
let is_cmd = claude_path.to_lowercase().ends_with(".cmd");
78+
diagnose::log(format!(
79+
"attempting Windows Claude token refresh via {claude_path}"
80+
));
7081

7182
let args: &[&str] = &["-p", "."];
7283

@@ -88,7 +99,10 @@ fn cli_refresh_windows_token() {
8899

89100
let mut child = match cmd.spawn() {
90101
Ok(c) => c,
91-
Err(_) => return,
102+
Err(error) => {
103+
diagnose::log_error("unable to spawn Windows Claude token refresh", error);
104+
return;
105+
}
92106
};
93107

94108
// Wait up to 30 seconds — don't block the poll thread forever
@@ -109,6 +123,9 @@ fn cli_refresh_windows_token() {
109123
}
110124

111125
fn cli_refresh_wsl_token(distro: &str) {
126+
diagnose::log(format!(
127+
"attempting WSL Claude token refresh in distro {distro}"
128+
));
112129
let mut cmd = Command::new("wsl.exe");
113130
cmd.arg("-d")
114131
.arg(distro)
@@ -125,7 +142,10 @@ fn cli_refresh_wsl_token(distro: &str) {
125142

126143
let mut child = match cmd.spawn() {
127144
Ok(c) => c,
128-
Err(_) => return,
145+
Err(error) => {
146+
diagnose::log_error("unable to spawn WSL Claude token refresh", error);
147+
return;
148+
}
129149
};
130150

131151
wait_for_refresh(&mut child);
@@ -234,7 +254,11 @@ fn fetch_usage_with_fallback(token: &str) -> Result<UsageData, PollError> {
234254
}
235255

236256
// Fall back to Messages API with rate limit headers
237-
fetch_usage_via_messages(token)
257+
let result = fetch_usage_via_messages(token);
258+
if result.is_err() {
259+
diagnose::log("usage endpoint and Messages API fallback both failed");
260+
}
261+
result
238262
}
239263

240264
fn try_usage_endpoint(token: &str) -> Option<UsageData> {
@@ -388,7 +412,18 @@ fn read_credentials() -> Option<Credentials> {
388412
fn read_windows_credentials() -> Option<Credentials> {
389413
let home = dirs::home_dir()?;
390414
let cred_path = home.join(".claude").join(".credentials.json");
391-
let content = std::fs::read_to_string(&cred_path).ok()?;
415+
let content = match std::fs::read_to_string(&cred_path) {
416+
Ok(content) => content,
417+
Err(error) => {
418+
if diagnose::is_enabled() {
419+
diagnose::log_error(
420+
&format!("unable to read Windows credentials at {}", cred_path.display()),
421+
error,
422+
);
423+
}
424+
return None;
425+
}
426+
};
392427
parse_credentials(&content, CredentialSource::Windows(cred_path))
393428
}
394429

@@ -418,6 +453,10 @@ fn read_wsl_credentials(distro: &str) -> Option<Credentials> {
418453
)?;
419454

420455
if !output.status.success() {
456+
diagnose::log(format!(
457+
"WSL credentials probe failed for distro {distro} with status {}",
458+
output.status
459+
));
421460
return None;
422461
}
423462

@@ -466,7 +505,10 @@ fn list_wsl_distros() -> Vec<String> {
466505
Duration::from_secs(5),
467506
) {
468507
Some(output) if output.status.success() => output,
469-
_ => return Vec::new(),
508+
_ => {
509+
diagnose::log("unable to enumerate WSL distros");
510+
return Vec::new();
511+
}
470512
};
471513

472514
let stdout = decode_wsl_text(&output.stdout);

src/window.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use windows::Win32::UI::HiDpi::*;
1515
use windows::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture};
1616
use windows::Win32::UI::WindowsAndMessaging::*;
1717

18+
use crate::diagnose;
1819
use crate::localization::{self, LanguageId, Strings};
1920
use crate::models::UsageData;
2021
use crate::native_interop::{
@@ -599,6 +600,7 @@ pub fn run() {
599600
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
600601
CURRENT_DPI.store(GetDpiForSystem(), Ordering::Relaxed);
601602
}
603+
diagnose::log("window::run started");
602604

603605
// Single-instance guard: silently exit if another instance is running
604606
let mutex_name = native_interop::wide_str("Global\\ClaudeCodeUsageMonitor");
@@ -607,11 +609,15 @@ pub fn run() {
607609
match handle {
608610
Ok(h) => {
609611
if GetLastError() == ERROR_ALREADY_EXISTS {
612+
diagnose::log("startup aborted: another instance is already running");
610613
return;
611614
}
612615
h
613616
}
614-
Err(_) => return,
617+
Err(error) => {
618+
diagnose::log_error("startup aborted: unable to create single-instance mutex", error);
619+
return;
620+
}
615621
}
616622
};
617623

@@ -631,7 +637,10 @@ pub fn run() {
631637
..Default::default()
632638
};
633639

634-
RegisterClassExW(&wc);
640+
let atom = RegisterClassExW(&wc);
641+
if atom == 0 {
642+
diagnose::log("RegisterClassExW returned 0");
643+
}
635644

636645
let settings = load_settings();
637646
let language_override = settings.language.as_deref().and_then(LanguageId::from_code);
@@ -655,6 +664,7 @@ pub fn run() {
655664
None,
656665
)
657666
.unwrap();
667+
diagnose::log(format!("main window created hwnd={:?}", hwnd));
658668

659669
let is_dark = theme::is_dark_mode();
660670
let mut embedded = false;
@@ -689,6 +699,7 @@ pub fn run() {
689699

690700
// Try to embed in taskbar
691701
if let Some(taskbar_hwnd) = native_interop::find_taskbar() {
702+
diagnose::log(format!("taskbar found hwnd={:?}", taskbar_hwnd));
692703
native_interop::embed_in_taskbar(hwnd, taskbar_hwnd);
693704
embedded = true;
694705

@@ -699,12 +710,24 @@ pub fn run() {
699710

700711
let tray_notify = native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd");
701712
s.tray_notify_hwnd = tray_notify;
713+
if tray_notify.is_some() {
714+
diagnose::log("TrayNotifyWnd found");
715+
} else {
716+
diagnose::log("TrayNotifyWnd not found");
717+
}
702718

703719
if let Some(tray_hwnd) = tray_notify {
704720
let thread_id = native_interop::get_window_thread_id(tray_hwnd);
705721
let hook = native_interop::set_tray_event_hook(thread_id, on_tray_location_changed);
706722
s.win_event_hook = hook;
723+
if hook.is_some() {
724+
diagnose::log("tray event hook installed");
725+
} else {
726+
diagnose::log("tray event hook could not be installed");
727+
}
707728
}
729+
} else {
730+
diagnose::log("taskbar not found; using fallback popup window");
708731
}
709732

710733
// If not embedded, fall back to topmost popup with SetLayeredWindowAttributes
@@ -724,6 +747,7 @@ pub fn run() {
724747
// Position and show
725748
position_at_taskbar();
726749
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
750+
diagnose::log("window shown");
727751

728752
// Initial render via UpdateLayeredWindow (for embedded) or InvalidateRect (fallback)
729753
render_layered();
@@ -741,6 +765,7 @@ pub fn run() {
741765
// Initial poll
742766
let send_hwnd = SendHwnd::from_hwnd(hwnd);
743767
std::thread::spawn(move || {
768+
diagnose::log("initial poll thread started");
744769
do_poll(send_hwnd);
745770
});
746771

@@ -1182,12 +1207,18 @@ fn position_at_taskbar() {
11821207

11831208
let taskbar_hwnd = match s.taskbar_hwnd {
11841209
Some(h) => h,
1185-
None => return,
1210+
None => {
1211+
diagnose::log("position_at_taskbar skipped: no taskbar handle");
1212+
return;
1213+
}
11861214
};
11871215

11881216
let taskbar_rect = match native_interop::get_taskbar_rect(taskbar_hwnd) {
11891217
Some(r) => r,
1190-
None => return,
1218+
None => {
1219+
diagnose::log("position_at_taskbar skipped: unable to query taskbar rect");
1220+
return;
1221+
}
11911222
};
11921223

11931224
let taskbar_height = taskbar_rect.bottom - taskbar_rect.top;
@@ -1207,11 +1238,17 @@ fn position_at_taskbar() {
12071238
let x = tray_left - taskbar_rect.left - widget_width - tray_offset;
12081239
let y = (taskbar_height - widget_height) / 2;
12091240
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
1241+
diagnose::log(format!(
1242+
"positioned embedded widget at x={x} y={y} w={widget_width} h={widget_height}"
1243+
));
12101244
} else {
12111245
// Topmost popup: screen coordinates
12121246
let x = tray_left - widget_width - tray_offset;
12131247
let y = taskbar_rect.top + (taskbar_height - widget_height) / 2;
12141248
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
1249+
diagnose::log(format!(
1250+
"positioned fallback widget at x={x} y={y} w={widget_width} h={widget_height}"
1251+
));
12151252
}
12161253
}
12171254

0 commit comments

Comments
 (0)