Skip to content

Commit 2173982

Browse files
0monishCopilotclockwork-labs-botbfops
authored
Use cli.toml for default start listen address (clockworklabs#4900)
# Description of Changes This change addresses reviewer feedback on clockworklabs#4576. As requested by `@cloutiertyler`, here are the changes: - moved the persistent default listen address for `spacetime start` from project `spacetime.json` to CLI `cli.toml` - threaded the already-loaded CLI `Config` into `spacetime start` instead of introducing a separate project-config lookup in that subcommand - preserved precedence so explicit `--listen-addr` / `-l` still wins over config, and config still wins over the built-in standalone default - documented the setting in the `spacetime start` CLI reference instead of the standalone runtime-config page Behavior precedence is now: 1. Explicit CLI flag, such as `spacetime start --listen-addr 127.0.0.1:5000` 2. `listen_addr` from `cli.toml` 3. Built-in standalone default `0.0.0.0:3000` Example: ```toml listen_addr = "0.0.0.0:4000" ``` With that config in place, `spacetime start` will bind to `0.0.0.0:4000` unless the user passes `--listen-addr` explicitly. Implementation details: - added a top-level `listen_addr` field to the existing CLI config parser and persistence path - updated `spacetime start` to use the loaded CLI config rather than reading project config directly - added/kept focused tests for `--listen-addr` and `-l` detection so config is only injected when the user did not provide an explicit listen address - updated the checked-in CLI reference text for `spacetime start` Related: - Follow-up to clockworklabs#4576 # API and ABI breaking changes None. This change does not modify any public API or ABI. It only changes where the CLI reads an optional default value for an existing `spacetime start` flag. # Expected complexity level and risk Complexity: 2/5 This is a small, localized CLI behavior change. The main interaction point is precedence between forwarded CLI args and persisted CLI config. Risk is low because this change: - preserves the existing standalone default when `listen_addr` is absent - preserves explicit CLI `--listen-addr` and `-l` precedence - reuses the existing `cli.toml` loading path instead of adding another config system # Testing Completed: - [x] `cargo fmt --all --check` - [x] `cargo check -p spacetimedb-cli` - [x] `cargo clippy -p spacetimedb-cli --all-targets` - [x] `cargo test -p spacetimedb-cli config::tests --lib` - [x] `cargo test -p spacetimedb-cli subcommands::start::tests --lib` Reviewer checks: - [ ] Run `spacetime start` without `listen_addr` in `cli.toml` and confirm it still binds to `0.0.0.0:3000` - [ ] Add `listen_addr = "0.0.0.0:4000"` to `cli.toml`, run `spacetime start`, and confirm it binds to `0.0.0.0:4000` - [ ] Run `spacetime start --listen-addr 127.0.0.1:5000` with config present and confirm the CLI flag still wins --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com>
1 parent 43d130f commit 2173982

5 files changed

Lines changed: 179 additions & 6 deletions

File tree

crates/cli/src/config.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use toml_edit::ArrayOfTables;
1212
const DEFAULT_SERVER_KEY: &str = "default_server";
1313
const WEB_SESSION_TOKEN_KEY: &str = "web_session_token";
1414
const SPACETIMEDB_TOKEN_KEY: &str = "spacetimedb_token";
15+
const LISTEN_ADDR_KEY: &str = "listen_addr";
1516
const SERVER_CONFIGS_KEY: &str = "server_configs";
1617
const NICKNAME_KEY: &str = "nickname";
1718
const HOST_KEY: &str = "host";
@@ -124,6 +125,7 @@ pub struct RawConfig {
124125
// TODO: Move these IDs/tokens out of config so we're no longer storing sensitive tokens in a human-edited file.
125126
web_session_token: Option<String>,
126127
spacetimedb_token: Option<String>,
128+
listen_addr: Option<String>,
127129
}
128130

129131
#[derive(Debug, Clone)]
@@ -173,6 +175,7 @@ impl RawConfig {
173175
server_configs: vec![maincloud, local],
174176
web_session_token: None,
175177
spacetimedb_token: None,
178+
listen_addr: None,
176179
}
177180
}
178181

