Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use toml_edit::ArrayOfTables;
const DEFAULT_SERVER_KEY: &str = "default_server";
const WEB_SESSION_TOKEN_KEY: &str = "web_session_token";
const SPACETIMEDB_TOKEN_KEY: &str = "spacetimedb_token";
const LISTEN_ADDR_KEY: &str = "listen_addr";
const SERVER_CONFIGS_KEY: &str = "server_configs";
const NICKNAME_KEY: &str = "nickname";
const HOST_KEY: &str = "host";
Expand Down Expand Up @@ -124,6 +125,7 @@ pub struct RawConfig {
// TODO: Move these IDs/tokens out of config so we're no longer storing sensitive tokens in a human-edited file.
web_session_token: Option<String>,
spacetimedb_token: Option<String>,
listen_addr: Option<String>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -173,6 +175,7 @@ impl RawConfig {
server_configs: vec![maincloud, local],
web_session_token: None,
spacetimedb_token: None,
listen_addr: None,
}
}

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

let mut server_configs = Vec::new();
if let Some(arr) = read_table(value, SERVER_CONFIGS_KEY)? {
Expand All @@ -474,6 +478,7 @@ impl TryFrom<&toml_edit::DocumentMut> for RawConfig {
server_configs,
web_session_token,
spacetimedb_token,
listen_addr,
})
}
}
Expand All @@ -483,6 +488,10 @@ impl Config {
self.home.default_server.as_deref()
}

pub fn start_listen_addr(&self) -> Option<&str> {
self.home.listen_addr.as_deref()
}

/// Add a `ServerConfig` to the home configuration.
///
/// Returns an `Err` on name conflict,
Expand Down Expand Up @@ -654,11 +663,13 @@ impl Config {
server_configs: old_server_configs,
web_session_token,
spacetimedb_token,
listen_addr,
} = &self.home;

set_value(DEFAULT_SERVER_KEY, default_server.as_deref());
set_value(WEB_SESSION_TOKEN_KEY, web_session_token.as_deref());
set_value(SPACETIMEDB_TOKEN_KEY, spacetimedb_token.as_deref());
set_value(LISTEN_ADDR_KEY, listen_addr.as_deref());

