Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/bsk-cli/skill/references/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ was replaced, check session state before continuing; do not blindly replay actio
A local process identity warning permits browser commands when IPC works.
For other startup failures, retry once, then use `bsk doctor`.

## Idle timeouts configuration

BrowserSkill provides configurable idle timeouts for both browser sessions and the background daemon:

- `BSK_SESSION_IDLE` (default `5m`): Duration before an inactive browser session is automatically reaped (e.g. `10m`, `30m`, `1h`). Can also be passed via `bsk daemon start --session-idle <duration>`.
- `BSK_DAEMON_IDLE` (default `10m`): Duration of total inactivity before an unattached local daemon terminates (e.g. `30m`, `2h`). Can also be passed via `bsk daemon start --daemon-idle <duration>`.

Both environment variables are respected during automatic daemon startup (`ensure_daemon`) as well as explicit `bsk daemon start` / `restart`.

## Extension connection

If the intended extension is still disconnected after the applicable setup above,
Expand Down
20 changes: 16 additions & 4 deletions crates/bsk-cli/src/cli/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ pub struct StartArgs {
#[arg(long)]
pub foreground: bool,

/// Session idle timeout, e.g. `5m`, `30s`. Default 5 minutes.
/// Session idle timeout, e.g. `5m`, `30s`. Default 5 minutes. Configurable via `BSK_SESSION_IDLE`.
#[arg(long, value_name = "DURATION", value_parser = parse_duration)]
pub session_idle: Option<Duration>,

/// Daemon idle timeout, e.g. `10m`, `2s`. Default 10 minutes.
/// Daemon idle timeout, e.g. `10m`, `2s`. Default 10 minutes. Configurable via `BSK_DAEMON_IDLE`.
#[arg(long, value_name = "DURATION", value_parser = parse_duration)]
pub daemon_idle: Option<Duration>,
}
Expand Down Expand Up @@ -138,11 +138,23 @@ impl StartArgs {
}

pub fn resolved_session_idle(&self) -> Duration {
self.session_idle.unwrap_or(DEFAULT_SESSION_IDLE)
self.session_idle
.or_else(|| {
std::env::var("BSK_SESSION_IDLE")
.ok()
.and_then(|v| parse_duration(&v).ok())
})
.unwrap_or(DEFAULT_SESSION_IDLE)
}

pub fn resolved_daemon_idle(&self) -> Duration {
self.daemon_idle.unwrap_or(DEFAULT_DAEMON_IDLE)
self.daemon_idle
.or_else(|| {
std::env::var("BSK_DAEMON_IDLE")
.ok()
.and_then(|v| parse_duration(&v).ok())
})
.unwrap_or(DEFAULT_DAEMON_IDLE)
}
}

Expand Down
15 changes: 13 additions & 2 deletions crates/bsk-cli/src/cli/ensure_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::time::{Duration, Instant};

use anyhow::{Context, Result, ensure};

use crate::cli::daemon::StartArgs;
use crate::cli::daemon::{StartArgs, parse_duration};
use crate::daemon::info::DaemonInfo;
use crate::daemon::probe::{self, PROBE_TIMEOUT, Probe};
use crate::daemon::start::start_background;
Expand All @@ -36,5 +36,16 @@ pub fn ensure_daemon() -> Result<DaemonInfo> {
return Ok(daemon.info);
}
ensure!(auto_start_enabled(), DaemonStartFailure::AutoStartDisabled);
start_background(&StartArgs::default(), deadline).context("automatic daemon startup failed")
let mut args = StartArgs::default();
if let Ok(val) = std::env::var("BSK_SESSION_IDLE") {
args.session_idle = Some(
parse_duration(&val).map_err(|e| anyhow::anyhow!("invalid BSK_SESSION_IDLE: {e}"))?,
);
}
if let Ok(val) = std::env::var("BSK_DAEMON_IDLE") {
args.daemon_idle = Some(
parse_duration(&val).map_err(|e| anyhow::anyhow!("invalid BSK_DAEMON_IDLE: {e}"))?,
);
}
start_background(&args, deadline).context("automatic daemon startup failed")
}
20 changes: 18 additions & 2 deletions crates/bsk-cli/src/daemon/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use anyhow::{Context, Result};
use bsk_protocol::StatusResult;
use tracing::{debug, error, info, warn};

