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",
]
[[package]]
name = "directories"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "4.0.0"
@ -681,7 +672,6 @@ dependencies = [
"anyhow",
"base64",
"battery",
"directories",
"env_logger",
"exitcode",
"image",
@ -697,6 +687,7 @@ dependencies = [
"sysinfo",
"tokio",
"toml",
"users",
"validator",
"void",
"zbus",
@ -1992,6 +1983,16 @@ dependencies = [
"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]]
name = "validator"
version = "0.16.0"

View file

@ -2,6 +2,7 @@
name = "hassliebe"
version = "0.1.0"
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
@ -12,7 +13,6 @@ dry_run = [] # will prevent some actions like shutting down
anyhow = "1.0.69"
base64 = "0.21.0"
battery = "0.7.8"
directories = "4.0.1"
env_logger = "0.10.0"
exitcode = "1.1.2"
image = "0.24.5"
@ -28,6 +28,7 @@ serde_json = "1.0.93"
sysinfo = { version = "0.28.2", default-features = false }
tokio = { version = "1.25.0", features = ["full"] }
toml = "0.7.2"
users = "0.11.0"
validator = { version = "0.16.0", features = ["derive"] }
void = "1.0.2"
zbus = { version = "3.10.0", default-features = false, features = ["tokio"] }

View file

@ -22,10 +22,10 @@ Ideas:
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`.
It can be changed using the `HASSLIEBE_CONFIG` environment variable.
- `[data]`: `/var/lib/hassliebe` / `$XDG_DATA_HOME/hassliebe`
- `[config]`: `/etc/hassliebe/config.toml` / `$XDG_CONFIG_HOME/hassliebe/config.toml`
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.
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.
## Modules
@ -100,6 +100,8 @@ battery = 60 # updates every 60 seconds
### Notifications
**Not available when running as a system service.**
The notifications module allows sending notifications according to the
[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
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.
- `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

View file

@ -1,16 +1,14 @@
use std::borrow::ToOwned;
use std::fs;
use std::fs::File;
use std::io::{ErrorKind, Read, Write};
use std::io::{ErrorKind, Read};
use std::path::Path;
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use anyhow::{Context, Result};
use serde::Deserialize;
use validator::Validate;
use crate::modules;
#[derive(Serialize, Deserialize, Validate)]
#[derive(Deserialize, Validate)]
pub struct Mqtt {
#[validate(length(min = 1))]
pub host: String,
@ -21,19 +19,19 @@ pub struct Mqtt {
pub credentials: Option<MqttCredentials>,
}
#[derive(Serialize, Deserialize, Validate)]
#[derive(Deserialize, Validate)]
pub struct MqttCredentials {
pub user: String,
pub password: String,
}
#[derive(Serialize, Deserialize, Validate)]
#[derive(Deserialize, Validate)]
pub struct Internal {
#[validate(length(min = 2))]
pub stable_id: String,
}
#[derive(Serialize, Deserialize, Validate)]
#[derive(Deserialize, Validate)]
pub struct Config {
#[validate(custom = "crate::util::validate_hass_id")]
pub friendly_id: String,
@ -48,49 +46,27 @@ pub struct 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>> {
match File::open(config_file_path) {
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();
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")?;
parsed.validate().context("while validating 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 configuration file")?;
Ok(Some(parsed))
}
Err(error) if error.kind() == ErrorKind::NotFound => {
if let Some(parent) = config_file_path.parent() {
fs::create_dir_all(parent)?;
}
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.");
log::error!("The configuration file does not exist: {}", config_file_path.to_string_lossy());
log::info!("See the documentation: {}", env!("CARGO_PKG_HOMEPAGE"));
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::env;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use anyhow::{bail, Result};
use crate::modules::{InitializationContext, ModuleContext, ModuleContextMqtt};
use crate::mqtt::OwnedTopicsService;
@ -14,28 +15,46 @@ mod modules;
mod mqtt;
mod util;
#[derive(Debug)]
struct Paths {
data_directory: Box<Path>,
config: Box<Path>,
}
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 {
config: std::env::var_os("HASS_CONFIG")
let paths = if is_root {
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)
// I dont like this clone
.or(project_dirs.clone().map(|d| d.config_dir().to_path_buf().join("config.toml")))
.ok_or_else(|| anyhow!("Please specify a config file via HASS_CONFIG"))?
.unwrap_or_else(|| home.clone().join(".config"))
.join("hassliebe/config.toml")
.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)
.or(project_dirs.map(|d| d.data_dir().to_path_buf()))
.ok_or_else(|| anyhow!("Please specify a data directory via HASS_DATA_DIR"))?
.unwrap_or_else(|| home.clone().join(".local/share"))
.join("hassliebe")
.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() {
tokio::fs::create_dir_all(p).await?;
}
@ -83,6 +102,12 @@ async fn load_machine_id(paths: &Paths) -> Result<String> {
async fn main() -> Result<()> {
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 machine_id = load_machine_id(&paths).await?;

View file

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

View file

@ -3,7 +3,7 @@ use std::time::Duration;
use anyhow::{anyhow, Result};
use rumqttc::QoS;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use sysinfo::{CpuExt, System, SystemExt};
use tokio::time::MissedTickBehavior;
use validator::Validate;
@ -12,7 +12,7 @@ use crate::modules::InitializationContext;
const MODULE_ID: &str = "info";
#[derive(Serialize, Deserialize, Validate)]
#[derive(Deserialize, Validate)]
pub struct Config {
#[serde(default)]
enabled: bool,
@ -79,6 +79,8 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
if config.battery != 0 {
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() {
log::debug!("Found {} batteries, using {}", battery_dirs.len(), dir.file_name().to_string_lossy());
let path = dir.path();

View file

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

View file

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