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}