Skip to content

Commit d54588b

Browse files
committed
v1.3.9
feat: check for other credential sources on permanent credential failure.
1 parent f2b3784 commit d54588b

3 files changed

Lines changed: 54 additions & 28 deletions

File tree

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.3.8"
3+
version = "1.3.9"
44
edition = "2021"
55
license = "MIT"
66
description = "Claude Code Usage Monitor"

src/poller.rs

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,43 @@ struct UsageBucket {
4444
}
4545

4646
pub fn poll() -> Result<UsageData, PollError> {
47-
let mut creds = match read_credentials() {
47+
let creds = match read_first_credentials() {
4848
Some(c) => c,
4949
None => {
5050
diagnose::log("poll failed: no Claude credentials found");
5151
return Err(PollError::NoCredentials);
5252
}
5353
};
5454

55-
if is_token_expired(creds.expires_at) {
56-
cli_refresh_token(&creds.source);
55+
let creds = refresh_or_fallback(creds)?;
5756

58-
match read_credentials_from_source(&creds.source) {
59-
Some(refreshed) => creds = refreshed,
60-
None => {
61-
diagnose::log("poll failed: credentials still unavailable after refresh attempt");
62-
return Err(PollError::NoCredentials);
63-
}
57+
fetch_usage_with_fallback(&creds.access_token)
58+
}
59+
60+
fn refresh_or_fallback(mut creds: Credentials) -> Result<Credentials, PollError> {
61+
loop {
62+
if !is_token_expired(creds.expires_at) {
63+
return Ok(creds);
6464
}
6565

66-
if is_token_expired(creds.expires_at) {
67-
diagnose::log("poll failed: token is still expired after refresh attempt");
68-
return Err(PollError::TokenExpired);
66+
let source = creds.source.clone();
67+
cli_refresh_token(&source);
68+
69+
match read_credentials_from_source(&source) {
70+
Some(refreshed) if !is_token_expired(refreshed.expires_at) => return Ok(refreshed),
71+
Some(_) => diagnose::log(format!(
72+
"credentials from {source:?} still expired after refresh attempt"
73+
)),
74+
None => diagnose::log(format!(
75+
"credentials from {source:?} unavailable after refresh attempt"
76+
)),
6977
}
70-
}
7178

72-
fetch_usage_with_fallback(&creds.access_token)
79+
match read_next_credentials_after(&source) {
80+
Some(next) => creds = next,
81+
None => return Err(PollError::TokenExpired),
82+
}
83+
}
7384
}
7485

7586
/// Invoke the Claude CLI with a minimal prompt to force its internal
@@ -245,7 +256,7 @@ fn build_agent() -> Result<ureq::Agent, PollError> {
245256

246257
pub fn credential_watch_snapshot(mode: CredentialWatchMode) -> CredentialWatchSnapshot {
247258
let sources = match mode {
248-
CredentialWatchMode::ActiveSource => read_credentials()
259+
CredentialWatchMode::ActiveSource => read_first_credentials()
249260
.map(|creds| vec![creds.source])
250261
.unwrap_or_else(all_known_credential_sources),
251262
CredentialWatchMode::AllSources => all_known_credential_sources(),
@@ -506,20 +517,18 @@ enum CredentialSource {
506517
Wsl { distro: String },
507518
}
508519

509-
fn read_credentials() -> Option<Credentials> {
510-
let mut candidates = Vec::new();
511-
520+
fn read_first_credentials() -> Option<Credentials> {
512521
if let Some(creds) = read_windows_credentials() {
513-
candidates.push(creds);
522+
return Some(creds);
514523
}
515524

516525
for distro in list_wsl_distros() {
517526
if let Some(creds) = read_wsl_credentials(&distro) {
518-
candidates.push(creds);
527+
return Some(creds);
519528
}
520529
}
521530

522-
choose_best_credentials(candidates)
531+
None
523532
}
524533

525534
fn read_windows_credentials() -> Option<Credentials> {
@@ -600,13 +609,30 @@ fn parse_credentials(content: &str, source: CredentialSource) -> Option<Credenti
600609
})
601610
}
602611

603-
fn choose_best_credentials(mut candidates: Vec<Credentials>) -> Option<Credentials> {
604-
if candidates.is_empty() {
605-
return None;
612+
fn read_next_credentials_after(source: &CredentialSource) -> Option<Credentials> {
613+
match source {
614+
CredentialSource::Windows(_) => {
615+
for distro in list_wsl_distros() {
616+
if let Some(creds) = read_wsl_credentials(&distro) {
617+
return Some(creds);
618+
}
619+
}
620+
}
621+
CredentialSource::Wsl { distro } => {
622+
let mut past_current = false;
623+
for candidate_distro in list_wsl_distros() {
624+
if !past_current {
625+
past_current = candidate_distro == *distro;
626+
continue;
627+
}
628+
if let Some(creds) = read_wsl_credentials(&candidate_distro) {
629+
return Some(creds);
630+
}
631+
}
632+
}
606633
}
607634

608-
candidates.sort_by_key(|creds| is_token_expired(creds.expires_at));
609-
candidates.into_iter().next()
635+
None
610636
}
611637

612638
fn list_wsl_distros() -> Vec<String> {

0 commit comments

Comments
 (0)