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