playwright_core/server.rs
1// Playwright server management
2//
3// Handles downloading, launching, and managing the lifecycle of the Playwright
4// Node.js server process.
5
6use crate::driver::get_driver_executable;
7use crate::{Error, Result};
8use tokio::process::{Child, Command};
9
10/// Manages the Playwright server process lifecycle
11///
12/// The PlaywrightServer wraps a Node.js child process that runs the Playwright
13/// driver. It communicates with the server via stdio pipes using JSON-RPC protocol.
14///
15/// # Example
16///
17/// ```ignore
18/// # use playwright_core::PlaywrightServer;
19/// # #[tokio::main]
20/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
21/// let server = PlaywrightServer::launch().await?;
22/// // Use the server...
23/// server.shutdown().await?;
24/// # Ok(())
25/// # }
26/// ```
27#[derive(Debug)]
28pub struct PlaywrightServer {
29 /// The Playwright server child process
30 ///
31 /// This is public to allow integration tests to access stdin/stdout pipes.
32 /// In production code, you should use the Connection layer instead of
33 /// accessing the process directly.
34 pub process: Child,
35}
36
37impl PlaywrightServer {
38 /// Launch the Playwright server process
39 ///
40 /// This will:
41 /// 1. Check if the Playwright driver exists (download if needed)
42 /// 2. Launch the server using `node <driver>/cli.js run-driver`
43 /// 3. Set environment variable `PW_LANG_NAME=rust`
44 ///
45 /// # Errors
46 ///
47 /// Returns `Error::ServerNotFound` if the driver cannot be located.
48 /// Returns `Error::LaunchFailed` if the process fails to start.
49 ///
50 /// See: <https://playwright.dev/docs/api>
51 pub async fn launch() -> Result<Self> {
52 // Get the driver executable paths
53 // The driver should already be downloaded by build.rs
54 let (node_exe, cli_js) = get_driver_executable()?;
55
56 // Launch the server process
57 let mut child = Command::new(&node_exe)
58 .arg(&cli_js)
59 .arg("run-driver")
60 .env("PW_LANG_NAME", "rust")
61 .env("PW_LANG_NAME_VERSION", env!("CARGO_PKG_RUST_VERSION"))
62 .env("PW_CLI_DISPLAY_VERSION", env!("CARGO_PKG_VERSION"))
63 .stdin(std::process::Stdio::piped())
64 .stdout(std::process::Stdio::piped())
65 .stderr(std::process::Stdio::inherit())
66 .spawn()
67 .map_err(|e| Error::LaunchFailed(format!("Failed to spawn process: {}", e)))?;
68
69 // Check if process started successfully
70 // Give it a moment to potentially fail
71 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
72
73 match child.try_wait() {
74 Ok(Some(status)) => {
75 return Err(Error::LaunchFailed(format!(
76 "Server process exited immediately with status: {}",
77 status
78 )));
79 }
80 Ok(None) => {
81 // Process is still running, good!
82 }
83 Err(e) => {
84 return Err(Error::LaunchFailed(format!(
85 "Failed to check process status: {}",
86 e
87 )));
88 }
89 }
90
91 Ok(Self { process: child })
92 }
93
94 /// Shut down the server gracefully
95 ///
96 /// Sends a shutdown signal to the server and waits for it to exit.
97 ///
98 /// # Platform-Specific Behavior
99 ///
100 /// **Windows**: Explicitly closes stdio pipes before killing the process to avoid
101 /// hangs. On Windows, tokio uses a blocking threadpool for child process stdio,
102 /// and failing to close pipes before terminating can cause the cleanup to hang
103 /// indefinitely. Uses a timeout to prevent permanent hangs.
104 ///
105 /// **Unix**: Uses standard process termination with graceful wait.
106 ///
107 /// # Errors
108 ///
109 /// Returns an error if the shutdown fails or times out.
110 pub async fn shutdown(mut self) -> Result<()> {
111 #[cfg(windows)]
112 {
113 // Windows-specific cleanup: Close stdio pipes BEFORE killing process
114 // This prevents hanging due to Windows' blocking threadpool for stdio
115 drop(self.process.stdin.take());
116 drop(self.process.stdout.take());
117 drop(self.process.stderr.take());
118
119 // Kill the process
120 self.process
121 .kill()
122 .await
123 .map_err(|e| Error::LaunchFailed(format!("Failed to kill process: {}", e)))?;
124
125 // Wait for process to exit with timeout (Windows can hang without this)
126 match tokio::time::timeout(std::time::Duration::from_secs(5), self.process.wait()).await
127 {
128 Ok(Ok(_)) => Ok(()),
129 Ok(Err(e)) => Err(Error::LaunchFailed(format!(
130 "Failed to wait for process: {}",
131 e
132 ))),
133 Err(_) => {
134 // Timeout - try one more kill
135 let _ = self.process.start_kill();
136 Err(Error::LaunchFailed(
137 "Process shutdown timeout after 5 seconds".to_string(),
138 ))
139 }
140 }
141 }
142
143 #[cfg(not(windows))]
144 {
145 // Unix: Standard graceful shutdown
146 self.process
147 .kill()
148 .await
149 .map_err(|e| Error::LaunchFailed(format!("Failed to kill process: {}", e)))?;
150
151 // Wait for process to exit
152 let _ = self.process.wait().await;
153
154 Ok(())
155 }
156 }
157
158 /// Force kill the server process
159 ///
160 /// This should only be used if graceful shutdown fails.
161 ///
162 /// # Platform-Specific Behavior
163 ///
164 /// **Windows**: Closes stdio pipes before killing to prevent hangs.
165 ///
166 /// **Unix**: Standard force kill operation.
167 ///
168 /// # Errors
169 ///
170 /// Returns an error if the kill operation fails.
171 pub async fn kill(mut self) -> Result<()> {
172 #[cfg(windows)]
173 {
174 // Windows: Close pipes before killing
175 drop(self.process.stdin.take());
176 drop(self.process.stdout.take());
177 drop(self.process.stderr.take());
178 }
179
180 self.process
181 .kill()
182 .await
183 .map_err(|e| Error::LaunchFailed(format!("Failed to kill process: {}", e)))?;
184
185 #[cfg(windows)]
186 {
187 // On Windows, wait with timeout
188 let _ =
189 tokio::time::timeout(std::time::Duration::from_secs(2), self.process.wait()).await;
190 }
191
192 #[cfg(not(windows))]
193 {
194 // On Unix, optionally wait (don't block)
195 let _ =
196 tokio::time::timeout(std::time::Duration::from_millis(500), self.process.wait())
197 .await;
198 }
199
200 Ok(())
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[tokio::test]
209 async fn test_server_launch_and_shutdown() {
210 // This test will attempt to launch the Playwright server
211 // If Playwright is not installed, it will try to download it
212 let result = PlaywrightServer::launch().await;
213
214 match result {
215 Ok(server) => {
216 println!("Server launched successfully!");
217 // Clean shutdown
218 let shutdown_result = server.shutdown().await;
219 assert!(
220 shutdown_result.is_ok(),
221 "Shutdown failed: {:?}",
222 shutdown_result
223 );
224 }
225 Err(Error::ServerNotFound) => {
226 // This can happen if npm is not installed or download fails
227 eprintln!(
228 "Could not launch server: Playwright not found and download may have failed"
229 );
230 eprintln!("To run this test, install Playwright manually: npm install playwright");
231 // Don't fail the test - this is expected in CI without Node.js
232 }
233 Err(Error::LaunchFailed(msg)) => {
234 eprintln!("Launch failed: {}", msg);
235 eprintln!("This may be expected if Node.js or npm is not installed");
236 // Don't fail - expected in environments without Node.js
237 }
238 Err(e) => panic!("Unexpected error: {:?}", e),
239 }
240 }
241
242 #[tokio::test]
243 async fn test_server_can_be_killed() {
244 // Test that we can force-kill a server
245 let result = PlaywrightServer::launch().await;
246
247 if let Ok(server) = result {
248 println!("Server launched, testing kill...");
249 let kill_result = server.kill().await;
250 assert!(kill_result.is_ok(), "Kill failed: {:?}", kill_result);
251 } else {
252 // Server didn't launch, that's okay for this test
253 eprintln!("Server didn't launch (expected without Node.js/Playwright)");
254 }
255 }
256}