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
66 changes: 64 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions aw-client-rust/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ mod test {
};
let aw_config = aw_server::config::AWConfig {
port,
testing: true,
auth: aw_server::config::AWAuthConfig {
api_key: api_key.map(str::to_owned),
},
Expand Down
9 changes: 9 additions & 0 deletions aw-models/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,14 @@ pub struct Info {
pub hostname: String,
pub version: String,
pub testing: bool,
/// Name of the running instance profile ("default" unless `--profile` was
/// given). Lets clients tell concurrent instances apart. Defaults on
/// deserialization so a new client can still talk to an older server.
#[serde(default = "default_profile")]
pub profile: String,
pub device_id: String,
}

fn default_profile() -> String {
"default".to_string()
}
4 changes: 2 additions & 2 deletions aw-server/src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub mod android {
match DATASTORE {
Some(ref ds) => ds.clone(),
None => {
let db_dir = dirs::db_path(false)
let db_dir = dirs::db_path("default")
.expect("Failed to get db path")
.to_str()
.unwrap()
Expand Down Expand Up @@ -125,7 +125,7 @@ pub mod android {
};
info!("Using server_state:: device_id: {}", server_state.device_id);

let mut server_config = crate::config::create_config(false, None);
let mut server_config = crate::config::create_config("default", None);
server_config.port = 5600;

endpoints::build_rocket(server_state, server_config)
Expand Down
85 changes: 66 additions & 19 deletions aw-server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

use rocket::config::Config;
use rocket::data::{Limits, ToByteUnit};
Expand All @@ -9,15 +10,46 @@ use serde::{Deserialize, Serialize};

use crate::dirs;

// Far from an optimal way to solve it, but works and is simple
static mut TESTING: bool = true;
pub fn set_testing(testing: bool) {
unsafe {
TESTING = testing;
static PROFILE: OnceLock<String> = OnceLock::new();

/// Return the current profile name (default: "default").
pub fn get_profile() -> &'static str {
PROFILE.get().map(|s| s.as_str()).unwrap_or("default")
}

/// Set the profile. Idempotent for the same value; panics if called with
/// a conflicting value (would silently redirect to a different datastore).
pub fn set_profile(profile: String) {
// Use the atomic OnceLock::set result to avoid a TOCTOU race: two callers
// with different values could both see get()==None before either sets it,
// causing the loser to silently proceed under the wrong profile.
match PROFILE.set(profile.clone()) {
Ok(()) => {} // we set it atomically, done
Err(_) => {
// Already set — verify no conflict (duplicate or concurrent same-value call is fine)
let existing = PROFILE
.get()
.expect("set returned Err but get returned None");
if existing != &profile {
panic!(
"set_profile called with conflicting value: existing={existing:?}, new={profile:?}"
);
}
// same value — idempotent, no action needed
}
}
}

/// True when running in the legacy "testing" profile or in debug builds.
pub fn is_testing() -> bool {
Comment thread
TimeToBuildBob marked this conversation as resolved.
unsafe { TESTING }
get_profile() == "testing"
}

/// Backwards-compat shim: callers that still use set_testing(bool) convert
/// true → profile "testing" and false → profile "default".
pub fn set_testing(testing: bool) {
let profile = if testing { "testing" } else { "default" };
set_profile(profile.to_string());
}
Comment thread
TimeToBuildBob marked this conversation as resolved.

/// Authentication configuration, serialised as `[auth]` in config.toml.
Expand Down Expand Up @@ -119,24 +151,29 @@ fn default_custom_static() -> std::collections::HashMap<String, String> {
std::collections::HashMap::new()
}

fn get_config_path(testing: bool, config_override: Option<&Path>) -> PathBuf {
/// Config filename for a profile.
///
/// Isolated profile roots (including new-style `activitywatch-testing/`) use
/// bare `config.toml` — the directory already isolates. Suffixed
/// `config-testing.toml` remains only in the legacy shared-root layout so
/// existing testing config is not orphaned.
pub fn config_filename(profile: &str) -> String {
crate::dirs::config_filename(profile)
}

fn get_config_path(profile: &str, config_override: Option<&Path>) -> PathBuf {
if let Some(config_path) = config_override {
return config_path.to_path_buf();
}

let mut config_path = dirs::get_config_dir().unwrap();
if !testing {
config_path.push("config.toml")
} else {
config_path.push("config-testing.toml")
}

config_path.push(config_filename(profile));
config_path
}

pub fn create_config(testing: bool, config_override: Option<&Path>) -> AWConfig {
set_testing(testing);
let config_path = get_config_path(testing, config_override);
pub fn create_config(profile: &str, config_override: Option<&Path>) -> AWConfig {
set_profile(profile.to_string());
let config_path = get_config_path(profile, config_override);
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).expect("Unable to create config dir");
}
Expand Down Expand Up @@ -203,15 +240,25 @@ mod tests {
}
}

#[test]
fn config_filename_isolated_named_profiles_are_bare() {
// default and named isolated roots are always config.toml. Suffixed
// config-testing.toml is filesystem-dependent (legacy shared root) and
// covered in dirs.rs against fake roots.
assert_eq!(super::config_filename("default"), "config.toml");
assert_eq!(super::config_filename("research"), "config.toml");
assert_eq!(super::config_filename("my-profile"), "config.toml");
}

#[test]
fn create_config_uses_override_path() {
// create_config mutates the TESTING global, so these tests must not overlap.
// create_config sets the PROFILE OnceLock, so these tests must not overlap.
let _lock = TEST_LOCK.lock().unwrap();
let paths = TestConfigPath::new("override");
fs::create_dir_all(paths.config_path.parent().unwrap()).unwrap();
fs::write(&paths.config_path, "address = \"0.0.0.0\"\nport = 5611\n").unwrap();

let config = create_config(false, Some(paths.config_path.as_path()));
let config = create_config("default", Some(paths.config_path.as_path()));

assert_eq!(config.address, "0.0.0.0");
assert_eq!(config.port, 5611);
Expand All @@ -222,7 +269,7 @@ mod tests {
let _lock = TEST_LOCK.lock().unwrap();
let paths = TestConfigPath::new("missing");

let config = create_config(false, Some(paths.config_path.as_path()));
let config = create_config("default", Some(paths.config_path.as_path()));

assert!(paths.config_path.is_file());
assert_eq!(config.address, "127.0.0.1");
Expand Down
Loading
Loading