Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

52 changed files with 1017 additions and 5647 deletions

1308
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[workspace]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui", "launch-gnome-software" ]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
[profile.release]
lto = true
codegen-units = 1

File diff suppressed because it is too large Load Diff

View File

@ -8,14 +8,14 @@ homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
rpassword = "7.3"
clap = { version = "4.5", features = ["cargo", "derive"] }
rpassword = "4.0"
clap = "2.33"
lazy_static = "1.4"
serde_derive = "1.0"
serde = "1.0"
toml = "0.8"
toml = "0.5"
hex = "0.4"
byteorder = "1"
dbus = "0.8.4"
pwhash = "1.0"
pwhash = "0.3.1"
tempfile = "3"

View File

@ -70,7 +70,7 @@ fn deploy_artifacts() -> Result<()> {
let run_images = Path::new(IMAGE_DIRECTORY);
if !run_images.exists() {
util::create_dir(run_images)?;
cmd!("/bin/mount", "-t tmpfs -o size=5g images /run/citadel/images")?;
cmd!("/bin/mount", "-t tmpfs -o size=4g images /run/citadel/images")?;
}
util::read_directory("/boot/images", |dent| {

View File

@ -1,97 +1,89 @@
use std::path::Path;
use std::process::exit;
use clap::{Arg,ArgMatches};
use clap::{command, ArgAction, Command};
use clap::{App,Arg,SubCommand,ArgMatches};
use clap::AppSettings::*;
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
use hex;
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
pub fn main(args: Vec<String>) {
pub fn main() {
let matches = command!()
let app = App::new("citadel-image")
.about("Citadel update image builder")
.arg_required_else_help(true)
.disable_help_subcommand(true)
.subcommand(
Command::new("metainfo")
.about("Display metainfo variables for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("info")
.about("Display metainfo variables for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("generate-verity")
.about("Generate dm-verity hash tree for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("verify")
.about("Verify dm-verity hash tree for an image file")
.arg(
Arg::new("option")
.long("option")
.required(true)
.help("Path to image file"),
),
)
.subcommand(
Command::new("install-rootfs")
.about("Install rootfs image file to a partition")
.arg(
Arg::new("choose")
.long("just-choose")
.action(ArgAction::SetTrue)
.help("Don't install anything, just show which partition would be chosen"),
)
.arg(
Arg::new("skip-sha")
.long("skip-sha")
.action(ArgAction::SetTrue)
.help("Skip verification of header sha256 value"),
)
.arg(
Arg::new("no-prefer")
.long("no-prefer")
.action(ArgAction::SetTrue)
.help("Don't set PREFER_BOOT flag"),
)
.arg(
Arg::new("path")
.required_unless_present("choose")
.help("Path to image file"),
),
)
.subcommand(Command::new("genkeys").about("Generate a pair of keys"))
.subcommand(
Command::new("decompress")
.about("Decompress a compressed image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("bless")
.about("Mark currently mounted rootfs partition as successfully booted"),
)
.subcommand(
Command::new("verify-shasum")
.about("Verify the sha256 sum of the image")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.get_matches();
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
.subcommand(SubCommand::with_name("metainfo")
.about("Display metainfo variables for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("info")
.about("Display metainfo variables for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("generate-verity")
.about("Generate dm-verity hash tree for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("verify")
.about("Verify dm-verity hash tree for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("install-rootfs")
.about("Install rootfs image file to a partition")
.arg(Arg::with_name("choose")
.long("just-choose")
.help("Don't install anything, just show which partition would be chosen"))
.arg(Arg::with_name("skip-sha")
.long("skip-sha")
.help("Skip verification of header sha256 value"))
.arg(Arg::with_name("no-prefer")
.long("no-prefer")
.help("Don't set PREFER_BOOT flag"))
.arg(Arg::with_name("path")
.required_unless("choose")
.help("Path to image file")))
.subcommand(SubCommand::with_name("genkeys")
.about("Generate a pair of keys"))
.subcommand(SubCommand::with_name("decompress")
.about("Decompress a compressed image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("bless")
.about("Mark currently mounted rootfs partition as successfully booted"))
.subcommand(SubCommand::with_name("verify-shasum")
.about("Verify the sha256 sum of the image")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")));
Logger::set_log_level(LogLevel::Debug);
let matches = app.get_matches_from(args);
let result = match matches.subcommand() {
Some(("metainfo", sub_m)) => metainfo(sub_m),
Some(("info", sub_m)) => info(sub_m),
Some(("generate-verity", sub_m)) => generate_verity(sub_m),
Some(("verify", sub_m)) => verify(sub_m),
Some(("sign-image", sub_m)) => sign_image(sub_m),
Some(("genkeys", _)) => genkeys(),
Some(("decompress", sub_m)) => decompress(sub_m),
Some(("verify-shasum", sub_m)) => verify_shasum(sub_m),
Some(("install-rootfs", sub_m)) => install_rootfs(sub_m),
Some(("install", sub_m)) => install_image(sub_m),
Some(("bless", _)) => bless(),
("metainfo", Some(m)) => metainfo(m),
("info", Some(m)) => info(m),
("generate-verity", Some(m)) => generate_verity(m),
("verify", Some(m)) => verify(m),
("sign-image", Some(m)) => sign_image(m),
("genkeys", Some(_)) => genkeys(),
("decompress", Some(m)) => decompress(m),
("verify-shasum", Some(m)) => verify_shasum(m),
("install-rootfs", Some(m)) => install_rootfs(m),
("install", Some(m)) => install_image(m),
("bless", Some(_)) => bless(),
_ => Ok(()),
};
@ -167,9 +159,7 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
}
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
let path = arg_matches.get_one::<String>("path")
.expect("path argument missing");
let path = arg_matches.value_of("path").expect("path argument missing");
if !Path::new(path).exists() {
bail!("Cannot load image {}: File does not exist", path);
}
@ -181,14 +171,14 @@ fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
}
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
if arg_matches.get_flag("choose") {
if arg_matches.is_present("choose") {
let _ = choose_install_partition(true)?;
return Ok(())
}
let img = load_image(arg_matches)?;
if !arg_matches.get_flag("skip-sha") {
if !arg_matches.is_present("skip-sha") {
info!("Verifying sha256 hash of image");
let shasum = img.generate_shasum()?;
if shasum != img.metainfo().shasum() {
@ -198,7 +188,7 @@ fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
let partition = choose_install_partition(true)?;
if !arg_matches.get_flag("no-prefer") {
if !arg_matches.is_present("no-prefer") {
clear_prefer_boot()?;
img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
}
@ -222,9 +212,7 @@ fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
}
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
let source = arg_matches.get_one::<String>("path")
.expect("path argument missing");
let source = arg_matches.value_of("path").expect("path argument missing");
let img = load_image(arg_matches)?;
let _hdr = img.header();
let metainfo = img.metainfo();

View File

@ -125,7 +125,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
loop {
println!("{}", prompt);
println!();
let passphrase = rpassword::prompt_password(" Passphrase : ")?;
let passphrase = rpassword::read_password_from_tty(Some(" Passphrase : "))?;
if passphrase.is_empty() {
println!("Passphrase cannot be empty");
continue;
@ -133,7 +133,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
if passphrase == "q" || passphrase == "Q" {
return Ok(None);
}
let confirm = rpassword::prompt_password(" Confirm : ")?;
let confirm = rpassword::read_password_from_tty(Some(" Confirm : "))?;
if confirm == "q" || confirm == "Q" {
return Ok(None);
}

View File

@ -425,7 +425,7 @@ impl Installer {
util::create_dir(&home)?;
util::chown_user(&home)?;
self.info("Copying /realms/skel into home directory")?;
self.info("Copying /realms/skel into home diectory")?;
util::copy_tree(&self.storage().join("realms/skel"), &home)?;
if let Some(scheme) = Base16Scheme::by_name(MAIN_TERMINAL_SCHEME) {
@ -452,7 +452,7 @@ impl Installer {
util::create_dir(&path)?;
util::chown_user(&path)?;
self.info("Copying /realms/skel into home directory")?;
self.info("Copying /realms/skel into home diectory")?;
util::copy_tree(&self.storage().join("realms/skel"), &home)?;
self.info("Creating apt-cacher config file")?;

View File

@ -34,9 +34,9 @@ fn main() {
} else if exe == Path::new("/usr/libexec/citadel-install-backend") {
install_backend::main();
} else if exe == Path::new("/usr/bin/citadel-image") {
image::main();
image::main(args);
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main();
realmfs::main(args);
} else if exe == Path::new("/usr/bin/citadel-update") {
update::main(args);
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
@ -57,8 +57,8 @@ fn dispatch_command(args: Vec<String>) {
match command.as_str() {
"boot" => boot::main(rebuild_args("citadel-boot", args)),
"install" => install::main(rebuild_args("citadel-install", args)),
"image" => image::main(),
"realmfs" => realmfs::main(),
"image" => image::main(rebuild_args("citadel-image", args)),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"update" => update::main(rebuild_args("citadel-update", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),

View File

@ -1,24 +1,28 @@
use clap::{command, Command};
use clap::{Arg, ArgMatches};
use clap::App;
use clap::ArgMatches;
use libcitadel::{Result,RealmFS,Logger,LogLevel};
use libcitadel::util::is_euid_root;
use clap::SubCommand;
use clap::AppSettings::*;
use clap::Arg;
use libcitadel::ResizeSize;
use std::process::exit;
pub fn main() {
pub fn main(args: Vec<String>) {
Logger::set_log_level(LogLevel::Debug);
let matches = command!()
.about("citadel-realmfs")
.arg_required_else_help(true)
.subcommand(Command::new("resize")
let app = App::new("citadel-realmfs")
.about("Citadel realmfs image tool")
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs])
.subcommand(SubCommand::with_name("resize")
.about("Resize an existing RealmFS image. If the image is currently sealed, it will also be unsealed.")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to resize")
.required(true))
.arg(Arg::new("size")
.arg(Arg::with_name("size")
.help("Size to increase RealmFS image to (or by if prefixed with '+')")
.long_help("\
The size can be followed by a 'g' or 'm' character \
@ -31,53 +35,53 @@ is the final absolute size of the image.")
.required(true)))
.subcommand(Command::new("fork")
.subcommand(SubCommand::with_name("fork")
.about("Create a new RealmFS image as an unsealed copy of an existing image")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to fork")
.required(true))
.arg(Arg::new("forkname")
.arg(Arg::with_name("forkname")
.help("Name of new image to create")
.required(true)))
.subcommand(Command::new("autoresize")
.subcommand(SubCommand::with_name("autoresize")
.about("Increase size of RealmFS image if not enough free space remains")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image")
.required(true)))
.subcommand(Command::new("update")
.subcommand(SubCommand::with_name("update")
.about("Open an update shell on the image")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image")
.required(true)))
.subcommand(Command::new("activate")
.subcommand(SubCommand::with_name("activate")
.about("Activate a RealmFS by creating a block device for the image and mounting it.")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to activate")
.required(true)))
.subcommand(Command::new("deactivate")
.subcommand(SubCommand::with_name("deactivate")
.about("Deactivate a RealmFS by unmounting it and removing block device created during activation.")
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to deactivate")
.required(true)))
.arg(Arg::new("image")
.arg(Arg::with_name("image")
.help("Name of or path to RealmFS image to display information about")
.required(true))
.get_matches();
.required(true));
let matches = app.get_matches_from(args);
let result = match matches.subcommand() {
Some(("resize", m)) => resize(m),
Some(("autoresize", m)) => autoresize(m),
Some(("fork", m)) => fork(m),
Some(("update", m)) => update(m),
Some(("activate", m)) => activate(m),
Some(("deactivate", m)) => deactivate(m),
("resize", Some(m)) => resize(m),
("autoresize", Some(m)) => autoresize(m),
("fork", Some(m)) => fork(m),
("update", Some(m)) => update(m),
("activate", Some(m)) => activate(m),
("deactivate", Some(m)) => deactivate(m),
_ => image_info(&matches),
};
@ -88,7 +92,7 @@ is the final absolute size of the image.")
}
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
let image = match arg_matches.get_one::<String>("image") {
let image = match arg_matches.value_of("image") {
Some(s) => s,
None => bail!("Image argument required."),
};
@ -132,9 +136,10 @@ fn parse_resize_size(s: &str) -> Result<ResizeSize> {
fn resize(arg_matches: &ArgMatches) -> Result<()> {
let img = realmfs_image(arg_matches)?;
info!("image is {}", img.path().display());
let size_arg = match arg_matches.get_one::<String>("size") {
let size_arg = match arg_matches.value_of("size") {
Some(size) => size,
None => "No size argument",
};
info!("Size is {}", size_arg);
let mode_add = size_arg.starts_with('+');
@ -160,7 +165,7 @@ fn autoresize(arg_matches: &ArgMatches) -> Result<()> {
fn fork(arg_matches: &ArgMatches) -> Result<()> {
let img = realmfs_image(arg_matches)?;
let forkname = match arg_matches.get_one::<String>("forkname") {
let forkname = match arg_matches.value_of("forkname") {
Some(name) => name,
None => bail!("No fork name argument"),
};
@ -185,7 +190,7 @@ fn update(arg_matches: &ArgMatches) -> Result<()> {
fn activate(arg_matches: &ArgMatches) -> Result<()> {
let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.get_one::<String>("image").unwrap();
let img_arg = arg_matches.value_of("image").unwrap();
if img.is_activated() {
info!("RealmFS image {} is already activated", img_arg);
@ -198,7 +203,7 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> {
fn deactivate(arg_matches: &ArgMatches) -> Result<()> {
let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.get_one::<String>("image").unwrap();
let img_arg = arg_matches.value_of("image").unwrap();
if !img.is_activated() {
info!("RealmFS image {} is not activated", img_arg);
} else if img.is_in_use() {

View File

@ -8,7 +8,6 @@ use crate::sync::parser::DesktopFileParser;
use std::fs::DirEntry;
use crate::sync::desktop_file::DesktopFile;
use crate::sync::icons::IconSync;
use crate::sync::REALM_BASE_PATHS;
/// Synchronize dot-desktop files from active realm to a target directory in Citadel.
pub struct DesktopFileSync {
@ -74,11 +73,8 @@ impl DesktopFileSync {
pub fn run_sync(&mut self, clear: bool) -> Result<()> {
IconSync::ensure_theme_index_exists()?;
for &base_path in REALM_BASE_PATHS {
self.collect_source_files(base_path)?;
}
self.collect_source_files("rootfs/usr/share/applications")?;
self.collect_source_files("home/.local/share/applications")?;
let target = Path::new(Self::CITADEL_APPLICATIONS);
@ -93,14 +89,12 @@ impl DesktopFileSync {
self.synchronize_items()?;
if let Some(ref icons) = self.icons {
icons.write_known_cache()?;
IconSync::update_mtime()?;
}
Ok(())
}
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<()> {
let mut directory = self.realm.run_path().join(directory.as_ref());
directory.push("share/applications");
let directory = Realms::current_realm_symlink().join(directory.as_ref());
if directory.exists() {
util::read_directory(&directory, |dent| {
self.process_source_entry(dent);
@ -126,11 +120,7 @@ impl DesktopFileSync {
}
fn remove_missing_target_files(&mut self) -> Result<()> {
let mut sources = self.source_filenames();
// If flatpak is enabled, don't remove the generated GNOME Software desktop file
if self.realm.config().flatpak() {
sources.insert(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
}
let sources = self.source_filenames();
let prefix = format!("realm-{}.", self.realm.name());
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
if let Some(filename) = dent.file_name().to_str() {
@ -186,11 +176,17 @@ impl DesktopFileSync {
fn sync_item(&self, item: &DesktopItem) -> Result<()> {
let mut dfp = DesktopFileParser::parse_from_path(&item.path, "/usr/libexec/citadel-run ")?;
// When use-flatpak is enabled a gnome-software desktop file will be generated
let flatpak_gs_hide = dfp.filename() == "org.gnome.Software.desktop" && self.realm.config().flatpak();
if dfp.is_showable() && !flatpak_gs_hide {
if dfp.is_showable() {
self.sync_item_icon(&mut dfp);
dfp.write_to_dir(Self::CITADEL_APPLICATIONS, Some(&self.realm))?;
/*
if let Some(icon_name)= dfp.icon() {
if let Some(ref icons) = self.icons {
icons.sync_icon(icon_name)?;
}
}
*/
} else {
debug!("Ignoring desktop file {} as not showable", dfp.filename());
}

View File

@ -5,10 +5,9 @@ use std::path::{Path, PathBuf};
use libcitadel::{Result, util, Realm};
use std::cell::{RefCell, Cell};
use crate::sync::desktop_file::DesktopFile;
use crate::sync::REALM_BASE_PATHS;
pub struct IconSync {
realm: Realm,
realm_base: PathBuf,
cache: IconCache,
known: RefCell<HashSet<String>>,
known_changed: Cell<bool>,
@ -16,94 +15,53 @@ pub struct IconSync {
impl IconSync {
const CITADEL_ICONS: &'static str = "/home/citadel/.local/share/icons";
const HICOLOR_THEME_INDEX: &'static str = "/usr/share/icons/hicolor/index.theme";
const KNOWN_ICONS_FILE: &'static str = "/home/citadel/.local/share/icons/known.cache";
const PAPER_ICON_CACHE: &'static str = "/usr/share/icons/Paper/icon-theme.cache";
const HOME_PATH_PREFIX: &'static str = "/home/user/";
pub fn ensure_theme_index_exists() -> Result<()> {
let target = Path::new(Self::CITADEL_ICONS).join("hicolor").join("index.theme");
let parent_dir = target.parent().unwrap();
if !parent_dir.exists() {
util::create_dir(parent_dir)?;
}
if !target.exists() {
util::copy_file(Self::HICOLOR_THEME_INDEX, &target)?;
}
Ok(())
}
pub fn update_mtime() -> Result<()> {
let target = Path::new(Self::CITADEL_ICONS).join("hicolor");
util::touch_mtime(&target)?;
Ok(())
}
pub fn new(realm: &Realm) -> Result<Self> {
let realm = realm.clone();
let realm_base= realm.base_path();
let cache = IconCache::open(Self::PAPER_ICON_CACHE)?;
let known = Self::read_known_cache()?;
let known = RefCell::new(known);
let known_changed = Cell::new(false);
Ok(IconSync { realm, cache, known, known_changed })
Ok(IconSync { realm_base, cache, known, known_changed })
}
fn realm_icon_path(&self, icon_path: &Path, prefix: &str, citadel_base_path: &Path) -> Result<PathBuf> {
let suffix = icon_path.strip_prefix(prefix)
.map_err(context!("Failed to strip prefix {} from icon path {}", prefix, icon_path.display()))?;
let base_path = citadel_base_path.canonicalize()
.map_err(context!("Failed to canonicalize base path {}", citadel_base_path.display()))?;
let joined = base_path.join(suffix);
let icon_path = joined.canonicalize()
.map_err(context!("Failed to canonicalize icon path {}", joined.display()))?;
if !icon_path.starts_with(&base_path) {
bail!("Icon path {} should start with realm base path {}", icon_path.display(), citadel_base_path.display());
}
Ok(icon_path)
}
fn sync_icon_filepath(&self, file: &mut DesktopFile, path: &Path) -> Result<()> {
let icon_path = if path.starts_with(Self::HOME_PATH_PREFIX) {
let realm_home = self.realm.base_path().join("home");
self.realm_icon_path(path, Self::HOME_PATH_PREFIX, &realm_home)?
} else {
let realm_root = self.realm.run_path().join("rootfs");
self.realm_icon_path(path, "/", &realm_root)?
};
let icon_path = path.canonicalize()
.map_err(context!("Failed to canonicalize icon path {}", path.display()))?;
if !icon_path.is_file() {
bail!("Failed to find icon file {}", icon_path.display());
let icon_path = icon_path.strip_prefix("/")
.map_err(context!("Failed to strip initial / from {}", icon_path.display()))?;
let realm_path = self.realm_base.join(icon_path);
if !realm_path.is_file() {
bail!("Failed to find icon file {}", realm_path.display());
}
let dir = Path::new(Self::CITADEL_ICONS).join("filepaths-icons");
let target = dir.join(icon_path.file_name()
let target = dir.join(realm_path.file_name()
.expect("Icon has no filename?"));
if !target.exists() {
util::create_dir(&dir)?;
util::copy_file(&icon_path, &target)?;
util::copy_file(&realm_path, &target)?;
util::chmod(&target, 0o644)?;
info!("Copied icon from {} to {}", icon_path.display(), target.display());
}
let target_str = target.display().to_string();
file.update_icon(&target_str);
info!("Copied icon from {} to {}", icon_path.display(), target_str);
Ok(())
}
pub fn sync_icon(&self, file: &mut DesktopFile, icon_name: &str) -> Result<()> {
debug!("sync_icon({})", icon_name);
if icon_name.starts_with("/") {
return self.sync_icon_filepath(file, Path::new(icon_name));
}
if self.is_known(icon_name) {
debug!("({}) is known", icon_name);
return Ok(())
}
if self.cache.find_image(icon_name)? {
@ -112,12 +70,9 @@ impl IconSync {
return Ok(());
}
for &base_path in REALM_BASE_PATHS {
if self.search(base_path, icon_name)? {
return Ok(())
if !self.search("rootfs/usr/share/icons/hicolor", icon_name)? {
self.search("home/.local/share/icons/hicolor", icon_name)?;
}
}
debug!("not found: {} ", icon_name);
Ok(())
}
@ -153,14 +108,10 @@ impl IconSync {
}
fn search(&self, subdir: impl AsRef<Path>, icon_name: &str) -> Result<bool> {
let mut base = self.realm.run_path().join(subdir.as_ref());
base.push("share/icons/hicolor");
let base = self.realm_base.join(subdir.as_ref());
if !base.exists() {
debug!("Does not exist: {:?} for {}", base, icon_name);
return Ok(false)
}
debug!("Searching {:?} for {}", base, icon_name);
let mut found = false;
util::read_directory(&base, |dent| {
let apps = dent.path().join("apps");

View File

@ -8,31 +8,20 @@ mod icon_cache;
use self::desktop_sync::DesktopFileSync;
fn has_arg(args: &[String], arg: &str) -> bool {
args.iter().any(|s| s.as_str() == arg)
fn has_first_arg(args: &[String], arg: &str) -> bool {
args.len() > 1 && args[1].as_str() == arg
}
pub const REALM_BASE_PATHS:&[&str] = &[
"rootfs/usr",
"flatpak/exports",
"home/.local",
"home/.local/share/flatpak/exports"
];
pub fn main(args: Vec<String>) {
if has_arg(&args, "-v") {
Logger::set_log_level(LogLevel::Debug);
} else {
Logger::set_log_level(LogLevel::Info);
}
if has_arg(&args, "--all") {
if has_first_arg(&args, "--all") {
if let Err(e) = DesktopFileSync::sync_active_realms() {
println!("Sync all active realms failed: {}", e);
}
} else {
let clear = has_arg(&args, "--clear");
let clear = has_first_arg(&args, "--clear");
if let Err(e) = sync(clear) {
println!("Desktop file sync failed: {}", e);
}

View File

@ -0,0 +1,30 @@
### Old Sync
#### citadel-current-watcher.path
[Path]
PathChanged=/run/citadel/realms/current
#### citadel-current-watcher.service
[Service]
Type=oneshot
ExecStart=/usr/libexec/citadel-desktop-sync --clear
ExecStart=/usr/bin/systemctl restart citadel-desktop-watcher.path
#### citadel-desktop-watcher.path
[Path]
PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications
#### citadel-desktop-watcher.service
[Service]
Type=oneshot
ExecStart=/usr/libexec/citadel-desktop-sync
### New Sync
* Added a new command line option `--all` for syncronizing all active realms

View File

@ -9,10 +9,10 @@ lazy_static! {
static ref KEY_WHITELIST: HashSet<&'static str> = [
"Type", "Version", "Name", "GenericName", "NoDisplay", "Comment", "Icon", "Hidden",
"OnlyShowIn", "NotShowIn", "Path", "Terminal", "Actions", "MimeType",
"Categories", "Keywords", "StartupNotify", "StartupWMClass", "URL", "DocPath", "SingleMainWindow",
"Categories", "Keywords", "StartupNotify", "StartupWMClass", "URL", "DocPath",
"X-GNOME-FullName", "X-GNOME-Provides", "X-Desktop-File-Install-Version", "X-GNOME-UsesNotifications",
"X-GNOME-DocPath", "X-Geoclue-Reason", "X-GNOME-SingleWindow", "X-GNOME-Gettext-Domain",
"X-MultipleArgs", "X-Flatpak", "X-Flatpak-Tags", "X-SingleMainWindow",
"X-MultipleArgs",
].iter().cloned().collect();
// These are keys which are recognized but deliberately ignored.
@ -23,7 +23,7 @@ lazy_static! {
"X-GNOME-Bugzilla-ExtraInfoScript", "X-GNOME-Bugzilla-OtherBinaries", "X-GNOME-Autostart-enabled",
"X-AppInstall-Package", "X-KDE-SubstituteUID", "X-Ubuntu-Gettext-Domain", "X-AppInstall-Keywords",
"X-Ayatana-Desktop-Shortcuts", "X-GNOME-Settings-Panel", "X-GNOME-WMSettingsModule", "X-GNOME-WMName",
"X-GnomeWMSettingsLibrary", "X-Purism-FormFactor", "X-ExecArg",
"X-GnomeWMSettingsLibrary",
].iter().cloned().collect();
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="com.subgraph.citadel" path="/com/subgraph/citadel/">
<key name="label-color-list" type="as">
<key name="frame-color-list" type="as">
<default>[
'rgb(153,193,241)',
'rgb(143,240,164)',
@ -14,7 +14,7 @@
<summary />
</key>
<key name="realm-label-colors" type="as">
<key name="realm-frame-colors" type="as">
<default>['main:rgb(153,193,241)']</default>
</key>

View File

@ -7,12 +7,10 @@
<busconfig>
<policy user="root">
<allow own="com.subgraph.realms"/>
<allow own="com.subgraph.Realms2"/>
</policy>
<policy context="default">
<allow send_destination="com.subgraph.realms"/>
<allow send_destination="com.subgraph.Realms2"/>
<allow send_destination="com.subgraph.realms"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="com.subgraph.realms"

View File

@ -1,8 +0,0 @@
[package]
name = "launch-gnome-software"
version = "0.1.0"
edition = "2021"
[dependencies]
libcitadel = { path = "../libcitadel" }
anyhow = "1.0"

View File

@ -1,67 +0,0 @@
use std::env;
use libcitadel::{Logger, LogLevel, Realm, Realms, util};
use libcitadel::flatpak::GnomeSoftwareLauncher;
use anyhow::{bail, Result};
fn realm_arg() -> Option<String> {
let mut args = env::args();
while let Some(arg) = args.next() {
if arg == "--realm" {
return args.next();
}
}
None
}
fn choose_realm() -> Result<Realm> {
let mut realms = Realms::load()?;
if let Some(realm_name) = realm_arg() {
match realms.by_name(&realm_name) {
None => bail!("realm '{}' not found", realm_name),
Some(realm) => return Ok(realm),
}
}
let realm = match realms.current() {
Some(realm) => realm,
None => bail!("no current realm"),
};
Ok(realm)
}
fn has_arg(arg: &str) -> bool {
env::args()
.skip(1)
.any(|s| s == arg)
}
fn launch() -> Result<()> {
let realm = choose_realm()?;
if !util::is_euid_root() {
bail!("Must be run with root euid");
}
let mut launcher = GnomeSoftwareLauncher::new(realm)?;
if has_arg("--quit") {
launcher.quit()?;
} else {
if has_arg("--shell") {
launcher.set_run_shell();
}
launcher.launch()?;
}
Ok(())
}
fn main() {
if has_arg("--verbose") {
Logger::set_log_level(LogLevel::Verbose);
}
if let Err(err) = launch() {
eprintln!("Error: {}", err);
}
}

View File

@ -10,7 +10,6 @@ nix = "0.17.0"
toml = "0.5"
serde = "1.0"
serde_derive = "1.0"
serde_json = "=1.0.1"
lazy_static = "1.4"
sodiumoxide = "0.2"
hex = "0.4"

View File

@ -1,221 +0,0 @@
use std::ffi::OsStr;
use std::{fs, io};
use std::fs::File;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::Command;
use crate::{Logger, LogLevel, Result, verbose};
const BWRAP_PATH: &str = "/usr/libexec/flatpak-bwrap";
pub struct BubbleWrap {
command: Command,
}
impl BubbleWrap {
pub fn new() -> Self {
BubbleWrap {
command: Command::new(BWRAP_PATH),
}
}
fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.command.arg(arg);
self
}
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
for arg in args {
self.add_arg(arg.as_ref());
}
self
}
pub fn ro_bind(&mut self, path_list: &[&str]) -> &mut Self {
for &path in path_list {
self.add_args(&["--ro-bind", path, path]);
}
self
}
pub fn ro_bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
self.add_args(&["--ro-bind", src, dest])
}
pub fn bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
self.add_args(&["--bind", src, dest])
}
pub fn create_dirs(&mut self, dir_list: &[&str]) -> &mut Self {
for &dir in dir_list {
self.add_args(&["--dir", dir]);
}
self
}
pub fn create_symlinks(&mut self, links: &[(&str, &str)]) -> &mut Self {
for (src,dest) in links {
self.add_args(&["--symlink", src, dest]);
}
self
}
pub fn mount_dev(&mut self) -> &mut Self {
self.add_args(&["--dev", "/dev"])
}
pub fn mount_proc(&mut self) -> &mut Self {
self.add_args(&["--proc", "/proc"])
}
pub fn dev_bind(&mut self, path: &str) -> &mut Self {
self.add_args(&["--dev-bind", path, path])
}
pub fn clear_env(&mut self) -> &mut Self {
self.add_arg("--clearenv")
}
pub fn set_env_list(&mut self, env_list: &[&str]) -> &mut Self {
for line in env_list {
if let Some((k,v)) = line.split_once("=") {
self.add_args(&["--setenv", k, v]);
} else {
eprintln!("Warning: environment variable '{}' does not have = delimiter. Ignoring", line);
}
}
self
}
pub fn unshare_all(&mut self) -> &mut Self {
self.add_arg("--unshare-all")
}
pub fn share_net(&mut self) -> &mut Self {
self.add_arg("--share-net")
}
pub fn log_command(&self) {
let mut buffer = String::new();
verbose!("{}", BWRAP_PATH);
for arg in self.command.get_args() {
if let Some(s) = arg.to_str() {
if s.starts_with("-") {
if !buffer.is_empty() {
verbose!(" {}", buffer);
buffer.clear();
}
}
if !buffer.is_empty() {
buffer.push(' ');
}
buffer.push_str(s);
}
}
if !buffer.is_empty() {
verbose!(" {}", buffer);
}
}
pub fn status_file(&mut self, status_file: &File) -> &mut Self {
// Rust sets O_CLOEXEC when opening files so we create
// a new descriptor that will remain open across exec()
let dup_fd = unsafe {
libc::dup(status_file.as_raw_fd())
};
if dup_fd == -1 {
warn!("Failed to dup() status file descriptor: {}", io::Error::last_os_error());
warn!("Skipping --json-status-fd argument");
self
} else {
self.add_arg("--json-status-fd")
.add_arg(dup_fd.to_string())
}
}
pub fn launch<S: AsRef<OsStr>>(&mut self, cmd: &[S]) -> Result<()> {
if Logger::is_log_level(LogLevel::Verbose) {
self.log_command();
let s = cmd.iter().map(|s| format!("{} ", s.as_ref().to_str().unwrap())).collect::<String>();
verbose!(" {}", s)
}
self.add_args(cmd);
let err = self.command.exec();
bail!("failed to exec bubblewrap: {}", err);
}
}
#[derive(Deserialize,Clone)]
#[serde(rename_all="kebab-case")]
pub struct BubbleWrapRunningStatus {
pub child_pid: u64,
pub cgroup_namespace: u64,
pub ipc_namespace: u64,
pub mnt_namespace: u64,
pub pid_namespace: u64,
pub uts_namespace: u64,
}
#[derive(Deserialize,Clone)]
#[serde(rename_all="kebab-case")]
pub struct BubbleWrapExitStatus {
pub exit_code: u32,
}
#[derive(Clone)]
pub struct BubbleWrapStatus {
running: BubbleWrapRunningStatus,
exit: Option<BubbleWrapExitStatus>,
}
impl BubbleWrapStatus {
pub fn parse_file(path: impl AsRef<Path>) -> Result<Option<Self>> {
if !path.as_ref().exists() {
return Ok(None)
}
let s = fs::read_to_string(path)
.map_err(context!("error reading status file"))?;
let mut lines = s.lines();
let running = match lines.next() {
None => return Ok(None),
Some(s) => serde_json::from_str::<BubbleWrapRunningStatus>(s)
.map_err(context!("failed to parse status line ({})", s))?
};
let exit = match lines.next() {
None => None,
Some(s) => Some(serde_json::from_str::<BubbleWrapExitStatus>(s)
.map_err(context!("failed to parse exit line ({})", s))?)
};
Ok(Some(BubbleWrapStatus {
running,
exit
}))
}
pub fn is_running(&self) -> bool {
self.exit.is_none()
}
pub fn running_status(&self) -> &BubbleWrapRunningStatus {
&self.running
}
pub fn child_pid(&self) -> u64 {
self.running.child_pid
}
pub fn pid_namespace(&self) -> u64 {
self.running.pid_namespace
}
pub fn exit_status(&self) -> Option<&BubbleWrapExitStatus> {
self.exit.as_ref()
}
}

View File

@ -1,259 +0,0 @@
use std::{fs, io};
use std::fs::File;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::prelude::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::{Realm, Result, util};
use crate::flatpak::{BubbleWrap, BubbleWrapStatus, SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
use crate::flatpak::netns::NetNS;
const FLATPAK_PATH: &str = "/usr/bin/flatpak";
const ENVIRONMENT: &[&str; 7] = &[
"HOME=/home/citadel",
"USER=citadel",
"XDG_RUNTIME_DIR=/run/user/1000",
"XDG_DATA_DIRS=/home/citadel/.local/share/flatpak/exports/share:/usr/share",
"TERM=xterm-256color",
"GTK_A11Y=none",
"FLATPAK_USER_DIR=/home/citadel/realm-flatpak",
];
const FLATHUB_URL: &str = "https://dl.flathub.org/repo/flathub.flatpakrepo";
pub struct GnomeSoftwareLauncher {
realm: Realm,
status: Option<BubbleWrapStatus>,
netns: NetNS,
run_shell: bool,
}
impl GnomeSoftwareLauncher {
pub fn new(realm: Realm) -> Result<Self> {
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
let status = sandbox_status.realm_status(&realm);
let netns = NetNS::new(NetNS::GS_NETNS_NAME);
Ok(GnomeSoftwareLauncher {
realm,
status,
netns,
run_shell: false,
})
}
pub fn set_run_shell(&mut self) {
self.run_shell = true;
}
fn ensure_flatpak_dir(&self) -> Result<()> {
let flatpak_user_dir = self.realm.base_path_file("flatpak");
if !flatpak_user_dir.exists() {
if let Err(err) = fs::create_dir(&flatpak_user_dir) {
bail!("failed to create realm flatpak directory ({}): {}", flatpak_user_dir.display(), err);
}
util::chown_user(&flatpak_user_dir)?;
}
Ok(())
}
fn add_flathub(&self) -> Result<()> {
let flatpak_user_dir = self.realm.base_path_file("flatpak");
match Command::new(FLATPAK_PATH)
.env("FLATPAK_USER_DIR", flatpak_user_dir)
.arg("remote-add")
.arg("--user")
.arg("--if-not-exists")
.arg("flathub")
.arg(FLATHUB_URL)
.status() {
Ok(status) => {
if status.success() {
Ok(())
} else {
bail!("failed to add flathub repo")
}
},
Err(err) => bail!("error running flatpak command: {}", err),
}
}
fn scan_tmp_directory(path: &Path) -> io::Result<Option<String>> {
for entry in fs::read_dir(&path)? {
let entry = entry?;
if entry.file_type()?.is_socket() {
if let Some(filename) = entry.path().file_name() {
if let Some(filename) = filename.to_str() {
if filename.starts_with("dbus-") {
return Ok(Some(format!("/tmp/{}", filename)));
}
}
}
}
}
Ok(None)
}
fn find_dbus_socket(&self) -> Result<String> {
let pid = self.running_pid()?;
let tmp_dir = PathBuf::from(format!("/proc/{}/root/tmp", pid));
if !tmp_dir.is_dir() {
bail!("no /tmp directory found for process pid={}", pid);
}
if let Some(s) = Self::scan_tmp_directory(&tmp_dir)
.map_err(context!("error reading directory {}", tmp_dir.display()))? {
Ok(s)
} else {
bail!("no dbus socket found in /tmp directory for process pid={}", pid);
}
}
fn launch_sandbox(&self, status_file: &File) -> Result<()> {
self.ensure_flatpak_dir()?;
if let Err(err) = self.netns.nsenter() {
bail!("Failed to enter 'gnome-software' network namespace: {}", err);
}
verbose!("Entered network namespace ({})", NetNS::GS_NETNS_NAME);
if let Err(err) = util::drop_privileges(1000, 1000) {
bail!("Failed to drop privileges to uid = gid = 1000: {}", err);
}
verbose!("Dropped privileges (uid=1000, gid=1000)");
self.add_flathub()?;
let flatpak_user_dir = self.realm.base_path_file("flatpak");
let flatpak_user_dir = flatpak_user_dir.to_str().unwrap();
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
verbose!("Running command in sandbox: {}", cmd);
BubbleWrap::new()
.ro_bind(&[
"/usr/bin",
"/usr/lib",
"/usr/libexec",
"/usr/share/dbus-1",
"/usr/share/icons",
"/usr/share/mime",
"/usr/share/X11",
"/usr/share/glib-2.0",
"/usr/share/xml",
"/usr/share/drirc.d",
"/usr/share/fontconfig",
"/usr/share/fonts",
"/usr/share/zoneinfo",
"/usr/share/swcatalog",
"/etc/passwd",
"/etc/machine-id",
"/etc/nsswitch.conf",
"/etc/fonts",
"/etc/ssl",
"/sys/dev/char", "/sys/devices",
"/run/user/1000/wayland-0",
])
.ro_bind_to("/run/NetworkManager/resolv.conf", "/etc/resolv.conf")
.bind_to(flatpak_user_dir, "/home/citadel/realm-flatpak")
.create_symlinks(&[
("usr/lib", "/lib64"),
("usr/bin", "/bin"),
("/tmp", "/var/tmp"),
])
.create_dirs(&[
"/var/lib/flatpak",
"/home/citadel",
"/tmp",
"/sys/block", "/sys/bus", "/sys/class",
])
.mount_dev()
.dev_bind("/dev/dri")
.mount_proc()
.unshare_all()
.share_net()
.clear_env()
.set_env_list(ENVIRONMENT)
.status_file(status_file)
.launch(&["dbus-run-session", "--", cmd])?;
Ok(())
}
pub fn new_realm_status_file(&self) -> Result<File> {
let path = Path::new(SANDBOX_STATUS_FILE_DIRECTORY).join(self.realm.name());
File::create(&path)
.map_err(context!("failed to open sandbox status file {}", path.display()))
}
pub fn launch(&self) -> Result<()> {
self.netns.ensure_exists()?;
if self.is_running() {
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
self.launch_in_running_sandbox(&[cmd])?;
} else {
let status_file = self.new_realm_status_file()?;
self.ensure_flatpak_dir()?;
self.launch_sandbox(&status_file)?;
}
Ok(())
}
pub fn quit(&self) -> Result<()> {
if self.is_running() {
self.launch_in_running_sandbox(&["/usr/bin/gnome-software", "--quit"])?;
} else {
warn!("No running sandbox found for realm {}", self.realm.name());
}
Ok(())
}
pub fn is_running(&self) -> bool {
self.status.as_ref()
.map(|s| s.is_running())
.unwrap_or(false)
}
fn running_pid(&self) -> Result<u64> {
self.status.as_ref()
.map(|s| s.child_pid())
.ok_or(format_err!("no sandbox status available for realm '{}',", self.realm.name()))
}
fn dbus_session_address(&self) -> Result<String> {
let dbus_socket = Self::find_dbus_socket(&self)?;
Ok(format!("unix:path={}", dbus_socket))
}
fn launch_in_running_sandbox(&self, command: &[&str]) -> Result<()> {
let dbus_address = self.dbus_session_address()?;
let pid = self.running_pid()?.to_string();
let mut env = ENVIRONMENT.iter()
.map(|s| s.split_once('=').unwrap())
.collect::<Vec<_>>();
env.push(("DBUS_SESSION_BUS_ADDRESS", dbus_address.as_str()));
let err = Command::new("/usr/bin/nsenter")
.env_clear()
.envs( env )
.args(&[
"--all",
"--target", pid.as_str(),
"--setuid", "1000",
"--setgid", "1000",
])
.args(command)
.exec();
Err(format_err!("failed to execute nsenter: {}", err))
}
}

View File

@ -1,16 +0,0 @@
pub(crate) mod setup;
pub(crate) mod status;
pub(crate) mod bubblewrap;
pub(crate) mod launcher;
pub(crate) mod netns;
pub use status::SandboxStatus;
pub use bubblewrap::{BubbleWrap,BubbleWrapStatus,BubbleWrapRunningStatus,BubbleWrapExitStatus};
pub use launcher::GnomeSoftwareLauncher;
pub const SANDBOX_STATUS_FILE_DIRECTORY: &str = "/run/citadel/realms/gs-sandbox-status";

View File

@ -1,132 +0,0 @@
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
use crate::{util,Result};
const BRIDGE_NAME: &str = "vz-clear";
const VETH0: &str = "gs-veth0";
const VETH1: &str = "gs-veth1";
const IP_ADDRESS: &str = "172.17.0.222/24";
const GW_ADDRESS: &str = "172.17.0.1";
pub struct NetNS {
name: String,
}
impl NetNS {
pub const GS_NETNS_NAME: &'static str = "gnome-software";
pub fn new(name: &str) -> Self {
NetNS {
name: name.to_string(),
}
}
fn create(&self) -> crate::Result<()> {
Ip::new().link_add_veth(VETH0, VETH1).run()?;
Ip::new().link_set_netns(VETH0, &self.name).run()?;
Ip::new().link_set_master(VETH1, BRIDGE_NAME).run()?;
Ip::new().link_set_dev_up(VETH1).run()?;
Ip::new().ip_netns_exec_ip(&self.name).addr_add(IP_ADDRESS, VETH0).run()?;
Ip::new().ip_netns_exec_ip(&self.name).link_set_dev_up(VETH0).run()?;
Ip::new().ip_netns_exec_ip(&self.name).route_add_default(GW_ADDRESS).run()?;
Ok(())
}
pub fn ensure_exists(&self) -> Result<()> {
if Path::new(&format!("/run/netns/{}", self.name)).exists() {
verbose!("Network namespace ({}) exists", self.name);
return Ok(())
}
verbose!("Setting up network namespace ({})", self.name);
Ip::new().netns_add(&self.name).run()
.map_err(context!("Failed to add network namespace '{}'", self.name))?;
if let Err(err) = self.create() {
Ip::new().netns_delete(&self.name).run()?;
Err(err)
} else {
Ok(())
}
}
pub fn nsenter(&self) -> Result<()> {
util::nsenter_netns(&self.name)
}
}
const IP_PATH: &str = "/usr/sbin/ip";
struct Ip {
command: Command,
}
impl Ip {
fn new() -> Self {
let mut command = Command::new(IP_PATH);
command.env_clear();
Ip { command }
}
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
for arg in args {
self.command.arg(arg);
}
self
}
pub fn netns_add(&mut self, name: &str) -> &mut Self {
self.add_args(&["netns", "add", name])
}
pub fn netns_delete(&mut self, name: &str) -> &mut Self {
self.add_args(&["netns", "delete", name])
}
pub fn link_add_veth(&mut self, name: &str, peer_name: &str) -> &mut Self {
self.add_args(&["link", "add", name, "type", "veth", "peer", "name", peer_name])
}
pub fn link_set_netns(&mut self, iface: &str, netns_name: &str) -> &mut Self {
self.add_args(&["link", "set", iface, "netns", netns_name])
}
pub fn link_set_master(&mut self, iface: &str, bridge_name: &str) -> &mut Self {
self.add_args(&["link", "set", iface, "master", bridge_name])
}
pub fn link_set_dev_up(&mut self, iface: &str) -> &mut Self {
self.add_args(&["link", "set", "dev", iface, "up"])
}
pub fn ip_netns_exec_ip(&mut self, netns_name: &str) -> &mut Self {
self.add_args(&["netns", "exec", netns_name, IP_PATH])
}
pub fn addr_add(&mut self, ip_address: &str, dev: &str) -> &mut Self {
self.add_args(&["addr", "add", ip_address, "dev", dev])
}
pub fn route_add_default(&mut self, gateway: &str) -> &mut Self {
self.add_args(&["route", "add", "default", "via", gateway])
}
fn run(&mut self) -> crate::Result<()> {
verbose!("{:?}", self.command);
match self.command.status() {
Ok(status) => {
if status.success() {
Ok(())
} else {
bail!("IP command ({:?}) did not succeeed.", self.command);
}
}
Err(err) => {
bail!("error running ip command ({:?}): {}", self.command, err);
}
}
}
}

View File

@ -1,58 +0,0 @@
use std::path::Path;
use crate::{Realm, Result, util};
const GNOME_SOFTWARE_DESKTOP_TEMPLATE: &str = "\
[Desktop Entry]
Name=Software
Comment=Add, remove or update software on this computer
Icon=org.gnome.Software
Exec=/usr/libexec/launch-gnome-software --realm $REALM_NAME
Terminal=false
Type=Application
Categories=GNOME;GTK;System;PackageManager;
Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store;
StartupNotify=true
";
const APPLICATION_DIRECTORY: &str = "/home/citadel/.local/share/applications";
pub struct FlatpakSetup<'a> {
realm: &'a Realm,
}
impl <'a> FlatpakSetup<'a> {
pub fn new(realm: &'a Realm) -> Self {
Self { realm }
}
pub fn setup(&self) -> Result<()> {
self.write_desktop_file()?;
self.ensure_flatpak_directory()?;
Ok(())
}
fn write_desktop_file(&self) -> Result<()> {
let appdir = Path::new(APPLICATION_DIRECTORY);
if !appdir.exists() {
util::create_dir(appdir)?;
if let Some(parent) = appdir.parent().and_then(|p| p.parent()) {
util::chown_tree(parent, (1000,1000), true)?;
}
}
let path = appdir.join(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
util::write_file(path, GNOME_SOFTWARE_DESKTOP_TEMPLATE.replace("$REALM_NAME", self.realm.name()))?;
Ok(())
}
fn ensure_flatpak_directory(&self) -> Result<()> {
let path = self.realm.base_path_file("flatpak");
if !path.exists() {
util::create_dir(&path)?;
util::chown_user(&path)?;
}
Ok(())
}
}

View File

@ -1,144 +0,0 @@
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::flatpak::bubblewrap::BubbleWrapStatus;
use crate::{Realm, Result, util};
/// Utility function to read modified time from a path.
fn modified_time(path: &Path) -> Result<SystemTime> {
path.metadata().and_then(|meta| meta.modified())
.map_err(context!("failed to read modified time from '{}'", path.display()))
}
/// Utility function to detect if current modified time of a path
/// matches an earlier recorded modified time.
fn modified_changed(path: &Path, old_modified: SystemTime) -> bool {
if !path.exists() {
// Path existed at some earlier point, so something changed
return true;
}
match modified_time(path) {
Ok(modified) => old_modified != modified,
Err(err) => {
// Print a warning but assume change
warn!("{}", err);
true
},
}
}
/// Records the content of single entry in a sandbox status directory.
///
/// The path to the status file as well as the last modified time are
/// recorded so that changes in status of a sandbox can be detected.
struct StatusEntry {
status: BubbleWrapStatus,
path: PathBuf,
modified: SystemTime,
}
impl StatusEntry {
fn load_timestamp_and_status(path: &Path) -> Result<Option<(SystemTime, BubbleWrapStatus)>> {
if path.exists() {
let modified = modified_time(path)?;
if let Some(status) = BubbleWrapStatus::parse_file(path)? {
return Ok(Some((modified, status)));
}
}
Ok(None)
}
fn load(base_dir: &Path, name: &str) -> Result<Option<Self>> {
let path = base_dir.join(name);
let result = StatusEntry::load_timestamp_and_status(&path)?
.map(|(modified, status)| StatusEntry { status, path, modified });
Ok(result)
}
fn is_modified(&self) -> bool {
modified_changed(&self.path, self.modified)
}
}
/// Holds information about entries in a sandbox status directory.
///
/// Bubblewrap accepts a command line argument that asks for status
/// information to be written as a json structure to a file descriptor.
///
pub struct SandboxStatus {
base_dir: PathBuf,
base_modified: SystemTime,
entries: HashMap<String, StatusEntry>,
}
impl SandboxStatus {
pub fn need_reload(&self) -> bool {
if modified_changed(&self.base_dir, self.base_modified) {
return true;
}
self.entries.values().any(|entry| entry.is_modified())
}
fn process_dir_entry(&mut self, dir_entry: PathBuf) -> Result<()> {
fn realm_name_for_path(path: &Path) -> Option<&str> {
path.file_name()
.and_then(|name| name.to_str())
.filter(|name| Realm::is_valid_name(name))
}
if dir_entry.is_file() {
if let Some(name) = realm_name_for_path(&dir_entry) {
if let Some(entry) = StatusEntry::load(&self.base_dir, name)? {
self.entries.insert(name.to_string(), entry);
}
}
}
Ok(())
}
pub fn reload(&mut self) -> Result<()> {
self.entries.clear();
self.base_modified = modified_time(&self.base_dir)?;
let base_dir = self.base_dir.clone();
util::read_directory(&base_dir, |entry| {
self.process_dir_entry(entry.path())
})
}
fn new(base_dir: &Path) -> Result<Self> {
let base_dir = base_dir.to_owned();
let base_modified = modified_time(&base_dir)?;
Ok(SandboxStatus {
base_dir,
base_modified,
entries: HashMap::new(),
})
}
pub fn load(directory: impl AsRef<Path>) -> Result<SandboxStatus> {
let base_dir = directory.as_ref();
if !base_dir.exists() {
util::create_dir(base_dir)?;
}
let mut status = SandboxStatus::new(base_dir)?;
status.reload()?;
Ok(status)
}
pub fn realm_status(&self, realm: &Realm) -> Option<BubbleWrapStatus> {
self.entries.get(realm.name()).map(|entry| entry.status.clone())
}
pub fn new_realm_status_file(&self, realm: &Realm) -> Result<File> {
let path = self.base_dir.join(realm.name());
File::create(&path)
.map_err(context!("failed to open sandbox status file {}", path.display()))
}
}

View File

@ -21,8 +21,6 @@ mod realm;
pub mod terminal;
mod system;
pub mod flatpak;
pub use crate::config::OsRelease;
pub use crate::blockdev::BlockDev;
pub use crate::cmdline::CommandLine;

View File

@ -62,11 +62,6 @@ impl Logger {
logger.level = level;
}
pub fn is_log_level(level: LogLevel) -> bool {
let logger = LOGGER.lock().unwrap();
logger.level >= level
}
pub fn set_log_output(output: Box<dyn LogOutput>) {
let mut logger = LOGGER.lock().unwrap();
logger.output = output;

View File

@ -77,9 +77,6 @@ pub struct RealmConfig {
#[serde(rename="use-fuse")]
pub use_fuse: Option<bool>,
#[serde(rename="use-flatpak")]
pub use_flatpak: Option<bool>,
#[serde(rename="use-gpu")]
pub use_gpu: Option<bool>,
@ -204,7 +201,6 @@ impl RealmConfig {
wayland_socket: Some("wayland-0".to_string()),
use_kvm: Some(false),
use_fuse: Some(false),
use_flatpak: Some(false),
use_gpu: Some(false),
use_gpu_card0: Some(false),
use_network: Some(true),
@ -237,7 +233,6 @@ impl RealmConfig {
wayland_socket: None,
use_kvm: None,
use_fuse: None,
use_flatpak: None,
use_gpu: None,
use_gpu_card0: None,
use_network: None,
@ -272,14 +267,6 @@ impl RealmConfig {
self.bool_value(|c| c.use_fuse)
}
/// If `true` flatpak directory will be mounted into realm
/// and a desktop file will be created to launch gnome-software
///
pub fn flatpak(&self) -> bool {
self.bool_value(|c| c.use_flatpak)
}
/// If `true` render node device /dev/dri/renderD128 will be added to realm.
///
/// This enables hardware graphics acceleration in realm.

View File

@ -12,9 +12,7 @@ use dbus::{Connection, BusType, ConnectionItem, Message, Path};
use inotify::{Inotify, WatchMask, WatchDescriptor, Event};
pub enum RealmEvent {
Starting(Realm),
Started(Realm),
Stopping(Realm),
Stopped(Realm),
New(Realm),
Removed(Realm),
@ -24,9 +22,7 @@ pub enum RealmEvent {
impl Display for RealmEvent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RealmEvent::Starting(ref realm) => write!(f, "RealmStarting({})", realm.name()),
RealmEvent::Started(ref realm) => write!(f, "RealmStarted({})", realm.name()),
RealmEvent::Stopping(ref realm) => write!(f, "RealmStopping({})", realm.name()),
RealmEvent::Stopped(ref realm) => write!(f, "RealmStopped({})", realm.name()),
RealmEvent::New(ref realm) => write!(f, "RealmNew({})", realm.name()),
RealmEvent::Removed(ref realm) => write!(f, "RealmRemoved({})", realm.name()),

View File

@ -60,7 +60,7 @@ impl <'a> RealmLauncher <'a> {
if config.kvm() {
self.add_device("/dev/kvm");
}
if config.fuse() || config.flatpak() {
if config.fuse() {
self.add_device("/dev/fuse");
}
@ -153,10 +153,6 @@ impl <'a> RealmLauncher <'a> {
writeln!(s, "BindReadOnly=/run/user/1000/{}:/run/user/host/wayland-0", config.wayland_socket())?;
}
if config.flatpak() {
writeln!(s, "BindReadOnly={}:/var/lib/flatpak", self.realm.base_path_file("flatpak").display())?;
}
for bind in config.extra_bindmounts() {
if Self::is_valid_bind_item(bind) {
writeln!(s, "Bind={}", bind)?;

View File

@ -4,8 +4,6 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use posix_acl::{ACL_EXECUTE, ACL_READ, PosixACL, Qualifier};
use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util};
use crate::flatpak::GnomeSoftwareLauncher;
use crate::flatpak::setup::FlatpakSetup;
use crate::realm::pidmapper::{PidLookupResult, PidMapper};
use crate::realmfs::realmfs_set::RealmFSSet;
@ -23,7 +21,6 @@ struct Inner {
events: RealmEventListener,
realms: Realms,
realmfs_set: RealmFSSet,
pid_mapper: PidMapper,
}
impl Inner {
@ -31,8 +28,7 @@ impl Inner {
let events = RealmEventListener::new();
let realms = Realms::load()?;
let realmfs_set = RealmFSSet::load()?;
let pid_mapper = PidMapper::new()?;
Ok(Inner { events, realms, realmfs_set, pid_mapper })
Ok(Inner { events, realms, realmfs_set })
}
}
@ -194,13 +190,7 @@ impl RealmManager {
return Ok(());
}
info!("Starting realm {}", realm.name());
self.inner().events.send_event(RealmEvent::Starting(realm.clone()));
if let Err(err) = self._start_realm(realm, &mut HashSet::new()) {
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
return Err(err);
}
self.inner().events.send_event(RealmEvent::Started(realm.clone()));
self._start_realm(realm, &mut HashSet::new())?;
if !Realms::is_some_realm_current() {
self.inner_mut().realms.set_realm_current(realm)
@ -240,10 +230,6 @@ impl RealmManager {
self.ensure_run_media_directory()?;
}
if realm.config().flatpak() {
FlatpakSetup::new(realm).setup()?;
}
self.systemd.start_realm(realm, &rootfs)?;
self.create_realm_namefile(realm)?;
@ -282,15 +268,6 @@ impl RealmManager {
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false)
}
fn stop_gnome_software_sandbox(&self, realm: &Realm) -> Result<()> {
let launcher = GnomeSoftwareLauncher::new(realm.clone())?;
if launcher.is_running() {
info!("Stopping GNOME Software sandbox for {}", realm.name());
launcher.quit()?;
}
Ok(())
}
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
if !realm.is_active() {
info!("ignoring stop request on realm '{}' which is not running", realm.name());
@ -298,21 +275,10 @@ impl RealmManager {
}
info!("Stopping realm {}", realm.name());
self.inner().events.send_event(RealmEvent::Stopping(realm.clone()));
if realm.config().flatpak() {
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
warn!("Error stopping GNOME Software sandbox: {}", err);
}
}
realm.set_active(false);
if let Err(err) = self.systemd.stop_realm(realm) {
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
return Err(err);
}
self.systemd.stop_realm(realm)?;
realm.cleanup_rootfs();
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
if realm.is_current() {
self.choose_some_current_realm();
@ -369,8 +335,8 @@ impl RealmManager {
}
pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult {
let realms = self.realm_list();
self.inner_mut().pid_mapper.lookup_pid(pid as libc::pid_t, realms)
let mapper = PidMapper::new(self.active_realms(false));
mapper.lookup_pid(pid as libc::pid_t)
}
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {

View File

@ -1,7 +1,6 @@
use std::ffi::OsStr;
use procfs::process::Process;
use crate::{Result, Realm};
use crate::flatpak::{SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
use crate::Realm;
pub enum PidLookupResult {
Unknown,
@ -10,15 +9,14 @@ pub enum PidLookupResult {
}
pub struct PidMapper {
sandbox_status: SandboxStatus,
active_realms: Vec<Realm>,
my_pid_ns_id: Option<u64>,
}
impl PidMapper {
pub fn new() -> Result<Self> {
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
pub fn new(active_realms: Vec<Realm>) -> Self {
let my_pid_ns_id = Self::self_pid_namespace_id();
Ok(PidMapper { sandbox_status, my_pid_ns_id })
PidMapper { active_realms, my_pid_ns_id }
}
fn read_process(pid: libc::pid_t) -> Option<Process> {
@ -74,30 +72,7 @@ impl PidMapper {
Self::read_process(ppid)
}
fn refresh_sandbox_status(&mut self) -> Result<()> {
if self.sandbox_status.need_reload() {
self.sandbox_status.reload()?;
}
Ok(())
}
fn search_sandbox_realms(&mut self, pid_ns: u64, realms: &[Realm]) -> Option<Realm> {
if let Err(err) = self.refresh_sandbox_status() {
warn!("error reloading sandbox status directory: {}", err);
return None;
}
for r in realms {
if let Some(status) = self.sandbox_status.realm_status(r) {
if status.pid_namespace() == pid_ns {
return Some(r.clone())
}
}
}
None
}
pub fn lookup_pid(&mut self, pid: libc::pid_t, realms: Vec<Realm>) -> PidLookupResult {
pub fn lookup_pid(&self, pid: libc::pid_t) -> PidLookupResult {
const MAX_PARENT_SEARCH: i32 = 8;
let mut n = 0;
@ -117,17 +92,13 @@ impl PidMapper {
return PidLookupResult::Citadel;
}
if let Some(realm) = realms.iter()
.find(|r| r.is_active() && r.has_pid_ns(pid_ns_id))
if let Some(realm) = self.active_realms.iter()
.find(|r| r.has_pid_ns(pid_ns_id))
.cloned()
{
return PidLookupResult::Realm(realm)
}
if let Some(r) = self.search_sandbox_realms(pid_ns_id, &realms) {
return PidLookupResult::Realm(r)
}
proc = match Self::parent_process(proc) {
Some(proc) => proc,
None => return PidLookupResult::Unknown,
@ -137,4 +108,5 @@ impl PidMapper {
}
PidLookupResult::Unknown
}
}

View File

@ -169,20 +169,6 @@ impl Realm {
self.inner.write().unwrap()
}
pub fn start(&self) -> Result<()> {
warn!("Realm({})::start()", self.name());
self.manager().start_realm(self)
}
pub fn stop(&self) -> Result<()> {
self.manager().stop_realm(self)
}
pub fn set_current(&self) -> Result<()> {
self.manager().set_current_realm(self)
}
pub fn is_active(&self) -> bool {
self.inner_mut().is_active()
}
@ -293,9 +279,6 @@ impl Realm {
symlink::write(&rootfs, self.rootfs_symlink(), false)?;
symlink::write(mountpoint.path(), self.realmfs_mountpoint_symlink(), false)?;
symlink::write(self.base_path().join("home"), self.run_path().join("home"), false)?;
if self.config().flatpak() {
symlink::write(self.base_path().join("flatpak"), self.run_path().join("flatpak"), false)?;
}
Ok(rootfs)
}
@ -317,9 +300,6 @@ impl Realm {
Self::remove_symlink(self.realmfs_mountpoint_symlink());
Self::remove_symlink(self.rootfs_symlink());
Self::remove_symlink(self.run_path().join("home"));
if self.config().flatpak() {
Self::remove_symlink(self.run_path().join("flatpak"));
}
if let Err(e) = fs::remove_dir(self.run_path()) {
warn!("failed to remove run directory {}: {}", self.run_path().display(), e);

View File

@ -31,28 +31,6 @@ impl Systemd {
if realm.config().ephemeral_home() {
self.setup_ephemeral_home(realm)?;
}
if realm.config().flatpak() {
self.setup_flatpak_workaround(realm)?;
}
Ok(())
}
// What even is this??
//
// Good question.
//
// https://bugzilla.redhat.com/show_bug.cgi?id=2210335#c10
//
fn setup_flatpak_workaround(&self, realm: &Realm) -> Result<()> {
let commands = &[
vec!["/usr/bin/mount", "-m", "-t","proc", "proc", "/run/flatpak-workaround/proc"],
vec!["/usr/bin/chmod", "700", "/run/flatpak-workaround"],
];
for cmd in commands {
Self::machinectl_shell(realm, cmd, "root", false, true)?;
}
Ok(())
}

View File

@ -23,8 +23,8 @@ impl ResizeSize {
pub fn gigs(n: usize) -> Self {
ResizeSize(BLOCKS_PER_GIG * n)
}
}
pub fn megs(n: usize) -> Self {
ResizeSize(BLOCKS_PER_MEG * n)
}
@ -45,8 +45,8 @@ impl ResizeSize {
self.0 / BLOCKS_PER_MEG
}
/// If the RealmFS has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new
/// size to resize the filesystem to and return it. Otherwise, return `None`
/// If the RealmFS needs to be resized to a larger size, returns the
/// recommended size.
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
let sb = match Superblock::load(realmfs.path(), 4096) {
Ok(sb) => sb,
@ -56,37 +56,22 @@ impl ResizeSize {
},
};
sb.free_block_count();
let free_blocks = sb.free_block_count() as usize;
if free_blocks >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
return None;
}
let metainfo_nblocks = realmfs.metainfo().nblocks();
if metainfo_nblocks >= AUTO_RESIZE_INCREASE_SIZE.nblocks() {
return Some(ResizeSize::blocks(metainfo_nblocks + AUTO_RESIZE_INCREASE_SIZE.nblocks()))
}
// If current size is under 4GB (AUTO_RESIZE_INCREASE_SIZE) and raising size to 4GB will create more than the
// minimum free space (1GB) then just do that.
if free_blocks + (AUTO_RESIZE_INCREASE_SIZE.nblocks() - metainfo_nblocks) >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
Some(AUTO_RESIZE_INCREASE_SIZE)
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
let mask = grow_size - 1;
let grow_blocks = (free_blocks + mask) & !mask;
Some(ResizeSize::blocks(grow_blocks))
} else {
// Otherwise for original size under 4GB, since raising to 4GB is not enough,
// raise size to 8GB
Some(ResizeSize::blocks(AUTO_RESIZE_INCREASE_SIZE.nblocks() * 2))
None
}
}
}
const SUPERBLOCK_SIZE: usize = 1024;
/// An EXT4 superblock structure.
///
/// A class for reading the first superblock from an EXT4 filesystem
/// and parsing the Free Block Count field. No other fields are parsed
/// since this is the only information needed for the resize operation.
///
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
impl Superblock {

View File

@ -76,6 +76,8 @@ impl <'a> Update<'a> {
}
self.realmfs.copy_image_file(self.target())?;
self.truncate_verity()?;
self.resize_image_file()?;
Ok(())
}
@ -113,8 +115,9 @@ impl <'a> Update<'a> {
}
// Return size of image file in blocks based on metainfo `nblocks` field.
// Include header block in count so add one block
fn metainfo_nblock_size(&self) -> usize {
self.realmfs.metainfo().nblocks()
self.realmfs.metainfo().nblocks() + 1
}
fn unmount_update_image(&mut self) {
@ -156,8 +159,7 @@ impl <'a> Update<'a> {
}
fn set_target_len(&self, nblocks: usize) -> Result<()> {
// add one block for header block
let len = ((nblocks + 1) * BLOCK_SIZE) as u64;
let len = (nblocks * BLOCK_SIZE) as u64;
let f = fs::OpenOptions::new()
.write(true)
.open(&self.target)
@ -174,7 +176,7 @@ impl <'a> Update<'a> {
if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
self.set_target_len(metainfo_nblocks)?;
} else if file_nblocks > (metainfo_nblocks + 1) {
} else if file_nblocks > metainfo_nblocks {
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
}
Ok(())
@ -183,7 +185,7 @@ impl <'a> Update<'a> {
// If resize was requested, adjust size of update copy of image file.
fn resize_image_file(&self) -> Result<()> {
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
Some(rs) => rs.nblocks() + 1,
None => return Ok(()),
};
@ -222,7 +224,7 @@ impl <'a> Update<'a> {
fn seal(&mut self) -> Result<()> {
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
None => self.metainfo_nblock_size(),
None => self.metainfo_nblock_size() - 1,
};
let salt = hex::encode(randombytes(32));
@ -230,11 +232,20 @@ impl <'a> Update<'a> {
.map_err(context!("failed to create verity context for realmfs update image {:?}", self.target()))?;
let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)
.map_err(context!("failed to generate dm-verity hashtree for realmfs update image {:?}", self.target()))?;
// XXX passes metainfo for nblocks
//let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?;
let root_hash = output.root_hash()
.ok_or_else(|| format_err!("no root hash returned from verity format operation"))?;
info!("root hash is {}", output.root_hash().unwrap());
/*
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
None => self.metainfo_nblock_size() - 1,
};
*/
info!("Signing new image with user realmfs keys");
let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash);
let keys = self.realmfs.sealing_keys().expect("No sealing keys");

View File

@ -7,8 +7,6 @@ use std::env;
use std::fs::{self, File, DirEntry};
use std::ffi::CString;
use std::io::{self, Seek, Read, BufReader, SeekFrom};
use std::os::fd::AsRawFd;
use std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir;
use libc;
@ -218,8 +216,7 @@ where
///
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let is_symlink = fs::symlink_metadata(path).is_ok();
if is_symlink || path.exists() {
if path.exists() {
fs::remove_file(path)
.map_err(context!("failed to remove file {:?}", path))?;
}
@ -337,71 +334,3 @@ pub fn is_euid_root() -> bool {
libc::geteuid() == 0
}
}
fn utimes(path: &Path, atime: i64, mtime: i64) -> Result<()> {
let cstr = CString::new(path.as_os_str().as_bytes())
.expect("path contains null byte");
let atimeval = libc::timeval {
tv_sec: atime,
tv_usec: 0,
};
let mtimeval = libc::timeval {
tv_sec: mtime,
tv_usec: 0,
};
let times = [atimeval,mtimeval];
let ret = unsafe { libc::utimes(cstr.as_ptr(), times.as_ptr()) };
if ret != 0 {
bail!("Failed to call utimes: {:?}", io::Error::last_os_error());
}
Ok(())
}
pub fn touch_mtime(path: &Path) -> Result<()> {
let meta = path.metadata()
.map_err(context!("failed to retrieve metadata from {:?}", path))?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)
.map_err(context!("Could not get system time as UNIX_EPOCH"))?;
let mtime = now.as_secs() as i64;
utimes(path, meta.atime(),mtime)?;
Ok(())
}
pub fn nsenter_netns(netns: &str) -> Result<()> {
let mut path = PathBuf::from("/run/netns");
path.push(netns);
if !path.exists() {
bail!("Network namespace '{}' does not exist", netns);
}
let f = File::open(&path)
.map_err(context!("error opening netns file {}", path.display()))?;
let fd = f.as_raw_fd();
unsafe {
if libc::setns(fd, libc::CLONE_NEWNET) == -1 {
let err = io::Error::last_os_error();
bail!("failed to setns() into network namespace '{}': {}", netns, err);
}
}
Ok(())
}
pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
unsafe {
if libc::setgid(gid) == -1 {
let err = io::Error::last_os_error();
bail!("failed to call setgid({}): {}", gid, err);
} else if libc::setuid(uid) == -1 {
let err = io::Error::last_os_error();
bail!("failed to call setuid({}): {}", uid, err);
}
}
Ok(())
}

View File

@ -9,7 +9,7 @@ homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
rand = "0.8"
zvariant = "4.2.0"
zvariant = "2.7.0"
serde = { version = "1.0", features = ["derive"] }
zbus = "4.4.0"
zbus = "=2.0.0-beta.5"
gtk = { version = "0.14.0", features = ["v3_24"] }

View File

@ -68,18 +68,18 @@ impl CitadelSettings {
let list = self.realms.iter().map(|r| r.to_string()).collect::<Vec<String>>();
let realms = list.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
self.settings.set_strv("realm-label-colors", &realms).is_ok()
self.settings.set_strv("realm-frame-colors", &realms).is_ok()
}
pub fn new() -> Self {
let settings = gio::Settings::new("com.subgraph.citadel");
let realms = settings.strv("realm-label-colors")
let realms = settings.strv("realm-frame-colors")
.into_iter()
.flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok())
.collect::<Vec<RealmFrameColor>>();
let frame_colors = settings.strv("label-color-list").into_iter()
let frame_colors = settings.strv("frame-color-list").into_iter()
.flat_map(|gs| gs.as_str().parse().ok())
.collect();

View File

@ -1,10 +1,10 @@
use std::collections::HashMap;
use std::rc::Rc;
use zvariant::Type;
use zbus::dbus_proxy;
use zvariant::derive::Type;
use serde::{Serialize,Deserialize};
use zbus::proxy;
use zbus::blocking::Connection;
use crate::error::{Error, Result};
#[derive(Deserialize,Serialize,Type)]
@ -85,12 +85,10 @@ impl RealmConfig {
}
}
#[proxy(
#[dbus_proxy(
default_service = "com.subgraph.realms",
interface = "com.subgraph.realms.Manager",
default_path = "/com/subgraph/realms",
gen_blocking = true,
gen_async = false,
default_path = "/com/subgraph/realms"
)]
pub trait RealmsManager {
fn get_current(&self) -> zbus::Result<String>;
@ -104,7 +102,7 @@ pub trait RealmsManager {
impl RealmsManagerProxy<'_> {
pub fn connect() -> Result<Self> {
let connection = Connection::system()?;
let connection = zbus::Connection::new_system()?;
let proxy = RealmsManagerProxy::new(&connection)
.map_err(|_| Error::ManagerConnect)?;

View File

@ -6,11 +6,8 @@ edition = "2018"
[dependencies]
libcitadel = { path = "../libcitadel" }
async-io = "2.3.2"
blocking = "1.6.1"
event-listener = "5.3.1"
zbus = "4.4.0"
zvariant = "4.2.0"
zbus = "=2.0.0-beta.5"
zvariant = "2.7.0"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1.8"

View File

@ -1,16 +1,15 @@
use async_io::block_on;
use zbus::blocking::Connection;
use zbus::SignalContext;
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) -> Self {
EventHandler { connection }
pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
EventHandler { connection, realms_server }
}
pub fn handle_event(&self, ev: &RealmEvent) {
@ -26,49 +25,44 @@ impl EventHandler {
RealmEvent::New(realm) => self.on_new(realm),
RealmEvent::Removed(realm) => self.on_removed(realm),
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
RealmEvent::Starting(_) => Ok(()),
RealmEvent::Stopping(_) => Ok(()),
}
}
fn with_signal_context<F>(&self, func: F) -> zbus::Result<()>
fn with_server<F>(&self, func: F) -> zbus::Result<()>
where
F: Fn(&SignalContext) -> zbus::Result<()>,
F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
{
let object_server = self.connection.object_server();
let iface = object_server.interface::<_, RealmsManagerServer>(REALMS_SERVER_OBJECT_PATH)?;
let ctx = iface.signal_context();
func(ctx)
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 pid_ns = realm.pid_ns().unwrap_or(0);
let status = realm_status(realm);
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_started(ctx, realm.name(), pid_ns, status)))
self.with_server(|server| server.realm_started(realm.name(), pid_ns, status))
}
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
let status = realm_status(realm);
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_stopped(ctx, realm.name(), status)))
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_signal_context(|ctx| block_on(RealmsManagerServer::realm_new(ctx, realm.name(), &description, status)))
self.with_server(|server| server.realm_new(realm.name(), &description, status))
}
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
self.with_signal_context(|ctx| block_on(RealmsManagerServer::realm_removed(ctx, realm.name())))
self.with_server(|server| server.realm_removed(realm.name()))
}
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
self.with_signal_context(|ctx| {
self.with_server(|server| {
match realm {
Some(realm) => block_on(RealmsManagerServer::realm_current(ctx, realm.name(), realm_status(realm))),
None => block_on(RealmsManagerServer::realm_current(ctx, "", 0)),
Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
None => server.realm_current("", 0),
}
})
}

View File

@ -1,18 +1,14 @@
#[macro_use] extern crate libcitadel;
use std::env;
use std::sync::Arc;
use event_listener::{Event, Listener};
use zbus::blocking::Connection;
use zbus::fdo::ObjectManager;
use libcitadel::{Logger, LogLevel, Result, RealmManager};
use crate::next::{RealmsManagerServer2, REALMS2_SERVER_OBJECT_PATH};
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH};
use zbus::{Connection, fdo};
use libcitadel::{Logger, LogLevel, Result};
use crate::realms_manager::RealmsManagerServer;
mod realms_manager;
mod events;
mod next;
fn main() {
if let Err(e) = run_realm_manager() {
@ -20,43 +16,24 @@ fn main() {
}
}
fn register_realms_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
let server = RealmsManagerServer::load(&connection, realm_manager.clone(), quit_event.clone())
.map_err(context!("Loading realms server"))?;
connection.object_server().at(REALMS_SERVER_OBJECT_PATH, server).map_err(context!("registering realms manager object"))?;
Ok(())
}
fn register_realms2_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
let server2 = RealmsManagerServer2::load(&connection, realm_manager.clone(), quit_event.clone())
.map_err(context!("Loading realms2 server"))?;
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, server2).map_err(context!("registering realms manager object"))?;
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, ObjectManager).map_err(context!("registering ObjectManager"))?;
Ok(())
fn create_system_connection() -> zbus::Result<Connection> {
let connection = zbus::Connection::new_system()?;
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?;
Ok(connection)
}
fn run_realm_manager() -> Result<()> {
Logger::set_log_level(LogLevel::Verbose);
let testing = env::args().skip(1).any(|s| s == "--testing");
let connection = Connection::system()
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);
}
}
let realm_manager = RealmManager::load()?;
let quit_event = Arc::new(Event::new());
if testing {
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
connection.request_name("com.subgraph.Realms2")
.map_err(context!("acquiring realms manager name"))?;
} else {
register_realms_manager_server(&connection, &realm_manager, &quit_event)?;
register_realms2_manager_server(&connection, &realm_manager, &quit_event)?;
connection.request_name("com.subgraph.realms")
.map_err(context!("acquiring realms manager name"))?;
};
quit_event.listen().wait();
Ok(())
}

View File

@ -1,201 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serde::{Deserialize, Serialize};
use zbus::fdo;
use zvariant::Type;
use libcitadel::{OverlayType, Realm, GLOBAL_CONFIG};
use libcitadel::terminal::Base16Scheme;
use crate::next::manager::failed;
const BOOL_CONFIG_VARS: &[&str] = &[
"use-gpu", "use-wayland", "use-x11", "use-sound",
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home",
"use-media-dir", "use-fuse", "use-flatpak", "use-gpu-card0"
];
fn is_bool_config_variable(variable: &str) -> bool {
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
}
#[derive(Deserialize,Serialize,Type)]
pub struct RealmConfigVars {
items: HashMap<String,String>,
}
impl RealmConfigVars {
fn new() -> Self {
RealmConfigVars { items: HashMap::new() }
}
pub fn new_global() -> Self {
Self::new_from_config(&GLOBAL_CONFIG)
}
fn new_from_realm(realm: &Realm) -> Self {
let config = realm.config();
Self::new_from_config(&config)
}
fn new_from_config(config: &libcitadel::RealmConfig) -> Self {
let mut vars = RealmConfigVars::new();
vars.add_bool("use-gpu", config.gpu());
vars.add_bool("use-gpu-card0", config.gpu_card0());
vars.add_bool("use-wayland", config.wayland());
vars.add_bool("use-x11", config.x11());
vars.add_bool("use-sound", config.sound());
vars.add_bool("use-shared-dir", config.shared_dir());
vars.add_bool("use-network", config.network());
vars.add_bool("use-kvm", config.kvm());
vars.add_bool("use-ephemeral-home", config.ephemeral_home());
vars.add_bool("use-media-dir", config.media_dir());
vars.add_bool("use-fuse", config.fuse());
vars.add_bool("use-flatpak", config.flatpak());
let overlay = match config.overlay() {
OverlayType::None => "none",
OverlayType::TmpFS => "tmpfs",
OverlayType::Storage => "storage",
};
vars.add("overlay", overlay);
let scheme = match config.terminal_scheme() {
Some(name) => name.to_string(),
None => String::new(),
};
vars.add("terminal-scheme", scheme);
vars.add("realmfs", config.realmfs());
vars
}
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());
}
}
#[derive(Clone)]
pub struct RealmConfig {
realm: Realm,
changed: Arc<AtomicBool>,
}
impl RealmConfig {
pub fn new(realm: Realm) -> Self {
let changed = Arc::new(AtomicBool::new(false));
RealmConfig { realm, changed }
}
fn mark_changed(&self) {
self.changed.store(true, Ordering::Relaxed);
}
fn is_changed(&self) -> bool {
self.changed.load(Ordering::Relaxed)
}
pub fn config_vars(&self) -> RealmConfigVars {
RealmConfigVars::new_from_realm(&self.realm)
}
fn set_bool_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
let v = match value {
"true" => true,
"false" => false,
_ => return failed(format!("Invalid boolean value '{}' for realm config variable '{}'", value, var)),
};
let mut has_changed = true;
self.realm.with_mut_config(|c| {
match var {
"use-gpu" if c.gpu() != v => c.use_gpu = Some(v),
_ => has_changed = false,
}
});
if has_changed {
self.mark_changed();
}
Ok(())
}
fn set_overlay(&mut self, value: &str) -> fdo::Result<()> {
let val = match value {
"tmpfs" => Some("tmpfs".to_string()),
"storage" => Some("storage".to_string()),
"none" => None,
_ => return failed(format!("Invalid value '{}' for overlay config", value)),
};
if self.realm.config().overlay != val {
self.realm.with_mut_config(|c| {
c.overlay = Some(value.to_string());
});
self.mark_changed();
}
Ok(())
}
fn set_terminal_scheme(&mut self, value: &str) -> fdo::Result<()> {
if Some(value) == self.realm.config().terminal_scheme() {
return Ok(())
}
let scheme = match Base16Scheme::by_name(value) {
Some(scheme) => scheme,
None => return failed(format!("Invalid terminal color scheme '{}'", value)),
};
let manager = self.realm.manager();
if let Err(err) = scheme.apply_to_realm(&manager, &self.realm) {
return failed(format!("Error applying terminal color scheme: {}", err));
}
self.realm.with_mut_config(|c| {
c.terminal_scheme = Some(value.to_string());
});
self.mark_changed();
Ok(())
}
fn set_realmfs(&mut self, value: &str) -> fdo::Result<()> {
let manager = self.realm.manager();
if manager.realmfs_by_name(value).is_none() {
return failed(format!("Failed to set 'realmfs' config for realm-{}: RealmFS named '{}' does not exist", self.realm.name(), value));
}
if self.realm.config().realmfs() != value {
self.realm.with_mut_config(|c| {
c.realmfs = Some(value.to_string())
});
self.mark_changed();
}
Ok(())
}
pub fn save_config(&self) -> fdo::Result<()> {
if self.is_changed() {
self.realm.config()
.write()
.map_err(|err| fdo::Error::Failed(format!("Error writing config file for realm-{}: {}", self.realm.name(), err)))?;
self.changed.store(false, Ordering::Relaxed);
}
Ok(())
}
pub fn set_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
if is_bool_config_variable(var) {
self.set_bool_var(var, value)
} else if var == "overlay" {
self.set_overlay(value)
} else if var == "terminal-scheme" {
self.set_terminal_scheme(value)
} else if var == "realmfs" {
self.set_realmfs(value)
} else {
failed(format!("Unknown realm configuration variable '{}'", var))
}
}
}

View File

@ -1,103 +0,0 @@
use std::sync::Arc;
use blocking::unblock;
use event_listener::{Event, EventListener};
use serde::Serialize;
use serde_repr::Serialize_repr;
use zbus::blocking::Connection;
use zbus::{fdo, interface};
use zvariant::Type;
use libcitadel::{PidLookupResult, RealmManager};
use crate::next::config::RealmConfigVars;
use crate::next::realm::RealmItemState;
use crate::next::realmfs::RealmFSState;
pub fn failed<T>(message: String) -> fdo::Result<T> {
Err(fdo::Error::Failed(message))
}
#[derive(Serialize_repr, Type, Debug, PartialEq)]
#[repr(u32)]
pub enum PidLookupResultCode {
Unknown = 1,
Realm = 2,
Citadel = 3,
}
#[derive(Debug, Type, Serialize)]
pub struct RealmFromCitadelPid {
code: PidLookupResultCode,
realm: String,
}
impl From<PidLookupResult> for RealmFromCitadelPid {
fn from(result: PidLookupResult) -> Self {
match result {
PidLookupResult::Unknown => RealmFromCitadelPid { code: PidLookupResultCode::Unknown, realm: String::new() },
PidLookupResult::Realm(realm) => RealmFromCitadelPid { code: PidLookupResultCode::Realm, realm: realm.name().to_string() },
PidLookupResult::Citadel => RealmFromCitadelPid { code: PidLookupResultCode::Citadel, realm: String::new() },
}
}
}
pub struct RealmsManagerServer2 {
realms: RealmItemState,
realmfs_state: RealmFSState,
manager: Arc<RealmManager>,
quit_event: Arc<Event>,
}
impl RealmsManagerServer2 {
fn new(connection: Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Self {
let realms = RealmItemState::new(connection.clone());
let realmfs_state = RealmFSState::new(connection.clone());
RealmsManagerServer2 {
realms,
realmfs_state,
manager,
quit_event,
}
}
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
let mut server = Self::new(connection.clone(), manager.clone(), quit_event);
server.realms.load_realms(&manager)?;
server.realmfs_state.load(&manager)?;
server.realms.populate_realmfs(&server.realmfs_state)?;
Ok(server)
}
}
#[interface(name = "com.subgraph.realms.Manager2")]
impl RealmsManagerServer2 {
async fn get_current(&self) -> u32 {
self.realms.get_current()
.map(|r| r.index())
.unwrap_or(0)
}
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
let manager = self.manager.clone();
unblock(move || {
manager.realm_by_pid(pid).into()
}).await
}
async fn create_realm(&self, name: &str) -> fdo::Result<()> {
let manager = self.manager.clone();
let name = name.to_string();
unblock(move || {
let _ = manager.new_realm(&name).map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(())
}).await
}
async fn get_global_config(&self) -> RealmConfigVars {
RealmConfigVars::new_global()
}
}

View File

@ -1,8 +0,0 @@
mod manager;
mod config;
mod realm;
mod realmfs;
pub use manager::RealmsManagerServer2;
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";

View File

@ -1,374 +0,0 @@
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use blocking::unblock;
use zbus::{interface, fdo};
use zbus::blocking::Connection;
use zbus::names::{BusName, InterfaceName};
use zvariant::{OwnedObjectPath, Value};
use libcitadel::{Realm, RealmEvent, RealmManager, Result};
use crate::next::config::{RealmConfig, RealmConfigVars};
use crate::next::realmfs::RealmFSState;
use crate::next::REALMS2_SERVER_OBJECT_PATH;
#[derive(Clone)]
pub struct RealmItem {
path: String,
index: u32,
realm: Realm,
config: RealmConfig,
in_run_transition: Arc<AtomicBool>,
realmfs_index: Arc<AtomicU32>,
last_timestamp: Arc<AtomicI64>,
}
#[derive(Copy,Clone)]
#[repr(u32)]
enum RealmRunStatus {
Stopped = 0,
Starting,
Running,
Current,
Stopping,
}
impl RealmRunStatus {
fn for_realm(realm: &Realm, in_transition: bool) -> Self {
if in_transition {
if realm.is_active() { Self::Stopping } else { Self::Starting }
} else if realm.is_active() {
if realm.is_current() { Self::Current } else {Self::Running }
} else {
Self::Stopped
}
}
}
impl RealmItem {
pub(crate) fn new_from_realm(index: u32, realm: Realm) -> RealmItem {
let path = format!("{}/Realm{}", REALMS2_SERVER_OBJECT_PATH, index);
let in_run_transition = Arc::new(AtomicBool::new(false));
let config = RealmConfig::new(realm.clone());
let realmfs_index = Arc::new(AtomicU32::new(0));
let last_timestamp = Arc::new(AtomicI64::new(realm.timestamp()));
RealmItem { path, index, realm, config, in_run_transition, realmfs_index, last_timestamp }
}
pub fn path(&self) -> &str {
&self.path
}
pub fn index(&self) -> u32 {
self.index
}
fn in_run_transition(&self) -> bool {
self.in_run_transition.load(Ordering::Relaxed)
}
fn get_run_status(&self) -> RealmRunStatus {
RealmRunStatus::for_realm(&self.realm, self.in_run_transition())
}
async fn do_start(&mut self) -> fdo::Result<()> {
if !self.realm.is_active() {
let realm = self.realm.clone();
let res = unblock(move || realm.start()).await;
if let Err(err) = res {
return Err(fdo::Error::Failed(format!("Failed to start realm: {}", err)));
}
}
Ok(())
}
async fn do_stop(&mut self) -> fdo::Result<()> {
if self.realm.is_active() {
let realm = self.realm.clone();
let res = unblock(move || realm.stop()).await;
if let Err(err) = res {
return Err(fdo::Error::Failed(format!("Failed to stop realm: {}", err)));
}
}
Ok(())
}
}
#[interface(
name = "com.subgraph.realms.Realm"
)]
impl RealmItem {
async fn start(
&mut self,
) -> fdo::Result<()> {
self.do_start().await?;
Ok(())
}
async fn stop(
&mut self,
) -> fdo::Result<()> {
self.do_stop().await?;
Ok(())
}
async fn restart(
&mut self,
) -> fdo::Result<()> {
self.do_stop().await?;
self.do_start().await?;
Ok(())
}
async fn set_current(&mut self) -> fdo::Result<()> {
let realm = self.realm.clone();
let res = unblock(move || realm.set_current()).await;
if let Err(err) = res {
return Err(fdo::Error::Failed(format!("Failed to set realm {} as current: {}", self.realm.name(), err)));
}
Ok(())
}
async fn get_config(&self) -> RealmConfigVars {
self.config.config_vars()
}
async fn set_config(&mut self, vars: Vec<(String, String)>) -> fdo::Result<()> {
for (var, val) in &vars {
self.config.set_var(var, val)?;
}
let config = self.config.clone();
unblock(move || config.save_config()).await
}
#[zbus(property, name = "RunStatus")]
fn run_status(&self) -> u32 {
self.get_run_status() as u32
}
#[zbus(property, name="IsSystemRealm")]
fn is_system_realm(&self) -> bool {
self.realm.is_system()
}
#[zbus(property, name = "Name")]
fn name(&self) -> &str {
self.realm.name()
}
#[zbus(property, name = "Description")]
fn description(&self) -> String {
self.realm.notes()
.unwrap_or(String::new())
}
#[zbus(property, name = "PidNS")]
fn pid_ns(&self) -> u64 {
self.realm.pid_ns().unwrap_or_default()
}
#[zbus(property, name = "RealmFS")]
fn realmfs(&self) -> u32 {
self.realmfs_index.load(Ordering::Relaxed)
}
#[zbus(property, name = "Timestamp")]
fn timestamp(&self) -> u64 {
self.realm.timestamp() as u64
}
}
#[derive(Clone)]
pub struct RealmItemState(Arc<Mutex<Inner>>);
struct Inner {
connection: Connection,
next_index: u32,
realms: HashMap<String, RealmItem>,
current_realm: Option<RealmItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
connection,
next_index: 1,
realms:HashMap::new(),
current_realm: None,
}
}
fn load_realms(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realm in manager.realm_list() {
self.add_realm(realm)?;
}
Ok(())
}
pub fn populate_realmfs(&mut self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
for item in self.realms.values_mut() {
if let Some(realmfs) = realmfs_state.realmfs_by_name(item.realm.config().realmfs()) {
item.realmfs_index.store(realmfs.index(), Ordering::Relaxed);
}
}
Ok(())
}
fn add_realm(&mut self, realm: Realm) -> zbus::Result<()> {
if self.realms.contains_key(realm.name()) {
warn!("Attempted to add duplicate realm '{}'", realm.name());
return Ok(())
}
let key = realm.name().to_string();
let item = RealmItem::new_from_realm(self.next_index, realm);
self.connection.object_server().at(item.path(), item.clone())?;
self.realms.insert(key, item);
self.next_index += 1;
Ok(())
}
fn remove_realm(&mut self, realm: &Realm) -> zbus::Result<()> {
if let Some(item) = self.realms.remove(realm.name()) {
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
} else {
warn!("Failed to find realm to remove with name '{}'", realm.name());
}
Ok(())
}
fn emit_property_changed(&self, object_path: OwnedObjectPath, propname: &str, value: Value<'_>) -> zbus::Result<()> {
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
let changed = HashMap::from([(propname.to_string(), value)]);
let inval: &[&str] = &[];
self.connection.emit_signal(
None::<BusName<'_>>,
&object_path,
"org.freedesktop.Dbus.Properties",
"PropertiesChanged",
&(iface_name, changed, inval))?;
Ok(())
}
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> zbus::Result<()> {
if let Some(realm) = self.realm_by_name(realm.name()) {
if let Some(transition) = transition {
realm.in_run_transition.store(transition, Ordering::Relaxed);
}
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "RunStatus", Value::U32(realm.get_run_status() as u32))?;
let timestamp = realm.realm.timestamp();
if realm.last_timestamp.load(Ordering::Relaxed) != realm.realm.timestamp() {
realm.last_timestamp.store(timestamp, Ordering::Relaxed);
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "Timestamp", Value::U64(timestamp as u64))?;
}
}
Ok(())
}
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
let res = self.realms.get(name);
if res.is_none() {
warn!("Failed to find realm with name '{}'", name);
}
res
}
fn on_starting(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(true))?;
Ok(())
}
fn on_started(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(false))
}
fn on_stopping(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(true))
}
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(false))
}
fn on_new(&mut self, realm: &Realm) -> zbus::Result<()> {
self.add_realm(realm.clone())?;
Ok(())
}
fn on_removed(&mut self, realm: &Realm) -> zbus::Result<()> {
self.remove_realm(&realm)?;
Ok(())
}
fn on_current(&mut self, realm: Option<&Realm>) -> zbus::Result<()> {
if let Some(r) = self.current_realm.take() {
self.realm_status_changed(&r.realm, None)?;
}
if let Some(realm) = realm {
self.realm_status_changed(realm, None)?;
if let Some(item) = self.realm_by_name(realm.name()) {
self.current_realm = Some(item.clone());
}
}
Ok(())
}
}
impl RealmItemState {
pub fn new(connection: Connection) -> Self {
RealmItemState(Arc::new(Mutex::new(Inner::new(connection))))
}
pub fn load_realms(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load_realms(manager)?;
self.add_event_handler(manager)
.map_err(|err| zbus::Error::Failure(err.to_string()))?;
Ok(())
}
pub fn populate_realmfs(&self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
self.inner().populate_realmfs(realmfs_state)
}
pub fn get_current(&self) -> Option<RealmItem> {
self.inner().current_realm.clone()
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
let state = self.clone();
manager.add_event_handler(move |ev| {
if let Err(err) = state.handle_event(ev) {
warn!("Error handling {}: {}", ev, err);
}
});
manager.start_event_task()?;
Ok(())
}
fn handle_event(&self, ev: &RealmEvent) -> zbus::Result<()> {
match ev {
RealmEvent::Started(realm) => self.inner().on_started(realm)?,
RealmEvent::Stopped(realm) => self.inner().on_stopped(realm)?,
RealmEvent::New(realm) => self.inner().on_new(realm)?,
RealmEvent::Removed(realm) => self.inner().on_removed(realm)?,
RealmEvent::Current(realm) => self.inner().on_current(realm.as_ref())?,
RealmEvent::Starting(realm) => self.inner().on_starting(realm)?,
RealmEvent::Stopping(realm) => self.inner().on_stopping(realm)?,
};
Ok(())
}
}

View File

@ -1,120 +0,0 @@
use std::collections::HashMap;
use std::convert::TryInto;
use zbus::blocking::Connection;
use zbus::{fdo, interface};
use zvariant::{ObjectPath, OwnedObjectPath};
use libcitadel::{RealmFS, RealmManager};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
const BLOCK_SIZE: u64 = 4096;
#[derive(Clone)]
pub struct RealmFSItem {
object_path: OwnedObjectPath,
index: u32,
realmfs: RealmFS,
}
impl RealmFSItem {
pub(crate) fn new_from_realmfs(index: u32, realmfs: RealmFS) -> RealmFSItem {
let object_path = format!("{}/RealmFS{}", REALMS2_SERVER_OBJECT_PATH, index).try_into().unwrap();
RealmFSItem {
object_path,
index,
realmfs,
}
}
pub fn index(&self) -> u32 {
self.index
}
pub fn object_path(&self) -> ObjectPath {
self.object_path.as_ref()
}
}
#[interface(
name = "com.subgraph.realms.RealmFS"
)]
impl RealmFSItem {
#[zbus(property, name = "Name")]
fn name(&self) -> &str {
self.realmfs.name()
}
#[zbus(property, name = "Activated")]
fn activated(&self) -> bool {
self.realmfs.is_activated()
}
#[zbus(property, name = "InUse")]
fn in_use(&self) -> bool {
self.realmfs.is_activated()
}
#[zbus(property, name = "Mountpoint")]
fn mountpoint(&self) -> String {
self.realmfs.mountpoint().to_string()
}
#[zbus(property, name = "Path")]
fn path(&self) -> String {
format!("{}", self.realmfs.path().display())
}
#[zbus(property, name = "FreeSpace")]
fn free_space(&self) -> fdo::Result<u64> {
let blocks = self.realmfs.free_size_blocks()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(blocks as u64 * BLOCK_SIZE)
}
#[zbus(property, name = "AllocatedSpace")]
fn allocated_space(&self) -> fdo::Result<u64> {
let blocks = self.realmfs.allocated_size_blocks()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(blocks as u64 * BLOCK_SIZE)
}
}
pub struct RealmFSState {
connection: Connection,
next_index: u32,
items: HashMap<String, RealmFSItem>,
}
impl RealmFSState {
pub fn new(connection: Connection) -> Self {
RealmFSState {
connection,
next_index: 1,
items: HashMap::new(),
}
}
pub fn load(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realmfs in manager.realmfs_list() {
self.add_realmfs(realmfs)?;
}
Ok(())
}
fn add_realmfs(&mut self, realmfs: RealmFS) -> zbus::Result<()> {
if !self.items.contains_key(realmfs.name()) {
let name = realmfs.name().to_string();
let item = RealmFSItem::new_from_realmfs(self.next_index, realmfs);
self.connection.object_server().at(item.object_path(), item.clone())?;
self.items.insert(name, item);
self.next_index += 1;
} else {
warn!("Attempted to add duplicate realmfs '{}'", realmfs.name());
}
Ok(())
}
pub fn realmfs_by_name(&self, name: &str) -> Option<&RealmFSItem> {
let res = self.items.get(name);
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
}

View File

@ -1,14 +1,11 @@
use libcitadel::{RealmManager, Realm, OverlayType, Result, PidLookupResult};
use std::sync::Arc;
use zbus::blocking::Connection;
use zvariant::Type;
use zbus::{dbus_interface, ObjectServer,Connection};
use zvariant::derive::Type;
use std::thread;
use std::collections::HashMap;
use blocking::unblock;
use event_listener::{Event, EventListener};
use serde::{Serialize,Deserialize};
use serde_repr::Serialize_repr;
use zbus::{interface, SignalContext};
use crate::events::EventHandler;
use libcitadel::terminal::Base16Scheme;
@ -42,7 +39,6 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
#[derive(Clone)]
pub struct RealmsManagerServer {
manager: Arc<RealmManager>,
quit_event: Arc<Event>,
}
const BOOL_CONFIG_VARS: &[&str] = &[
@ -125,40 +121,40 @@ fn configure_realm(manager: &RealmManager, realm: &Realm, variable: &str, value:
impl RealmsManagerServer {
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Result<RealmsManagerServer> {
let server = RealmsManagerServer { manager, quit_event };
let events = EventHandler::new(connection.clone());
server.manager.add_event_handler(move |ev| events.handle_event(ev));
server.manager.start_event_task()?;
Ok(server)
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)
}
}
#[interface(name = "com.subgraph.realms.Manager")]
#[dbus_interface(name = "com.subgraph.realms.Manager")]
impl RealmsManagerServer {
async fn set_current(&self, name: &str) {
let manager = self.manager.clone();
let name = name.to_string();
unblock(move || {
if let Some(realm) = manager.realm_by_name(&name) {
if let Err(err) = manager.set_current_realm(&realm) {
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);
}
}
}).await
}
async fn get_current(&self) -> String {
let manager = self.manager.clone();
unblock(move || {
match manager.current_realm() {
fn get_current(&self) -> String {
match self.manager.current_realm() {
Some(realm) => realm.name().to_string(),
None => String::new(),
}
}).await
}
fn list(&self) -> Vec<RealmItem> {
@ -253,12 +249,8 @@ impl RealmsManagerServer {
});
}
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
let manager = self.manager.clone();
unblock(move || {
manager.realm_by_pid(pid).into()
}).await
fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
self.manager.realm_by_pid(pid).into()
}
fn realm_config(&self, name: &str) -> RealmConfig {
@ -269,7 +261,7 @@ impl RealmsManagerServer {
RealmConfig::new_from_realm(&realm)
}
async fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
let realm = match self.manager.realm_by_name(name) {
Some(r) => r,
None => {
@ -278,12 +270,8 @@ impl RealmsManagerServer {
},
};
for var in vars {
let manager = self.manager.clone();
let realm = realm.clone();
unblock( move || {
configure_realm(&manager, &realm, &var.0, &var.1);
}).await;
for var in &vars {
configure_realm(&self.manager, &realm, &var.0, &var.1);
}
}
@ -291,18 +279,13 @@ impl RealmsManagerServer {
Realm::is_valid_name(name) && self.manager.realm_by_name(name).is_some()
}
async fn create_realm(&self, name: &str) -> bool {
let manager = self.manager.clone();
let name = name.to_string();
unblock(move || {
if let Err(err) = manager.new_realm(&name) {
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
}
}).await
}
fn list_realm_f_s(&self) -> Vec<String> {
@ -316,23 +299,23 @@ impl RealmsManagerServer {
}
#[zbus(signal)]
pub async fn realm_started(ctx: &SignalContext<'_>, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_started(&self, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_stopped(ctx: &SignalContext<'_>, realm: &str, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_stopped(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_new(ctx: &SignalContext<'_>, realm: &str, description: &str, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_new(&self, realm: &str, description: &str, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_removed(ctx: &SignalContext<'_>, realm: &str) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_removed(&self, realm: &str) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn realm_current(ctx: &SignalContext<'_>, realm: &str, status: u8) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn realm_current(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
#[zbus(signal)]
pub async fn service_started(ctx: &SignalContext<'_>) -> zbus::Result<()>;
#[dbus_interface(signal)]
pub fn service_started(&self) -> zbus::Result<()> { Ok(()) }
}

View File

@ -1,5 +1,6 @@
[Unit]
Description=Current realm directory watcher
Before=launch-default-realm.service
[Path]
PathChanged=/run/citadel/realms/current

View File

@ -4,6 +4,4 @@ StartLimitIntervalSec=0
[Path]
PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications
PathChanged=/run/citadel/realms/current/current.realm/flatpak/exports/share/applications
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/flatpak/exports/share/applications