use crate::server::driver::get_driver_executable;
use crate::{Error, Result};
use tokio::process::{Child, Command};
#[derive(Debug)]
pub struct PlaywrightServer {
pub process: Child,
}
impl PlaywrightServer {
pub async fn launch() -> Result<Self> {
let (node_exe, cli_js) = get_driver_executable()?;
let mut child = Command::new(&node_exe)
.arg(&cli_js)
.arg("run-driver")
.env("PW_LANG_NAME", "rust")
.env("PW_LANG_NAME_VERSION", env!("CARGO_PKG_RUST_VERSION"))
.env("PW_CLI_DISPLAY_VERSION", env!("CARGO_PKG_VERSION"))
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| Error::LaunchFailed(format!("Failed to spawn process: {}", e)))?;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
match child.try_wait() {
Ok(Some(status)) => {
return Err(Error::LaunchFailed(format!(
"Server process exited immediately with status: {}",
status
)));
}
Ok(None) => {
}
Err(e) => {
return Err(Error::LaunchFailed(format!(
"Failed to check process status: {}",
e
)));
}
}
Ok(Self { process: child })
}
pub async fn shutdown(mut self) -> Result<()> {
#[cfg(windows)]
{
drop(self.process.stdin.take());
drop(self.process.stdout.take());
drop(self.process.stderr.take());
self.process
.kill()
.await
.map_err(|e| Error::LaunchFailed(format!("Failed to kill process: {}", e)))?;
match tokio::time::timeout(std::time::Duration::from_secs(5), self.process.wait()).await
{
Ok(Ok(_)) => Ok(()),
Ok(Err(e)) => Err(Error::LaunchFailed(format!(
"Failed to wait for process: {}",
e
))),
Err(_) => {
let _ = self.process.start_kill();
Err(Error::LaunchFailed(
"Process shutdown timeout after 5 seconds".to_string(),
))
}
}
}
#[cfg(not(windows))]
{
self.process
.kill()
.await
.map_err(|e| Error::LaunchFailed(format!("Failed to kill process: {}", e)))?;
let _ = self.process.wait().await;
Ok(())
}
}
pub async fn kill(mut self) -> Result<()> {
#[cfg(windows)]
{
drop(self.process.stdin.take());
drop(self.process.stdout.take());
drop(self.process.stderr.take());
}
self.process
.kill()
.await
.map_err(|e| Error::LaunchFailed(format!("Failed to kill process: {}", e)))?;
#[cfg(windows)]
{
let _ =
tokio::time::timeout(std::time::Duration::from_secs(2), self.process.wait()).await;
}
#[cfg(not(windows))]
{
let _ =
tokio::time::timeout(std::time::Duration::from_millis(500), self.process.wait())
.await;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_server_launch_and_shutdown() {
let result = PlaywrightServer::launch().await;
match result {
Ok(server) => {
tracing::info!("Server launched successfully!");
let shutdown_result = server.shutdown().await;
assert!(
shutdown_result.is_ok(),
"Shutdown failed: {:?}",
shutdown_result
);
}
Err(Error::ServerNotFound) => {
tracing::warn!(
"Could not launch server: Playwright not found and download may have failed"
);
tracing::warn!(
"To run this test, install Playwright manually: npm install playwright"
);
}
Err(Error::LaunchFailed(msg)) => {
tracing::warn!("Launch failed: {}", msg);
tracing::warn!("This may be expected if Node.js or npm is not installed");
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[tokio::test]
async fn test_server_can_be_killed() {
let result = PlaywrightServer::launch().await;
if let Ok(server) = result {
tracing::info!("Server launched, testing kill...");
let kill_result = server.kill().await;
assert!(kill_result.is_ok(), "Kill failed: {:?}", kill_result);
} else {
tracing::warn!("Server didn't launch (expected without Node.js/Playwright)");
}
}
}