Skip to content
Merged
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
32 changes: 32 additions & 0 deletions Cargo.lock

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

18 changes: 15 additions & 3 deletions crates/benshi/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,16 @@ fn daemon(tray: bool) -> ExitCode {
ExitCode::from(ended)
}

/// Bind the socket, open the database and supervise the daemon's tasks, until
/// every one of them has stopped or a signal stops the daemon. The answer is
/// the code the process ends with.
/// Read the settings, bind the socket, open the database and supervise the
/// daemon's tasks, until every one of them has stopped or a signal stops the
/// daemon. The answer is the code the process ends with.
#[cfg(target_os = "linux")]
async fn serve(voice: &Arc<Voice>) -> u8 {
use std::sync::Mutex;
use std::time::SystemTime;

use benshi_core::clock::SystemClock;
use benshi_daemon::config;
use benshi_daemon::paths::{DATABASE, Role, directory};
use benshi_daemon::recognition::Recogniser;
use benshi_detect::mpris::{MprisWatcher, POLL_INTERVAL, SOURCE_DEADLINE};
Expand All @@ -305,6 +306,16 @@ async fn serve(voice: &Arc<Voice>) -> u8 {
Ok(stops) => stops,
Err(code) => return code,
};
// Read ahead of the socket and the database, so that a file that cannot
// be used ends the daemon having touched neither. No task reads a
// setting.
let (_settings, origin) = match config::load(directory(Role::Config)) {
Ok(loaded) => loaded,
Err(unusable) => {
voice.say(format_args!("{unusable}"));
return FAILED;
}
};
let socket = benshi_daemon::ipc::socket_path();
// The XDG Base Directory Specification asks for a warning where the
// runtime directory is replaced.
Expand Down Expand Up @@ -342,6 +353,7 @@ async fn serve(voice: &Arc<Voice>) -> u8 {
return FAILED;
}
};
voice.say(format_args!("{origin}"));
voice.say(format_args!("listening on {}", socket.display()));
voice.say(format_args!("recording in {}", database.display()));

Expand Down
8 changes: 4 additions & 4 deletions crates/benshi/tests/a_task_is_told_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn the_daemon_says_that_detection_is_started_again() {
("BENSHI_DATA_DIR", data.as_os_str()),
]);
let started = daemon.says();
assert_eq!(started.len(), 2, "the daemon started: {started:?}");
assert_eq!(started.len(), 3, "the daemon started: {started:?}");

