mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Fix browser wait during daemon startup
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -214,6 +214,24 @@ impl BrowserRegistry {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Poll until at least one browser is registered or `connect_wait` elapses.
|
||||
/// Returns immediately when the registry is already non-empty.
|
||||
pub async fn wait_for_any_connected(&self, connect_wait: Duration) {
|
||||
if connect_wait.is_zero() || !self.is_empty() {
|
||||
return;
|
||||
}
|
||||
let deadline = Instant::now() + connect_wait;
|
||||
loop {
|
||||
if !self.is_empty() {
|
||||
return;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(EXTENSION_CONNECT_POLL).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the only connected browser when exactly one is online, or
|
||||
/// match a specific id / label when supplied. Returns an error code
|
||||
/// aligned with §4.5.
|
||||
@@ -511,21 +529,29 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn select_with_connect_wait_does_not_wait_on_ambiguous_label() {
|
||||
async fn wait_for_any_connected_returns_when_browser_registers_late() {
|
||||
let reg = std::sync::Arc::new(BrowserRegistry::new());
|
||||
let reg_bg = reg.clone();
|
||||
let waiter = tokio::spawn(async move {
|
||||
reg_bg
|
||||
.wait_for_any_connected(Duration::from_millis(500))
|
||||
.await;
|
||||
});
|
||||
tokio::time::sleep(Duration::from_millis(80)).await;
|
||||
reg.insert(fake_client("late", ""));
|
||||
waiter.await.expect("join");
|
||||
assert_eq!(reg.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_any_connected_skips_when_already_populated() {
|
||||
let reg = BrowserRegistry::new();
|
||||
reg.insert(fake_client("alpha", "Shared"));
|
||||
reg.insert(fake_client("beta", "Shared"));
|
||||
reg.insert(fake_client("a", ""));
|
||||
let started = Instant::now();
|
||||
match reg
|
||||
.select_with_connect_wait(Some("Shared"), Duration::from_secs(1))
|
||||
.await
|
||||
{
|
||||
Err(SelectError::AmbiguousLabel { label, .. }) => assert_eq!(label, "Shared"),
|
||||
other => panic!("expected AmbiguousLabel, got {other:?}"),
|
||||
}
|
||||
reg.wait_for_any_connected(Duration::from_secs(1)).await;
|
||||
assert!(
|
||||
started.elapsed() < Duration::from_millis(50),
|
||||
"ambiguous label should fail immediately"
|
||||
"should return immediately when browsers already connected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bsk_protocol::system::{
|
||||
BrowserStatusEntry, SessionStatusEntry, StatusParams, StatusResult, VersionSkewEntry,
|
||||
BrowserListParams, BrowserStatusEntry, SessionStatusEntry, StatusParams, StatusResult,
|
||||
VersionSkewEntry,
|
||||
};
|
||||
use bsk_protocol::tools::{ReturnFailure, WaitMsParams, WaitMsResult};
|
||||
use bsk_protocol::{
|
||||
CancelParams, CancelResult, ErrorCode, Method, PingResult, ResponseBody, RpcError, RpcId,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Notify;
|
||||
@@ -57,6 +59,8 @@ pub type RpcHandler = Arc<
|
||||
>;
|
||||
|
||||
const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
/// Upper bound on `wait_for_browser_ms` accepted over IPC.
|
||||
const MAX_BROWSER_WAIT: Duration = Duration::from_secs(60);
|
||||
// `session.stop` fast-fails while another tool is active; this budget
|
||||
// only needs to cover the stop RPC itself and IPC scheduling grace.
|
||||
const DEFAULT_SESSION_STOP_TIMEOUT: Duration =
|
||||
@@ -213,7 +217,10 @@ pub fn full_handler(status: DaemonStatus, state: Arc<DaemonState>) -> RpcHandler
|
||||
Err(e) => ResponseBody::Err(e),
|
||||
},
|
||||
Method::SessionList => handle_session_list(&state),
|
||||
Method::BrowserList => handle_browser_list(&state),
|
||||
Method::BrowserList => match handle_browser_list(&state, params).await {
|
||||
Ok(v) => ResponseBody::Ok(v),
|
||||
Err(e) => ResponseBody::Err(e),
|
||||
},
|
||||
Method::ToolTabList
|
||||
| Method::ToolTabCreate
|
||||
| Method::ToolTabClose
|
||||
@@ -554,14 +561,40 @@ async fn handle_status(
|
||||
state: &Arc<DaemonState>,
|
||||
params: Value,
|
||||
) -> Result<Value, RpcError> {
|
||||
if !params.is_null() {
|
||||
let _: StatusParams = serde_json::from_value(params).map_err(|err| RpcError {
|
||||
let params: StatusParams = parse_params_or_default(params)?;
|
||||
maybe_wait_for_browser(state, params.wait_for_browser_ms).await;
|
||||
Ok(serde_json::to_value(status.snapshot_with(state)).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
fn parse_params_or_default<T>(params: Value) -> Result<T, RpcError>
|
||||
where
|
||||
T: DeserializeOwned + Default,
|
||||
{
|
||||
if params.is_null() {
|
||||
Ok(T::default())
|
||||
} else {
|
||||
serde_json::from_value(params).map_err(|err| RpcError {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
})
|
||||
}
|
||||
Ok(serde_json::to_value(status.snapshot_with(state)).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
async fn maybe_wait_for_browser(state: &Arc<DaemonState>, wait_ms: Option<u64>) {
|
||||
if let Some(wait) = clamp_browser_wait(wait_ms) {
|
||||
state.browsers.wait_for_any_connected(wait).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_browser_wait(wait_ms: Option<u64>) -> Option<Duration> {
|
||||
let ms = wait_ms?;
|
||||
if ms == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Duration::from_millis(
|
||||
ms.min(MAX_BROWSER_WAIT.as_millis() as u64),
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_session_start(state: &Arc<DaemonState>, params: Value) -> Result<Value, RpcError> {
|
||||
@@ -788,14 +821,16 @@ fn handle_session_list(state: &Arc<DaemonState>) -> ResponseBody {
|
||||
ResponseBody::Ok(serde_json::to_value(SessionListResult { sessions }).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
fn handle_browser_list(state: &Arc<DaemonState>) -> ResponseBody {
|
||||
async fn handle_browser_list(state: &Arc<DaemonState>, params: Value) -> Result<Value, RpcError> {
|
||||
let params: BrowserListParams = parse_params_or_default(params)?;
|
||||
maybe_wait_for_browser(state, params.wait_for_browser_ms).await;
|
||||
// Reuse the same helper that produces the
|
||||
// `multiple_browsers_online.error.data.browsers` payload so the
|
||||
// two surfaces always agree on order — sorted by `connected_at_ms`
|
||||
// ascending, with `instance_id` as a deterministic tiebreaker
|
||||
// (review I1).
|
||||
let browsers = snapshot_status_entries(&state.browsers, &state.sessions);
|
||||
ResponseBody::Ok(serde_json::to_value(BrowserListResult { browsers }).unwrap_or(Value::Null))
|
||||
Ok(serde_json::to_value(BrowserListResult { browsers }).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
// ----- Test-helper IpcServer wrapper around the transport layer -----
|
||||
|
||||
Reference in New Issue
Block a user