Compare commits

..

8 Commits

Author SHA1 Message Date
11b3e8a016 New improved realms daemon implementation 2024-11-13 11:54:40 -05:00
24f786cf75 Fix broken realmfs autoresize 2024-09-06 10:24:53 -04:00
2dc8bf2922 Support for flatpak and GNOME Software in Realms
When a realm has enabled 'use-flatpak' a .desktop file for GNOME
Software will be automatically generated while that realm is running.

This .desktop file will launch GNOME Software from Citadel inside a
bubblewrap sandbox. The sandbox has been prepared so that GNOME
Software will install flatpak applications into a directory that belongs
to the realm associated with the .desktop file.

When a realm has enabled 'use-flatpak' this directory will be bind
mounted (read-only) into the root filesystem of the realm so that
applications installed by GNOME Software are visible and can be launched.
2024-09-06 10:24:28 -04:00
isa
2a16bd4c41 Upgrade clap, rpassword and pwhash to prepare for new code using them 2024-08-30 13:11:47 -04:00
isa
b7e6ee3b3c Increase size of tmpfs to allow base-realmfs to decompress 2024-07-17 06:58:11 -04:00
isa
4fc3fb55db Fix user-facing typo 2024-07-12 16:53:12 -04:00
421b0e27d7 Various fixes to desktop_sync including Flatpak support 2024-06-03 12:05:58 -04:00
44c545a4a3 Updated to use new gsettings keys 2024-06-03 11:56:40 -04:00
52 changed files with 5661 additions and 1031 deletions

