Skip to content

Commit daeb576

Browse files
authored
Catch keyboard interrupt in rerun-sdk CLI and return exit codes (#12496)
Fixes two issues: * Exit codes of the `main()` function weren't returned to the OS by the executable. * uncaught `KeyboardInterrupt` * if you installed `rerun-sdk` and terminate `rerun` with ctrl+c, there was an ugly traceback * we can catch it and return 130, which is the standard POSIX code for SIGINT termination ``` [2026-01-16T16:11:55Z INFO rerun::commands::entrypoint] Caught Ctrl-C, quitting Rerun Viewer… [2026-01-16T16:11:55Z DEBUG eframe::native::epi_integration] Closing root viewport (ViewportCommand::CancelClose was not sent) [2026-01-16T16:11:55Z DEBUG eframe::native::run] Asking to exit event loop… [2026-01-16T16:11:55Z DEBUG eframe::native::run] Received Event::LoopExiting - saving app state… [2026-01-16T16:11:55Z DEBUG eframe::native::run] eframe window closed Traceback (most recent call last): File "/Users/michael/code/rerun/rerun_py/rerun_sdk/rerun/__main__.py", line 25, in <module> main() File "/Users/michael/code/rerun/rerun_py/rerun_sdk/rerun/__main__.py", line 21, in main return cli_main() ^^^^^^^^^^ File "/Users/michael/code/rerun/rerun_py/rerun_sdk/rerun_cli/__main__.py", line 35, in main return subprocess.call([target_path, *sys.argv[1:]]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/michael/code/rerun/.pixi/envs/default/lib/python3.11/subprocess.py", line 391, in call return p.wait(timeout=timeout) ^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/michael/code/rerun/.pixi/envs/default/lib/python3.11/subprocess.py", line 1264, in wait return self._wait(timeout=timeout) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/michael/code/rerun/.pixi/envs/default/lib/python3.11/subprocess.py", line 2053, in _wait (pid, sts) = self._try_wait(0) ^^^^^^^^^^^^^^^^^ File "/Users/michael/code/rerun/.pixi/envs/default/lib/python3.11/subprocess.py", line 2011, in _try_wait (pid, sts) = os.waitpid(self.pid, wait_flags) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ KeyboardInterrupt ``` ..now: ``` 2026-01-16T16:22:03Z INFO rerun::commands::entrypoint] Caught Ctrl-C, quitting Rerun Viewer… [2026-01-16T16:22:03Z DEBUG eframe::native::epi_integration] Closing root viewport (ViewportCommand::CancelClose was not sent) [2026-01-16T16:22:03Z DEBUG eframe::native::run] Asking to exit event loop… [2026-01-16T16:22:03Z DEBUG eframe::native::run] Received Event::LoopExiting - saving app state… [2026-01-16T16:22:03Z DEBUG eframe::native::run] eframe window closed ``` ```sh echo $? 130 ``` Source-Ref: b0354d8c24725faf304d42319dcf029aa5535a3f
1 parent ee22b45 commit daeb576

3 files changed

Lines changed: 77 additions & 70 deletions

File tree

crates/top/rerun/src/commands/entrypoint.rs

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,84 +1242,86 @@ fn assert_receive_into_entity_db(rx: &LogReceiverSet) -> anyhow::Result<re_entit
12421242
anyhow::bail!("Channel disconnected without a Goodbye message.");
12431243
}
12441244

