diff --git a/Cargo.lock b/Cargo.lock index ff6ee7d53c..c49a8133f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,6 +1134,19 @@ dependencies = [ "syn 2.0.22", ] +[[package]] +name = "dashmap" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +dependencies = [ + "cfg-if", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core 0.9.8", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -3854,6 +3867,31 @@ dependencies = [ "syn 2.0.22", ] +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.1", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + [[package]] name = "sha1" version = "0.10.5" @@ -4337,6 +4375,7 @@ version = "0.1.0" dependencies = [ "anyhow", "serde_json", + "serial_test", "spacetimedb-client-api", "spacetimedb-core", "spacetimedb-lib", diff --git a/Cargo.toml b/Cargo.toml index 45270e5a94..215be8b76b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ serde = "1.0.136" serde_json = { version = "1.0.87", features = ["raw_value"] } serde_path_to_error = "0.1.9" serde_with = { version = "2.2.0", features = ["base64", "hex"] } +serial_test = "2.0.0" sha1 = "0.10.1" sha3 = "0.10.0" slab = "0.4.7" diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs new file mode 100644 index 0000000000..fb95523ee7 --- /dev/null +++ b/crates/core/src/config.rs @@ -0,0 +1,154 @@ +use std::env::temp_dir; +use std::path::{Path, PathBuf}; + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +mod paths { + use super::*; + + /// The default path for the database files. + pub(super) fn db_path() -> PathBuf { + PathBuf::from("/stdb") + } + + /// The default path for the database logs. + pub(super) fn logs_path() -> PathBuf { + PathBuf::from("/var/log") + } + + /// The default path for the database config files. + pub(super) fn config_path() -> PathBuf { + PathBuf::from("/etc/spacetimedb/") + } +} + +#[cfg(target_os = "macos")] +mod paths { + use super::*; + + /// The default path for the database files. + pub(super) fn db_path() -> PathBuf { + PathBuf::from("/usr/local/var/stdb") + } + + /// The default path for the database logs. + pub(super) fn logs_path() -> PathBuf { + PathBuf::from("/var/log") + } + + /// The default path for the database config files. + pub(super) fn config_path() -> PathBuf { + PathBuf::from("/etc/spacetimedb/") + } +} + +#[cfg(target_os = "windows")] +mod paths { + use super::*; + + /// The default path for the database files. + pub(super) fn db_path() -> PathBuf { + dirs::data_dir() + .map(|x| x.join("stdb")) + .expect("failed to read the windows `data directory`") + } + + /// The default path for the database logs. + pub(super) fn logs_path() -> PathBuf { + db_path().join("log") + } + + /// The default path for the database config files. + pub(super) fn config_path() -> PathBuf { + dirs::config_dir() + .map(|x| x.join("stdb")) + .expect("Fail to read the windows `config directory`") + } +} + +/// Returns the default path for the database in the `OS` temporary directory. +pub fn stdb_path_temp() -> PathBuf { + temp_dir().join("stdb") +} + +/// Types specifying where to find various files needed by spacetimedb. +pub trait SpacetimeDbFiles { + /// The path for the database files. + fn db_path(&self) -> PathBuf; + + /// The path for the database logs. + fn logs(&self) -> PathBuf; + + /// The path for the database config files. + fn config(&self) -> PathBuf; + + /// The path of the database config file `log.conf` for logs. + fn log_config(&self) -> PathBuf { + self.config().join("log.conf") + } + + /// The path of the private key file `id_ecdsa`. + fn private_key(&self) -> PathBuf { + self.config().join("id_ecdsa") + } + + /// The path of the public key file `id_ecdsa.pub`. + fn public_key(&self) -> PathBuf { + self.config().join("id_ecdsa.pub") + } +} + +/// The location of paths for the database in a local OR temp folder. +pub struct FilesLocal { + dir: PathBuf, +} + +impl FilesLocal { + /// Create a new [FilesLocal], appending `name` to the `temp` folder returned by [stdb_path_temp]. + pub fn temp(name: &str) -> Self { + assert!(!name.is_empty(), "`name` should be filled"); + + Self { + dir: stdb_path_temp().join(name), + } + } + + /// Create a new [FilesLocal] that is in a hidden `path + .spacetime` folder. + pub fn hidden>(path: P) -> Self { + Self { + dir: path.as_ref().join(".spacetime"), + } + } +} + +impl SpacetimeDbFiles for FilesLocal { + fn db_path(&self) -> PathBuf { + self.dir.clone() + } + + fn logs(&self) -> PathBuf { + self.db_path().join("logs") + } + + fn config(&self) -> PathBuf { + self.db_path().join("conf") + } +} + +/// The global location of paths for the database. +/// +/// NOTE: This location varies by OS. +pub struct FilesGlobal; + +impl SpacetimeDbFiles for FilesGlobal { + fn db_path(&self) -> PathBuf { + paths::db_path() + } + + fn logs(&self) -> PathBuf { + paths::logs_path() + } + + fn config(&self) -> PathBuf { + paths::config_path() + } +} diff --git a/crates/core/src/database_logger.rs b/crates/core/src/database_logger.rs index f03b4f31f6..d42baff709 100644 --- a/crates/core/src/database_logger.rs +++ b/crates/core/src/database_logger.rs @@ -177,7 +177,7 @@ impl DatabaseLogger { let filepath = root.join("0.log"); // TODO: Read backwards from the end of the file to only read in the latest lines - let text = tokio::fs::read_to_string(&filepath).await.expect("reading file"); + let text = tokio::fs::read_to_string(&filepath).await.expect("reading log file"); let Some(num_lines) = num_lines else { return text }; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index a2e5eaa882..ac44494523 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -48,6 +48,7 @@ pub mod protobuf { pub use spacetimedb_client_api_messages::*; } pub mod client; +pub mod config; pub mod control_db; pub mod database_instance_context; pub mod database_instance_context_controller; diff --git a/crates/standalone/src/subcommands/start.rs b/crates/standalone/src/subcommands/start.rs index 0ce7fec277..6c9d2694ae 100644 --- a/crates/standalone/src/subcommands/start.rs +++ b/crates/standalone/src/subcommands/start.rs @@ -3,6 +3,7 @@ use crate::util::{create_dir_or_err, create_file_with_contents}; use crate::StandaloneEnv; use clap::ArgAction::SetTrue; use clap::{Arg, ArgMatches}; +use spacetimedb::config::{FilesGlobal, FilesLocal, SpacetimeDbFiles}; use spacetimedb::db::{db_metrics, Storage}; use spacetimedb::{startup, worker_metrics}; use std::net::TcpListener; @@ -85,20 +86,22 @@ pub fn cli(mode: ProgramMode) -> clap::Command { // The standalone mode instead uses global directories. match mode { ProgramMode::CLI => { - log_conf_path_arg = log_conf_path_arg.default_value(format!("{}/.spacetime/log.conf", default_root)); - log_dir_path_arg = log_dir_path_arg.default_value(format!("{}/.spacetime", default_root)); - database_path_arg = database_path_arg.default_value(format!("{}/.spacetime/stdb", default_root)); - jwt_pub_key_path_arg = - jwt_pub_key_path_arg.default_value(format!("{}/.spacetime/id_ecdsa.pub", default_root)); - jwt_priv_key_path_arg = - jwt_priv_key_path_arg.default_value(format!("{}/.spacetime/id_ecdsa", default_root)); + let paths = FilesLocal::hidden(default_root); + + log_conf_path_arg = log_conf_path_arg.default_value(paths.log_config().into_os_string()); + log_dir_path_arg = log_dir_path_arg.default_value(paths.logs().into_os_string()); + database_path_arg = database_path_arg.default_value(paths.db_path().into_os_string()); + jwt_pub_key_path_arg = jwt_pub_key_path_arg.default_value(paths.public_key().into_os_string()); + jwt_priv_key_path_arg = jwt_priv_key_path_arg.default_value(paths.private_key().into_os_string()); } ProgramMode::Standalone => { - log_conf_path_arg = log_conf_path_arg.default_value("/etc/spacetimedb/log.conf"); - log_dir_path_arg = log_dir_path_arg.default_value("/var/log"); - database_path_arg = database_path_arg.default_value("/stdb"); - jwt_pub_key_path_arg = jwt_pub_key_path_arg.default_value("/etc/spacetimedb/id_ecdsa.pub"); - jwt_priv_key_path_arg = jwt_priv_key_path_arg.default_value("/etc/spacetimedb/id_ecdsa"); + let paths = FilesGlobal; + + log_conf_path_arg = log_conf_path_arg.default_value(paths.log_config().into_os_string()); + log_dir_path_arg = log_dir_path_arg.default_value(paths.logs().into_os_string()); + database_path_arg = database_path_arg.default_value(paths.db_path().into_os_string()); + jwt_pub_key_path_arg = jwt_pub_key_path_arg.default_value(paths.public_key().into_os_string()); + jwt_priv_key_path_arg = jwt_priv_key_path_arg.default_value(paths.private_key().into_os_string()); } } diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index cdd9b59a5b..09d4744349 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -13,3 +13,6 @@ anyhow.workspace = true serde_json.workspace = true tokio.workspace = true wasmbin.workspace = true + +[dev-dependencies] +serial_test.workspace = true diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs index 53a18e18ec..50842351b2 100644 --- a/crates/testing/src/lib.rs +++ b/crates/testing/src/lib.rs @@ -1,18 +1,18 @@ +use spacetimedb::config::{FilesLocal, SpacetimeDbFiles}; use std::env; -use std::path::{Path, PathBuf}; pub mod modules; -pub fn set_key_env_vars() { +pub fn set_key_env_vars(paths: &FilesLocal) { let set_if_not_exist = |var, path| { if env::var_os(var).is_none() { - env::set_var(var, Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").join(path)); + env::set_var(var, path); } }; - set_if_not_exist("STDB_PATH", PathBuf::from("/stdb")); - set_if_not_exist("SPACETIMEDB_LOGS_PATH", PathBuf::from("/var/log")); - set_if_not_exist("SPACETIMEDB_LOG_CONFIG", PathBuf::from("/stdb/log.conf")); - set_if_not_exist("SPACETIMEDB_JWT_PUB_KEY", PathBuf::from("/stdb/id_ecdsa.pub")); - set_if_not_exist("SPACETIMEDB_JWT_PRIV_KEY", PathBuf::from("/stdb/id_ecdsa")); + set_if_not_exist("STDB_PATH", paths.db_path()); + set_if_not_exist("SPACETIMEDB_LOGS_PATH", paths.logs()); + set_if_not_exist("SPACETIMEDB_LOG_CONFIG", paths.log_config()); + set_if_not_exist("SPACETIMEDB_JWT_PUB_KEY", paths.public_key()); + set_if_not_exist("SPACETIMEDB_JWT_PRIV_KEY", paths.private_key()); } diff --git a/crates/testing/src/modules.rs b/crates/testing/src/modules.rs index b1bf914c27..9c248d5261 100644 --- a/crates/testing/src/modules.rs +++ b/crates/testing/src/modules.rs @@ -10,6 +10,7 @@ use spacetimedb::database_logger::DatabaseLogger; use spacetimedb::db::Storage; use spacetimedb::hash::hash_bytes; +use spacetimedb::config::{FilesLocal, SpacetimeDbFiles}; use spacetimedb::messages::control_db::HostType; use spacetimedb_client_api::{ControlCtx, ControlStateDelegate, WorkerCtx}; use spacetimedb_standalone::StandaloneEnv; @@ -129,7 +130,12 @@ pub async fn load_module(name: &str) -> ModuleHandle { // exercise functionality like restarting the database. let storage = Storage::Disk; - crate::set_key_env_vars(); + let paths = FilesLocal::temp(name); + // The database created in the `temp` folder can't be randomized, + // so it persists after running the test. + std::fs::remove_dir(paths.db_path()).ok(); + + crate::set_key_env_vars(&paths); let env = spacetimedb_standalone::StandaloneEnv::init(storage).await.unwrap(); let identity = env.control_db().alloc_spacetime_identity().await.unwrap(); let address = env.control_db().alloc_spacetime_address().await.unwrap(); diff --git a/crates/testing/tests/standalone_integration_test.rs b/crates/testing/tests/standalone_integration_test.rs index abf3beace2..8232ca2598 100644 --- a/crates/testing/tests/standalone_integration_test.rs +++ b/crates/testing/tests/standalone_integration_test.rs @@ -1,7 +1,12 @@ use serde_json::Value; +use serial_test::serial; use spacetimedb_testing::modules::{compile, with_module_async}; +// The tests MUST be run in sequence because they read the OS environment +// and can cause a race when run in parallel. + #[test] +#[serial] fn test_calling_a_reducer() { compile("spacetimedb-quickstart"); with_module_async("spacetimedb-quickstart", |module| async move { @@ -23,6 +28,7 @@ fn test_calling_a_reducer() { } #[test] +#[serial] fn test_calling_a_reducer_with_private_table() { compile("rust-wasm-test"); with_module_async("rust-wasm-test", |module| async move { diff --git a/run_standalone.sh b/run_standalone_temp.sh similarity index 86% rename from run_standalone.sh rename to run_standalone_temp.sh index 719b698af4..fc681ab53a 100755 --- a/run_standalone.sh +++ b/run_standalone_temp.sh @@ -1,4 +1,5 @@ #!/bin/bash +# Run a ephemeral database inside a `temp` folder set -euo pipefail SRC_TREE="$(dirname "$0")" @@ -15,6 +16,13 @@ cargo build -p spacetimedb-standalone export STDB_PATH="${STDB_PATH:-$(mktemp -d)}" mkdir -p "$STDB_PATH/logs" +function cleanup { + echo "Removing ${STDB_PATH}" + rm -rf "$STDB_PATH" +} + +trap cleanup EXIT + cp crates/standalone/log.conf "$STDB_PATH/log.conf" # -i differs between GNU and BSD sed, so use a temp file sed 's/spacetimedb=debug/spacetimedb=trace/g' "$STDB_PATH/log.conf" > "$STDB_PATH/log.conf.tmp" && \