inertia_rust/
node_process.rs

1use std::error::Error;
2use std::path::Path;
3use std::process::{Child, Command};
4use std::{fmt, io};
5
6use reqwest::Url;
7
8#[derive(Debug, Clone)]
9pub struct NodeJsError {
10    cause: String,
11    description: String,
12}
13
14impl NodeJsError {
15    pub fn new(cause: String, description: String) -> Self {
16        NodeJsError { cause, description }
17    }
18
19    pub fn get_cause(&self) -> String {
20        self.cause.clone()
21    }
22
23    pub fn get_description(&self) -> String {
24        self.description.clone()
25    }
26}
27
28impl fmt::Display for NodeJsError {
29    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
30        write!(
31            f,
32            "cause: {};\ndescription: {}",
33            self.cause, self.description
34        )
35    }
36}
37
38impl Error for NodeJsError {
39    fn description(&self) -> &str {
40        &self.description
41    }
42}
43
44#[derive(Debug)]
45pub struct NodeJsProc {
46    child: Child,
47    server: String,
48}
49
50impl NodeJsProc {
51    /// Starts a Node.js child process that runs an Inertia ssr file.
52    ///
53    /// # Arguments
54    /// * `server_path`     - The path to the `ssr.js` file. E.g. "dist/server/ssr.js".
55    /// * `server_url`      - The url where the server is running.
56    ///
57    /// # Errors
58    /// Returns an [`NodeJsError`] if it fails to start the process, e.g. if the machine do not have
59    /// [node] installed.
60    ///
61    /// [node]: https://nodejs.org
62    ///
63    /// # Return
64    /// Returns an `NodeJsProc` instance. Note that you should call the `NodeJsProc::kill(self)`
65    /// method before the application fully shuts down, or else the Node.js process will keep alive.
66    ///
67    /// # Example
68    /// ```rust
69    /// use inertia_rust::node_process::{NodeJsError, NodeJsProc};
70    /// use std::str::FromStr;
71    ///
72    /// async fn server() {
73    ///     let local_url = match reqwest::Url::from_str("localhost:15000") {
74    ///         Ok(url) => url,
75    ///         Err(err) => panic!("Failed to parse url: {}", err),
76    ///     };
77    ///
78    ///     let node = NodeJsProc::start("dist/server/ssr.js".into(), &local_url);
79    ///
80    ///     if node.is_err() {
81    ///         let err: NodeJsError = node.unwrap_err();
82    ///         panic!("Failed to start node server: {}", err);
83    ///     }
84    ///
85    ///     let node = node.unwrap();
86    ///
87    ///     // runs the server asynchronously,blocking the function on .await
88    ///     // when the server stops running, don't forget to:
89    ///     let _ = node.kill();
90    /// }
91    /// ```
92    pub fn start(server_path: String, server_url: &Url) -> Result<Self, NodeJsError> {
93        let path = Path::new(&server_path);
94
95        if !path.exists() {
96            return Err(NodeJsError::new(
97                "Invalid path".into(),
98                format!("Server javascript file not found in {}.", &server_path),
99            ));
100        }
101
102        let string_path = match path.to_str() {
103            None => {
104                return Err(NodeJsError::new(
105                    "Invalid path".into(),
106                    "The given path contains invalid UTF-8 characters.".into(),
107                ))
108            }
109            Some(path) => path,
110        };
111
112        let child = match Command::new("node")
113            .arg(string_path)
114            .arg("--port")
115            .arg(server_url.port().unwrap_or(10000).to_string())
116            .spawn()
117        {
118            Err(err) => {
119                return Err(NodeJsError::new(
120                    "Process error".into(),
121                    format!("Something went wrong on invoking a node server: {}", err),
122                ))
123            }
124            Ok(child) => child,
125        };
126
127        Ok(NodeJsProc {
128            child,
129            server: server_url.to_string(),
130        })
131    }
132
133    /// Kills the current Node.js process.
134    pub async fn kill(self) -> io::Result<()> {
135        let resp = reqwest::Client::new()
136            .get(format!("{}/shutdown", self.server))
137            .send()
138            .await;
139
140        if resp.is_err() {
141            let _ = self.force_kill();
142        }
143
144        Ok(())
145    }
146
147    fn force_kill(mut self) -> io::Result<()> {
148        self.child.kill()
149    }
150}