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 hide_welcome_screen: bool,
61
62 pub detach_process: bool,
64}
65
66const 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 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 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 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#[derive(thiserror::Error)]
135pub enum SpawnError {
136 #[error("Failed to find Rerun Viewer executable in PATH.\n{message}\nPATH={search_path:?}")]
138 ExecutableNotFoundInPath {
139 message: String,
141
142 executable_name: String,
144
145 search_path: String,
147 },
148
149 #[error("Failed to find Rerun Viewer executable at {executable_path:?}")]
151 ExecutableNotFound {
152 executable_path: String,
154 },
155
156 #[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 <Self as std::fmt::Display>::fmt(self, f)
170 }
171}
172
173pub 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 const MSG_INSTALL_HOW_TO: &str = "
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 = "
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 = "
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 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 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 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 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 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 #[cfg(target_family = "unix")]
322 #[expect(unsafe_code)]
323 unsafe {
324 rerun_bin.pre_exec(|| {
325 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 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 _ = rerun_bin;
354
355 Ok(())
356}