re_sdk/
spawn.rs

1/// Options to control the behavior of [`spawn`].
2///
3/// Refer to the field-level documentation for more information about each individual options.
4///
5/// The defaults are ok for most use cases: `SpawnOptions::default()`.
6/// Use the partial-default pattern to customize them further:
7/// ```no_run
8/// let opts = re_sdk::SpawnOptions {
9///     port: 1234,
10///     memory_limit: "25%".into(),
11///     ..Default::default()
12/// };
13/// ```
14#[derive(Debug, Clone)]
15pub struct SpawnOptions {
16    /// The port to listen on.
17    ///
18    /// Defaults to `9876`.
19    pub port: u16,
20
21    /// If `true`, the call to [`spawn`] will block until the Rerun Viewer
22    /// has successfully bound to the port.
23    pub wait_for_bind: bool,
24
25    /// An upper limit on how much memory the Rerun Viewer should use.
26    /// When this limit is reached, Rerun will drop the oldest data.
27    /// Example: `16GB` or `50%` (of system total).
28    ///
29    /// Defaults to `75%`.
30    pub memory_limit: String,
31
32    /// Specifies the name of the Rerun executable.
33    ///
34    /// You can omit the `.exe` suffix on Windows.
35    ///
36    /// Defaults to `rerun`.
37    pub executable_name: String,
38
39    /// Enforce a specific executable to use instead of searching though PATH
40    /// for [`Self::executable_name`].
41    ///
42    /// Unspecified by default.
43    pub executable_path: Option<String>,
44
45    /// Extra arguments that will be passed as-is to the Rerun Viewer process.
46    pub extra_args: Vec<String>,
47
48    /// Extra environment variables that will be passed as-is to the Rerun Viewer process.
49    pub extra_env: Vec<(String, String)>,
50
51    /// Hide the welcome screen.
52    pub hide_welcome_screen: bool,
53
54    /// Detach Rerun Viewer process from the application process.
55    pub detach_process: bool,
56}
57
58// NOTE: No need for .exe extension on windows.
59const RERUN_BINARY: &str = "rerun";
60
61impl Default for SpawnOptions {
62    fn default() -> Self {
63        Self {
64            port: re_grpc_server::DEFAULT_SERVER_PORT,
65            wait_for_bind: false,
66            memory_limit: "75%".into(),
67            executable_name: RERUN_BINARY.into(),
68            executable_path: None,
69            extra_args: Vec::new(),
70            extra_env: Vec::new(),
71            hide_welcome_screen: false,
72            detach_process: true,
73        }
74    }
75}
76
77impl SpawnOptions {
78    /// Resolves the final connect address value.
79    pub fn connect_addr(&self) -> std::net::SocketAddr {
80        std::net::SocketAddr::new("127.0.0.1".parse().unwrap(), self.port)
81    }
82
83    /// Resolves the final listen address value.
84    pub fn listen_addr(&self) -> std::net::SocketAddr {
85        std::net::SocketAddr::new("0.0.0.0".parse().unwrap(), self.port)
86    }
87
88    /// Resolves the final executable path.
89    pub fn executable_path(&self) -> String {
90        if let Some(path) = self.executable_path.as_deref() {
91            return path.to_owned();
92        }
93
94        #[cfg(debug_assertions)]
95        {
96            let local_build_path = format!("target/debug/{RERUN_BINARY}");
97            if std::fs::metadata(&local_build_path).is_ok() {
98                re_log::info!("Spawning the locally built rerun at {local_build_path}");
99                return local_build_path;
100            }
101        }
102
103        self.executable_name.clone()
104    }
105}
106
107/// Errors that can occur when [`spawn`]ing a Rerun Viewer.
108#[derive(thiserror::Error)]
109pub enum SpawnError {
110    /// Failed to find Rerun Viewer executable in PATH.
111    #[error("Failed to find Rerun Viewer executable in PATH.\n{message}\nPATH={search_path:?}")]
112    ExecutableNotFoundInPath {
113        /// High-level error message meant to be printed to the user (install tips etc).
114        message: String,
115
116        /// Name used for the executable search.
117        executable_name: String,
118
119        /// Value of the `PATH` environment variable, if any.
120        search_path: String,
121    },
122
123    /// Failed to find Rerun Viewer executable at explicit path.
124    #[error("Failed to find Rerun Viewer executable at {executable_path:?}")]
125    ExecutableNotFound {
126        /// Explicit path of the executable (specified by the caller).
127        executable_path: String,
128    },
129
130    /// Other I/O error.
131    #[error("Failed to spawn the Rerun Viewer process: {0}")]
132    Io(#[from] std::io::Error),
133}
134
135impl std::fmt::Debug for SpawnError {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        // Due to how recording streams are initialized in practice, most of the time `SpawnError`s
138        // will bubble all the way up to `main` and crash the program, which will call into the
139        // `Debug` implementation.
140        //
141        // Spawn errors include a user guide, and so we need them to render in a nice way.
142        // Hence we redirect the debug impl to the display impl generated by `thiserror`.
143        <Self as std::fmt::Display>::fmt(self, f)
144    }
145}
146
147/// Spawns a new Rerun Viewer process ready to listen for connections.
148///
149/// If there is already a process listening on this port (Rerun or not), this function returns `Ok`
150/// WITHOUT spawning a `rerun` process (!).
151///
152/// Refer to [`SpawnOptions`]'s documentation for configuration options.
153///
154/// This only starts a Viewer process: if you'd like to connect to it and start sending data, refer
155/// to [`crate::RecordingStream::connect_grpc`] or use [`crate::RecordingStream::spawn`] directly.
156#[allow(unsafe_code)]
157pub fn spawn(opts: &SpawnOptions) -> Result<(), SpawnError> {
158    #[cfg(target_family = "unix")]
159    use std::os::unix::process::CommandExt as _;
160
161    use std::{net::TcpStream, process::Command, time::Duration};
162
163    // NOTE: These are indented on purpose, it just looks better and reads easier.
164
165    const MSG_INSTALL_HOW_TO: &str = //
166    "
167    You can install binary releases of the Rerun Viewer:
168    * Using `cargo`: `cargo binstall rerun-cli` (see https://github.com/cargo-bins/cargo-binstall)
169    * Via direct download from our release assets: https://github.com/rerun-io/rerun/releases/latest/
170    * Using `pip`: `pip3 install rerun-sdk`
171
172    For more information, refer to our complete install documentation over at:
173    https://rerun.io/docs/getting-started/installing-viewer
174    ";
175
176    const MSG_INSTALL_HOW_TO_VERSIONED: &str = //
177    "
178    You can install an appropriate version of the Rerun Viewer via binary releases:
179    * Using `cargo`: `cargo binstall --force rerun-cli@__VIEWER_VERSION__` (see https://github.com/cargo-bins/cargo-binstall)
180    * Via direct download from our release assets: https://github.com/rerun-io/rerun/releases/__VIEWER_VERSION__/
181    * Using `pip`: `pip3 install rerun-sdk==__VIEWER_VERSION__`
182
183    For more information, refer to our complete install documentation over at:
184    https://rerun.io/docs/getting-started/installing-viewer
185    ";
186
187    const MSG_VERSION_MISMATCH: &str = //
188        "
189    ⚠ The version of the Rerun Viewer available on your PATH does not match the version of your Rerun SDK ⚠
190
191    Rerun does not make any kind of backwards/forwards compatibility guarantee yet: this can lead to (subtle) bugs.
192
193    > Rerun Viewer: v__VIEWER_VERSION__ (executable: \"__VIEWER_PATH__\")
194    > Rerun SDK: v__SDK_VERSION__";
195
196    let port = opts.port;
197    let connect_addr = opts.connect_addr();
198    let memory_limit = &opts.memory_limit;
199    let executable_path = opts.executable_path();
200
201    // TODO(#4019): application-level handshake
202    if TcpStream::connect_timeout(&connect_addr, Duration::from_secs(1)).is_ok() {
203        re_log::info!(
204            addr = %opts.listen_addr(),
205            "A process is already listening at this address. Assuming it's a Rerun Viewer."
206        );
207        return Ok(());
208    }
209
210    let map_err = |err: std::io::Error| -> SpawnError {
211        if err.kind() == std::io::ErrorKind::NotFound {
212            if let Some(executable_path) = opts.executable_path.as_ref() {
213                SpawnError::ExecutableNotFound {
214                    executable_path: executable_path.clone(),
215                }
216            } else {
217                let sdk_version = re_build_info::build_info!().version;
218                SpawnError::ExecutableNotFoundInPath {
219                    // Only recommend a specific Viewer version for non-alpha/rc/dev SDKs.
220                    message: if sdk_version.is_release() {
221                        MSG_INSTALL_HOW_TO_VERSIONED
222                            .replace("__VIEWER_VERSION__", &sdk_version.to_string())
223                    } else {
224                        MSG_INSTALL_HOW_TO.to_owned()
225                    },
226                    executable_name: opts.executable_name.clone(),
227                    search_path: std::env::var("PATH").unwrap_or_else(|_| String::new()),
228                }
229            }
230        } else {
231            err.into()
232        }
233    };
234
235    // Try to check the version of the Viewer.
236    // Do not fail if we can't retrieve the version, it's not a critical error.
237    let viewer_version = Command::new(&executable_path)
238        .arg("--version")
239        .output()
240        .ok()
241        .and_then(|output| {
242            let output = String::from_utf8_lossy(&output.stdout);
243            re_build_info::CrateVersion::try_parse_from_build_info_string(output).ok()
244        });
245
246    if let Some(viewer_version) = viewer_version {
247        let sdk_version = re_build_info::build_info!().version;
248
249        if !viewer_version.is_compatible_with(sdk_version) {
250            eprintln!(
251                "{}",
252                MSG_VERSION_MISMATCH
253                    .replace("__VIEWER_VERSION__", &viewer_version.to_string())
254                    .replace("__VIEWER_PATH__", &executable_path)
255                    .replace("__SDK_VERSION__", &sdk_version.to_string())
256            );
257
258            // Don't recommend installing stuff through registries if the user is running some
259            // weird version.
260            if sdk_version.is_release() {
261                eprintln!(
262                    "{}",
263                    MSG_INSTALL_HOW_TO_VERSIONED
264                        .replace("__VIEWER_VERSION__", &sdk_version.to_string())
265                );
266            } else {
267                eprintln!();
268            }
269        }
270    }
271
272    let mut rerun_bin = Command::new(&executable_path);
273
274    // By default stdin is inherited which may cause issues in some debugger setups.
275    // Also, there's really no reason to forward stdin to the child process in this case.
276    // `stdout`/`stderr` we leave at default inheritance because it can be useful to see the Viewer's output.
277    rerun_bin
278        .stdin(std::process::Stdio::null())
279        .arg(format!("--port={port}"))
280        .arg(format!("--memory-limit={memory_limit}"))
281        .arg("--expect-data-soon");
282
283    if opts.hide_welcome_screen {
284        rerun_bin.arg("--hide-welcome-screen");
285    }
286
287    rerun_bin.args(opts.extra_args.clone());
288    rerun_bin.envs(opts.extra_env.clone());
289
290    if opts.detach_process {
291        // SAFETY: This code is only run in the child fork, we are not modifying any memory
292        // that is shared with the parent process.
293        #[cfg(target_family = "unix")]
294        unsafe {
295            rerun_bin.pre_exec(|| {
296                // On unix systems, we want to make sure that the child process becomes its
297                // own session leader, so that it doesn't die if the parent process crashes
298                // or is killed.
299                libc::setsid();
300
301                Ok(())
302            })
303        };
304    }
305
306    rerun_bin.spawn().map_err(map_err)?;
307
308    if opts.wait_for_bind {
309        // Give the newly spawned Rerun Viewer some time to bind.
310        //
311        // NOTE: The timeout only covers the TCP handshake: if no process is bound to that address
312        // at all, the connection will fail immediately, irrelevant of the timeout configuration.
313        // For that reason we use an extra loop.
314        for i in 0..5 {
315            re_log::debug!("connection attempt {}", i + 1);
316            if TcpStream::connect_timeout(&connect_addr, Duration::from_secs(1)).is_ok() {
317                break;
318            }
319            std::thread::sleep(Duration::from_millis(100));
320        }
321    }
322
323    // Simply forget about the child process, we want it to outlive the parent process if needed.
324    _ = rerun_bin;
325
326    Ok(())
327}