@@ -461,6 +464,7 @@ impl TryFrom<&toml_edit::DocumentMut> for RawConfig {
461464
let default_server = read_opt_str(value, DEFAULT_SERVER_KEY)?;
462465
let web_session_token = read_opt_str(value, WEB_SESSION_TOKEN_KEY)?;
463466
let spacetimedb_token = read_opt_str(value, SPACETIMEDB_TOKEN_KEY)?;
467+
let listen_addr = read_opt_str(value, LISTEN_ADDR_KEY)?;
464468

465469
let mut server_configs = Vec::new();
466470
if let Some(arr) = read_table(value, SERVER_CONFIGS_KEY)? {
@@ -474,6 +478,7 @@ impl TryFrom<&toml_edit::DocumentMut> for RawConfig {
474478
server_configs,
475479
web_session_token,
476480
spacetimedb_token,
481+
listen_addr,
477482
})
478483
}
479484
}
@@ -483,6 +488,10 @@ impl Config {
483488
self.home.default_server.as_deref()
484489
}
485490

491+
pub fn start_listen_addr(&self) -> Option<&str> {
492+
self.home.listen_addr.as_deref()
493+
}
494+
486495
/// Add a `ServerConfig` to the home configuration.
487496
///
488497
/// Returns an `Err` on name conflict,
@@ -654,11 +663,13 @@ impl Config {
654663
server_configs: old_server_configs,
655664
web_session_token,
656665
spacetimedb_token,
666+
listen_addr,
657667
} = &self.home;
658668

659669
set_value(DEFAULT_SERVER_KEY, default_server.as_deref());
660670
set_value(WEB_SESSION_TOKEN_KEY, web_session_token.as_deref());
661671
set_value(SPACETIMEDB_TOKEN_KEY, spacetimedb_token.as_deref());
672+
set_value(LISTEN_ADDR_KEY, listen_addr.as_deref());
662673

