Use system-wide directories when run as root

This commit is contained in:
Moritz Ruth 2023-03-09 23:18:29 +01:00
parent 603fff0094
commit 9143d30de3
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
9 changed files with 103 additions and 96 deletions

21
Cargo.lock generated
View file

@ -342,15 +342,6 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "directories"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "4.0.0" version = "4.0.0"
@ -681,7 +672,6 @@ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"battery", "battery",
"directories",
"env_logger", "env_logger",
"exitcode", "exitcode",
"image", "image",
@ -697,6 +687,7 @@ dependencies = [
"sysinfo", "sysinfo",
"tokio", "tokio",
"toml", "toml",
"users",
"validator", "validator",
"void", "void",
"zbus", "zbus",
@ -1992,6 +1983,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "users"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
dependencies = [
"libc",
"log",
]
[[package]] [[package]]
name = "validator" name = "validator"
version = "0.16.0" version = "0.16.0"

View file

@ -2,6 +2,7 @@
name = "hassliebe" name = "hassliebe"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
homepage = "https://git.moritzruth.de/moritzruth/Hassliebe"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -12,7 +13,6 @@ dry_run = [] # will prevent some actions like shutting down
anyhow = "1.0.69" anyhow = "1.0.69"
base64 = "0.21.0" base64 = "0.21.0"
battery = "0.7.8" battery = "0.7.8"
directories = "4.0.1"
env_logger = "0.10.0" env_logger = "0.10.0"
exitcode = "1.1.2" exitcode = "1.1.2"
image = "0.24.5" image = "0.24.5"
@ -28,6 +28,7 @@ serde_json = "1.0.93"
sysinfo = { version = "0.28.2", default-features = false } sysinfo = { version = "0.28.2", default-features = false }
tokio = { version = "1.25.0", features = ["full"] } tokio = { version = "1.25.0", features = ["full"] }
toml = "0.7.2" toml = "0.7.2"
users = "0.11.0"
validator = { version = "0.16.0", features = ["derive"] } validator = { version = "0.16.0", features = ["derive"] }
void = "1.0.2" void = "1.0.2"
zbus = { version = "3.10.0", default-features = false, features = ["tokio"] } zbus = { version = "3.10.0", default-features = false, features = ["tokio"] }

View file

@ -22,10 +22,10 @@ Ideas:
TBD TBD
If the configuration file does not exist, an example file is created and the program exits. Hassliebe uses different paths depending on whether it is run as root or as a normal user:
The default path is `$XDG_CONFIG_HOME/hassliebe/config.toml`. - `[data]`: `/var/lib/hassliebe` / `$XDG_DATA_HOME/hassliebe`
It can be changed using the `HASSLIEBE_CONFIG` environment variable. - `[config]`: `/etc/hassliebe/config.toml` / `$XDG_CONFIG_HOME/hassliebe/config.toml`
Example: Example:
@ -61,7 +61,7 @@ Hassliebe needs a way to uniquely identify a machine.
`friendly_id` is not suited well for this because the user may want to change it from time to time. `friendly_id` is not suited well for this because the user may want to change it from time to time.
If `/etc/machine-id` exists (as is the case with systemd-based systems), it will be used. If `/etc/machine-id` exists (as is the case with systemd-based systems), it will be used.
Otherwise, a random ID will be generated on the first run and stored in `$XDG_DATA_HOME/hassliebe/machine_id`. Otherwise, a random ID will be generated on the first run and stored in `[data]/machine_id`.
The latter always takes precedence. The latter always takes precedence.
## Modules ## Modules
@ -100,6 +100,8 @@ battery = 60 # updates every 60 seconds
### Notifications ### Notifications
**Not available when running as a system service.**
The notifications module allows sending notifications according to the The notifications module allows sending notifications according to the
[Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup). [Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup).
@ -128,7 +130,7 @@ Complex messages have these properties:
- `long_content` (optional) — Optional additional content - `long_content` (optional) — Optional additional content
which [supports a subset of HTML](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup). which [supports a subset of HTML](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup).
- `transient` (optional) — Boolean indicating whether to bypass the notification servers persistence capability. - `transient` (optional) — Boolean indicating whether to bypass the notification servers persistence capability.
- `encoded_image` — Optional padded base64-encoded image attached to the notification. - `encoded_image` (optional) — Padded Base64-encoded image attached to the notification.
- `actions` (optional) — Object with the keys being IDs and the values being labels. - `actions` (optional) — Object with the keys being IDs and the values being labels.
#### Actions #### Actions

View file

@ -1,16 +1,14 @@
use std::borrow::ToOwned;
use std::fs;
use std::fs::File; use std::fs::File;
use std::io::{ErrorKind, Read, Write}; use std::io::{ErrorKind, Read};
use std::path::Path; use std::path::Path;
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use validator::Validate; use validator::Validate;
use crate::modules; use crate::modules;
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct Mqtt { pub struct Mqtt {
#[validate(length(min = 1))] #[validate(length(min = 1))]
pub host: String, pub host: String,
@ -21,19 +19,19 @@ pub struct Mqtt {
pub credentials: Option<MqttCredentials>, pub credentials: Option<MqttCredentials>,
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct MqttCredentials { pub struct MqttCredentials {
pub user: String, pub user: String,
pub password: String, pub password: String,
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct Internal { pub struct Internal {
#[validate(length(min = 2))] #[validate(length(min = 2))]
pub stable_id: String, pub stable_id: String,
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct Config { pub struct Config {
#[validate(custom = "crate::util::validate_hass_id")] #[validate(custom = "crate::util::validate_hass_id")]
pub friendly_id: String, pub friendly_id: String,
@ -48,49 +46,27 @@ pub struct Config {
pub modules: modules::Config, pub modules: modules::Config,
} }
fn create_example_config() -> Config {
Config {
friendly_id: "my_pc".to_owned(),
display_name: "My PC".to_owned(),
mqtt: Mqtt {
host: "".to_owned(),
port: 1883,
credentials: None,
},
modules: modules::Config::default(),
}
}
pub fn load(config_file_path: &Path) -> Result<Option<Config>> { pub fn load(config_file_path: &Path) -> Result<Option<Config>> {
match File::open(config_file_path) { match File::open(config_file_path) {
Ok(mut file) => { Ok(mut file) => {
log::info!("Reading config file: {}", config_file_path.to_string_lossy()); log::info!("Reading configuration file: {}", config_file_path.to_string_lossy());
let mut string_content = String::new(); let mut string_content = String::new();
file.read_to_string(&mut string_content).context("while reading the config file")?; file.read_to_string(&mut string_content).context("while reading the configuration file")?;
let parsed = toml::from_str::<Config>(string_content.as_str()).context("while parsing the config file")?; let parsed = toml::from_str::<Config>(string_content.as_str()).context("while parsing the configuration file")?;
parsed.validate().context("while validating the config file")?; parsed.validate().context("while validating the configuration file")?;
Ok(Some(parsed)) Ok(Some(parsed))
} }
Err(error) if error.kind() == ErrorKind::NotFound => { Err(error) if error.kind() == ErrorKind::NotFound => {
if let Some(parent) = config_file_path.parent() { log::error!("The configuration file does not exist: {}", config_file_path.to_string_lossy());
fs::create_dir_all(parent)?; log::info!("See the documentation: {}", env!("CARGO_PKG_HOMEPAGE"));
}
let mut file = File::create(config_file_path).context("while creating the default config file")?;
let default = toml::to_string::<Config>(&create_example_config()).expect("create_example_config() should be valid");
file.write_all(default.as_bytes())?;
log::warn!("Created an example config file at {}", config_file_path.to_string_lossy());
log::warn!("Make sure to edit the file before running again.");
Ok(None) Ok(None)
} }
Err(error) => bail!("Config file could not be opened: {}", error), Err(error) => Err(error).context("while opening the configuration file"),
} }
} }

View file

@ -1,9 +1,10 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::exit; use std::process::exit;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{bail, Result};
use crate::modules::{InitializationContext, ModuleContext, ModuleContextMqtt}; use crate::modules::{InitializationContext, ModuleContext, ModuleContextMqtt};
use crate::mqtt::OwnedTopicsService; use crate::mqtt::OwnedTopicsService;
@ -14,28 +15,46 @@ mod modules;
mod mqtt; mod mqtt;
mod util; mod util;
#[derive(Debug)]
struct Paths { struct Paths {
data_directory: Box<Path>, data_directory: Box<Path>,
config: Box<Path>, config: Box<Path>,
} }
async fn get_paths_and_create_directories() -> Result<Paths> { async fn get_paths_and_create_directories() -> Result<Paths> {
let project_dirs = directories::ProjectDirs::from("", "", "Hassliebe"); let is_root: bool = users::get_current_uid() == 0;
let paths = Paths { let paths = if is_root {
config: std::env::var_os("HASS_CONFIG") Paths {
config: PathBuf::from("/etc/hassliebe/config.toml").into_boxed_path(),
data_directory: PathBuf::from("/var/lib/hassliebe").into_boxed_path(),
}
} else {
let home = env::var_os("HOME").map(PathBuf::from);
if let Some(home) = home {
Paths {
config: env::var_os("XDG_CONFIG_HOME")
.filter(|p| !p.is_empty())
.map(PathBuf::from) .map(PathBuf::from)
// I dont like this clone .unwrap_or_else(|| home.clone().join(".config"))
.or(project_dirs.clone().map(|d| d.config_dir().to_path_buf().join("config.toml"))) .join("hassliebe/config.toml")
.ok_or_else(|| anyhow!("Please specify a config file via HASS_CONFIG"))?
.into_boxed_path(), .into_boxed_path(),
data_directory: std::env::var_os("HASS_DATA_DIR")
data_directory: env::var_os("XDG_DATA_HOME")
.filter(|p| !p.is_empty())
.map(PathBuf::from) .map(PathBuf::from)
.or(project_dirs.map(|d| d.data_dir().to_path_buf())) .unwrap_or_else(|| home.clone().join(".local/share"))
.ok_or_else(|| anyhow!("Please specify a data directory via HASS_DATA_DIR"))? .join("hassliebe")
.into_boxed_path(), .into_boxed_path(),
}
} else {
bail!("The HOME environment variable must be set when not running as root.")
}
}; };
log::debug!("{:?}", &paths);
if let Some(p) = paths.config.parent() { if let Some(p) = paths.config.parent() {
tokio::fs::create_dir_all(p).await?; tokio::fs::create_dir_all(p).await?;
} }
@ -83,6 +102,12 @@ async fn load_machine_id(paths: &Paths) -> Result<String> {
async fn main() -> Result<()> { async fn main() -> Result<()> {
initialize_logger(); initialize_logger();
if env::args().any(|a| a == "-v" || a == "--version" || a == "-h" || a == "--help") {
println!("Hassliebe v{} is licensed under the Blue Oak Model License 1.0.0.", env!("CARGO_PKG_VERSION"));
println!("See https://git.moritzruth.de/moritzruth/Hassliebe for more information.");
exit(exitcode::OK);
}
let paths = get_paths_and_create_directories().await?; let paths = get_paths_and_create_directories().await?;
let machine_id = load_machine_id(&paths).await?; let machine_id = load_machine_id(&paths).await?;

View file

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use tokio::process::Command; use tokio::process::Command;
use validator::Validate; use validator::Validate;
@ -8,7 +8,7 @@ use crate::modules::InitializationContext;
const MODULE_ID: &str = "buttons"; const MODULE_ID: &str = "buttons";
const BUTTON_TRIGGER_TEXT: &str = "press"; const BUTTON_TRIGGER_TEXT: &str = "press";
#[derive(Serialize, Deserialize, Validate, Clone)] #[derive(Deserialize, Validate, Clone)]
pub struct ButtonConfig { pub struct ButtonConfig {
#[validate(custom = "crate::util::validate_hass_id")] #[validate(custom = "crate::util::validate_hass_id")]
pub id: String, pub id: String,
@ -23,7 +23,7 @@ pub struct ButtonConfig {
pub run_in_shell: bool, pub run_in_shell: bool,
} }
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct Config { pub struct Config {
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,

View file

@ -3,7 +3,7 @@ use std::time::Duration;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use rumqttc::QoS; use rumqttc::QoS;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use sysinfo::{CpuExt, System, SystemExt}; use sysinfo::{CpuExt, System, SystemExt};
use tokio::time::MissedTickBehavior; use tokio::time::MissedTickBehavior;
use validator::Validate; use validator::Validate;
@ -12,7 +12,7 @@ use crate::modules::InitializationContext;
const MODULE_ID: &str = "info"; const MODULE_ID: &str = "info";
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct Config { pub struct Config {
#[serde(default)] #[serde(default)]
enabled: bool, enabled: bool,
@ -79,6 +79,8 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
if config.battery != 0 { if config.battery != 0 {
let battery_dirs = fs::read_dir("/sys/class/power_supply")?.filter_map(|d| d.ok()).collect::<Vec<_>>(); let battery_dirs = fs::read_dir("/sys/class/power_supply")?.filter_map(|d| d.ok()).collect::<Vec<_>>();
// TODO: Filter battery_dirs by the existence of "capacity"
if let Some(dir) = battery_dirs.first() { if let Some(dir) = battery_dirs.first() {
log::debug!("Found {} batteries, using {}", battery_dirs.len(), dir.file_name().to_string_lossy()); log::debug!("Found {} batteries, using {}", battery_dirs.len(), dir.file_name().to_string_lossy());
let path = dir.path(); let path = dir.path();

View file

@ -4,14 +4,14 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use json::JsonValue; use json::JsonValue;
use rumqttc::{AsyncClient as MqttClient, ClientError, QoS}; use rumqttc::{AsyncClient as MqttClient, ClientError, QoS};
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use validator::Validate; use validator::Validate;
mod buttons; mod buttons;
mod info; mod info;
mod notifications; mod notifications;
#[derive(Serialize, Deserialize, Validate, Default)] #[derive(Deserialize, Validate, Default)]
pub struct Config { pub struct Config {
#[validate] #[validate]
pub buttons: Option<buttons::Config>, pub buttons: Option<buttons::Config>,

View file

@ -5,7 +5,7 @@ use anyhow::{Context, Result};
use base64::Engine; use base64::Engine;
use notify_rust::Hint; use notify_rust::Hint;
use rumqttc::QoS; use rumqttc::QoS;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use validator::Validate; use validator::Validate;
@ -14,7 +14,7 @@ use crate::util::{generate_alphanumeric_id, hash_string_to_u32, spawn_nonessenti
const MODULE_ID: &str = "notifications"; const MODULE_ID: &str = "notifications";
#[derive(Serialize, Deserialize, Validate)] #[derive(Deserialize, Validate)]
pub struct Config { pub struct Config {
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,