use claude_runner_core::{ ClaudeCommand, ErrorKind, ExecutionOutput, signal_exit_code };
use super::parse::{ CliArgs, ExpectStrategy };
use super::fence::strip_fences;
fn spawn_error_msg( e : &std::io::Error ) -> String
{
if e.kind() == std::io::ErrorKind::NotFound
{
"[Runner] claude binary not found in PATH — install with: npm i -g @anthropic-ai/claude-code".to_string()
}
else
{
format!( "[Runner] Failed to execute Claude Code: {e}" )
}
}
fn check_timeout( child : &mut std::process::Child, deadline : std::time::Instant ) -> bool
{
if std::time::Instant::now() >= deadline
{
let _ = child.kill();
let _ = child.wait();
return true;
}
std::thread::sleep( core::time::Duration::from_millis( 50 ) );
false
}
fn write_output_file( path : Option< &str >, content : &str )
{
if let Some( p ) = path
{
if let Err( e ) = std::fs::write( p, content.as_bytes() )
{
eprintln!( "Error: failed to write output file '{p}': {e}" );
std::process::exit( 1 );
}
}
}
#[ derive( Clone, Copy ) ]
enum ErrorClass
{
Transient,
Account,
Auth,
Service,
Process,
Unknown,
}
impl ErrorClass
{
fn label( self ) -> &'static str
{
match self
{
ErrorClass::Transient => "Transient",
ErrorClass::Account => "Account",
ErrorClass::Auth => "Auth",
ErrorClass::Service => "Service",
ErrorClass::Process => "Process",
ErrorClass::Unknown => "Unknown",
}
}
fn fallback_message( self ) -> &'static str
{
match self
{
ErrorClass::Transient => "rate limit",
ErrorClass::Account => "quota exhausted",
ErrorClass::Auth => "auth error",
ErrorClass::Service => "API error",
ErrorClass::Process => "terminated by signal",
ErrorClass::Unknown => "unknown error",
}
}
}
fn classify_to_class( kind : Option< &ErrorKind >, exit_code : i32 ) -> ErrorClass
{
if exit_code == 4 { return ErrorClass::Process; }
match kind
{
Some( ErrorKind::RateLimit ) => ErrorClass::Transient,
Some( ErrorKind::QuotaExhausted ) => ErrorClass::Account,
Some( ErrorKind::AuthError ) => ErrorClass::Auth,
Some( ErrorKind::ApiError ) => ErrorClass::Service,
Some( ErrorKind::Signal ) => ErrorClass::Process,
Some( ErrorKind::Unknown ) | None => ErrorClass::Unknown,
}
}
fn resolve_count(
over : Option< u8 >,
class_cli : Option< u8 >,
fallback : Option< u8 >,
) -> u8
{
over.or( class_cli ).or( fallback ).unwrap_or( 2 )
}
fn resolve_delay( over : Option< u32 >, class : Option< u32 >, fallback : Option< u32 > ) -> u32
{
over.or( class ).or( fallback ).unwrap_or( 30 )
}
fn class_fields( cli : &CliArgs, class : ErrorClass ) -> ( Option< u8 >, Option< u32 > )
{
match class
{
ErrorClass::Transient => ( cli.retry_on_transient, cli.transient_delay ),
ErrorClass::Account => ( cli.retry_on_account, cli.account_delay ),
ErrorClass::Auth => ( cli.retry_on_auth, cli.auth_delay ),
ErrorClass::Service => ( cli.retry_on_service, cli.service_delay ),
ErrorClass::Process => ( cli.retry_on_process, cli.process_delay ),
ErrorClass::Unknown => ( cli.retry_on_unknown, cli.unknown_delay ),
}
}
fn first_message( output : &ExecutionOutput, class : ErrorClass ) -> String
{
for s in [ &output.stdout, &output.stderr ]
{
for line in s.lines()
{
let t = line.trim();
if !t.is_empty() { return t.to_string(); }
}
}
class.fallback_message().to_string()
}
fn delay_suffix( delay : u32 ) -> String
{
if delay > 0 { format!( " in {delay}s" ) } else { String::new() }
}
fn apply_expect_validation( cli : &CliArgs, builder : &ClaudeCommand, out : String ) -> String
{
let Some( ref pattern ) = cli.expect else { return out; };
let allowed : Vec< String > = pattern.split( '|' )
.map( | s | s.trim().to_lowercase() )
.collect();
let trimmed = out.trim().to_lowercase();
if allowed.iter().any( | v | v.as_str() == trimmed ) { return out; }
match &cli.expect_strategy
{
Some( ExpectStrategy::Retry ) =>
{
let retries = resolve_count(
cli.retry_override,
cli.retry_on_validation,
cli.retry_default,
) as usize;
let delay = resolve_delay(
cli.retry_override_delay,
cli.validation_delay,
cli.retry_default_delay,
);
let msg = format!( "expected \"{pattern}\", got \"{}\"", out.trim() );
for attempt in 1 ..= retries
{
let suf = delay_suffix( delay );
eprintln!(
"[Validation] {msg} — retrying{suf} (attempt {attempt}/{})…",
retries + 1
);
if delay > 0
{
std::thread::sleep( core::time::Duration::from_secs( u64::from( delay ) ) );
}
let retry_output = match builder.execute()
{
Ok( o ) => o,
Err( e ) => { eprintln!( "Error: [Runner] {e}" ); std::process::exit( 1 ); }
};
if !retry_output.stderr.is_empty() { eprint!( "{}", retry_output.stderr ); }
if retry_output.exit_code != 0 { std::process::exit( retry_output.exit_code ); }
let retry_out = if cli.strip_fences
{
strip_fences( &retry_output.stdout )
}
else
{
retry_output.stdout
};
if allowed.iter().any( | v | v.as_str() == retry_out.trim().to_lowercase() )
{
write_output_file( cli.output_file.as_deref(), &retry_out );
print!( "{retry_out}" );
std::process::exit( 0 );
}
}
eprintln!(
"Error: [Validation] expected \"{pattern}\", got \"{}\" — retries exhausted (exit 3)",
out.trim()
);
std::process::exit( 3 );
}
Some( ExpectStrategy::Default( fallback ) ) =>
{
let fallback = fallback.clone();
write_output_file( cli.output_file.as_deref(), &fallback );
print!( "{fallback}" );
std::process::exit( 0 );
}
Some( ExpectStrategy::Fail ) | None =>
{
eprintln!(
"Error: [Validation] expected \"{pattern}\", got \"{}\" (exit 3)",
out.trim()
);
std::process::exit( 3 );
}
}
}
fn execute_print_attempt( builder : &ClaudeCommand, timeout_secs : u32 )
-> Result< ExecutionOutput, std::io::Error >
{
if timeout_secs == 0
{
return match builder.execute()
{
Ok( o ) => Ok( o ),
Err( e ) => Err( std::io::Error::other( e.to_string() ) ),
};
}
let mut child = match builder.spawn_piped()
{
Ok( c ) => c,
Err( e ) =>
{
let msg = if e.kind() == std::io::ErrorKind::NotFound
{
"claude binary not found in PATH — install with: npm i -g @anthropic-ai/claude-code".to_string()
}
else
{
format!( "Failed to execute Claude Code: {e}" )
};
return Err( std::io::Error::other( msg ) );
}
};
let deadline = std::time::Instant::now()
+ core::time::Duration::from_secs( u64::from( timeout_secs ) );
loop
{
match child.try_wait()
{
Ok( Some( _ ) ) =>
{
let raw = match child.wait_with_output()
{
Ok( o ) => o,
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
};
let exit_code = signal_exit_code( &raw.status );
let stdout = String::from_utf8_lossy( &raw.stdout ).to_string();
let stderr = String::from_utf8_lossy( &raw.stderr ).to_string();
return Ok( ExecutionOutput { stdout, stderr, exit_code } );
}
Ok( None ) =>
{
if check_timeout( &mut child, deadline )
{
return Ok( ExecutionOutput
{
stdout : String::new(),
stderr : format!( "timeout after {timeout_secs}s" ),
exit_code : 4,
} );
}
}
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
}
}
}
pub( super ) fn apply_runner_retry( cli : &CliArgs, err : &std::io::Error, attempt : &mut u32 )
{
let count = resolve_count( cli.retry_override, cli.retry_on_runner, cli.retry_default );
let delay = resolve_delay( cli.retry_override_delay, cli.runner_delay, cli.retry_default_delay );
let msg = err.to_string();
if *attempt < u32::from( count )
{
*attempt += 1;
let suf = delay_suffix( delay );
eprintln!(
"[Runner] {msg} — retrying{suf} (attempt {}/{})…",
*attempt,
u32::from( count ) + 1
);
if delay > 0
{
std::thread::sleep( core::time::Duration::from_secs( u64::from( delay ) ) );
}
return;
}
eprintln!( "Error: [Runner] {msg} — retries exhausted (exit 1)" );
std::process::exit( 1 );
}
const DEFAULT_PRINT_TIMEOUT_SECS : u32 = 3600;
fn default_print_timeout() -> u32
{
std::env::var( "_CLR_DEFAULT_TIMEOUT" )
.ok()
.and_then( | s | s.parse().ok() )
.unwrap_or( DEFAULT_PRINT_TIMEOUT_SECS )
}
#[ allow( clippy::too_many_lines ) ]
pub( super ) fn run_print_mode( builder : &ClaudeCommand, cli : &CliArgs )
{
let verbosity = cli.verbosity.unwrap_or_default();
let timeout_secs = cli.timeout.unwrap_or( default_print_timeout() );
if let Some( ref file_path ) = cli.file
{
if let Err( e ) = std::fs::File::open( file_path )
{
eprintln!( "Error: cannot open stdin file '{file_path}': {e}" );
std::process::exit( 1 );
}
}
let mut attempts = [ 0usize; 6 ];
let mut runner_attempt = 0u32;
loop
{
let output = match execute_print_attempt( builder, timeout_secs )
{
Ok( o ) => o,
Err( e ) => { apply_runner_retry( cli, &e, &mut runner_attempt ); continue; }
};
if !output.stderr.is_empty() { eprint!( "{}", output.stderr ); }
if output.exit_code != 0
{
let kind = output.classify_error();
let class = classify_to_class( kind.as_ref(), output.exit_code );
let class_idx = class as usize;
let label = class.label();
let ( count_field, delay_field ) = class_fields( cli, class );
let limit = resolve_count( cli.retry_override, count_field, cli.retry_default ) as usize;
let delay = resolve_delay( cli.retry_override_delay, delay_field, cli.retry_default_delay );
let msg = first_message( &output, class );
if attempts[ class_idx ] < limit
{
attempts[ class_idx ] += 1;
if verbosity.shows_warnings()
{
let suf = delay_suffix( delay );
eprintln!(
"[{label}] {msg} — retrying{suf} (attempt {}/{})…",
attempts[ class_idx ],
limit + 1
);
}
if delay > 0
{
std::thread::sleep( core::time::Duration::from_secs( u64::from( delay ) ) );
}
continue;
}
if verbosity.shows_errors()
{
if attempts[ class_idx ] > 0
{
eprintln!( "Error: [{label}] {msg} — retries exhausted (exit {})", output.exit_code );
}
else
{
eprintln!( "Error: [{label}] {msg} (exit {})", output.exit_code );
}
}
if !output.stdout.is_empty() { eprint!( "{}", output.stdout ); }
std::process::exit( output.exit_code );
}
let out = if cli.strip_fences { strip_fences( &output.stdout ) } else { output.stdout };
let out = if cli.output_style.as_deref().unwrap_or( "summary" ) == "summary"
{
super::summary::render_summary( &out ).unwrap_or( out )
}
else
{
out
};
let out = apply_expect_validation( cli, builder, out );
write_output_file( cli.output_file.as_deref(), &out );
print!( "{out}" );
return;
}
}
pub( super ) fn run_interactive( builder : &ClaudeCommand, cli : &CliArgs )
{
let timeout_secs = cli.timeout.unwrap_or( 0 );
if timeout_secs == 0
{
let status = match builder.execute_interactive()
{
Ok( s ) => s,
Err( e ) => { eprintln!( "Error: [Runner] {e}" ); std::process::exit( 1 ); }
};
if !status.success()
{
std::process::exit( signal_exit_code( &status ) );
}
return;
}
let mut child = match builder.spawn_tty()
{
Ok( c ) => c,
Err( e ) => { eprintln!( "Error: {}", spawn_error_msg( &e ) ); std::process::exit( 1 ); }
};
let deadline = std::time::Instant::now()
+ core::time::Duration::from_secs( u64::from( timeout_secs ) );
loop
{
match child.try_wait()
{
Ok( Some( status ) ) =>
{
if !status.success()
{
std::process::exit( signal_exit_code( &status ) );
}
return;
}
Ok( None ) =>
{
if check_timeout( &mut child, deadline )
{
eprintln!( "Error: timeout after {timeout_secs}s" );
std::process::exit( 4 );
}
}
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
}
}
}