// Short-circuit if there are no servers.
if old_server_configs.is_empty() {
Expand Down Expand Up @@ -929,6 +940,10 @@ protocol = "https"
"#;
const CONFIG_EMPTY: &str = r#"
# Comment end
"#;
const CONFIG_LISTEN_ADDR: &str = r#"listen_addr = "0.0.0.0:4000"

# Comment end
"#;
const CONFIG_INVALID_START: &str = r#"
this="not a valid key"
Expand Down Expand Up @@ -992,6 +1007,10 @@ default_server = "local"
fn test_config_adds() -> ResultTest<()> {
check_config(CONFIG_FULL, CONFIG_FULL, |_| Ok(()))?;
check_config(CONFIG_EMPTY, CONFIG_EMPTY, |_| Ok(()))?;
check_config(CONFIG_EMPTY, CONFIG_LISTEN_ADDR, |config| {
config.home.listen_addr = Some("0.0.0.0:4000".to_string());
Ok(())
})?;

check_config(CONFIG_EMPTY, CONFIG_FULL_NO_COMMENT, |config| {
config.home.default_server = Some("local".to_string());
Expand Down Expand Up @@ -1057,6 +1076,14 @@ default_server = "local"
found: Box::new(toml_edit::value(1)),
},
)?;
check_invalid(
r#"listen_addr =1"#,
CliError::ConfigType {
key: "listen_addr".to_string(),
kind: "string",
found: Box::new(toml_edit::value(1)),
},
)?;
check_invalid(
r#"
[server_configs]
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub async fn exec_subcommand(
"build" => build::exec(config, args).await.map(drop),
"server" => server::exec(config, paths, args).await,
"subscribe" => subscribe::exec(config, args).await,
"start" => return start::exec(paths, args).await,
"start" => return start::exec(config, paths, args).await,
"login" => login::exec(config, args).await,
"logout" => logout::exec(config, args).await,
"version" => return subcommands::version::exec(paths, root_dir, args).await,
Expand Down
115 changes: 111 additions & 4 deletions crates/cli/src/subcommands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use clap::{Arg, ArgMatches};
use spacetimedb_paths::SpacetimePaths;

use crate::util::resolve_sibling_binary;
use crate::Config;

pub fn cli() -> clap::Command {
clap::Command::new("start")
Expand All @@ -15,6 +16,11 @@ pub fn cli() -> clap::Command {
"\
Start a local SpacetimeDB instance

Set a persistent default listen address in cli.toml with:
listen_addr = \"0.0.0.0:4000\"

When present, `listen_addr` is used unless `--listen-addr` is passed explicitly.

Run `spacetime start --help` to see all options.",
)
.disable_help_flag(true)
Expand All @@ -40,9 +46,33 @@ enum Edition {
Cloud,
}

pub async fn exec(paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<ExitCode> {
/// Check whether the forwarded args already contain `--listen-addr` or `-l`.
///
/// Handles all common forms:
/// - `--listen-addr <value>` (two separate tokens)
/// - `--listen-addr=<value>`
/// - `-l <value>` (two separate tokens)
/// - `-l<value>` (short flag with attached value, e.g. `-l0.0.0.0:4000`)
fn has_listen_addr_arg(args: impl Iterator<Item = impl AsRef<std::ffi::OsStr>>) -> bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function can be removed entirely. if --listen-addr is passed multiple times to spacetimedb-standalone, the last one will be respected.

I would add a comment to this effect when passing --listen-addr.

for arg in args {
let s = arg.as_ref().to_string_lossy();
if s == "--listen-addr" || s.starts_with("--listen-addr=") {
return true;
}
if s == "-l"
|| (s.starts_with("-l")
&& !s.starts_with("--")
&& s.as_bytes().get(2).is_some_and(|b| !b.is_ascii_alphabetic()))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain this block? at first glance it seems like if s.starts_with("-l") passes, these other checks would as well.

{
return true;
}
}
false
}

pub async fn exec(config: Config, paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<ExitCode> {
let edition = args.get_one::<Edition>("edition").unwrap();
let args = args.get_many::<OsString>("args").unwrap_or_default();
let forwarded_args: Vec<OsString> = args.get_many::<OsString>("args").unwrap_or_default().cloned().collect();
let bin_name = match edition {
Edition::Standalone => "spacetimedb-standalone",
Edition::Cloud => "spacetimedb-cloud",
Expand All @@ -53,8 +83,15 @@ pub async fn exec(paths: &SpacetimePaths, args: &ArgMatches) -> anyhow::Result<E
.arg("--data-dir")
.arg(&paths.data_dir)
.arg("--jwt-key-dir")
.arg(&paths.cli_config_dir)
.args(args);
.arg(&paths.cli_config_dir);

if !has_listen_addr_arg(forwarded_args.iter())
&& let Some(config_addr) = config.start_listen_addr()
{
cmd.arg("--listen-addr").arg(config_addr);
}

cmd.args(&forwarded_args);

exec_replace(&mut cmd).with_context(|| format!("exec failed for {}", bin_path.display()))
}
Expand Down Expand Up @@ -103,3 +140,73 @@ pub(crate) fn exec_replace(cmd: &mut Command) -> io::Result<ExitCode> {
.map(|status| ExitCode::from(status.code().unwrap_or(1).try_into().unwrap_or(1)))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn detects_long_flag_separate_value() {
assert!(has_listen_addr_arg(["--listen-addr", "0.0.0.0:4000"].iter()));
}

#[test]
fn detects_long_flag_equals_value() {
assert!(has_listen_addr_arg(["--listen-addr=0.0.0.0:4000"].iter()));
}

#[test]
fn detects_short_flag_separate_value() {
assert!(has_listen_addr_arg(["-l", "0.0.0.0:4000"].iter()));
}

#[test]
fn detects_short_flag_attached_value() {
assert!(has_listen_addr_arg(["-l0.0.0.0:4000"].iter()));
}

#[test]
fn detects_short_flag_attached_ipv6() {
assert!(has_listen_addr_arg(["-l[::1]:4000"].iter()));
}

#[test]
fn ignores_unrelated_long_flag() {
assert!(!has_listen_addr_arg(["--data-dir", "/tmp"].iter()));
}

#[test]
fn ignores_unrelated_short_flag() {
assert!(!has_listen_addr_arg(["-d", "/tmp"].iter()));
}

#[test]
fn no_false_positive_on_hyphen_l_prefix_flag() {
assert!(!has_listen_addr_arg(["-log"].iter()));
}

#[test]
fn no_false_positive_on_hyphen_li() {
assert!(!has_listen_addr_arg(["-li"].iter()));
}

#[test]
fn returns_false_for_empty() {
let empty: Vec<&str> = vec![];
assert!(!has_listen_addr_arg(empty.iter()));
}

#[test]
fn detects_among_many_args() {
assert!(has_listen_addr_arg(
["--data-dir", "/tmp", "--listen-addr", "0.0.0.0:4000", "--in-memory"].iter()
));
}

#[test]
fn detects_short_among_many_args() {
assert!(has_listen_addr_arg(
["--data-dir", "/tmp", "-l", "127.0.0.1:5000"].iter()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,14 @@ Subscribe to SQL queries on the database. WARNING: This command is UNSTABLE and

Start a local SpacetimeDB instance

Set a persistent default listen address in `cli.toml` with:

```toml
listen_addr = "0.0.0.0:4000"
```

When present, `listen_addr` is used unless `--listen-addr` is passed explicitly. Precedence is: explicit CLI flag, then `cli.toml`, then the standalone default `0.0.0.0:3000`.

Run `spacetime start --help` to see all options.

**Usage:** `spacetime start [OPTIONS] [args]...`
Expand Down