config: Overhaul config format and implementation

- Split the config system into multiple files in its own folder.
- Use a manual Deserialize implementation for VtSelection.
- Change config format to use sections for better organization.
- Add temporary fallback to parse old format.
This commit is contained in:
Kenny Levinsen
2020-04-01 18:07:20 +02:00
parent 939519fdcb
commit 91b7e3d58f
10 changed files with 303 additions and 162 deletions

View File

@@ -1,141 +0,0 @@
use std::{env, fs::read_to_string};
use getopts::Options;
use serde::Deserialize;
fn default_vt() -> toml::Value {
toml::Value::String("next".to_string())
}
fn default_greeter_user() -> String {
"greeter".to_string()
}
fn default_socket_path() -> String {
"/run/greetd.sock".to_string()
}
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(default = "default_vt")]
pub vt: toml::Value,
pub greeter: String,
#[serde(default = "default_greeter_user")]
pub greeter_user: String,
#[serde(default = "default_socket_path")]
pub socket_path: String,
#[serde(skip_deserializing)]
pub session_worker: usize,
}
pub enum VtSelection {
Next,
Current,
None,
Specific(usize),
}
impl Config {
pub fn vt(&self) -> VtSelection {
match &self.vt {
toml::Value::String(s) => match s.as_str() {
"next" => VtSelection::Next,
"current" => VtSelection::Current,
"none" => VtSelection::None,
_ => panic!("unknown value of vt, expect next, current, none, or vt number"),
},
toml::Value::Integer(u) => VtSelection::Specific(*u as usize),
_ => panic!("unknown value of vt, expect next, current, or vt number"),
}
}
}
fn print_usage(program: &str, opts: Options) {
let brief = format!("Usage: {} [options]", program);
print!("{}", opts.usage(&brief));
}
pub fn read_config() -> Config {
let args: Vec<String> = env::args().collect();
let program = args[0].clone();
let mut opts = Options::new();
opts.optflag("h", "help", "print this help menu");
opts.optopt("t", "vt", "VT to run on", "VT");
opts.optopt("s", "socket-path", "socket path to use", "SOCKET_PATH");
opts.optopt("g", "greeter", "greeter to run", "GREETER");
opts.optopt("u", "greeter-user", "user to run greeter as", "USER");
opts.optopt("c", "config", "config file to use", "CONFIG_FILE");
opts.optopt(
"w",
"session-worker",
"start a session worker (internal)",
"FD",
);
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => panic!(f.to_string()),
};
if matches.opt_present("h") {
print_usage(&program, opts);
std::process::exit(0);
}
let mut config = match read_to_string(
matches
.opt_str("config")
.unwrap_or_else(|| "/etc/greetd/config.toml".to_string()),
) {
Ok(s) => match toml::from_str(&s) {
Ok(v) => v,
Err(e) => {
eprintln!("Unable to parse configuration file: {:?}", e);
eprintln!("Please fix the configuration file and try again.");
std::process::exit(1);
}
},
Err(_) => Config {
vt: default_vt(),
greeter: "".to_string(),
greeter_user: "".to_string(),
socket_path: default_socket_path(),
session_worker: 0,
},
};
if let Some(vt) = matches.opt_str("vt") {
config.vt = match vt.as_str() {
"next" => toml::Value::String("next".to_string()),
"current" => toml::Value::String("current".to_string()),
"none" => toml::Value::String("none".to_string()),
v => toml::Value::Integer(v.parse().expect("could not parse vt number")),
}
}
if let Some(greeter) = matches.opt_str("greeter") {
config.greeter = greeter;
}
if let Some(user) = matches.opt_str("greeter-user") {
config.greeter_user = user;
}
if let Some(socket_path) = matches.opt_str("socket-path") {
config.socket_path = socket_path;
}
if let Some(session_worker) = matches
.opt_get("session-worker")
.expect("unable to parse session-worker")
{
config.session_worker = session_worker
}
if config.greeter.is_empty() {
eprintln!("No greeter specified. Run with --help for more information.");
std::process::exit(1);
}
if config.greeter_user.is_empty() {
eprintln!("No greeter user specified. Run with --help for more information.");
std::process::exit(1);
}
config
}

View File

@@ -0,0 +1,9 @@
use super::vtselection::VtSelection;
pub fn default_vt() -> VtSelection {
VtSelection::Next
}
pub fn default_greeter_user() -> String {
"greeter".to_string()
}

93
greetd/src/config/mod.rs Normal file
View File