let said = daemon
.says_next()
Expand All @@ -46,7 +46,7 @@ fn the_daemon_says_that_detection_is_started_again() {
fn a_daemon_that_cannot_say_it_goes_on_running() {
// A daemon that outlives the terminal it was started from writes to a
// terminal that has gone, and the write fails. This one is read for its
// two lines and the first it says of detection, and the second is the
// three lines and the first it says of detection, and the second is the
// one it cannot write.
let temporary = Directory::of("t-unheard");
let runtime = temporary.path().join("run");
Expand All @@ -56,10 +56,10 @@ fn a_daemon_that_cannot_say_it_goes_on_running() {
("XDG_RUNTIME_DIR", runtime.as_os_str()),
("BENSHI_DATA_DIR", data.as_os_str()),
],
3,
4,
);
let started = daemon.says();
assert_eq!(started.len(), 2, "the daemon started: {started:?}");
assert_eq!(started.len(), 3, "the daemon started: {started:?}");
let said = daemon.says_next().expect("the daemon said something");
assert!(said.starts_with(STARTED_AGAIN), "{said}");

Expand Down
3 changes: 2 additions & 1 deletion crates/benshi/tests/a_tray_is_asked_for.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

mod common;

use common::{Daemon, Directory};
use common::{Daemon, Directory, NO_SETTINGS};

#[test]
fn a_daemon_asked_for_a_tray_says_that_there_is_none() {
Expand All @@ -31,6 +31,7 @@ fn a_daemon_asked_for_a_tray_says_that_there_is_none() {
said,
[
"benshi: there is no tray yet, running without one".to_owned(),
NO_SETTINGS.to_owned(),
format!(
"benshi: listening on {}",
runtime.join("benshi.sock").display()
Expand Down
8 changes: 8 additions & 0 deletions crates/benshi/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ pub const NO_SOURCE: &str = "no source is open\n";
/// What the last line a daemon says as it starts begins with.
const STARTED: &str = "benshi: recording in ";

/// What a daemon says of its settings in a session that names no
/// configuration directory, which a session a test chose does unless it sets
/// one.
// Dead in a test binary that reads no daemon's first lines whole.
#[allow(dead_code)]
pub const NO_SETTINGS: &str = "benshi: default settings: no configuration directory: no absolute \
path in `XDG_CONFIG_HOME` or `HOME`";

/// The flag that asks for a daemon without a tray icon.
pub const NO_TRAY: &str = "--no-tray";

Expand Down
28 changes: 26 additions & 2 deletions crates/benshi/tests/nothing_can_be_said.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ fn a_daemon_that_cannot_say_where_its_socket_went_goes_on() {
);
}

#[test]
fn a_daemon_whose_settings_cannot_be_used_ends_as_a_failure_unheard() {
let temporary = Directory::of("n-settings");
let config = temporary.path().join("config");
fs::create_dir_all(config.join("benshi")).expect("a configuration directory");
fs::write(
config.join("benshi").join("config.toml"),
"[send]\nwhen = \"sometimes\"\n",
)
.expect("a file of settings");

let ended = ends_unheard(
&[OsStr::new(NO_TRAY)],
&[
("XDG_RUNTIME_DIR", temporary.path().join("run").as_os_str()),
("XDG_CONFIG_HOME", config.as_os_str()),
("BENSHI_DATA_DIR", temporary.path().join("data").as_os_str()),
],
Descriptors::Plenty,
);

assert_eq!(ended, FAILED);
}

#[test]
fn a_daemon_with_nowhere_to_record_ends_as_a_failure_unheard() {
let temporary = Directory::of("n-nowhere");
Expand Down Expand Up @@ -166,7 +190,7 @@ fn a_daemon_that_cannot_listen_ends_as_a_failure_unheard() {
),
]);
let started = first.says();
assert_eq!(started.len(), 2, "the first daemon started: {started:?}");
assert_eq!(started.len(), 3, "the first daemon started: {started:?}");

let ended = ends_unheard(
&[OsStr::new(NO_TRAY)],
Expand Down Expand Up @@ -234,7 +258,7 @@ fn a_client_that_cannot_say_where_it_looks_is_answered_as_one_that_can() {
("BENSHI_DATA_DIR", data.as_os_str()),
]);
let started = daemon.says();
assert_eq!(started.len(), 3, "the daemon started: {started:?}");
assert_eq!(started.len(), 4, "the daemon started: {started:?}");
let mut heard = the_binary(&[OsStr::new("sources")], &session, Descriptors::Plenty);
heard.stderr(Stdio::piped());
let mut unheard = the_binary(&[OsStr::new("sources")], &session, Descriptors::Plenty);
Expand Down
5 changes: 4 additions & 1 deletion crates/benshi/tests/the_fallback_is_said.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod common;
use std::ffi::OsStr;
use std::process::{Command, Stdio};

use common::{Directory, the_daemon_says};
use common::{Directory, NO_SETTINGS, the_daemon_says};

/// What the client says in this session, asked for the sources.
fn the_client_says(session: &[(&str, &OsStr)], arguments: &[&OsStr]) -> Vec<String> {
Expand Down Expand Up @@ -52,6 +52,7 @@ fn the_daemon_says_that_the_runtime_directory_is_not_set() {
"benshi: `XDG_RUNTIME_DIR` is not set, so socket is {}",
socket.display()
),
NO_SETTINGS.to_owned(),
format!("benshi: listening on {}", socket.display()),
format!("benshi: recording in {}", data.join("benshi.db").display()),
]
Expand All @@ -78,6 +79,7 @@ fn the_daemon_says_what_is_wrong_with_the_runtime_directory_it_was_given() {
socket is {}",
socket.display()
),
NO_SETTINGS.to_owned(),
format!("benshi: listening on {}", socket.display()),
format!("benshi: recording in {}", data.join("benshi.db").display()),
]
Expand All @@ -99,6 +101,7 @@ fn the_daemon_says_nothing_of_a_runtime_directory_it_uses() {
assert_eq!(
said,
[
NO_SETTINGS.to_owned(),
format!(
"benshi: listening on {}",
runtime.join("benshi.sock").display()
Expand Down
100 changes: 100 additions & 0 deletions crates/benshi/tests/the_settings_are_read.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! What the daemon says of the settings it runs on, and where a file of
//! settings it cannot use leaves it.
//!
//! The file is read before the socket is bound and the database opened, so
//! a daemon that cannot use it ends having touched neither.
//!
//! The binary itself is run, in a session the test chose, because what is
//! under test is what a person reads.

#![cfg(target_os = "linux")]

mod common;

use std::fs;

use common::{Directory, the_daemon_says};

/// How a process ends on Linux whose `main` answered `ExitCode::FAILURE`.
const FAILED: Option<i32> = Some(1);

#[test]
fn the_daemon_says_which_file_its_settings_are_read_from() {
let temporary = Directory::of("g-read");
let runtime = temporary.path().join("run");
let data = temporary.path().join("data");
let config = temporary.path().join("config");
let file = config.join("benshi").join("config.toml");
fs::create_dir_all(config.join("benshi")).expect("a configuration directory");
fs::write(&file, "[send]\nwhen = \"manual\"\n").expect("a file of settings");

let (said, _ended) = the_daemon_says(&[
("XDG_RUNTIME_DIR", runtime.as_os_str()),
("XDG_CONFIG_HOME", config.as_os_str()),
("BENSHI_DATA_DIR", data.as_os_str()),
]);

assert_eq!(
said,
[
format!("benshi: settings from {}", file.display()),
format!(
"benshi: listening on {}",
runtime.join("benshi.sock").display()
),
format!("benshi: recording in {}", data.join("benshi.db").display()),
]
);
}

#[test]
fn a_daemon_with_no_file_of_settings_says_it_runs_on_the_defaults() {
let temporary = Directory::of("g-none");
let runtime = temporary.path().join("run");
let config = temporary.path().join("config");

let (said, _ended) = the_daemon_says(&[
("XDG_RUNTIME_DIR", runtime.as_os_str()),
("XDG_CONFIG_HOME", config.as_os_str()),
("BENSHI_DATA_DIR", temporary.path().join("data").as_os_str()),
]);

assert_eq!(
said.first(),
Some(&format!(
"benshi: default settings: no {}",
config.join("benshi").join("config.toml").display()
)),
"{said:?}"
);
}

#[test]
fn a_daemon_whose_settings_cannot_be_used_says_why_and_ends_before_it_listens() {
let temporary = Directory::of("g-unusable");
let runtime = temporary.path().join("run");
let data = temporary.path().join("data");
let config = temporary.path().join("config");
let file = config.join("benshi").join("config.toml");
fs::create_dir_all(config.join("benshi")).expect("a configuration directory");
fs::write(&file, "[send]\nwhen = \"sometimes\"\n").expect("a file of settings");

let (said, ended) = the_daemon_says(&[
("XDG_RUNTIME_DIR", runtime.as_os_str()),
("XDG_CONFIG_HOME", config.as_os_str()),
("BENSHI_DATA_DIR", data.as_os_str()),
]);

assert_eq!(
said,
[format!(
"benshi: {} cannot be used: line 2, column 8: unknown variant `sometimes`, \
expected one of `each`, `close`, `interval`, `manual`",
file.display()
)]
);
assert_eq!(ended.and_then(|status| status.code()), FAILED);
let socket = runtime.join("benshi.sock");
assert!(!socket.exists(), "{} is there", socket.display());
assert!(!data.exists(), "{} is there", data.display());
}
5 changes: 3 additions & 2 deletions crates/benshi/tests/the_store_is_opened.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod common;
use std::fs;

use benshi_store::{SCHEMA_VERSION, Store};
use common::{Daemon, Directory, the_daemon_says};
use common::{Daemon, Directory, NO_SETTINGS, the_daemon_says};

/// How a process ends on Linux whose `main` answered `ExitCode::FAILURE`.
const FAILED: Option<i32> = Some(1);
Expand All @@ -35,6 +35,7 @@ fn the_daemon_says_where_it_records_and_the_database_is_there() {
assert_eq!(
said,
[
NO_SETTINGS.to_owned(),
format!(
"benshi: listening on {}",
runtime.join("benshi.sock").display()
Expand Down Expand Up @@ -125,7 +126,7 @@ fn a_daemon_that_cannot_listen_opens_no_database() {
),
]);
let started = first.says();
assert_eq!(started.len(), 2, "the first daemon started: {started:?}");
assert_eq!(started.len(), 3, "the first daemon started: {started:?}");
let untouched = temporary.path().join("second");

let (said, ended) = the_daemon_says(&[
Expand Down
2 changes: 2 additions & 0 deletions crates/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ benshi-detect = { path = "../detect" }
benshi-sinks = { path = "../sinks" }
benshi-store = { path = "../store" }
benshi-sync = { path = "../sync" }
humantime = "2.4.0"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
socket2 = "0.6.5"
thiserror = "2.0.20"
tokio = { version = "1.53.1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }
toml = { version = "1.1.6", default-features = false, features = ["std", "serde", "parse"] }

[lints]
workspace = true
Expand Down
Loading
Loading