diff --git a/docs/cli-reference.md b/docs/cli-reference.md index be8b4c3..fbe489a 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -395,7 +395,7 @@ steel browser start [options] - -s, **--session**: Named session key for create-or-attach behavior - **--stealth**: Apply stealth preset on new sessions (humanized interactions + auto CAPTCHA solving / solveCaptcha=true) - -p, **--proxy**: Proxy URL for new sessions (for example, http://user:pass@host:port) -- **--session-timeout**: Session timeout in milliseconds (create-time only) +- **--session-timeout**: Session timeout in milliseconds (create-time only; defaults to `browser.sessionTimeoutMs` in `~/.config/steel/config.json`) - **--inactivity-timeout**: Inactivity timeout in milliseconds — release the session when no CDP command or remote input is received for this long (create-time only; defaults to 120000 / 2 minutes, pass 0 to disable) - **--session-headless**: Create new sessions in headless mode (create-time only) - **--session-region**: Preferred session region (create-time only) diff --git a/src/commands/browser/start.rs b/src/commands/browser/start.rs index 210eb5a..83e709d 100644 --- a/src/commands/browser/start.rs +++ b/src/commands/browser/start.rs @@ -8,6 +8,7 @@ use crate::browser::daemon::protocol::{ }; use crate::browser::lifecycle::sanitize_connect_url; use crate::browser::profile_store; +use crate::config::settings::read_config; use crate::status; use crate::util::{api, output}; @@ -21,7 +22,7 @@ pub struct Args { #[arg(short = 'p', long)] pub proxy: Option, - /// Session timeout in milliseconds (create-time only) + /// Session timeout in milliseconds (create-time only; defaults to browser.sessionTimeoutMs in config.json) #[arg(long = "session-timeout")] pub session_timeout: Option, @@ -60,6 +61,10 @@ pub struct Args { pub async fn run(args: Args, session: Option<&str>) -> anyhow::Result<()> { let (mode, base_url, auth) = api::resolve_with_auth(); + let configured_session_timeout = read_config() + .ok() + .and_then(|config| config.browser_session_timeout_ms()); + let session_timeout = resolve_session_timeout(args.session_timeout, configured_session_timeout); let session_name = session.unwrap_or("default").to_string(); @@ -82,11 +87,11 @@ pub async fn run(args: Args, session: Option<&str>) -> anyhow::Result<()> { let proxy_enabled = args.proxy.is_some(); let namespace_set = args.namespace.is_some(); let inactivity_timeout_ms = resolve_inactivity_timeout(args.inactivity_timeout); - if let (Some(timeout), Some(inactivity)) = (args.session_timeout, inactivity_timeout_ms) + if let (Some(timeout), Some(inactivity)) = (session_timeout, inactivity_timeout_ms) && inactivity >= timeout { eprintln!( - "warning: --inactivity-timeout ({inactivity}ms) >= --session-timeout ({timeout}ms); inactivity timeout has no effect because the session timeout elapses first" + "warning: --inactivity-timeout ({inactivity}ms) >= session timeout ({timeout}ms); inactivity timeout has no effect because the session timeout elapses first" ); } @@ -109,7 +114,7 @@ pub async fn run(args: Args, session: Option<&str>) -> anyhow::Result<()> { session_name: session_name.clone(), stealth: args.stealth, proxy_url: args.proxy, - timeout_ms: args.session_timeout, + timeout_ms: session_timeout, inactivity_timeout_ms, headless: args.session_headless, region: args.session_region, @@ -235,6 +240,13 @@ fn humanize_secs(secs: u64) -> String { } } +const fn resolve_session_timeout(explicit: Option, configured: Option) -> Option { + match explicit { + Some(timeout) => Some(timeout), + None => configured, + } +} + const fn resolve_inactivity_timeout(explicit: Option) -> Option { match explicit { None => Some(DEFAULT_INACTIVITY_TIMEOUT_MS), @@ -247,6 +259,24 @@ const fn resolve_inactivity_timeout(explicit: Option) -> Option { mod tests { use super::*; + #[test] + fn session_timeout_uses_config_when_flag_is_unset() { + assert_eq!(resolve_session_timeout(None, Some(900000)), Some(900000)); + } + + #[test] + fn session_timeout_flag_overrides_config() { + assert_eq!( + resolve_session_timeout(Some(300000), Some(900000)), + Some(300000) + ); + } + + #[test] + fn session_timeout_explicit_zero_overrides_config() { + assert_eq!(resolve_session_timeout(Some(0), Some(900000)), Some(0)); + } + #[test] fn inactivity_default_when_unset() { assert_eq!( diff --git a/src/commands/config.rs b/src/commands/config.rs index 29acac7..53a3e1a 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -33,9 +33,13 @@ pub async fn run(_args: Args) -> anyhow::Result<()> { if let Some(ref cfg) = config && let Some(ref browser) = cfg.browser - && let Some(ref url) = browser.api_url { - println!("browser.apiUrl: {url}"); + if let Some(ref url) = browser.api_url { + println!("browser.apiUrl: {url}"); + } + if let Some(timeout_ms) = cfg.browser_session_timeout_ms() { + println!("browser.sessionTimeoutMs: {timeout_ms}"); + } } if let Some(ref cfg) = config { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 37d0325..3081013 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -50,7 +50,7 @@ Browser Sessions: steel browser start [--session ] Create or attach to a browser session --stealth Enable stealth mode (humanize + auto CAPTCHA) -p, --proxy Use residential proxy - --session-timeout Session timeout + --session-timeout Session timeout (defaults to browser.sessionTimeoutMs in config.json) --session-solve-captcha Enable manual CAPTCHA solving --profile Named profile to persist browser state --update-profile Save state back to profile on session end diff --git a/src/config/settings.rs b/src/config/settings.rs index b084110..1098744 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -119,6 +119,8 @@ pub struct Config { pub struct BrowserConfig { #[serde(skip_serializing_if = "Option::is_none")] pub api_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub session_timeout_ms: Option, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] @@ -137,6 +139,14 @@ impl Config { .filter(|s| !s.trim().is_empty()) } + /// Extract the default browser session timeout from config.browser.sessionTimeoutMs. + pub fn browser_session_timeout_ms(&self) -> Option { + self.browser + .as_ref() + .and_then(|b| b.session_timeout_ms) + .filter(|timeout| *timeout > 0) + } + pub fn telemetry_disabled(&self) -> bool { self.telemetry .as_ref() @@ -196,6 +206,7 @@ mod tests { instance: Some("cloud".into()), browser: Some(BrowserConfig { api_url: Some("http://localhost:4000/v1".into()), + session_timeout_ms: Some(3_600_000), }), telemetry: Some(TelemetryConfig { disabled: Some(true), @@ -209,6 +220,7 @@ mod tests { assert_eq!(loaded.name.as_deref(), Some("CLI")); assert_eq!(loaded.instance.as_deref(), Some("cloud")); assert_eq!(loaded.local_api_url(), Some("http://localhost:4000/v1")); + assert_eq!(loaded.browser_session_timeout_ms(), Some(3_600_000)); assert!(loaded.telemetry_disabled()); } @@ -229,13 +241,14 @@ mod tests { let path = dir.path().join("config.json"); std::fs::write( &path, - r#"{"apiKey":"k","name":"n","instance":"cloud","browser":{"apiUrl":"http://x"},"telemetry":{"disabled":true}}"#, + r#"{"apiKey":"k","name":"n","instance":"cloud","browser":{"apiUrl":"http://x","sessionTimeoutMs":900000},"telemetry":{"disabled":true}}"#, ) .unwrap(); let config = read_config_from(&path).unwrap(); assert_eq!(config.api_key.as_deref(), Some("k")); assert_eq!(config.local_api_url(), Some("http://x")); + assert_eq!(config.browser_session_timeout_ms(), Some(900000)); assert!(config.telemetry_disabled()); } @@ -257,10 +270,12 @@ mod tests { let config = Config { browser: Some(BrowserConfig { api_url: Some(" ".into()), + session_timeout_ms: Some(0), }), ..Default::default() }; assert_eq!(config.local_api_url(), None); + assert_eq!(config.browser_session_timeout_ms(), None); } #[test]