663674
// Short-circuit if there are no servers.
664675
if old_server_configs.is_empty() {
@@ -929,6 +940,10 @@ protocol = "https"
929940
"#;
930941
const CONFIG_EMPTY: &str = r#"
931942
# Comment end
943+
"#;
944+
const CONFIG_LISTEN_ADDR: &str = r#"listen_addr = "0.0.0.0:4000"
945+
946+
# Comment end
932947
"#;
933948
const CONFIG_INVALID_START: &str = r#"
934949
this="not a valid key"
@@ -992,6 +1007,10 @@ default_server = "local"
9921007
fn test_config_adds() -> ResultTest<()> {
9931008
check_config(CONFIG_FULL, CONFIG_FULL, |_| Ok(()))?;
9941009
check_config(CONFIG_EMPTY, CONFIG_EMPTY, |_| Ok(()))?;
1010+
check_config(CONFIG_EMPTY, CONFIG_LISTEN_ADDR, |config| {
1011+
config.home.listen_addr = Some("0.0.0.0:4000".to_string());
1012+
Ok(())
1013+
})?;
9951014

9961015
check_config(CONFIG_EMPTY, CONFIG_FULL_NO_COMMENT, |config| {
9971016
config.home.default_server = Some("local".to_string());
@@ -1057,6 +1076,14 @@ default_server = "local"
10571076
found: Box::new(toml_edit::value(1)),
10581077
},
10591078
)?;
1079+
check_invalid(
1080+
r#"listen_addr =1"#,
1081+
CliError::ConfigType {
1082+
key: "listen_addr".to_string(),
1083+
kind: "string",
1084+
found: Box::new(toml_edit::value(1)),
1085+
},
1086+
)?;
10601087
check_invalid(
10611088
r#"
10621089
[server_configs]

crates/cli/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub async fn exec_subcommand(
6464
"build" => build::exec(config, args).await.map(drop),
6565
"server" => server::exec(config, paths, args).await,
6666
"subscribe" => subscribe::exec(config, args).await,
67-
"start" => return start::exec(paths, args).await,
67+
"start" => return start::exec(config, paths, args).await,
6868
"login" => login::exec(config, args).await,
6969
"logout" => logout::exec(config, args).await,
7070
"version" => return subcommands::version::exec(paths, root_dir, args).await,

crates/cli/src/subcommands/start.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use clap::{Arg, ArgMatches};
77
use spacetimedb_paths::SpacetimePaths;
88

99
use crate::util::resolve_sibling_binary;
10+
use crate::Config;
1011

1112
pub fn cli() -> clap::Command {
1213
clap::Command::new("start")
@@ -15,6 +16,11 @@ pub fn cli() -> clap::Command {
1516
"\
1617
Start a local SpacetimeDB instance
1718
19+
Set a persistent default listen address in cli.toml with:
20+
listen_addr = \"0.0.0.0:4000\"
21+
22+
When present, `listen_addr` is used unless `--listen-addr` is passed explicitly.
23+
1824
Run `spacetime start --help` to see all options.",
1925
)
2026
.disable_help_flag(true)
@@ -40,9 +46,9 @@ enum Edition {
4046
Cloud,
4147
}
4248

43-
pub async fn exec(paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<ExitCode> {
49+
pub async fn exec(config: Config, paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<ExitCode> {
4450
let edition = args.get_one::<Edition>("edition").unwrap();
45-
let args = args.get_many::<OsString>("args").unwrap_or_default();
51+
let forwarded_args: Vec<OsString> = args.get_many::<OsString>("args").unwrap_or_default().cloned().collect();
4652
let bin_name = match edition {
4753
Edition::Standalone => "spacetimedb-standalone",
4854
Edition::Cloud => "spacetimedb-cloud",
@@ -53,8 +59,15 @@ pub async fn exec(paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<E
5359
.arg("--data-dir")
5460
.arg(&paths.data_dir)
5561
.arg("--jwt-key-dir")
56-
.arg(&paths.cli_config_dir)
57-
.args(args);
62+
.arg(&paths.cli_config_dir);
63+
64+
if let Some(config_addr) = config.start_listen_addr() {
65+
// Prepend the config default; standalone takes the last --listen-addr value,
66+
// so an explicit flag in forwarded_args wins.
67+
cmd.arg("--listen-addr").arg(config_addr);
68+
}
69+
70+
cmd.args(&forwarded_args);
5871

5972
exec_replace(&mut cmd).with_context(|| format!("exec failed for {}", bin_path.display()))
6073
}

crates/smoketests/tests/smoketests/cli/server.rs

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,102 @@
11
//! CLI server command tests
22
33
use spacetimedb_guard::{ensure_binaries_built, SpacetimeDbGuard};
4-
use std::process::Command;
4+
use std::fs;
5+
use std::io::Read;
6+
use std::net::TcpListener;
7+
use std::path::Path;
8+
use std::process::{Child, Command, Output, Stdio};
9+
use std::time::{Duration, Instant};
510

611
fn cli_cmd() -> Command {
712
Command::new(ensure_binaries_built())
813
}
914

15+
fn output_stdout(output: &Output) -> String {
16+
String::from_utf8_lossy(&output.stdout).to_string()
17+
}
18+
19+
fn output_stderr(output: &Output) -> String {
20+
String::from_utf8_lossy(&output.stderr).to_string()
21+
}
22+
23+
fn assert_success(output: &Output, context: &str) {
24+
assert!(
25+
output.status.success(),
26+
"{context} failed:\nstdout: {}\nstderr: {}",
27+
output_stdout(output),
28+
output_stderr(output),
29+
);
30+
}
31+
32+
fn free_local_port() -> u16 {
33+
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("failed to bind local port");
34+
let port = listener.local_addr().expect("missing local addr").port();
35+
drop(listener);
36+
port
37+
}
38+
39+
fn write_cli_toml(root_dir: &Path, listen_addr: &str) {
40+
let config_dir = root_dir.join("config");
41+
fs::create_dir_all(&config_dir).expect("failed to create config dir");
42+
fs::write(
43+
config_dir.join("cli.toml"),
44+
format!("listen_addr = \"{listen_addr}\"\n"),
45+
)
46+
.expect("failed to write cli.toml");
47+
}
48+
49+
fn spawn_start(root_dir: &Path, extra_args: &[&str]) -> Child {
50+
let root_dir = root_dir.to_str().expect("root dir should be utf8");
51+
cli_cmd()
52+
.args(["--root-dir", root_dir, "start"])
53+
.args(extra_args)
54+
.stdout(Stdio::piped())
55+
.stderr(Stdio::piped())
56+
.spawn()
57+
.expect("failed to spawn spacetime start")
58+
}
59+
60+
fn ping_url(url: &str) -> Output {
61+
cli_cmd()
62+
.args(["server", "ping", url])
63+
.output()
64+
.expect("failed to execute server ping")
65+
}
66+
67+
fn wait_for_server(url: &str, child: &mut Child, timeout: Duration) {
68+
let start = Instant::now();
69+
loop {
70+
let ping = ping_url(url);
71+
if ping.status.success() {
72+
return;
73+
}
74+
75+
if let Some(status) = child.try_wait().expect("failed to poll child") {
76+
let mut stdout = String::new();
77+
let mut stderr = String::new();
78+
child.stdout.take().unwrap().read_to_string(&mut stdout).unwrap();
79+
child.stderr.take().unwrap().read_to_string(&mut stderr).unwrap();
80+
panic!(
81+
"spacetime start exited early ({status}) while waiting for {url}:\nstdout: {stdout}\nstderr: {stderr}"
82+
);
83+
}
84+
85+
if start.elapsed() > timeout {
86+
child.kill().ok();
87+
let _ = child.wait();
88+
panic!("timed out waiting for spacetime start to answer on {url}");
89+
}
90+
91+
std::thread::sleep(Duration::from_millis(200));
92+
}
93+
}
94+
95+
fn stop_child(mut child: Child) {
96+
child.kill().ok();
97+
let _ = child.wait();
98+
}
99+
10100
#[test]
11101
fn cli_can_ping_spacetimedb_on_disk() {
12102
let spacetime = SpacetimeDbGuard::spawn_in_temp_data_dir();
@@ -20,3 +110,41 @@ fn cli_can_ping_spacetimedb_on_disk() {
20110
String::from_utf8_lossy(&output.stderr)
21111
);
22112
}
113+
114+
#[test]
115+
fn cli_start_uses_listen_addr_from_cli_toml() {
116+
let root = tempfile::tempdir().expect("failed to create tempdir");
117+
let port = free_local_port();
118+
let listen_addr = format!("127.0.0.1:{port}");
119+
let url = format!("http://{listen_addr}");
120+
write_cli_toml(root.path(), &listen_addr);
121+
122+
let mut child = spawn_start(root.path(), &[]);
123+
wait_for_server(&url, &mut child, Duration::from_secs(20));
124+
125+
let ping = ping_url(&url);
126+
assert_success(&ping, "server ping after config-based start");
127+
stop_child(child);
128+
}
129+
130+
#[test]
131+
fn cli_start_explicit_listen_addr_overrides_cli_toml() {
132+
let root = tempfile::tempdir().expect("failed to create tempdir");
133+
let config_port = free_local_port();
134+
let mut explicit_port = free_local_port();
135+
while explicit_port == config_port {
136+
explicit_port = free_local_port();
137+
}
138+
139+
let config_addr = format!("127.0.0.1:{config_port}");
140+
let explicit_addr = format!("127.0.0.1:{explicit_port}");
141+
let explicit_url = format!("http://{explicit_addr}");
142+
write_cli_toml(root.path(), &config_addr);
143+
144+
let mut child = spawn_start(root.path(), &["--listen-addr", &explicit_addr]);
145+
wait_for_server(&explicit_url, &mut child, Duration::from_secs(20));
146+
147+
let ping = ping_url(&explicit_url);
148+
assert_success(&ping, "server ping after explicit listen-addr override");
149+
stop_child(child);
150+
}

docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,11 @@ Subscribe to SQL queries on the database. WARNING: This command is UNSTABLE and
615615

616616
Start a local SpacetimeDB instance
617617

618+
Set a persistent default listen address in cli.toml with:
619+
listen_addr = "0.0.0.0:4000"
620+
621+
When present, `listen_addr` is used unless `--listen-addr` is passed explicitly.
622+
618623
Run `spacetime start --help` to see all options.
619624

620625
**Usage:** `spacetime start [OPTIONS] [args]...`

0 commit comments

Comments
 (0)