mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72c10aab75 | ||
|
|
1eeb55af02 |
@@ -37,6 +37,21 @@ jobs:
|
|||||||
- name: Run Rust tests
|
- name: Run Rust tests
|
||||||
run: cargo test --workspace --locked
|
run: cargo test --workspace --locked
|
||||||
|
|
||||||
|
windows-daemon:
|
||||||
|
name: Windows daemon regression tests
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Run Windows named-pipe tests
|
||||||
|
run: cargo test -p bsk --lib --locked daemon::ipc::windows::tests
|
||||||
|
|
||||||
|
- name: Run Windows process liveness tests
|
||||||
|
run: cargo test -p bsk --lib --locked daemon::lockfile::tests
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
name: Frontend lint, typecheck, tests, build
|
name: Frontend lint, typecheck, tests, build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -1399,6 +1399,28 @@ mod windows {
|
|||||||
connected = pipe.connect() => {
|
connected = pipe.connect() => {
|
||||||
match connected {
|
match connected {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
// Keep the connected instance alive until its replacement
|
||||||
|
// exists. Otherwise a fast handler can close the last
|
||||||
|
// instance and make new clients fail with NotFound.
|
||||||
|
loop {
|
||||||
|
match ServerOptions::new()
|
||||||
|
.access_inbound(true)
|
||||||
|
.access_outbound(true)
|
||||||
|
.create(&listener.pipe_name)
|
||||||
|
{
|
||||||
|
Ok(next) => {
|
||||||
|
listener.first = Some(next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(?err, "create next named-pipe instance failed");
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut shutdown => return,
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
on_open();
|
on_open();
|
||||||
let handler = handler.clone();
|
let handler = handler.clone();
|
||||||
let on_act = on_activity.clone();
|
let on_act = on_activity.clone();
|
||||||
@@ -1467,6 +1489,118 @@ mod windows {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::net::windows::named_pipe::ClientOptions;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
fn isolated_listener() -> NamedPipeListener {
|
||||||
|
let pipe_name = format!(r"\\.\pipe\bsk-test-{}", uuid::Uuid::new_v4());
|
||||||
|
let first = ServerOptions::new()
|
||||||
|
.first_pipe_instance(true)
|
||||||
|
.create(&pipe_name)
|
||||||
|
.unwrap();
|
||||||
|
NamedPipeListener {
|
||||||
|
pipe_name,
|
||||||
|
first: Some(first),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn next_instance_exists_before_connection_is_handed_off() {
|
||||||
|
let listener = isolated_listener();
|
||||||
|
let name = listener.pipe_name.clone();
|
||||||
|
let probe_name = name.clone();
|
||||||
|
let (probe_tx, probe_rx) = oneshot::channel();
|
||||||
|
let probe_tx = Mutex::new(Some(probe_tx));
|
||||||
|
let (stop_tx, stop_rx) = oneshot::channel();
|
||||||
|
let server = tokio::spawn(serve(
|
||||||
|
listener,
|
||||||
|
crate::daemon::ipc::default_ping_handler(),
|
||||||
|
move || {
|
||||||
|
if let Some(tx) = probe_tx.lock().unwrap().take() {
|
||||||
|
// This callback runs before the connection task can finish.
|
||||||
|
// A second client must already have an instance to open.
|
||||||
|
let result = ClientOptions::new().open(&probe_name).map(drop);
|
||||||
|
let _ = tx.send(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|| {},
|
||||||
|
|| {},
|
||||||
|
async {
|
||||||
|
let _ = stop_rx.await;
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let client = ClientOptions::new().open(&name).unwrap();
|
||||||
|
let result = tokio::time::timeout(Duration::from_secs(5), probe_rx).await;
|
||||||
|
drop(client);
|
||||||
|
let _ = stop_tx.send(());
|
||||||
|
server.await.unwrap();
|
||||||
|
result
|
||||||
|
.expect("accept callback ran")
|
||||||
|
.unwrap()
|
||||||
|
.expect("next pipe instance is ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn rapid_disconnects_and_concurrent_rpc_connections() {
|
||||||
|
let listener = isolated_listener();
|
||||||
|
let name = listener.pipe_name.clone();
|
||||||
|
let (stop_tx, stop_rx) = oneshot::channel();
|
||||||
|
let server = tokio::spawn(serve(
|
||||||
|
listener,
|
||||||
|
crate::daemon::ipc::default_ping_handler(),
|
||||||
|
|| {},
|
||||||
|
|| {},
|
||||||
|
|| {},
|
||||||
|
async {
|
||||||
|
let _ = stop_rx.await;
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let exercise = async {
|
||||||
|
// Include clients that close without sending any request.
|
||||||
|
for _ in 0..64 {
|
||||||
|
drop(
|
||||||
|
crate::ipc_client::Client::connect_path(name.clone().into())
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut clients = tokio::task::JoinSet::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
let name = name.clone();
|
||||||
|
clients.spawn(async move {
|
||||||
|
for _ in 0..32 {
|
||||||
|
let mut client =
|
||||||
|
crate::ipc_client::Client::connect_path(name.clone().into())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let reply: bsk_protocol::PingResult = client
|
||||||
|
.call(
|
||||||
|
bsk_protocol::Method::SystemPing,
|
||||||
|
&serde_json::json!({}),
|
||||||
|
Duration::from_secs(5),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(reply.pong);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
while let Some(result) = clients.join_next().await {
|
||||||
|
result.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = tokio::time::timeout(Duration::from_secs(30), exercise).await;
|
||||||
|
let _ = stop_tx.send(());
|
||||||
|
server.await.unwrap();
|
||||||
|
result.expect("connection exercise completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -90,17 +90,21 @@ pub fn pid_alive(pid: u32) -> bool {
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
use windows_sys::Win32::Foundation::CloseHandle;
|
use windows_sys::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT};
|
||||||
use windows_sys::Win32::System::Threading::{
|
use windows_sys::Win32::System::Threading::{
|
||||||
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
|
OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject,
|
||||||
};
|
};
|
||||||
unsafe {
|
unsafe {
|
||||||
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
let handle = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid);
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// An exited process can still be opened while another handle
|
||||||
|
// keeps its kernel object alive. Poll its termination signal;
|
||||||
|
// unlike checking STILL_ACTIVE, this also handles exit code 259.
|
||||||
|
let alive = WaitForSingleObject(handle, 0) == WAIT_TIMEOUT;
|
||||||
CloseHandle(handle);
|
CloseHandle(handle);
|
||||||
true
|
alive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,3 +114,39 @@ pub fn pid_alive(pid: u32) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, windows))]
|
||||||
|
mod tests {
|
||||||
|
use super::pid_alive;
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_process_is_alive() {
|
||||||
|
assert!(pid_alive(std::process::id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_process_is_not_alive() {
|
||||||
|
assert!(!pid_alive(0));
|
||||||
|
assert!(!pid_alive(u32::MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exited_process_with_retained_handle_is_not_alive() {
|
||||||
|
for exit_code in [0, 259] {
|
||||||
|
let mut child = Command::new("cmd.exe")
|
||||||
|
.args(["/D", "/C", &format!("exit {exit_code}")])
|
||||||
|
.creation_flags(0x0800_0000) // CREATE_NO_WINDOW
|
||||||
|
.spawn()
|
||||||
|
.expect("spawn short-lived process");
|
||||||
|
assert_eq!(child.wait().unwrap().code(), Some(exit_code));
|
||||||
|
// Keep Child (and its process handle) alive during the probe.
|
||||||
|
assert!(
|
||||||
|
!pid_alive(child.id()),
|
||||||
|
"exited process with code {exit_code}"
|
||||||
|
);
|
||||||
|
drop(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user