Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug with Lockfile sticking around #1341

Merged
merged 9 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
75 changes: 16 additions & 59 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ use anyhow::Context;
use jsonwebtoken::DecodingKey;
use serde::{Deserialize, Serialize};
use spacetimedb::auth::identity::decode_token;
use spacetimedb_fs_utils::{create_parent_dir, lockfile::Lockfile};
use spacetimedb_fs_utils::create_parent_dir;
use spacetimedb_lib::Identity;
use std::{
fs,
path::{Path, PathBuf},
};
use tempfile::NamedTempFile;

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IdentityConfig {
Expand Down Expand Up @@ -109,20 +110,6 @@ pub struct RawConfig {
pub struct Config {
proj: RawConfig,
home: RawConfig,

/// The path from which we loaded the system config `home`,
/// and a [`Lockfile`] which guarantees us exclusive access to it.
///
/// The lockfile is created before reading or creating the home config,
/// and closed upon dropping the config.
/// This allows us to mutate the config via [`Config::save`] without worrying about other processes.
///
/// None only in tests, which are allowed to concurrently mutate configurations as much as they want,
/// since they use a hardcoded configuration rather than loading from a file.
///
/// Note that it's not necessary to lock or remember the path of the project config,
/// since we never write to that file.
home_file: Option<(PathBuf, Lockfile)>,
}

const HOME_CONFIG_DIR: &str = ".spacetime";
Expand Down Expand Up @@ -594,21 +581,16 @@ Fetch the server's fingerprint with:
}
}