1245-
match rx.recv_timeout(timeout) {
1246-
Some((_, msg)) => {
1247-
re_log::info_once!("Received first message.");
1248-
1249-
match msg.payload {
1250-
SmartMessagePayload::Msg(msg) => {
1251-
match msg {
1252-
DataSourceMessage::RrdManifest(store_id, rrd_manifest) => {
1253-
let mut_db =
1254-
match store_id.kind() {
1255-
re_log_types::StoreKind::Recording => rec
1256-
.get_or_insert_with(|| {
1257-
re_entity_db::EntityDb::new(store_id.clone())
1258-
}),
1259-
re_log_types::StoreKind::Blueprint => bp
1260-
.get_or_insert_with(|| {
1261-
re_entity_db::EntityDb::new(store_id.clone())
1262-
}),
1263-
};
1264-
1265-
mut_db.add_rrd_manifest_message(*rrd_manifest);
1266-
}
1267-
1268-
DataSourceMessage::LogMsg(msg) => {
1269-
let mut_db =
1270-
match msg.store_id().kind() {
1271-
re_log_types::StoreKind::Recording => rec
1272-
.get_or_insert_with(|| {
1273-
re_entity_db::EntityDb::new(msg.store_id().clone())
1274-
}),
1275-
re_log_types::StoreKind::Blueprint => bp
1276-
.get_or_insert_with(|| {
1277-
re_entity_db::EntityDb::new(msg.store_id().clone())
1278-
}),
1279-
};
1280-
1281-
mut_db.add_log_msg(&msg)?;
1282-
}
1283-
1284-
DataSourceMessage::TableMsg(_) => {
1285-
anyhow::bail!(
1286-
"Received a TableMsg which can't be stored in an EntityDb"
1287-
);
1288-
}
1289-
1290-
DataSourceMessage::UiCommand(ui_command) => {
1291-
anyhow::bail!(
1292-
"Received a UI command which can't be stored in an EntityDb: {ui_command:?}"
1293-
);
1294-
}
1245+
if let Some((_, msg)) = rx.recv_timeout(timeout) {
1246+
re_log::info_once!("Received first message.");
1247+
1248+
match msg.payload {
1249+
SmartMessagePayload::Msg(msg) => {
1250+
match msg {
1251+
DataSourceMessage::RrdManifest(store_id, rrd_manifest) => {
1252+
let mut_db = match store_id.kind() {
1253+
re_log_types::StoreKind::Recording => {
1254+
rec.get_or_insert_with(|| {
1255+
re_entity_db::EntityDb::new(store_id.clone())
1256+
})
1257+
}
1258+
re_log_types::StoreKind::Blueprint => bp.get_or_insert_with(|| {
1259+
re_entity_db::EntityDb::new(store_id.clone())
1260+
}),
1261+
};
1262+
1263+
mut_db.add_rrd_manifest_message(*rrd_manifest);
12951264
}
12961265

1297-
num_messages += 1;
1298-
}
1266+
DataSourceMessage::LogMsg(msg) => {
1267+
let mut_db = match msg.store_id().kind() {
1268+
re_log_types::StoreKind::Recording => {
1269+
rec.get_or_insert_with(|| {
1270+
re_entity_db::EntityDb::new(msg.store_id().clone())
1271+
})
1272+
}
1273+
re_log_types::StoreKind::Blueprint => bp.get_or_insert_with(|| {
1274+
re_entity_db::EntityDb::new(msg.store_id().clone())
1275+
}),
1276+
};
1277+
1278+
mut_db.add_log_msg(&msg)?;
1279+
}
12991280

1300-
re_log_channel::SmartMessagePayload::Flush { on_flush_done } => {
1301-
on_flush_done();
1302-
}
1281+
DataSourceMessage::TableMsg(_) => {
1282+
anyhow::bail!(
1283+
"Received a TableMsg which can't be stored in an EntityDb"
1284+
);
1285+
}
13031286

1304-
SmartMessagePayload::Quit(err) => {
1305-
if let Some(err) = err {
1306-
anyhow::bail!("data source has disconnected unexpectedly: {err}")
1307-
} else if let Some(db) = rec {
1308-
anyhow::ensure!(0 < num_messages, "No messages received");
1309-
re_log::info!("Successfully ingested {num_messages} messages.");
1310-
return Ok(db);
1311-
} else {
1312-
anyhow::bail!("EntityDb never initialized");
1287+
DataSourceMessage::UiCommand(ui_command) => {
1288+
anyhow::bail!(
1289+
"Received a UI command which can't be stored in an EntityDb: {ui_command:?}"
1290+
);
13131291
}
13141292
}
1293+
1294+
num_messages += 1;
1295+
}
1296+
1297+
re_log_channel::SmartMessagePayload::Flush { on_flush_done } => {
1298+
on_flush_done();
1299+
}
1300+
1301+
SmartMessagePayload::Quit(err) => {
1302+
if let Some(err) = err {
1303+
anyhow::bail!("data source has disconnected unexpectedly: {err}")
1304+
} else if let Some(db) = rec {
1305+
anyhow::ensure!(0 < num_messages, "No messages received");
1306+
re_log::info!("Successfully ingested {num_messages} messages.");
1307+
return Ok(db);
1308+
} else {
1309+
anyhow::bail!("EntityDb never initialized");
1310+
}
13151311
}
13161312
}
1317-
None => {
1318-
anyhow::bail!(
1319-
"Didn't receive any messages within {} seconds. Giving up.",
1320-
timeout.as_secs()
1313+
} else {
1314+
if let Some(db) = rec {
1315+
// TODO(RR-3373): find a proper way to detect client disconnect without timing out.
1316+
re_log::info!(
1317+
"Timed out after successfully receiving {num_messages} messages. Assuming the client disconnected cleanly.",
13211318
);
1319+
return Ok(db);
13221320
}
1321+
anyhow::bail!(
1322+
"Didn't receive any messages within {} seconds. Giving up.",
1323+
timeout.as_secs()
1324+
);
13231325
}
13241326
}
13251327
}

rerun_py/rerun_sdk/rerun/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from __future__ import annotations
1010

11+
import sys
12+
1113
from rerun_cli.__main__ import main as cli_main
1214

1315
from rerun import unregister_shutdown
@@ -22,4 +24,4 @@ def main() -> int:
2224

2325

2426
if __name__ == "__main__":
25-
main()
27+
sys.exit(main())

rerun_py/rerun_sdk/rerun_cli/__main__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ def main() -> int:
3232
print(f"Error: Could not find rerun binary at {target_path}", file=sys.stderr)
3333
return 1
3434

35-
return subprocess.call([target_path, *sys.argv[1:]])
35+
try:
36+
return subprocess.call([target_path, *sys.argv[1:]])
37+
except KeyboardInterrupt:
38+
return 130
3639

3740

3841
if __name__ == "__main__":

0 commit comments

Comments
 (0)