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