@@ -0,0 +1,93 @@
mod defaults;
mod old_config;
mod types;
mod vtselection;
use std::{env, fs::read_to_string};
use getopts::Options;
use super::error::Error;
use old_config::*;
pub use types::*;
pub use vtselection::VtSelection;
fn print_usage(program: &str, opts: Options) {
let brief = format!("Usage: {} [options]", program);
print!("{}", opts.usage(&brief));
}
pub fn read_config() -> Result<Config, Error> {
let args: Vec<String> = env::args().collect();
let program = args[0].clone();
let mut opts = Options::new();
opts.optflag("h", "help", "print this help menu");
opts.optopt("s", "socket-path", "socket path to use", "SOCKET_PATH");
opts.optopt("c", "config", "config file to use", "CONFIG_FILE");
opts.optopt(
"w",
"session-worker",
"start a session worker (internal)",
"FD",
);
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => return Err(format!("could not parse arguments: {}", f).into()),
};
if matches.opt_present("h") {
print_usage(&program, opts);
std::process::exit(0);
}
let internal = ConfigInternal {
socket_path: matches
.opt_str("socket-path")
.unwrap_or_else(|| "/run/greetd.sock".to_string()),
session_worker: matches
.opt_get("session-worker")
.expect("unable to parse session-worker")
.unwrap_or(0),
};
let file = match read_to_string(
matches
.opt_str("config")
.unwrap_or_else(|| "/etc/greetd/config.toml".to_string()),
) {
Ok(s) => match toml::from_str(&s) {
Ok(v) => v,
Err(e) => match try_read_old_config(&s) {
Ok(v) => {
eprintln!("warning: Fallback to old config format, caused by : {}", e);
v
}
Err(_e) => {
return Err(Error::ConfigError(
format!("unable to parse configuration file: {}", e).to_string(),
))
}
},
},
Err(_) => ConfigFile {
default_session: ConfigDefaultSession {
user: "greeter".to_string(),
command: "".to_string(),
},
terminal: Default::default(),
},
};
if file.default_session.command.is_empty() {
return Err(Error::ConfigError(
"no default session user specified".to_string(),
));
}
if file.default_session.user.is_empty() {
return Err(Error::ConfigError(
"no default session user specified".to_string(),
));
}
Ok(Config { file, internal })
}

View File

@@ -0,0 +1,35 @@
use serde::Deserialize;
use crate::error::Error;
use super::vtselection::VtSelection;
use super::{defaults::*, types::*};
#[derive(Debug, Deserialize)]
pub struct OldConfig {
#[serde(default = "default_vt")]
pub vt: VtSelection,
pub greeter: String,
#[serde(default = "default_greeter_user")]
pub greeter_user: String,
}
pub fn try_read_old_config(s: &str) -> Result<super::ConfigFile, Error> {
let oldconfig: OldConfig = match toml::from_str(&s) {
Ok(v) => v,
Err(e) => {
return Err(Error::ConfigError(
format!("unable to parse configuration file: {}", e).to_string(),
))
}
};
Ok(ConfigFile {
terminal: ConfigTerminal { vt: oldconfig.vt },
default_session: ConfigDefaultSession {
user: oldconfig.greeter_user,
command: oldconfig.greeter,
},
})
}

View File

@@ -0,0 +1,37 @@
use super::{defaults::*, vtselection::VtSelection};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct ConfigDefaultSession {
pub command: String,
#[serde(default = "default_greeter_user")]
pub user: String,
}
pub struct ConfigInternal {
pub socket_path: String,
pub session_worker: usize,
}
#[derive(Debug, Deserialize)]
pub struct ConfigTerminal {
#[serde(default = "default_vt")]
pub vt: VtSelection,
}
impl Default for ConfigTerminal {
fn default() -> ConfigTerminal {
ConfigTerminal { vt: default_vt() }
}
}
#[derive(Debug, Deserialize)]
pub struct ConfigFile {
pub terminal: ConfigTerminal,
pub default_session: ConfigDefaultSession,
}
pub struct Config {
pub file: ConfigFile,
pub internal: ConfigInternal,
}

View File

@@ -0,0 +1,62 @@
use std::fmt;
use serde::{
de::{self, Deserializer, Visitor},
Deserialize,
};
struct VtSelectionVisitor;
impl<'de> Visitor<'de> for VtSelectionVisitor {
type Value = VtSelection;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("next, currrent, none or a positive vt number")
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
if value < 1 {
Err(de::Error::invalid_value(
de::Unexpected::Signed(value),
&"next, current, none or a positive vt number",
))
} else {
Ok(VtSelection::Specific(value as usize))
}
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value {
"next" => Ok(VtSelection::Next),
"current" => Ok(VtSelection::Current),
"none" => Ok(VtSelection::None),
_ => Err(de::Error::invalid_value(
de::Unexpected::Str(value),
&"next, current, none or a postive vt number",
)),
}
}
}
#[derive(Debug)]
pub enum VtSelection {
Next,
Current,
None,
Specific(usize),
}
impl<'de> Deserialize<'de> for VtSelection {
fn deserialize<D>(deserializer: D) -> Result<VtSelection, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(VtSelectionVisitor)
}
}

View File

