#[cfg(test)]
#[path = "usage_probe_drive_tests.rs"]
mod tests;
use std::{
path::Path,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
use super::{
classify_idle_screen, classify_usage_screen, trust_yes_selected, IdleScreen, ProbeBudget,
RateLimitState, UsageProbeError, UsageScreen,
};
use crate::claude_runtime::{rate_limit, usage_pty::PtySession};
const PROMPT_WAIT: Duration = Duration::from_secs(60);
const TRUST_DOWN: &[u8] = b"\x1b[B";
const TRUST_ENTER: &[u8] = b"\r";
const TRUST_RETRY: Duration = Duration::from_millis(400);
const PTY_ROWS: u16 = 36;
const PTY_COLS: u16 = 140;
const PROMPT_SETTLE: Duration = Duration::from_millis(50);
const ENTER_SETTLE: Duration = Duration::from_millis(80);
const POLL_SLICE: Duration = Duration::from_millis(25);
const SETTLE_DRAIN: Duration = Duration::from_millis(80);
pub(super) fn read_usage_from_binary(
binary: &Path,
args: &[&str],
env: &[(String, String)],
cwd: &Path,
abort: &AtomicBool,
budget: ProbeBudget,
) -> Result<RateLimitState, UsageProbeError> {
let mut session = PtySession::spawn(binary, args, env, cwd, PTY_ROWS, PTY_COLS)
.map_err(UsageProbeError::Spawn)?;
send_usage_command(&mut session, abort)?;
wait_for_usage(&mut session, abort, budget)
}
fn send_usage_command(session: &mut PtySession, abort: &AtomicBool) -> Result<(), UsageProbeError> {
wait_for_prompt(session, abort)?;
poll_until(session, abort, Instant::now() + PROMPT_SETTLE)?;
session
.inject_bytes(b"/usage")
.map_err(UsageProbeError::Spawn)?;
poll_until(session, abort, Instant::now() + ENTER_SETTLE)?;
session.inject_bytes(b"\r").map_err(UsageProbeError::Spawn)
}
fn check_abort(abort: &AtomicBool) -> Result<(), UsageProbeError> {
if abort.load(Ordering::Relaxed) {
return Err(UsageProbeError::Cancelled);
}
Ok(())
}
fn poll_until(
session: &mut PtySession,
abort: &AtomicBool,
until: Instant,
) -> Result<(), UsageProbeError> {
while Instant::now() < until {
check_abort(abort)?;
session.poll(POLL_SLICE);
}
check_abort(abort)
}
fn wait_for_prompt(session: &mut PtySession, abort: &AtomicBool) -> Result<(), UsageProbeError> {
let mut trust = TrustDrive::NeedDown;
let deadline = Instant::now() + PROMPT_WAIT;
loop {
check_abort(abort)?;
session.poll(POLL_SLICE);
let screen = session.contents();
match classify_idle_screen(&screen) {
IdleScreen::Prompt => return Ok(()),
IdleScreen::Login => return Err(UsageProbeError::NotSignedIn),
IdleScreen::Trust => trust = step_trust(session, trust, &screen)?,
IdleScreen::Other => {}
}
if !session.is_running() {
return Err(UsageProbeError::Exited {
what: "the claude prompt",
screen,
});
}
if Instant::now() >= deadline {
tracing::debug!(
screen = %screen,
"claude usage probe timed out waiting for the idle prompt"
);
return Err(UsageProbeError::TimeoutScreen {
what: "the claude prompt",
screen,
});
}
}
}
fn wait_for_usage(
session: &mut PtySession,
abort: &AtomicBool,
budget: ProbeBudget,
) -> Result<RateLimitState, UsageProbeError> {
let deadline = Instant::now() + budget.panel_wait;
let mut grow_until: Option<Instant> = None;
let mut ready_since: Option<Instant> = None;
loop {
check_abort(abort)?;
let running = session.is_running();
session.poll(POLL_SLICE);
let screen = session.contents();
let now = Instant::now();
match classify_usage_screen(&screen, rate_limit::now_unix()) {
UsageScreen::Failed(reason) => {
return Err(UsageProbeError::RefreshFailed { reason, screen })
}
UsageScreen::Ready(state) => {
grow_until = None;
if !running || now >= *ready_since.get_or_insert(now) + SETTLE_DRAIN {
return Ok(state);
}
}
UsageScreen::Incomplete => {
ready_since = None;
if now >= *grow_until.get_or_insert(now + budget.grow) {
return Err(UsageProbeError::Unparseable);
}
}
UsageScreen::NoPanel | UsageScreen::Refreshing => {
grow_until = None;
ready_since = None;
}
}
if !running {
return Err(UsageProbeError::Exited {
what: "the /usage refresh",
screen,
});
}
if now >= deadline {
return Err(UsageProbeError::TimeoutScreen {
what: "the /usage refresh",
screen,
});
}
}
}
#[derive(Clone, Copy)]
enum TrustDrive {
NeedDown,
AwaitYes { down_at: Instant },
AwaitPrompt { enter_at: Instant },
}
fn step_trust(
session: &mut PtySession,
drive: TrustDrive,
screen: &str,
) -> Result<TrustDrive, UsageProbeError> {
match drive {
TrustDrive::NeedDown => {
session
.inject_bytes(TRUST_DOWN)
.map_err(UsageProbeError::Spawn)?;
Ok(TrustDrive::AwaitYes {
down_at: Instant::now(),
})
}
TrustDrive::AwaitYes { down_at } => {
if trust_yes_selected(screen) {
session
.inject_bytes(TRUST_ENTER)
.map_err(UsageProbeError::Spawn)?;
Ok(TrustDrive::AwaitPrompt {
enter_at: Instant::now(),
})
} else if Instant::now() >= down_at + TRUST_RETRY {
Ok(TrustDrive::NeedDown)
} else {
Ok(drive)
}
}
TrustDrive::AwaitPrompt { enter_at } => {
if Instant::now() >= enter_at + TRUST_RETRY {
Ok(TrustDrive::NeedDown)
} else {
Ok(drive)
}
}
}
}