Realmsd rewritten to use zbus
This commit is contained in:
parent
2e6542e9f7
commit
13516fe024
@ -6,5 +6,7 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcitadel = { path = "../libcitadel" }
|
libcitadel = { path = "../libcitadel" }
|
||||||
dbus = "0.8"
|
zbus = "^2.0.0-beta.5"
|
||||||
|
zvariant = "2.7.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
|
@ -1,496 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{result, thread};
|
|
||||||
|
|
||||||
use dbus::tree::{self, Factory, MTFn, MethodResult, Tree, MethodErr};
|
|
||||||
use dbus::blocking::LocalConnection;
|
|
||||||
use dbus::Message;
|
|
||||||
use libcitadel::{Result, RealmManager, Realm, RealmEvent, OverlayType, RealmFS, terminal};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
type MethodInfo<'a> = tree::MethodInfo<'a, MTFn<TData>, TData>;
|
|
||||||
|
|
||||||
// XXX
|
|
||||||
const UPDATE_TOOL_PATH: &str = "/realms/Shared/citadel-realmfs";
|
|
||||||
const SUDO_PATH: &str = "/usr/bin/sudo";
|
|
||||||
|
|
||||||
const STATUS_REALM_RUNNING: u8 = 1;
|
|
||||||
const STATUS_REALM_CURRENT: u8 = 2;
|
|
||||||
const STATUS_REALM_SYSTEM_REALM: u8 = 4;
|
|
||||||
|
|
||||||
const OBJECT_PATH: &str = "/com/subgraph/realms";
|
|
||||||
const INTERFACE_NAME: &str = "com.subgraph.realms.Manager";
|
|
||||||
const BUS_NAME: &str = "com.subgraph.realms";
|
|
||||||
|
|
||||||
pub struct DbusServer {
|
|
||||||
connection: Arc<LocalConnection>,
|
|
||||||
manager: Arc<RealmManager>,
|
|
||||||
events: EventHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbusServer {
|
|
||||||
|
|
||||||
pub fn connect(manager: Arc<RealmManager>) -> Result<DbusServer> {
|
|
||||||
let connection = LocalConnection::new_system()
|
|
||||||
.map_err(|e| format_err!("Failed to connect to DBUS system bus: {}", e))?;
|
|
||||||
let connection = Arc::new(connection);
|
|
||||||
let events = EventHandler::new(connection.clone());
|
|
||||||
let server = DbusServer { events, connection, manager };
|
|
||||||
Ok(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_tree(&self) -> Tree<MTFn<TData>, TData> {
|
|
||||||
let f = Factory::new_fn::<TData>();
|
|
||||||
let data = TreeData::new(self.manager.clone());
|
|
||||||
let interface = f.interface(INTERFACE_NAME, ())
|
|
||||||
// Methods
|
|
||||||
.add_m(f.method("SetCurrent", (), Self::do_set_current)
|
|
||||||
.in_arg(("name", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("GetCurrent", (), Self::do_get_current)
|
|
||||||
.out_arg(("name", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("List", (), Self::do_list)
|
|
||||||
.out_arg(("realms", "a(sssy)")))
|
|
||||||
|
|
||||||
.add_m(f.method("Start", (), Self::do_start)
|
|
||||||
.in_arg(("name", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("Stop", (), Self::do_stop)
|
|
||||||
.in_arg(("name", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("Restart", (), Self::do_restart)
|
|
||||||
.in_arg(("name", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("Terminal", (), Self::do_terminal)
|
|
||||||
.in_arg(("name", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("Run", (), Self::do_run)
|
|
||||||
.in_arg(("name", "s"))
|
|
||||||
.in_arg(("args", "as")))
|
|
||||||
|
|
||||||
.add_m(f.method("RealmFromCitadelPid", (), Self::do_pid_to_realm)
|
|
||||||
.in_arg(("pid", "u"))
|
|
||||||
.out_arg(("realm", "s")))
|
|
||||||
|
|
||||||
.add_m(f.method("RealmConfig", (), Self::do_get_realm_config)
|
|
||||||
.in_arg(("name", "s"))
|
|
||||||
.out_arg(("config", "a(ss)")))
|
|
||||||
|
|
||||||
.add_m(f.method("ListRealmFS", (), Self::do_list_realmfs)
|
|
||||||
.out_arg(("realmfs", "as")))
|
|
||||||
|
|
||||||
.add_m(f.method("UpdateRealmFS", (), Self::do_update)
|
|
||||||
.in_arg(("name", "s")))
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
.add_s(f.signal("RealmStarted", ())
|
|
||||||
.arg(("realm", "s")))
|
|
||||||
.add_s(f.signal("RealmStopped", ())
|
|
||||||
.arg(("realm", "s")))
|
|
||||||
.add_s(f.signal("RealmNew", ())
|
|
||||||
.arg(("realm", "s")))
|
|
||||||
.add_s(f.signal("RealmRemoved", ())
|
|
||||||
.arg(("realm","s")))
|
|
||||||
.add_s(f.signal("RealmCurrent", ())
|
|
||||||
.arg(("realm", "s")))
|
|
||||||
.add_s(f.signal("ServiceStarted", ()));
|
|
||||||
|
|
||||||
let obpath = f.object_path(OBJECT_PATH, ())
|
|
||||||
.introspectable()
|
|
||||||
.add(interface);
|
|
||||||
|
|
||||||
f.tree(data).add(obpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_list(m: &MethodInfo) -> MethodResult {
|
|
||||||
let list = m.tree.get_data().realm_list();
|
|
||||||
Ok(vec![m.msg.method_return().append1(list)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_set_current(m: &MethodInfo) -> MethodResult {
|
|
||||||
let manager = m.tree.get_data().manager();
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
if let Some(realm) = manager.realm_by_name(name) {
|
|
||||||
if let Err(err) = manager.set_current_realm(&realm) {
|
|
||||||
warn!("set_current_realm({}) failed: {}", name, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_get_current(m: &MethodInfo) -> MethodResult {
|
|
||||||
let manager = m.tree.get_data().manager();
|
|
||||||
let ret = m.msg.method_return();
|
|
||||||
let msg = match manager.current_realm() {
|
|
||||||
Some(realm) => ret.append1(realm.name()),
|
|
||||||
None => ret.append1(""),
|
|
||||||
};
|
|
||||||
Ok(vec![msg])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_start(m: &MethodInfo) -> MethodResult {
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let realm = data.realm_by_name(name)?;
|
|
||||||
thread::spawn(move || {
|
|
||||||
if let Err(e) = data.manager().start_realm(&realm) {
|
|
||||||
warn!("failed to start realm {}: {}", realm.name(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_stop(m: &MethodInfo) -> MethodResult {
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let realm = data.realm_by_name(name)?;
|
|
||||||
thread::spawn(move || {
|
|
||||||
if let Err(e) = data.manager().stop_realm(&realm) {
|
|
||||||
warn!("failed to stop realm {}: {}", realm.name(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_restart(m: &MethodInfo) -> MethodResult {
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let realm = data.realm_by_name(name)?;
|
|
||||||
thread::spawn(move || {
|
|
||||||
if let Err(e) = data.manager().stop_realm(&realm) {
|
|
||||||
warn!("failed to stop realm {}: {}", realm.name(), e);
|
|
||||||
} else if let Err(e) = data.manager().start_realm(&realm) {
|
|
||||||
warn!("failed to restart realm {}: {}", realm.name(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_terminal(m: &MethodInfo) -> MethodResult {
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let realm = data.realm_by_name(name)?;
|
|
||||||
thread::spawn(move || {
|
|
||||||
if !realm.is_active() {
|
|
||||||
if let Err(err) = data.manager().start_realm(&realm) {
|
|
||||||
warn!("failed to start realm {}: {}", realm.name(), err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = data.manager().launch_terminal(&realm) {
|
|
||||||
warn!("error launching terminal for realm {}: {}", realm.name(), err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_update(m: &MethodInfo) -> MethodResult {
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let realmfs = data.realmfs_by_name(name)?;
|
|
||||||
|
|
||||||
let command = format!("{} {} update {}", SUDO_PATH, UPDATE_TOOL_PATH, realmfs.name());
|
|
||||||
terminal::spawn_citadel_gnome_terminal(Some(command));
|
|
||||||
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_run(m: &MethodInfo) -> MethodResult {
|
|
||||||
let (name,args) = m.msg.read2::<&str, Vec<String>>()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let realm = data.realm_by_name(name)?;
|
|
||||||
thread::spawn(move || {
|
|
||||||
if !realm.is_active() {
|
|
||||||
if let Err(err) = data.manager().start_realm(&realm) {
|
|
||||||
warn!("failed to start realm {}: {}", realm.name(), err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = data.manager().run_in_realm(&realm, &args, true) {
|
|
||||||
warn!("error running {:?} in realm {}: {}", args, realm.name(), err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(vec![m.msg.method_return()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_pid_to_realm(m: &MethodInfo) -> MethodResult {
|
|
||||||
let pid = m.msg.read1::<u32>()?;
|
|
||||||
let manager = m.tree.get_data().manager();
|
|
||||||
let ret = m.msg.method_return();
|
|
||||||
let msg = match manager.realm_by_pid(pid) {
|
|
||||||
Some(realm) => ret.append1(realm.name()),
|
|
||||||
None => ret.append1(""),
|
|
||||||
};
|
|
||||||
Ok(vec![msg])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_get_realm_config(m: &MethodInfo) -> MethodResult {
|
|
||||||
let name = m.msg.read1()?;
|
|
||||||
let data = m.tree.get_data().clone();
|
|
||||||
let config = data.realm_config(name)?;
|
|
||||||
Ok(vec![m.msg.method_return().append1(config)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_list_realmfs(m: &MethodInfo) -> MethodResult {
|
|
||||||
let list = m.tree.get_data().realmfs_list();
|
|
||||||
Ok(vec![m.msg.method_return().append1(list)])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(&self) -> Result<()> {
|
|
||||||
let tree = self.build_tree();
|
|
||||||
if let Err(err) = self.connection.request_name(BUS_NAME, false, true, false) {
|
|
||||||
bail!("failed to register DBUS name {}: {}", BUS_NAME, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
tree.start_receive(self.connection.as_ref());
|
|
||||||
|
|
||||||
self.manager.add_event_handler({
|
|
||||||
let events = self.events.clone();
|
|
||||||
move |ev| events.handle_event(ev)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = self.manager.start_event_task() {
|
|
||||||
warn!("error starting realm manager event task: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.send_service_started();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
self.connection
|
|
||||||
.process(Duration::from_millis(1000))
|
|
||||||
.map_err(context!("Error handling dbus messages"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_service_started(&self) {
|
|
||||||
let signal = Self::create_signal("ServiceStarted");
|
|
||||||
if self.connection.channel().send(signal).is_err() {
|
|
||||||
warn!("failed to send ServiceStarted signal");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_signal(name: &str) -> Message {
|
|
||||||
let path = dbus::Path::new(OBJECT_PATH).unwrap();
|
|
||||||
let iface = dbus::strings::Interface::new(INTERFACE_NAME).unwrap();
|
|
||||||
let member = dbus::strings::Member::new(name).unwrap();
|
|
||||||
Message::signal(&path, &iface, &member)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps a connection instance and only expose the send() method.
|
|
||||||
/// Sending a message does not read or write any of the internal
|
|
||||||
/// Connection object state other than the native handle for the
|
|
||||||
/// connection. It should be safe to share this across threads as
|
|
||||||
/// internally libdbus uses a mutex to control concurrent access
|
|
||||||
/// to the dbus_connection_send() function.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ConnectionSender(Arc<LocalConnection>);
|
|
||||||
|
|
||||||
unsafe impl Send for ConnectionSender {}
|
|
||||||
unsafe impl Sync for ConnectionSender {}
|
|
||||||
|
|
||||||
impl ConnectionSender {
|
|
||||||
fn new(connection: Arc<LocalConnection>) -> Self {
|
|
||||||
ConnectionSender(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, msg: Message) -> Result<()> {
|
|
||||||
if let Err(()) = self.0.channel().send(msg) {
|
|
||||||
bail!("failed to send DBUS message");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct EventHandler {
|
|
||||||
sender: ConnectionSender,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventHandler {
|
|
||||||
fn new(conn: Arc<LocalConnection>) -> EventHandler {
|
|
||||||
EventHandler {
|
|
||||||
sender: ConnectionSender::new(conn),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(&self, ev: &RealmEvent) {
|
|
||||||
match ev {
|
|
||||||
RealmEvent::Started(realm) => self.on_started(realm),
|
|
||||||
RealmEvent::Stopped(realm) => self.on_stopped(realm),
|
|
||||||
RealmEvent::New(realm) => self.on_new(realm),
|
|
||||||
RealmEvent::Removed(realm) => self.on_removed(realm),
|
|
||||||
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_started(&self, realm: &Realm) {
|
|
||||||
self.send_realm_signal("RealmStarted", Some(realm));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_stopped(&self, realm: &Realm) {
|
|
||||||
self.send_realm_signal("RealmStopped", Some(realm));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_new(&self, realm: &Realm) {
|
|
||||||
self.send_realm_signal("RealmNew", Some(realm));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_removed(&self, realm: &Realm) {
|
|
||||||
self.send_realm_signal("RealmRemoved", Some(realm));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_current(&self, realm: Option<&Realm>) {
|
|
||||||
self.send_realm_signal("RealmCurrent", realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_realm_signal(name: &str) -> Message {
|
|
||||||
let path = dbus::Path::new(OBJECT_PATH).unwrap();
|
|
||||||
let iface = dbus::strings::Interface::new(INTERFACE_NAME).unwrap();
|
|
||||||
let member = dbus::strings::Member::new(name).unwrap();
|
|
||||||
Message::signal(&path, &iface, &member)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_realm_signal(&self, sig_name: &str, realm: Option<&Realm>) {
|
|
||||||
let realm_name = match realm {
|
|
||||||
Some(r) => r.name(),
|
|
||||||
None => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let msg = Self::create_realm_signal(sig_name)
|
|
||||||
.append1(realm_name);
|
|
||||||
|
|
||||||
if let Err(e) = self.sender.send(msg) {
|
|
||||||
warn!("Could not send signal '{}': {}", sig_name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct TreeData {
|
|
||||||
manager: Arc<RealmManager>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TreeData {
|
|
||||||
fn new(manager: Arc<RealmManager>) -> TreeData {
|
|
||||||
TreeData {
|
|
||||||
manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn manager(&self) -> &RealmManager {
|
|
||||||
&self.manager
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realm_by_name(&self, name: &str) -> result::Result<Realm, MethodErr> {
|
|
||||||
if let Some(realm) = self.manager.realm_by_name(name) {
|
|
||||||
Ok(realm)
|
|
||||||
} else {
|
|
||||||
result::Result::Err(MethodErr::failed(&format!("Cannot find realm {}", name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realmfs_by_name(&self, name: &str) -> result::Result<RealmFS, MethodErr> {
|
|
||||||
if let Some(realmfs) = self.manager.realmfs_by_name(name) {
|
|
||||||
Ok(realmfs)
|
|
||||||
} else {
|
|
||||||
result::Result::Err(MethodErr::failed(&format!("Cannot find realmfs {}", name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_config_flag(list: &mut Vec<(String,String)>, val: bool, name: &str) {
|
|
||||||
let valstr = if val { "true".to_string() } else { "false".to_string() };
|
|
||||||
list.push((name.to_string(), valstr));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realm_config(&self, name: &str) -> result::Result<Vec<(String,String)>, MethodErr> {
|
|
||||||
let realm = self.realm_by_name(name)?;
|
|
||||||
let config = realm.config();
|
|
||||||
let mut list = Vec::new();
|
|
||||||
Self::append_config_flag(&mut list, config.gpu(), "use-gpu");
|
|
||||||
Self::append_config_flag(&mut list, config.wayland(), "use-wayland");
|
|
||||||
Self::append_config_flag(&mut list, config.x11(), "use-x11");
|
|
||||||
Self::append_config_flag(&mut list, config.sound(), "use-sound");
|
|
||||||
Self::append_config_flag(&mut list, config.shared_dir(), "use-shared-dir");
|
|
||||||
Self::append_config_flag(&mut list, config.network(), "use-network");
|
|
||||||
Self::append_config_flag(&mut list, config.kvm(), "use-kvm");
|
|
||||||
Self::append_config_flag(&mut list, config.ephemeral_home(), "use-ephemeral-home");
|
|
||||||
let overlay = match config.overlay() {
|
|
||||||
OverlayType::None => "none",
|
|
||||||
OverlayType::TmpFS => "tmpfs",
|
|
||||||
OverlayType::Storage => "storage",
|
|
||||||
};
|
|
||||||
let scheme = match config.terminal_scheme() {
|
|
||||||
Some(name) => name.to_string(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
list.push(("realmfs".to_string(), config.realmfs().to_string()));
|
|
||||||
list.push(("overlay".to_string(), overlay.to_string()));
|
|
||||||
list.push(("terminal-scheme".to_string(), scheme));
|
|
||||||
|
|
||||||
Ok(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realm_element(realm: &Realm) -> (String, String, String, u8) {
|
|
||||||
let name = realm.name().to_owned();
|
|
||||||
let desc = Self::realm_description(realm);
|
|
||||||
let realmfs = realm.config().realmfs().to_owned();
|
|
||||||
let status = Self::realm_status(realm);
|
|
||||||
(name, desc, realmfs, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realm_list(&self) -> Vec<(String, String, String, u8)> {
|
|
||||||
self.manager.realm_list()
|
|
||||||
.iter()
|
|
||||||
.map(Self::realm_element)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realm_description(realm: &Realm) -> String {
|
|
||||||
match realm.notes() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realm_status(realm: &Realm) -> u8 {
|
|
||||||
let mut status = 0;
|
|
||||||
if realm.is_active() {
|
|
||||||
status |= STATUS_REALM_RUNNING;
|
|
||||||
}
|
|
||||||
if realm.is_current() {
|
|
||||||
status |= STATUS_REALM_CURRENT;
|
|
||||||
}
|
|
||||||
if realm.is_system() {
|
|
||||||
status |= STATUS_REALM_SYSTEM_REALM;
|
|
||||||
}
|
|
||||||
status
|
|
||||||
}
|
|
||||||
|
|
||||||
fn realmfs_list(&self) -> Vec<String> {
|
|
||||||
self.manager.realmfs_list()
|
|
||||||
.into_iter()
|
|
||||||
.map(|fs| fs.name().to_owned())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Debug for TreeData {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "<TreeData>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Debug)]
|
|
||||||
struct TData;
|
|
||||||
|
|
||||||
impl tree::DataType for TData {
|
|
||||||
type Tree = TreeData;
|
|
||||||
type ObjectPath = ();
|
|
||||||
type Property = ();
|
|
||||||
type Interface = ();
|
|
||||||
type Method = ();
|
|
||||||
type Signal = ();
|
|
||||||
}
|
|
69
realmsd/src/events.rs
Normal file
69
realmsd/src/events.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use zbus::{Connection, ObjectServer};
|
||||||
|
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH, realm_status};
|
||||||
|
use libcitadel::{RealmEvent, Realm};
|
||||||
|
|
||||||
|
pub struct EventHandler {
|
||||||
|
connection: Connection,
|
||||||
|
realms_server: RealmsManagerServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventHandler {
|
||||||
|
pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
|
||||||
|
EventHandler { connection, realms_server }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&self, ev: &RealmEvent) {
|
||||||
|
if let Err(err) = self.dispatch_event(ev) {
|
||||||
|
warn!("Error emitting signal for realm event {}: {}", ev, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_event(&self, ev: &RealmEvent) -> zbus::Result<()> {
|
||||||
|
match ev {
|
||||||
|
RealmEvent::Started(realm) => self.on_started(realm),
|
||||||
|
RealmEvent::Stopped(realm) => self.on_stopped(realm),
|
||||||
|
RealmEvent::New(realm) => self.on_new(realm),
|
||||||
|
RealmEvent::Removed(realm) => self.on_removed(realm),
|
||||||
|
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_server<F>(&self, func: F) -> zbus::Result<()>
|
||||||
|
where
|
||||||
|
F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
|
||||||
|
{
|
||||||
|
let mut object_server = ObjectServer::new(&self.connection);
|
||||||
|
object_server.at(REALMS_SERVER_OBJECT_PATH, self.realms_server.clone())?;
|
||||||
|
object_server.with(REALMS_SERVER_OBJECT_PATH, |iface: &RealmsManagerServer| func(iface))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_started(&self, realm: &Realm) -> zbus::Result<()> {
|
||||||
|
let namespace = realm.pid_namespace().unwrap_or(String::new());
|
||||||
|
let status = realm_status(realm);
|
||||||
|
self.with_server(|server| server.realm_started(realm.name(), namespace.as_str(), status))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
|
||||||
|
let status = realm_status(realm);
|
||||||
|
self.with_server(|server| server.realm_stopped(realm.name(), status))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_new(&self, realm: &Realm) -> zbus::Result<()> {
|
||||||
|
let status = realm_status(realm);
|
||||||
|
let description = realm.notes().unwrap_or(String::new());
|
||||||
|
self.with_server(|server| server.realm_new(realm.name(), &description, status))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
|
||||||
|
self.with_server(|server| server.realm_removed(realm.name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
|
||||||
|
self.with_server(|server| {
|
||||||
|
match realm {
|
||||||
|
Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
|
||||||
|
None => server.realm_current("", 0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,39 @@
|
|||||||
#[macro_use] extern crate libcitadel;
|
#[macro_use] extern crate libcitadel;
|
||||||
use libcitadel::{RealmManager, Result, Logger, LogLevel};
|
|
||||||
|
|
||||||
mod dbus;
|
use zbus::{Connection, fdo};
|
||||||
mod devices;
|
|
||||||
|
use libcitadel::{Logger, LogLevel, Result};
|
||||||
|
|
||||||
|
use crate::realms_manager::RealmsManagerServer;
|
||||||
|
|
||||||
|
mod realms_manager;
|
||||||
|
mod events;
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(e) = run_dbus_server() {
|
if let Err(e) = run_realm_manager() {
|
||||||
warn!("Error: {}", e);
|
warn!("Error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_dbus_server() -> Result<()> {
|
fn create_system_connection() -> zbus::Result<Connection> {
|
||||||
Logger::set_log_level(LogLevel::Verbose);
|
let connection = zbus::Connection::new_system()?;
|
||||||
let manager = RealmManager::load()?;
|
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?;
|
||||||
let server = dbus::DbusServer::connect(manager)?;
|
Ok(connection)
|
||||||
server.start()?;
|
}
|
||||||
Ok(())
|
|
||||||
|
fn run_realm_manager() -> Result<()> {
|
||||||
|
Logger::set_log_level(LogLevel::Verbose);
|
||||||
|
|
||||||
|
let connection = create_system_connection()
|
||||||
|
.map_err(context!("ZBus Connection error"))?;
|
||||||
|
|
||||||
|
let mut object_server = RealmsManagerServer::register(&connection)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Err(err) = object_server.try_handle_next() {
|
||||||
|
warn!("Error handling DBus message: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
376
realmsd/src/realms_manager.rs
Normal file
376
realmsd/src/realms_manager.rs
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
use libcitadel::{RealmManager, Realm, OverlayType, Result};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use zbus::{dbus_interface, ObjectServer,Connection};
|
||||||
|
use zvariant::derive::Type;
|
||||||
|
use std::thread;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::{Serialize,Deserialize};
|
||||||
|
use crate::events::EventHandler;
|
||||||
|
use libcitadel::terminal::Base16Scheme;
|
||||||
|
|
||||||
|
pub const REALMS_SERVER_OBJECT_PATH: &str = "/com/subgraph/realms";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RealmsManagerServer {
|
||||||
|
manager: Arc<RealmManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||||
|
"use-gpu", "use-wayland", "use-x11", "use-sound",
|
||||||
|
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home"
|
||||||
|
];
|
||||||
|
|
||||||
|
fn is_bool_config_variable(variable: &str) -> bool {
|
||||||
|
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_config(realm: &Realm) {
|
||||||
|
let path = realm.base_path_file("config");
|
||||||
|
if let Err(e) = realm.config().write_to(&path) {
|
||||||
|
warn!("Error writing config file {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_realm_boolean_config(realm: &Realm, variable: &str, value: &str) {
|
||||||
|
|
||||||
|
let val = match value {
|
||||||
|
"true" => true,
|
||||||
|
"false" => false,
|
||||||
|
_ => {
|
||||||
|
warn!("Not a valid boolean value '{}'", value);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
realm.with_mut_config(|c| {
|
||||||
|
match variable {
|
||||||
|
"use-gpu" if c.gpu() != val => c.use_gpu = Some(val),
|
||||||
|
"use-wayland" if c.wayland() != val => c.use_wayland = Some(val),
|
||||||
|
"use-x11" if c.x11() != val => c.use_x11 = Some(val),
|
||||||
|
"use-sound" if c.sound() != val => c.use_sound = Some(val),
|
||||||
|
"use-shared-dir" if c.shared_dir() != val => c.use_shared_dir = Some(val),
|
||||||
|
"use-network" if c.network() != val => c.use_network = Some(val),
|
||||||
|
"use-kvm" if c.kvm() != val => c.use_kvm = Some(val),
|
||||||
|
"use-ephemeral-home" if c.ephemeral_home() != val => c.use_ephemeral_home = Some(val),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
save_config(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_realm(realm: &Realm, variable: &str, value: &str) {
|
||||||
|
if is_bool_config_variable(variable) {
|
||||||
|
configure_realm_boolean_config(realm, variable, value);
|
||||||
|
} else if variable == "overlay" {
|
||||||
|
if value == "tmpfs" || value == "storage" || value == "none" {
|
||||||
|
realm.with_mut_config(|c| {
|
||||||
|
c.overlay = Some(value.to_string());
|
||||||
|
});
|
||||||
|
save_config(realm);
|
||||||
|
} else {
|
||||||
|
warn!("Invalid storage type '{}'", value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if variable == "terminal-scheme" {
|
||||||
|
if Base16Scheme::by_name(value).is_none() {
|
||||||
|
warn!("No terminal color scheme with name '{}' available", value);
|
||||||
|
}
|
||||||
|
realm.with_mut_config(|c| {
|
||||||
|
c.terminal_scheme = Some(value.to_string());
|
||||||
|
});
|
||||||
|
save_config(realm);
|
||||||
|
} else if variable == "realmfs" {
|
||||||
|
warn!("Changing realmfs config variable not implemented");
|
||||||
|
} else {
|
||||||
|
warn!("Unknown config variable '{}'", variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmsManagerServer {
|
||||||
|
|
||||||
|
fn register_events(&self, connection: &Connection) -> Result<()> {
|
||||||
|
let events = EventHandler::new(connection.clone(), self.clone());
|
||||||
|
self.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||||
|
self.manager.start_event_task()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(connection: &Connection) -> Result<ObjectServer> {
|
||||||
|
let manager = RealmManager::load()?;
|
||||||
|
let iface = RealmsManagerServer { manager };
|
||||||
|
iface.register_events(connection)?;
|
||||||
|
let mut object_server = ObjectServer::new(connection);
|
||||||
|
object_server.at(REALMS_SERVER_OBJECT_PATH, iface).map_err(context!("ZBus error"))?;
|
||||||
|
Ok(object_server)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[dbus_interface(name = "com.subgraph.realms.Manager")]
|
||||||
|
impl RealmsManagerServer {
|
||||||
|
|
||||||
|
fn set_current(&self, name: &str) {
|
||||||
|
if let Some(realm) = self.manager.realm_by_name(name) {
|
||||||
|
if let Err(err) = self.manager.set_current_realm(&realm) {
|
||||||
|
warn!("set_current_realm({}) failed: {}", name, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current(&self) -> String {
|
||||||
|
match self.manager.current_realm() {
|
||||||
|
Some(realm) => realm.name().to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self) -> Vec<RealmItem> {
|
||||||
|
let mut realms = Vec::new();
|
||||||
|
for r in self.manager.realm_list() {
|
||||||
|
realms.push(RealmItem::new_from_realm(&r));
|
||||||
|
}
|
||||||
|
realms
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self, name: &str) {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let manager = self.manager.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if let Err(e) = manager.start_realm(&realm) {
|
||||||
|
warn!("failed to start realm {}: {}", realm.name(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, name: &str) {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let manager = self.manager.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if let Err(e) = manager.stop_realm(&realm) {
|
||||||
|
warn!("failed to stop realm {}: {}", realm.name(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart(&self, name: &str) {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let manager = self.manager.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if let Err(e) = manager.stop_realm(&realm) {
|
||||||
|
warn!("failed to stop realm {}: {}", realm.name(), e);
|
||||||
|
} else if let Err(e) = manager.start_realm(&realm) {
|
||||||
|
warn!("failed to restart realm {}: {}", realm.name(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal(&self, name: &str) {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let manager = self.manager.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if !realm.is_active() {
|
||||||
|
if let Err(err) = manager.start_realm(&realm) {
|
||||||
|
warn!("failed to start realm {}: {}", realm.name(), err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(err) = manager.launch_terminal(&realm) {
|
||||||
|
warn!("error launching terminal for realm {}: {}", realm.name(), err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, name: &str, args: Vec<String>) {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let manager = self.manager.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if !realm.is_active() {
|
||||||
|
if let Err(err) = manager.start_realm(&realm) {
|
||||||
|
warn!("failed to start realm {}: {}", realm.name(), err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(err) = manager.run_in_realm(&realm, &args, true) {
|
||||||
|
warn!("error running {:?} in realm {}: {}", args, realm.name(), err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realm_from_citadel_pid(&self, pid: u32) -> String {
|
||||||
|
match self.manager.realm_by_pid(pid) {
|
||||||
|
Some(r) => r.name().to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realm_config(&self, name: &str) -> RealmConfig {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return RealmConfig::new(),
|
||||||
|
};
|
||||||
|
RealmConfig::new_from_realm(&realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||||
|
let realm = match self.manager.realm_by_name(name) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
warn!("No realm named '{}' found in RealmSetConfig", name);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for var in &vars {
|
||||||
|
configure_realm(&realm, &var.0, &var.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realm_exists(&self, name: &str) -> bool {
|
||||||
|
Realm::is_valid_name(name) && self.manager.realm_by_name(name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_realm(&self, name: &str) -> bool {
|
||||||
|
if let Err(err) = self.manager.new_realm(name) {
|
||||||
|
warn!("Error creating realm ({}): {}", name, err);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_realm_f_s(&self) -> Vec<String> {
|
||||||
|
self.manager.realmfs_list()
|
||||||
|
.into_iter()
|
||||||
|
.map(|fs| fs.name().to_owned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_realm_f_s(&self, _name: &str) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[dbus_interface(signal)]
|
||||||
|
pub fn realm_started(&self, realm: &str, namespace: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||||
|
|
||||||
|
#[dbus_interface(signal)]
|
||||||
|
pub fn realm_stopped(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||||
|
|
||||||
|
#[dbus_interface(signal)]
|
||||||
|
pub fn realm_new(&self, realm: &str, description: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||||
|
|
||||||
|
#[dbus_interface(signal)]
|
||||||
|
pub fn realm_removed(&self, realm: &str) -> zbus::Result<()> { Ok(()) }
|
||||||
|
|
||||||
|
#[dbus_interface(signal)]
|
||||||
|
pub fn realm_current(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||||
|
|
||||||
|
#[dbus_interface(signal)]
|
||||||
|
pub fn service_started(&self) -> zbus::Result<()> { Ok(()) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_REALM_RUNNING: u8 = 1;
|
||||||
|
const STATUS_REALM_CURRENT: u8 = 2;
|
||||||
|
const STATUS_REALM_SYSTEM_REALM: u8 = 4;
|
||||||
|
|
||||||
|
pub fn realm_status(realm: &Realm) -> u8 {
|
||||||
|
let mut status = 0;
|
||||||
|
if realm.is_active() {
|
||||||
|
status |= STATUS_REALM_RUNNING;
|
||||||
|
}
|
||||||
|
if realm.is_current() {
|
||||||
|
status |= STATUS_REALM_CURRENT;
|
||||||
|
}
|
||||||
|
if realm.is_system() {
|
||||||
|
status |= STATUS_REALM_SYSTEM_REALM;
|
||||||
|
}
|
||||||
|
status
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize,Serialize,Type)]
|
||||||
|
struct RealmItem {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
realmfs: String,
|
||||||
|
namespace: String,
|
||||||
|
status: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmItem {
|
||||||
|
fn new_from_realm(realm: &Realm) -> Self {
|
||||||
|
let name = realm.name().to_string();
|
||||||
|
let description = realm.notes().unwrap_or(String::new());
|
||||||
|
let realmfs = realm.config().realmfs().to_string();
|
||||||
|
let namespace = realm.pid_namespace().unwrap_or(String::new());
|
||||||
|
let status = realm_status(realm);
|
||||||
|
RealmItem { name, description, realmfs, namespace, status }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize,Serialize,Type)]
|
||||||
|
struct RealmConfig {
|
||||||
|
items: HashMap<String,String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealmConfig {
|
||||||
|
fn new() -> Self {
|
||||||
|
RealmConfig { items: HashMap::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_from_realm(realm: &Realm) -> Self {
|
||||||
|
let mut this = RealmConfig { items: HashMap::new() };
|
||||||
|
let config = realm.config();
|
||||||
|
this.add_bool("use-gpu", config.gpu());
|
||||||
|
this.add_bool("use-wayland", config.wayland());
|
||||||
|
this.add_bool("use-x11", config.x11());
|
||||||
|
this.add_bool("use-sound", config.sound());
|
||||||
|
this.add_bool("use-shared-dir", config.shared_dir());
|
||||||
|
this.add_bool("use-network", config.network());
|
||||||
|
this.add_bool("use-kvm", config.kvm());
|
||||||
|
this.add_bool("use-ephemeral-home", config.ephemeral_home());
|
||||||
|
|
||||||
|
let overlay = match config.overlay() {
|
||||||
|
OverlayType::None => "none",
|
||||||
|
OverlayType::TmpFS => "tmpfs",
|
||||||
|
OverlayType::Storage => "storage",
|
||||||
|
};
|
||||||
|
this.add("overlay", overlay);
|
||||||
|
|
||||||
|
let scheme = match config.terminal_scheme() {
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
this.add("terminal-scheme", scheme);
|
||||||
|
this.add("realmfs", config.realmfs());
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_bool(&mut self, name: &str, val: bool) {
|
||||||
|
let valstr = if val { "true".to_string() } else { "false".to_string() };
|
||||||
|
self.add(name, valstr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add<S,T>(&mut self, k: S, v: T) where S: Into<String>, T: Into<String> {
|
||||||
|
self.items.insert(k.into(), v.into());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user