1320
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -425,7 +425,7 @@ impl Installer {
util::create_dir(&home)?; util::create_dir(&home)?;
util::chown_user(&home)?; util::chown_user(&home)?;
self.info("Copying /realms/skel into home diectory")?; self.info("Copying /realms/skel into home directory")?;
util::copy_tree(&self.storage().join("realms/skel"), &home)?; util::copy_tree(&self.storage().join("realms/skel"), &home)?;
if let Some(scheme) = Base16Scheme::by_name(MAIN_TERMINAL_SCHEME) { if let Some(scheme) = Base16Scheme::by_name(MAIN_TERMINAL_SCHEME) {
@ -452,7 +452,7 @@ impl Installer {
util::create_dir(&path)?; util::create_dir(&path)?;
util::chown_user(&path)?; util::chown_user(&path)?;
self.info("Copying /realms/skel into home diectory")?; self.info("Copying /realms/skel into home directory")?;
util::copy_tree(&self.storage().join("realms/skel"), &home)?; util::copy_tree(&self.storage().join("realms/skel"), &home)?;
self.info("Creating apt-cacher config file")?; 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") { } else if exe == Path::new("/usr/libexec/citadel-install-backend") {
install_backend::main(); install_backend::main();
} else if exe == Path::new("/usr/bin/citadel-image") { } else if exe == Path::new("/usr/bin/citadel-image") {
image::main(args); image::main();
} else if exe == Path::new("/usr/bin/citadel-realmfs") { } else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main(args); realmfs::main();
} else if exe == Path::new("/usr/bin/citadel-update") { } else if exe == Path::new("/usr/bin/citadel-update") {
update::main(args); update::main(args);
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") { } else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
@ -57,8 +57,8 @@ fn dispatch_command(args: Vec<String>) {
match command.as_str() { match command.as_str() {
"boot" => boot::main(rebuild_args("citadel-boot", args)), "boot" => boot::main(rebuild_args("citadel-boot", args)),
"install" => install::main(rebuild_args("citadel-install", args)), "install" => install::main(rebuild_args("citadel-install", args)),
"image" => image::main(rebuild_args("citadel-image", args)), "image" => image::main(),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)), "realmfs" => realmfs::main(),
"update" => update::main(rebuild_args("citadel-update", args)), "update" => update::main(rebuild_args("citadel-update", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)), "mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)), "sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +0,0 @@
### 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> = [ static ref KEY_WHITELIST: HashSet<&'static str> = [
"Type", "Version", "Name", "GenericName", "NoDisplay", "Comment", "Icon", "Hidden", "Type", "Version", "Name", "GenericName", "NoDisplay", "Comment", "Icon", "Hidden",
"OnlyShowIn", "NotShowIn", "Path", "Terminal", "Actions", "MimeType", "OnlyShowIn", "NotShowIn", "Path", "Terminal", "Actions", "MimeType",
"Categories", "Keywords", "StartupNotify", "StartupWMClass", "URL", "DocPath", "Categories", "Keywords", "StartupNotify", "StartupWMClass", "URL", "DocPath", "SingleMainWindow",
"X-GNOME-FullName", "X-GNOME-Provides", "X-Desktop-File-Install-Version", "X-GNOME-UsesNotifications", "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-GNOME-DocPath", "X-Geoclue-Reason", "X-GNOME-SingleWindow", "X-GNOME-Gettext-Domain",
"X-MultipleArgs", "X-MultipleArgs", "X-Flatpak", "X-Flatpak-Tags", "X-SingleMainWindow",
].iter().cloned().collect(); ].iter().cloned().collect();
// These are keys which are recognized but deliberately ignored. // 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-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-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-Ayatana-Desktop-Shortcuts", "X-GNOME-Settings-Panel", "X-GNOME-WMSettingsModule", "X-GNOME-WMName",
"X-GnomeWMSettingsLibrary", "X-GnomeWMSettingsLibrary", "X-Purism-FormFactor", "X-ExecArg",
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,67 @@
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,6 +10,7 @@ nix = "0.17.0"
toml = "0.5" toml = "0.5"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "=1.0.1"
lazy_static = "1.4" lazy_static = "1.4"
sodiumoxide = "0.2" sodiumoxide = "0.2"
hex = "0.4" hex = "0.4"

View File

@ -0,0 +1,221 @@
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

@ -0,0 +1,259 @@
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

@ -0,0 +1,16 @@
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

@ -0,0 +1,132 @@
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

@ -0,0 +1,58 @@
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

@ -0,0 +1,144 @@
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,6 +21,8 @@ mod realm;
pub mod terminal; pub mod terminal;
mod system; mod system;
pub mod flatpak;
pub use crate::config::OsRelease; pub use crate::config::OsRelease;
pub use crate::blockdev::BlockDev; pub use crate::blockdev::BlockDev;
pub use crate::cmdline::CommandLine; pub use crate::cmdline::CommandLine;

View File

@ -62,6 +62,11 @@ impl Logger {
logger.level = level; 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>) { pub fn set_log_output(output: Box<dyn LogOutput>) {
let mut logger = LOGGER.lock().unwrap(); let mut logger = LOGGER.lock().unwrap();
logger.output = output; logger.output = output;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use procfs::process::Process; use procfs::process::Process;
use crate::Realm; use crate::{Result, Realm};
use crate::flatpak::{SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
pub enum PidLookupResult { pub enum PidLookupResult {
Unknown, Unknown,
@ -9,14 +10,15 @@ pub enum PidLookupResult {
} }
pub struct PidMapper { pub struct PidMapper {
active_realms: Vec<Realm>, sandbox_status: SandboxStatus,
my_pid_ns_id: Option<u64>, my_pid_ns_id: Option<u64>,
} }
impl PidMapper { impl PidMapper {
pub fn new(active_realms: Vec<Realm>) -> Self { pub fn new() -> Result<Self> {
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
let my_pid_ns_id = Self::self_pid_namespace_id(); let my_pid_ns_id = Self::self_pid_namespace_id();
PidMapper { active_realms, my_pid_ns_id } Ok(PidMapper { sandbox_status, my_pid_ns_id })
} }
fn read_process(pid: libc::pid_t) -> Option<Process> { fn read_process(pid: libc::pid_t) -> Option<Process> {
@ -72,7 +74,30 @@ impl PidMapper {
Self::read_process(ppid) Self::read_process(ppid)
} }
pub fn lookup_pid(&self, pid: libc::pid_t) -> PidLookupResult { 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 {
const MAX_PARENT_SEARCH: i32 = 8; const MAX_PARENT_SEARCH: i32 = 8;
let mut n = 0; let mut n = 0;
@ -92,13 +117,17 @@ impl PidMapper {
return PidLookupResult::Citadel; return PidLookupResult::Citadel;
} }
if let Some(realm) = self.active_realms.iter() if let Some(realm) = realms.iter()
.find(|r| r.has_pid_ns(pid_ns_id)) .find(|r| r.is_active() && r.has_pid_ns(pid_ns_id))
.cloned() .cloned()
{ {
return PidLookupResult::Realm(realm) 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) { proc = match Self::parent_process(proc) {
Some(proc) => proc, Some(proc) => proc,
None => return PidLookupResult::Unknown, None => return PidLookupResult::Unknown,
@ -108,5 +137,4 @@ impl PidMapper {
} }
PidLookupResult::Unknown PidLookupResult::Unknown
} }
} }

View File

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

View File

@ -31,6 +31,28 @@ impl Systemd {
if realm.config().ephemeral_home() { if realm.config().ephemeral_home() {
self.setup_ephemeral_home(realm)?; 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(()) Ok(())
} }

View File

@ -23,8 +23,8 @@ impl ResizeSize {
pub fn gigs(n: usize) -> Self { pub fn gigs(n: usize) -> Self {
ResizeSize(BLOCKS_PER_GIG * n) ResizeSize(BLOCKS_PER_GIG * n)
} }
pub fn megs(n: usize) -> Self { pub fn megs(n: usize) -> Self {
ResizeSize(BLOCKS_PER_MEG * n) ResizeSize(BLOCKS_PER_MEG * n)
} }
@ -45,8 +45,8 @@ impl ResizeSize {
self.0 / BLOCKS_PER_MEG self.0 / BLOCKS_PER_MEG
} }
/// If the RealmFS needs to be resized to a larger size, returns the /// If the RealmFS has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new
/// recommended size. /// size to resize the filesystem to and return it. Otherwise, return `None`
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> { pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
let sb = match Superblock::load(realmfs.path(), 4096) { let sb = match Superblock::load(realmfs.path(), 4096) {
Ok(sb) => sb, Ok(sb) => sb,
@ -56,22 +56,37 @@ impl ResizeSize {
}, },
}; };
sb.free_block_count();
let free_blocks = sb.free_block_count() as usize; let free_blocks = sb.free_block_count() as usize;
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() { if free_blocks >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1; return None;
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 metainfo_nblocks = realmfs.metainfo().nblocks();
let grow_blocks = (free_blocks + mask) & !mask;
Some(ResizeSize::blocks(grow_blocks)) 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)
} else { } else {
None // 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))
} }
} }
} }
const SUPERBLOCK_SIZE: usize = 1024; 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]); pub struct Superblock([u8; SUPERBLOCK_SIZE]);
impl Superblock { impl Superblock {

View File

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

View File

@ -7,6 +7,8 @@ use std::env;
use std::fs::{self, File, DirEntry}; use std::fs::{self, File, DirEntry};
use std::ffi::CString; use std::ffi::CString;
use std::io::{self, Seek, Read, BufReader, SeekFrom}; use std::io::{self, Seek, Read, BufReader, SeekFrom};
use std::os::fd::AsRawFd;
use std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir; use walkdir::WalkDir;
use libc; use libc;
@ -216,7 +218,8 @@ where
/// ///
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> { pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref(); let path = path.as_ref();
if path.exists() { let is_symlink = fs::symlink_metadata(path).is_ok();
if is_symlink || path.exists() {
fs::remove_file(path) fs::remove_file(path)
.map_err(context!("failed to remove file {:?}", path))?; .map_err(context!("failed to remove file {:?}", path))?;
} }
@ -334,3 +337,71 @@ pub fn is_euid_root() -> bool {
libc::geteuid() == 0 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] [dependencies]
libcitadel = { path = "../libcitadel" } libcitadel = { path = "../libcitadel" }
rand = "0.8" rand = "0.8"
zvariant = "2.7.0" zvariant = "4.2.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
zbus = "=2.0.0-beta.5" zbus = "4.4.0"
gtk = { version = "0.14.0", features = ["v3_24"] } 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 list = self.realms.iter().map(|r| r.to_string()).collect::<Vec<String>>();
let realms = list.iter().map(|s| s.as_str()).collect::<Vec<&str>>(); let realms = list.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
self.settings.set_strv("realm-frame-colors", &realms).is_ok() self.settings.set_strv("realm-label-colors", &realms).is_ok()
} }
pub fn new() -> Self { pub fn new() -> Self {
let settings = gio::Settings::new("com.subgraph.citadel"); let settings = gio::Settings::new("com.subgraph.citadel");
let realms = settings.strv("realm-frame-colors") let realms = settings.strv("realm-label-colors")
.into_iter() .into_iter()
.flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok()) .flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok())
.collect::<Vec<RealmFrameColor>>(); .collect::<Vec<RealmFrameColor>>();
let frame_colors = settings.strv("frame-color-list").into_iter() let frame_colors = settings.strv("label-color-list").into_iter()
.flat_map(|gs| gs.as_str().parse().ok()) .flat_map(|gs| gs.as_str().parse().ok())
.collect(); .collect();

View File

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

View File

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

View File

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

View File

@ -1,14 +1,18 @@
#[macro_use] extern crate libcitadel; #[macro_use] extern crate libcitadel;
use zbus::{Connection, fdo}; use std::env;
use std::sync::Arc;
use libcitadel::{Logger, LogLevel, Result}; use event_listener::{Event, Listener};
use zbus::blocking::Connection;
use crate::realms_manager::RealmsManagerServer; 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};
mod realms_manager; mod realms_manager;
mod events; mod events;
mod next;
fn main() { fn main() {
if let Err(e) = run_realm_manager() { if let Err(e) = run_realm_manager() {
@ -16,24 +20,43 @@ fn main() {
} }
} }
fn create_system_connection() -> zbus::Result<Connection> { fn register_realms_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
let connection = zbus::Connection::new_system()?; let server = RealmsManagerServer::load(&connection, realm_manager.clone(), quit_event.clone())
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?; .map_err(context!("Loading realms server"))?;
Ok(connection) 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 run_realm_manager() -> Result<()> { fn run_realm_manager() -> Result<()> {
Logger::set_log_level(LogLevel::Verbose); Logger::set_log_level(LogLevel::Verbose);
let connection = create_system_connection() let testing = env::args().skip(1).any(|s| s == "--testing");
let connection = Connection::system()
.map_err(context!("ZBus Connection error"))?; .map_err(context!("ZBus Connection error"))?;
let mut object_server = RealmsManagerServer::register(&connection)?;
loop { let realm_manager = RealmManager::load()?;
if let Err(err) = object_server.try_handle_next() { let quit_event = Arc::new(Event::new());
warn!("Error handling DBus message: {}", err);
}
}
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(())
} }

201
realmsd/src/next/config.rs Normal file
View File

@ -0,0 +1,201 @@
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))
}
}
}

103
realmsd/src/next/manager.rs Normal file
View File

@ -0,0 +1,103 @@
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()
}
}

8
realmsd/src/next/mod.rs Normal file
View File

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

374
realmsd/src/next/realm.rs Normal file
View File

@ -0,0 +1,374 @@
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(())
}
}

120
realmsd/src/next/realmfs.rs Normal file
View File

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

View File

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

View File

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