@@ -16,6 +16,9 @@ pub enum Error {
#[error("i/o error: {0}")]
Io(String),
#[error("configuration error: {0}")]
ConfigError(String),
}
impl From<Box<dyn std::error::Error>> for Error {

View File

@@ -21,7 +21,7 @@ use tokio::task;
use crate::{error::Error, session::worker};
async fn session_worker_main(config: config::Config) -> Result<(), Error> {
let raw_fd = config.session_worker as RawFd;
let raw_fd = config.internal.session_worker as RawFd;
let mut cur_flags = unsafe { FdFlag::from_bits_unchecked(fcntl(raw_fd, FcntlArg::F_GETFD)?) };
cur_flags.insert(FdFlag::FD_CLOEXEC);
fcntl(raw_fd, FcntlArg::F_SETFD(cur_flags))?;
@@ -31,11 +31,17 @@ async fn session_worker_main(config: config::Config) -> Result<(), Error> {
#[tokio::main]
async fn main() {
let config = config::read_config();
let config = match config::read_config() {
Ok(config) => config,
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
};
mlockall(MlockAllFlags::all()).expect("unable to lock pages");
let res = task::LocalSet::new()
.run_until(async move {
if config.session_worker > 0 {
if config.internal.session_worker > 0 {
session_worker_main(config).await
} else {
server::main(config).await

View File

@@ -77,17 +77,12 @@ async fn client_handler(ctx: &Context, mut s: UnixStream) -> Result<(), Error> {
}
pub async fn main(config: Config) -> Result<(), Error> {
std::env::set_var("GREETD_SOCK", &config.socket_path);
std::env::set_var("GREETD_SOCK", &config.internal.socket_path);
let _ = std::fs::remove_file(config.socket_path.clone());
let mut listener = UnixListener::bind(&config.socket_path)
let _ = std::fs::remove_file(&config.internal.socket_path);
let mut listener = UnixListener::bind(&config.internal.socket_path)
.map_err(|e| format!("unable to open listener: {}", e))?;
let u = users::get_user_by_name(&config.greeter_user).ok_or(format!(
"configured greeter user '{}' not found",
&config.greeter_user
))?;
let service = if Path::new("/etc/pam.d/greetd").exists() {
"greetd"
} else if Path::new("/etc/pam.d/login").exists() {
@@ -97,13 +92,22 @@ pub async fn main(config: Config) -> Result<(), Error> {
return Err("PAM 'greetd' service missing".into());
};
let u = users::get_user_by_name(&config.file.default_session.user).ok_or(format!(
"configured default session user '{}' not found",
&config.file.default_session.user
))?;
let uid = Uid::from_raw(u.uid());
let gid = Gid::from_raw(u.primary_group_id());
chown(config.socket_path.as_str(), Some(uid), Some(gid))
.map_err(|e| format!("unable to chown greetd socket: {}", e))?;
chown(config.internal.socket_path.as_str(), Some(uid), Some(gid)).map_err(|e| {
format!(
"unable to chown greetd socket at {}: {}",
&config.internal.socket_path, e
)
})?;
let term = Terminal::open(0).map_err(|e| format!("unable to open terminal: {}", e))?;
let vt = match config.vt() {
let vt = match config.file.terminal.vt {
VtSelection::Current => term
.vt_get_current()
.map_err(|e| format!("unable to get current VT: {}", e))?,
@@ -116,11 +120,12 @@ pub async fn main(config: Config) -> Result<(), Error> {
drop(term);
let ctx = Rc::new(Context::new(
config.greeter,
config.greeter_user,
config.file.default_session.command,
config.file.default_session.user,
vt,
service.to_string(),
));
if let Err(e) = ctx.greet().await {
eprintln!("unable to start greeter: {}", e);
reset_vt(vt).map_err(|e| format!("unable to reset VT: {}", e))?;

View File

@@ -10,6 +10,10 @@ greetd uses a simple TOML configuration file to define its behavior.
# CONFIGURATION
## terminal
This section contains terminal configuration.
*vt* = num|"next"|"current"
The VT to run on. Can be the number of a specific VT, "next" to select the
next available VT, or "current" to stay wherever greetd was started. The
@@ -23,16 +27,44 @@ greetd uses a simple TOML configuration file to define its behavior.
Use of a specific VT with appropriate conflict avoidance is recommended.
*greeter* = command-line
The command-line to run to start the greeter, e.g. "agreety -c sway". The
greeter will be run when greetd is initially launched, and every time a
created session terminates.
## default_session
This section describes the default session, also referred to as the "greeter".
*command* = command-line
The command-line to run to start the default session, e.g. "agreety -c sway".
The default session is automatically started when no other session is
running, such as when user session terminate, and when greetd is initially
started with no initial session configured.
See *greetd-ipc*(7) for information on how a greeter can create sessions.
*greeter_user* = user
*user* = user
The user to use for running the greeter. Defaults to "greeter".
## initial_session
This optional section describes the initial session, commonly referred to as
"auto-login".
*command* = command-line
The command-line to run to start the initial session, e.g. "sway". The
initial session will be run when exactly once when greetd is initially
launched.
*user* = user
The user to use for running the initial session.
# EXAMPLE
```toml
[terminal]
vt = 1
[default_session]
command = "agreety -c sway"
```
# AUTHORS
Maintained by Kenny Levinsen <contact@kl.wtf>. For more information about