Skip to main content

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