impl Config {
/// Release the system configuration `Lockfile` contained in `self`, if it exists,
/// allowing other processes to access the configuration.
///
/// This is equivalent to dropping `self`, but more explicit in purpose.
pub fn release_lock(self) {
// Just drop `self`, cleaning up its lockfile.
//
// If we had CLI subcommands which wanted to read a `Config` but never `Config::save` it,
// we could have this message take `&mut self` and set the `home_lock` to `None`.
// This seems unlikely to be useful,
// as many accesses to the `Config` will implicitly mutate and `save`,
// e.g. by creating a new identity if none exists.
}
fn atomic_write(file_path: &PathBuf, data: String) -> anyhow::Result<()> {
let temp_file = NamedTempFile::new()?;
// Close the file, but keep the path to it around.
let temp_path = temp_file.into_temp_path();
std::fs::write(&temp_path, data)?;
std::fs::rename(&temp_path, file_path)?;
Ok(())
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

move this to the fs_utils crate


impl Config {
pub fn default_server_name(&self) -> Option<&str> {
self.proj
.default_server
Expand Down Expand Up @@ -850,7 +832,6 @@ impl Config {

pub fn load() -> Self {
let home_path = Self::system_config_path();
let home_lock = Lockfile::for_file(&home_path).unwrap();
let home = if home_path.exists() {
Self::load_from_file(&home_path)
.inspect_err(|e| eprintln!("config file {home_path:?} is invalid: {e:#?}"))
Expand All @@ -861,50 +842,26 @@ impl Config {

let proj = Self::load_proj_config();

Self {
home,
proj,

home_file: Some((home_path, home_lock)),
}
Self { home, proj }
}

#[doc(hidden)]
/// Used in tests; not backed by a file.
/// Used in tests.
pub fn new_with_localhost() -> Self {
Self {
home: RawConfig::new_with_localhost(),
proj: RawConfig::default(),

home_file: None,
}
}

#[doc(hidden)]
pub fn clone_for_test(&self) -> Self {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no longer needed since Config is back to being "plain ol' data"

assert!(
self.home_file.is_none(),
"Cannot clone_for_test a Config derived from file {:?}",
self.home_file,
);
Config {
home: self.home.clone(),
proj: self.proj.clone(),
home_file: None,
}
}

pub fn save(&self) {
let Some((home_path, _)) = &self.home_file else {
return;
};

let home_path = Self::system_config_path();
// If the `home_path` is in a directory, ensure it exists.
create_parent_dir(home_path).unwrap();
create_parent_dir(home_path.as_ref()).unwrap();

let config = toml::to_string_pretty(&self.home).unwrap();

if let Err(e) = std::fs::write(home_path, config) {
if let Err(e) = atomic_write(&home_path, config) {
eprintln!("could not save config file: {e}")
}
}
Expand Down
6 changes: 1 addition & 5 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@ pub async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Re
"build" => build::exec(config, args).await,
"server" => server::exec(config, args).await,
#[cfg(feature = "standalone")]
"start" => {
// Release the lockfile on the config, since we don't need it.
config.release_lock();
start::exec(args).await
}
"start" => start::exec(args).await,
"upgrade" => upgrade::exec(config, args).await,
unknown => Err(anyhow::anyhow!("Invalid subcommand: {}", unknown)),
}
Expand Down
5 changes: 1 addition & 4 deletions crates/cli/src/subcommands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ pub fn cli() -> clap::Command {
)
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
// Release the lockfile on the config, since we don't need it.
config.release_lock();

pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let project_path = args.get_one::<PathBuf>("project_path").unwrap();
let skip_clippy = args.get_flag("skip_clippy");
let build_debug = args.get_flag("debug");
Expand Down
5 changes: 1 addition & 4 deletions crates/cli/src/subcommands/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,7 @@ pub fn cli() -> clap::Command {
.after_help("Run `spacetime help publish` for more detailed information.")
}

pub fn exec(config: Config, args: &clap::ArgMatches) -> anyhow::Result<()> {
// Release the lockfile on the config, since we don't need it.
config.release_lock();

pub fn exec(_config: Config, args: &clap::ArgMatches) -> anyhow::Result<()> {
let project_path = args.get_one::<PathBuf>("project_path").unwrap();
let wasm_file = args.get_one::<PathBuf>("wasm_file").cloned();
let out_dir = args.get_one::<PathBuf>("out_dir").unwrap();
Expand Down
5 changes: 1 addition & 4 deletions crates/cli/src/subcommands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,7 @@ fn check_for_git() -> bool {
false
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
// Release the lockfile on the config, since we don't need it.
config.release_lock();

pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let project_path = args.get_one::<PathBuf>("project-path").unwrap();
let project_lang = *args.get_one::<ModuleLanguage>("lang").unwrap();

Expand Down
5 changes: 1 addition & 4 deletions crates/cli/src/subcommands/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Result
}
}

async fn exec_clear(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
// Release the lockfile on the config, since we don't need it.
config.release_lock();

async fn exec_clear(_config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let force = args.get_flag("force");
if std::env::var_os("STDB_PATH").map(PathBuf::from).is_none() {
let mut path = dirs::home_dir().unwrap_or_default();
Expand Down
1 change: 0 additions & 1 deletion crates/cli/src/subcommands/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let query_parms = LogsParams { num_lines, follow };

let host_url = config.get_host_url(server)?;
config.release_lock();

let builder = reqwest::Client::new().get(format!("{}/database/logs/{}", host_url, address));
let builder = add_auth_header_opt(builder, &auth_header);
Expand Down
5 changes: 1 addition & 4 deletions crates/cli/src/subcommands/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,7 @@ async fn download_with_progress(client: &reqwest::Client, url: &str, temp_path:
Ok(())
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
// Release the lockfile on the config, since we don't need it.
config.release_lock();

pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let version = args.get_one::<String>("version");
let current_exe_path = env::current_exe()?;
let force = args.get_flag("force");
Expand Down
5 changes: 1 addition & 4 deletions crates/cli/src/subcommands/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ pub fn cli() -> clap::Command {
)
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
// Release the lockfile on the config, since we don't need it.
config.release_lock();

pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
if args.get_flag("cli") {
println!("{}", CLI_VERSION);
return Ok(());
Expand Down
Loading