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}