1#[derive(Debug, Clone)]
15pub struct SpawnOptions {
16 pub port: u16,
20
21 pub wait_for_bind: bool,
24
25 pub memory_limit: String,
31
32 pub server_memory_limit: String,
39
40 pub executable_name: String,
46
47 pub executable_path: Option<String>,
52
53 pub extra_args: Vec<String>,
55
56 pub extra_env: Vec<(String, String)>,
58
59 pub new: bool,
63
64 pub hide_welcome_screen: bool,
66
67 pub detach_process: bool,
69}
70
71const 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 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 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 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#[derive(thiserror::Error)]
141pub enum SpawnError {
142 #[error("Failed to find Rerun Viewer executable in PATH.\n{message}\nPATH={search_path:?}")]
144 ExecutableNotFoundInPath {
145 message: String,
147
148 executable_name: String,
150
151 search_path: String,
153 },
154
155 #[error("Failed to find Rerun Viewer executable at {executable_path:?}")]
157 ExecutableNotFound {
158 executable_path: String,
160 },
161
162 #[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 <Self as std::fmt::Display>::fmt(self, f)
176 }
177}
178
179pub 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 const MSG_INSTALL_HOW_TO: &str = "
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 = "
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 = "
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 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 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 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 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 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 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 #[cfg(target_family = "unix")]
347 #[expect(unsafe_code)]
348 unsafe {
349 rerun_bin.pre_exec(|| {
350 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 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 _ = rerun_bin;
388
389 Ok(port)
390}