use crate::cli::daemon::StartArgs;
use crate::cli::daemon::{StartArgs, parse_duration};
use crate::cli::ensure_daemon::SPAWN_DEADLINE;
use crate::cli::update::state::UpdateRecord;
use crate::daemon::{
Expand Down Expand Up @@ -137,7 +137,23 @@ impl From<&StartArgs> for DaemonConfig {
}

/// `bsk daemon start` entrypoint.
pub fn run_start(args: StartArgs) -> Result<()> {
pub fn run_start(mut args: StartArgs) -> Result<()> {
if args.session_idle.is_none() {
if let Ok(val) = std::env::var("BSK_SESSION_IDLE") {
args.session_idle = Some(
parse_duration(&val)
.map_err(|e| anyhow::anyhow!("invalid BSK_SESSION_IDLE: {e}"))?,
);
}
}
if args.daemon_idle.is_none() {
if let Ok(val) = std::env::var("BSK_DAEMON_IDLE") {
args.daemon_idle = Some(
parse_duration(&val)
.map_err(|e| anyhow::anyhow!("invalid BSK_DAEMON_IDLE: {e}"))?,
);
}
}
let mut cfg = DaemonConfig::from(&args);
cfg.server = args.server_config()?;

Expand Down
92 changes: 92 additions & 0 deletions crates/bsk-cli/tests/cli_parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,42 @@ fn parses_upload_modes() {
);
}

static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

struct EnvGuard {
key: &'static str,
original: Option<String>,
}

impl EnvGuard {
fn set(key: &'static str, value: &str) -> Self {
let original = std::env::var(key).ok();
unsafe { std::env::set_var(key, value) };
Self { key, original }
}

fn remove(key: &'static str) -> Self {
let original = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
Self { key, original }
}
}

impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.original {
Some(v) => unsafe { std::env::set_var(self.key, v) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}

#[test]
fn parses_daemon_start_with_defaults() {
let _lock = ENV_LOCK.lock().unwrap();
let _g1 = EnvGuard::remove("BSK_SESSION_IDLE");
let _g2 = EnvGuard::remove("BSK_DAEMON_IDLE");

let cli = parse(&["bsk", "daemon", "start"]);
let Command::Daemon(DaemonCmd::Start(args)) = cli.command else {
panic!("expected daemon start subcommand");
Expand All @@ -142,10 +176,15 @@ fn parses_daemon_start_with_defaults() {
assert!(!args.foreground);
assert_eq!(args.resolved_port(), 52800);
assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(600));
assert_eq!(args.resolved_session_idle(), Duration::from_secs(300));
}

#[test]
fn parses_daemon_start_with_flags() {
let _lock = ENV_LOCK.lock().unwrap();
let _g1 = EnvGuard::remove("BSK_SESSION_IDLE");
let _g2 = EnvGuard::remove("BSK_DAEMON_IDLE");

let cli = parse(&[
"bsk",
"daemon",
Expand All @@ -167,6 +206,59 @@ fn parses_daemon_start_with_flags() {
assert_eq!(args.resolved_session_idle(), Duration::from_secs(30));
}

#[test]
fn parses_daemon_start_with_env_idle() {
let _lock = ENV_LOCK.lock().unwrap();
let _g1 = EnvGuard::set("BSK_SESSION_IDLE", "15m");
let _g2 = EnvGuard::set("BSK_DAEMON_IDLE", "45m");

let cli = parse(&["bsk", "daemon", "start"]);
let Command::Daemon(DaemonCmd::Start(args)) = cli.command else {
panic!("expected daemon start subcommand");
};
assert_eq!(args.session_idle, None);
assert_eq!(args.daemon_idle, None);
assert_eq!(args.resolved_session_idle(), Duration::from_secs(15 * 60));
assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(45 * 60));
}

#[test]
fn cli_flags_override_env_idle() {
let _lock = ENV_LOCK.lock().unwrap();
let _g1 = EnvGuard::set("BSK_SESSION_IDLE", "15m");
let _g2 = EnvGuard::set("BSK_DAEMON_IDLE", "45m");

let cli = parse(&[
"bsk",
"daemon",
"start",
"--session-idle",
"2m",
"--daemon-idle",
"5m",
]);
let Command::Daemon(DaemonCmd::Start(args)) = cli.command else {
panic!("expected daemon start subcommand");
};
assert_eq!(args.session_idle, Some(Duration::from_secs(2 * 60)));
assert_eq!(args.daemon_idle, Some(Duration::from_secs(5 * 60)));
assert_eq!(args.resolved_session_idle(), Duration::from_secs(2 * 60));
assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(5 * 60));
}

#[test]
fn resolved_idle_falls_back_to_env_when_unpopulated() {
let _lock = ENV_LOCK.lock().unwrap();
let _g1 = EnvGuard::set("BSK_SESSION_IDLE", "20m");
let _g2 = EnvGuard::set("BSK_DAEMON_IDLE", "50m");

let args = bsk::cli::daemon::StartArgs::default();
assert_eq!(args.session_idle, None);
assert_eq!(args.daemon_idle, None);
assert_eq!(args.resolved_session_idle(), Duration::from_secs(20 * 60));
assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(50 * 60));
}

#[test]
fn parses_daemon_stop_and_restart() {
let cli = parse(&["bsk", "daemon", "stop"]);
Expand Down