1
0
forked from brl/citadel-tools

2 Commits

Author SHA1 Message Date
isa
96f364cfe8 Fix unused function error return value 2024-08-29 15:42:42 -04:00
isa
04457a47ef Improve error handling by using std lib rust code 2024-08-29 14:54:31 -04:00
88 changed files with 3803 additions and 7221 deletions

1885
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
[workspace] [workspace]
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "launch-gnome-software", "update-realmfs" ] members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
resolver = "2"
[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

@@ -21,8 +21,6 @@ use cursive::vec::Vec2;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::fs::File; use std::fs::File;
use std::io::{self,BufWriter,Write}; use std::io::{self,BufWriter,Write};
use std::os::fd::AsFd;
use std::os::fd::OwnedFd;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
@@ -51,10 +49,19 @@ pub struct Backend {
input_receiver: Receiver<TEvent>, input_receiver: Receiver<TEvent>,
resize_receiver: Receiver<()>, resize_receiver: Receiver<()>,
tty_fd: OwnedFd, tty_fd: RawFd,
input_thread: JoinHandle<()>, input_thread: JoinHandle<()>,
} }
fn close_fd(fd: RawFd) -> io::Result<()> {
unsafe {
if libc::close(fd) != 0 {
return Err(io::Error::last_os_error());
}
};
Ok(())
}
fn pthread_kill(tid: libc::pthread_t, sig: libc::c_int) -> io::Result<()> { fn pthread_kill(tid: libc::pthread_t, sig: libc::c_int) -> io::Result<()> {
unsafe { unsafe {
if libc::pthread_kill(tid, sig) != 0 { if libc::pthread_kill(tid, sig) != 0 {
@@ -91,7 +98,7 @@ impl Backend {
// Read input from a separate thread // Read input from a separate thread
let input = std::fs::File::open("/dev/tty").unwrap(); let input = std::fs::File::open("/dev/tty").unwrap();
let tty_fd = input.as_fd().try_clone_to_owned().unwrap(); let tty_fd = input.as_raw_fd();
let input_thread = thread::spawn(move || { let input_thread = thread::spawn(move || {
let mut events = input.events(); let mut events = input.events();
@@ -120,6 +127,12 @@ impl Backend {
Ok(Box::new(c)) Ok(Box::new(c))
} }
fn close_tty(&self) {
if let Err(e) = close_fd(self.tty_fd) {
warn!("error closing tty fd: {}", e);
}
}
fn kill_thread(&self) { fn kill_thread(&self) {
if let Err(e) = pthread_kill(self.input_thread.as_pthread_t(), libc::SIGWINCH) { if let Err(e) = pthread_kill(self.input_thread.as_pthread_t(), libc::SIGWINCH) {
warn!("error sending signal to input thread: {}", e); warn!("error sending signal to input thread: {}", e);
@@ -232,6 +245,7 @@ impl backend::Backend for Backend {
) )
.unwrap(); .unwrap();
self.close_tty();
self.kill_thread(); self.kill_thread();
} }

View File

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

View File

@@ -4,7 +4,7 @@ use std::fs;
use std::thread::{self,JoinHandle}; use std::thread::{self,JoinHandle};
use std::time::{self,Instant}; use std::time::{self,Instant};
use libcitadel::{Result, UtsName, util}; use libcitadel::{UtsName, util};
use libcitadel::ResourceImage; use libcitadel::ResourceImage;
use crate::boot::disks; use crate::boot::disks;
@@ -13,34 +13,33 @@ use crate::install::installer::Installer;
const IMAGE_DIRECTORY: &str = "/run/citadel/images"; const IMAGE_DIRECTORY: &str = "/run/citadel/images";
pub fn live_rootfs() -> Result<()> { pub fn live_rootfs() -> Result<(), Box<dyn std::error::Error>> {
copy_artifacts()?; copy_artifacts()?;
let rootfs = find_rootfs_image()?; let rootfs = find_rootfs_image()?;
setup_rootfs_resource(&rootfs) setup_rootfs_resource(&rootfs)
} }
pub fn live_setup() -> Result<()> { pub fn live_setup() -> Result<(), Box<dyn std::error::Error>> {
decompress_images(true)?; decompress_images(true)?;
info!("Starting live setup"); info!("Starting live setup");
let live = Installer::new_livesetup(); let live = Installer::new_livesetup();
live.run() live.run()
} }
fn copy_artifacts() -> Result<()> { fn copy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
for _ in 0..3 { for _ in 0..3 {
if try_copy_artifacts()? { if try_copy_artifacts()? {
//decompress_images()?; //decompress_images()?;
return Ok(()) return Ok(());
} }
// Try again after waiting for more devices to be discovered // Try again after waiting for more devices to be discovered
info!("Failed to find partition with images, trying again in 2 seconds"); info!("Failed to find partition with images, trying again in 2 seconds");
thread::sleep(time::Duration::from_secs(2)); thread::sleep(time::Duration::from_secs(2));
} }
bail!("could not find partition containing resource images") Result::Err("could not find partition containing resource images".into())
} }
fn try_copy_artifacts() -> Result<bool> { fn try_copy_artifacts() -> Result<bool, Box<dyn std::error::Error>> {
let rootfs_image = Path::new("/boot/images/citadel-rootfs.img"); let rootfs_image = Path::new("/boot/images/citadel-rootfs.img");
// Already mounted? // Already mounted?
if rootfs_image.exists() { if rootfs_image.exists() {
@@ -60,13 +59,13 @@ fn try_copy_artifacts() -> Result<bool> {
Ok(false) Ok(false)
} }
fn kernel_version() -> String { fn kernel_version() -> Result<String, Box<dyn std::error::Error>> {
let utsname = UtsName::uname(); let utsname = UtsName::uname();
let v = utsname.release().split('-').collect::<Vec<_>>(); let v = utsname.release().split('-').collect::<Vec<_>>();
v[0].to_string() Ok(v[0].to_string())
} }
fn deploy_artifacts() -> Result<()> { fn deploy_artifacts() -> Result<(), Box<dyn std::error::Error>> {
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)?;
@@ -78,7 +77,7 @@ fn deploy_artifacts() -> Result<()> {
util::copy_file(dent.path(), run_images.join(dent.file_name())) util::copy_file(dent.path(), run_images.join(dent.file_name()))
})?; })?;
let kv = kernel_version(); let kv = kernel_version()?;
println!("Copying bzImage-{} to /run/citadel/images", kv); println!("Copying bzImage-{} to /run/citadel/images", kv);
let from = format!("/boot/bzImage-{}", kv); let from = format!("/boot/bzImage-{}", kv);
let to = format!("/run/citadel/images/bzImage-{}", kv); let to = format!("/run/citadel/images/bzImage-{}", kv);
@@ -92,7 +91,7 @@ fn deploy_artifacts() -> Result<()> {
Ok(()) Ok(())
} }
fn deploy_syslinux_artifacts() -> Result<()> { fn deploy_syslinux_artifacts() -> Result<(), Box<dyn std::error::Error>> {
let boot_syslinux = Path::new("/boot/syslinux"); let boot_syslinux = Path::new("/boot/syslinux");
if !boot_syslinux.exists() { if !boot_syslinux.exists() {
@@ -112,10 +111,11 @@ fn deploy_syslinux_artifacts() -> Result<()> {
} }
} }
Ok(()) Ok(())
}) })?;
Ok(())
} }
fn find_rootfs_image() -> Result<ResourceImage> { fn find_rootfs_image() -> Result<ResourceImage, Box<dyn std::error::Error>> {
let entries = fs::read_dir(IMAGE_DIRECTORY) let entries = fs::read_dir(IMAGE_DIRECTORY)
.map_err(context!("error reading directory {}", IMAGE_DIRECTORY))?; .map_err(context!("error reading directory {}", IMAGE_DIRECTORY))?;
for entry in entries { for entry in entries {
@@ -123,15 +123,21 @@ fn find_rootfs_image() -> Result<ResourceImage> {
if entry.path().extension() == Some(OsStr::new("img")) { if entry.path().extension() == Some(OsStr::new("img")) {
if let Ok(image) = ResourceImage::from_path(&entry.path()) { if let Ok(image) = ResourceImage::from_path(&entry.path()) {
if image.metainfo().image_type() == "rootfs" { if image.metainfo().image_type() == "rootfs" {
return Ok(image) return Ok(image);
} }
} }
} }
} }
bail!("unable to find rootfs resource image in {}", IMAGE_DIRECTORY) Result::Err(
format!(
"unable to find rootfs resource image in {}",
IMAGE_DIRECTORY
)
.into(),
)
} }
fn decompress_images(sync: bool) -> Result<()> { fn decompress_images(sync: bool) -> Result<(), Box<dyn std::error::Error>> {
info!("Decompressing images"); info!("Decompressing images");
let mut threads = Vec::new(); let mut threads = Vec::new();
util::read_directory("/run/citadel/images", |dent| { util::read_directory("/run/citadel/images", |dent| {
@@ -140,9 +146,8 @@ fn decompress_images(sync: bool) -> Result<()> {
if image.is_compressed() { if image.is_compressed() {
if sync { if sync {
if let Err(err) = decompress_one_image_sync(image) { if let Err(err) = decompress_one_image_sync(image) {
warn!("Error: {}", err); warn!("Error decompressing image: {}", err);
} }
} else { } else {
threads.push(decompress_one_image(image)); threads.push(decompress_one_image(image));
} }
@@ -161,9 +166,11 @@ fn decompress_images(sync: bool) -> Result<()> {
Ok(()) Ok(())
} }
fn decompress_one_image_sync(image: ResourceImage) -> Result<()> { fn decompress_one_image_sync(
let start = Instant::now(); image: ResourceImage,
info!("Decompressing {}", image.path().display()); ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let start = Instant::now();
info!("Decompressing {}", image.path().display());
image.decompress(true) image.decompress(true)
.map_err(|e| format_err!("Failed to decompress image file {}: {}", image.path().display(), e))?; .map_err(|e| format_err!("Failed to decompress image file {}: {}", image.path().display(), e))?;
cmd!("/usr/bin/du", "-h {}", image.path().display())?; cmd!("/usr/bin/du", "-h {}", image.path().display())?;
@@ -173,8 +180,7 @@ fn decompress_one_image_sync(image: ResourceImage) -> Result<()> {
Ok(()) Ok(())
} }
fn decompress_one_image(image: ResourceImage) -> JoinHandle<Result<()>> { fn decompress_one_image(image: ResourceImage,) ->
thread::spawn(move || { JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> {
decompress_one_image_sync(image) thread::spawn(move || decompress_one_image_sync(image))
})
} }

View File

@@ -1,7 +1,6 @@
use std::fs; use std::fs;
use std::process::exit;
use libcitadel::{Result, ResourceImage, CommandLine, KeyRing, LogLevel, Logger, util}; use libcitadel::{ResourceImage, CommandLine, KeyRing, LogLevel, Logger, util};
use libcitadel::RealmManager; use libcitadel::RealmManager;
use crate::boot::disks::DiskPartition; use crate::boot::disks::DiskPartition;
use std::path::Path; use std::path::Path;
@@ -10,44 +9,40 @@ mod live;
mod disks; mod disks;
mod rootfs; mod rootfs;
pub fn main(args: Vec<String>) { pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::debug() { if CommandLine::debug() {
Logger::set_log_level(LogLevel::Debug); Logger::set_log_level(LogLevel::Debug);
} else if CommandLine::verbose() { } else if CommandLine::verbose() {
Logger::set_log_level(LogLevel::Info); Logger::set_log_level(LogLevel::Info);
} }
let result = match args.get(1) { match args.get(1) {
Some(s) if s == "rootfs" => do_rootfs(), Some(s) if s == "rootfs" => do_rootfs(),
Some(s) if s == "setup" => do_setup(), Some(s) if s == "setup" => do_setup(),
Some(s) if s == "boot-automount" => do_boot_automount(), Some(s) if s == "boot-automount" => do_boot_automount(),
Some(s) if s == "start-realms" => do_start_realms(), Some(s) if s == "start-realms" => do_start_realms(),
_ => Err(format_err!("Bad or missing argument").into()), _ => Err(format_err!("Bad or missing argument").into()),
}; }?;
if let Err(ref e) = result { Ok(())
warn!("Failed: {}", e);
exit(1);
}
} }
fn do_rootfs() -> Result<(), Box<dyn std::error::Error>> {
fn do_rootfs() -> Result<()> {
if CommandLine::live_mode() || CommandLine::install_mode() { if CommandLine::live_mode() || CommandLine::install_mode() {
live::live_rootfs() live::live_rootfs()
} else { } else {
rootfs::setup_rootfs() Ok(rootfs::setup_rootfs()?)
} }
} }
fn setup_keyring() -> Result<()> { fn setup_keyring() -> Result<(), Box<dyn std::error::Error>> {
ResourceImage::ensure_storage_mounted()?; ResourceImage::ensure_storage_mounted()?;
let keyring = KeyRing::load_with_cryptsetup_passphrase("/sysroot/storage/keyring")?; let keyring = KeyRing::load_with_cryptsetup_passphrase("/sysroot/storage/keyring")?;
keyring.add_keys_to_kernel()?; keyring.add_keys_to_kernel()?;
Ok(()) Ok(())
} }
fn do_setup() -> Result<()> { fn do_setup() -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::live_mode() || CommandLine::install_mode() { if CommandLine::live_mode() || CommandLine::install_mode() {
live::live_setup()?; live::live_setup()?;
} else if let Err(err) = setup_keyring() { } else if let Err(err) = setup_keyring() {
@@ -64,8 +59,7 @@ fn do_setup() -> Result<()> {
Ok(()) Ok(())
} }
fn mount_overlay() -> Result<(), Box<dyn std::error::Error>> {
fn mount_overlay() -> Result<()> {
info!("Creating rootfs overlay"); info!("Creating rootfs overlay");
info!("Moving /sysroot mount to /rootfs.ro"); info!("Moving /sysroot mount to /rootfs.ro");
@@ -89,13 +83,13 @@ fn mount_overlay() -> Result<()> {
Ok(()) Ok(())
} }
fn do_start_realms() -> Result<()> { fn do_start_realms() -> Result<(), Box<dyn std::error::Error>> {
let manager = RealmManager::load()?; let manager = RealmManager::load()?;
manager.start_boot_realms() Ok(manager.start_boot_realms()?)
} }
// Write automount unit for /boot partition // Write automount unit for /boot partition
fn do_boot_automount() -> Result<()> { fn do_boot_automount() -> Result<(), Box<dyn std::error::Error>> {
Logger::set_log_level(LogLevel::Info); Logger::set_log_level(LogLevel::Info);
if CommandLine::live_mode() || CommandLine::install_mode() { if CommandLine::live_mode() || CommandLine::install_mode() {
@@ -105,10 +99,10 @@ fn do_boot_automount() -> Result<()> {
let boot_partition = find_boot_partition()?; let boot_partition = find_boot_partition()?;
info!("Creating /boot automount units for boot partition {}", boot_partition); info!("Creating /boot automount units for boot partition {}", boot_partition);
cmd!("/usr/bin/systemd-mount", "-A --timeout-idle-sec=300 {} /boot", boot_partition) Ok(cmd!("/usr/bin/systemd-mount", "-A --timeout-idle-sec=300 {} /boot", boot_partition)?)
} }
fn find_boot_partition() -> Result<String> { fn find_boot_partition() -> Result<String, Box<dyn std::error::Error>> {
let loader_dev = read_loader_dev_efi_var()?; let loader_dev = read_loader_dev_efi_var()?;
let boot_partitions = DiskPartition::boot_partitions(true)? let boot_partitions = DiskPartition::boot_partitions(true)?
.into_iter() .into_iter()
@@ -116,7 +110,7 @@ fn find_boot_partition() -> Result<String> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if boot_partitions.len() != 1 { if boot_partitions.len() != 1 {
return Err(format_err!("Cannot uniquely determine boot partition")); return Result::Err("Cannot uniquely determine boot partition".into());
} }
Ok(boot_partitions[0].path().display().to_string()) Ok(boot_partitions[0].path().display().to_string())
@@ -141,7 +135,7 @@ fn matches_loader_dev(partition: &DiskPartition, dev: &Option<String>) -> bool {
const LOADER_EFI_VAR_PATH: &str = const LOADER_EFI_VAR_PATH: &str =
"/sys/firmware/efi/efivars/LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; "/sys/firmware/efi/efivars/LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
fn read_loader_dev_efi_var() -> Result<Option<String>> { fn read_loader_dev_efi_var() -> Result<Option<String>, Box<dyn std::error::Error>> {
let efi_var = Path::new(LOADER_EFI_VAR_PATH); let efi_var = Path::new(LOADER_EFI_VAR_PATH);
if efi_var.exists() { if efi_var.exists() {
let s = fs::read(efi_var) let s = fs::read(efi_var)

View File

@@ -1,10 +1,10 @@
use std::path::Path; use std::path::Path;
use std::process::{Command,Stdio}; use std::process::{Command,Stdio};
use libcitadel::{BlockDev, ResourceImage, CommandLine, ImageHeader, Partition, Result, LoopDevice}; use libcitadel::{BlockDev, ResourceImage, CommandLine, ImageHeader, Partition, LoopDevice};
use libcitadel::verity::Verity; use libcitadel::verity::Verity;
pub fn setup_rootfs() -> Result<()> { pub fn setup_rootfs() -> Result<(), Box<dyn std::error::Error>> {
let mut p = choose_boot_partiton(true, CommandLine::revert_rootfs())?; let mut p = choose_boot_partiton(true, CommandLine::revert_rootfs())?;
if CommandLine::noverity() { if CommandLine::noverity() {
setup_partition_unverified(&p) setup_partition_unverified(&p)
@@ -13,7 +13,7 @@ pub fn setup_rootfs() -> Result<()> {
} }
} }
pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<()> { pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::noverity() { if CommandLine::noverity() {
setup_resource_unverified(&rootfs) setup_resource_unverified(&rootfs)
} else { } else {
@@ -21,7 +21,7 @@ pub fn setup_rootfs_resource(rootfs: &ResourceImage) -> Result<()> {
} }
} }
fn setup_resource_unverified(img: &ResourceImage) -> Result<()> { fn setup_resource_unverified(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if img.is_compressed() { if img.is_compressed() {
img.decompress(false)?; img.decompress(false)?;
} }
@@ -30,25 +30,31 @@ fn setup_resource_unverified(img: &ResourceImage) -> Result<()> {
setup_linear_mapping(loopdev.device()) setup_linear_mapping(loopdev.device())
} }
fn setup_resource_verified(img: &ResourceImage) -> Result<()> { fn setup_resource_verified(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
let _ = img.setup_verity_device()?; let _ = img.setup_verity_device()?;
Ok(()) Ok(())
} }
fn setup_partition_unverified(p: &Partition) -> Result<()> { fn setup_partition_unverified(p: &Partition) -> Result<(), Box<dyn std::error::Error>> {
info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)"); info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)");
setup_linear_mapping(p.path()) setup_linear_mapping(p.path())
} }
fn setup_partition_verified(p: &mut Partition) -> Result<()> { fn setup_partition_verified(p: &mut Partition) -> Result<(), Box<dyn std::error::Error>> {
info!("Creating /dev/mapper/rootfs dm-verity device"); info!("Creating /dev/mapper/rootfs dm-verity device");
if !CommandLine::nosignatures() { if !CommandLine::nosignatures() {
if !p.has_public_key() { if !p.has_public_key() {
bail!("no public key available for channel {}", p.metainfo().channel()) return Result::Err(
format!(
"no public key available for channel {}",
p.metainfo().channel()
)
.into(),
);
} }
if !p.is_signature_valid() { if !p.is_signature_valid() {
p.write_status(ImageHeader::STATUS_BAD_SIG)?; p.write_status(ImageHeader::STATUS_BAD_SIG)?;
bail!("signature verification failed on partition"); return Result::Err("signature verification failed on partition".into());
} }
info!("Image signature is valid for channel {}", p.metainfo().channel()); info!("Image signature is valid for channel {}", p.metainfo().channel());
} }
@@ -56,7 +62,7 @@ fn setup_partition_verified(p: &mut Partition) -> Result<()> {
Ok(()) Ok(())
} }
fn setup_linear_mapping(blockdev: &Path) -> Result<()> { fn setup_linear_mapping(blockdev: &Path) -> Result<(), Box<dyn std::error::Error>> {
let dev = BlockDev::open_ro(blockdev)?; let dev = BlockDev::open_ro(blockdev)?;
let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display()); let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display());
@@ -70,7 +76,9 @@ fn setup_linear_mapping(blockdev: &Path) -> Result<()> {
.success(); .success();
if !ok { if !ok {
bail!("failed to set up linear identity mapping with /usr/sbin/dmsetup"); return Result::Err(
"failed to set up linear identity mapping with /usr/sbin/dmsetup".into(),
);
} }
Ok(()) Ok(())
} }
@@ -94,7 +102,8 @@ fn choose_revert_partition(best: Option<Partition>) -> Option<Partition> {
best best
} }
fn choose_boot_partiton(scan: bool, revert_rootfs: bool) -> Result<Partition> { fn choose_boot_partiton(scan: bool, revert_rootfs: bool,
) -> Result<Partition, Box<dyn std::error::Error>> {
let mut partitions = Partition::rootfs_partitions()?; let mut partitions = Partition::rootfs_partitions()?;
if scan { if scan {
@@ -136,16 +145,17 @@ fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partiti
} }
// Compare versions and channels // Compare versions and channels
let bind_a = a.metainfo(); let meta_a = a.metainfo();
let bind_b = b.metainfo(); let meta_b = b.metainfo();
let a_v = bind_a.version();
let b_v = bind_b.version(); let ver_a = meta_a.version();
let ver_b = meta_b.version();
// Compare versions only if channels match // Compare versions only if channels match
if a.metainfo().channel() == b.metainfo().channel() { if a.metainfo().channel() == b.metainfo().channel() {
if a_v > b_v { if ver_a > ver_b {
return Some(a); return Some(a);
} else if b_v > a_v { } else if ver_b > ver_a {
return Some(b); return Some(b);
} }
} }

View File

@@ -1,97 +1,88 @@
use std::path::Path; use std::path::Path;
use std::process::exit; use std::process::exit;
use clap::{Arg,ArgMatches};
use clap::{command, ArgAction, Command}; use clap::{App,Arg,SubCommand,ArgMatches};
use clap::AppSettings::*;
use libcitadel::{ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
use hex; use hex;
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util}; pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let app = App::new("citadel-image")
pub fn main() {
let matches = command!()
.about("Citadel update image builder") .about("Citadel update image builder")
.arg_required_else_help(true) .settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
.disable_help_subcommand(true)
.subcommand(
Command::new("metainfo")
.about("Display metainfo variables for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("info")
.about("Display metainfo variables for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("generate-verity")
.about("Generate dm-verity hash tree for an image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("verify")
.about("Verify dm-verity hash tree for an image file")
.arg(
Arg::new("option")
.long("option")
.required(true)
.help("Path to image file"),
),
)
.subcommand(
Command::new("install-rootfs")
.about("Install rootfs image file to a partition")
.arg(
Arg::new("choose")
.long("just-choose")
.action(ArgAction::SetTrue)
.help("Don't install anything, just show which partition would be chosen"),
)
.arg(
Arg::new("skip-sha")
.long("skip-sha")
.action(ArgAction::SetTrue)
.help("Skip verification of header sha256 value"),
)
.arg(
Arg::new("no-prefer")
.long("no-prefer")
.action(ArgAction::SetTrue)
.help("Don't set PREFER_BOOT flag"),
)
.arg(
Arg::new("path")
.required_unless_present("choose")
.help("Path to image file"),
),
)
.subcommand(Command::new("genkeys").about("Generate a pair of keys"))
.subcommand(
Command::new("decompress")
.about("Decompress a compressed image file")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.subcommand(
Command::new("bless")
.about("Mark currently mounted rootfs partition as successfully booted"),
)
.subcommand(
Command::new("verify-shasum")
.about("Verify the sha256 sum of the image")
.arg(Arg::new("path").required(true).help("Path to image file")),
)
.get_matches();
.subcommand(SubCommand::with_name("metainfo")
.about("Display metainfo variables for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("info")
.about("Display metainfo variables for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("generate-verity")
.about("Generate dm-verity hash tree for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("verify")
.about("Verify dm-verity hash tree for an image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("install-rootfs")
.about("Install rootfs image file to a partition")
.arg(Arg::with_name("choose")
.long("just-choose")
.help("Don't install anything, just show which partition would be chosen"))
.arg(Arg::with_name("skip-sha")
.long("skip-sha")
.help("Skip verification of header sha256 value"))
.arg(Arg::with_name("no-prefer")
.long("no-prefer")
.help("Don't set PREFER_BOOT flag"))
.arg(Arg::with_name("path")
.required_unless("choose")
.help("Path to image file")))
.subcommand(SubCommand::with_name("genkeys")
.about("Generate a pair of keys"))
.subcommand(SubCommand::with_name("decompress")
.about("Decompress a compressed image file")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")))
.subcommand(SubCommand::with_name("bless")
.about("Mark currently mounted rootfs partition as successfully booted"))
.subcommand(SubCommand::with_name("verify-shasum")
.about("Verify the sha256 sum of the image")
.arg(Arg::with_name("path")
.required(true)
.help("Path to image file")));
Logger::set_log_level(LogLevel::Debug);
let matches = app.get_matches_from(args);
let result = match matches.subcommand() { let result = match matches.subcommand() {
Some(("metainfo", sub_m)) => metainfo(sub_m), ("metainfo", Some(m)) => metainfo(m),
Some(("info", sub_m)) => info(sub_m), ("info", Some(m)) => info(m),
Some(("generate-verity", sub_m)) => generate_verity(sub_m), ("generate-verity", Some(m)) => generate_verity(m),
Some(("verify", sub_m)) => verify(sub_m), ("verify", Some(m)) => verify(m),
Some(("sign-image", sub_m)) => sign_image(sub_m), ("sign-image", Some(m)) => sign_image(m),
Some(("genkeys", _)) => genkeys(), ("genkeys", Some(_)) => genkeys(),
Some(("decompress", sub_m)) => decompress(sub_m), ("decompress", Some(m)) => decompress(m),
Some(("verify-shasum", sub_m)) => verify_shasum(sub_m), ("verify-shasum", Some(m)) => verify_shasum(m),
Some(("install-rootfs", sub_m)) => install_rootfs(sub_m), ("install-rootfs", Some(m)) => install_rootfs(m),
Some(("install", sub_m)) => install_image(sub_m), ("install", Some(m)) => install_image(m),
Some(("bless", _)) => bless(), ("bless", Some(_)) => bless(),
_ => Ok(()), _ => Ok(()),
}; };
@@ -99,16 +90,18 @@ pub fn main() {
println!("Error: {}", e); println!("Error: {}", e);
exit(1); exit(1);
} }
Ok(())
} }
fn info(arg_matches: &ArgMatches) -> Result<()> { fn info(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?; let img = load_image(arg_matches)?;
print!("{}",String::from_utf8(img.header().metainfo_bytes())?); print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
info_signature(&img)?; info_signature(&img)?;
Ok(()) Ok(())
} }
fn info_signature(img: &ResourceImage) -> Result<()> { fn info_signature(img: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if img.header().has_signature() { if img.header().has_signature() {
println!("Signature: {}", hex::encode(&img.header().signature())); println!("Signature: {}", hex::encode(&img.header().signature()));
} else { } else {
@@ -124,15 +117,15 @@ fn info_signature(img: &ResourceImage) -> Result<()> {
}, },
None => { println!("No public key found for channel '{}'", img.metainfo().channel()) }, None => { println!("No public key found for channel '{}'", img.metainfo().channel()) },
} }
Ok(()) Ok(())
} }
fn metainfo(arg_matches: &ArgMatches) -> Result<()> { fn metainfo(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?; let img = load_image(arg_matches)?;
print!("{}",String::from_utf8(img.header().metainfo_bytes())?); print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
Ok(()) Ok(())
} }
fn generate_verity(arg_matches: &ArgMatches) -> Result<()> { fn generate_verity(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?; let img = load_image(arg_matches)?;
if img.has_verity_hashtree() { if img.has_verity_hashtree() {
info!("Image already has dm-verity hashtree appended, doing nothing."); info!("Image already has dm-verity hashtree appended, doing nothing.");
@@ -142,7 +135,7 @@ fn generate_verity(arg_matches: &ArgMatches) -> Result<()> {
Ok(()) Ok(())
} }
fn verify(arg_matches: &ArgMatches) -> Result<()> { fn verify(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?; let img = load_image(arg_matches)?;
let ok = img.verify_verity()?; let ok = img.verify_verity()?;
if ok { if ok {
@@ -153,7 +146,7 @@ fn verify(arg_matches: &ArgMatches) -> Result<()> {
Ok(()) Ok(())
} }
fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> { fn verify_shasum(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?; let img = load_image(arg_matches)?;
let shasum = img.generate_shasum()?; let shasum = img.generate_shasum()?;
if shasum == img.metainfo().shasum() { if shasum == img.metainfo().shasum() {
@@ -166,39 +159,37 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
Ok(()) Ok(())
} }
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> { fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage, Box<dyn std::error::Error>> {
let path = arg_matches.get_one::<String>("path") let path = arg_matches.value_of("path").expect("path argument missing");
.expect("path argument missing");
if !Path::new(path).exists() { if !Path::new(path).exists() {
bail!("Cannot load image {}: File does not exist", path); panic!("Cannot load image {}: File does not exist", path);
} }
let img = ResourceImage::from_path(path)?; let img = ResourceImage::from_path(path)?;
if !img.is_valid_image() { if !img.is_valid_image() {
bail!("File {} is not a valid image file", path); panic!("File {} is not a valid image file", path);
} }
Ok(img) Ok(img)
} }
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> { fn install_rootfs(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
if arg_matches.get_flag("choose") { if arg_matches.is_present("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.get_flag("skip-sha") { if !arg_matches.is_present("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() {
bail!("image file does not have expected sha256 value"); panic!("image file does not have expected sha256 value");
} }
} }
let partition = choose_install_partition(true)?; let partition = choose_install_partition(true)?;
if !arg_matches.get_flag("no-prefer") { if !arg_matches.is_present("no-prefer") {
clear_prefer_boot()?; clear_prefer_boot()?;
img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT); img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
} }
@@ -206,7 +197,7 @@ fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
Ok(()) Ok(())
} }
fn clear_prefer_boot() -> Result<()> { fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
for mut p in Partition::rootfs_partitions()? { for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) { if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?; p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
@@ -215,16 +206,14 @@ fn clear_prefer_boot() -> Result<()> {
Ok(()) Ok(())
} }
fn sign_image(arg_matches: &ArgMatches) -> Result<()> { fn sign_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let _img = load_image(arg_matches)?; let _img = load_image(arg_matches)?;
info!("Not implemented yet"); info!("Not implemented yet");
Ok(()) Ok(())
} }
fn install_image(arg_matches: &ArgMatches) -> Result<()> { fn install_image(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let source = arg_matches.get_one::<String>("path") let source = arg_matches.value_of("path").expect("path argument missing");
.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();
@@ -232,12 +221,12 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
// XXX verify signature? // XXX verify signature?
if !(metainfo.image_type() == "kernel" || metainfo.image_type() == "extra") { if !(metainfo.image_type() == "kernel" || metainfo.image_type() == "extra") {
bail!("Cannot install image type {}", metainfo.image_type()); panic!("Cannot install image type {}", metainfo.image_type());
} }
let shasum = img.generate_shasum()?; let shasum = img.generate_shasum()?;
if shasum != img.metainfo().shasum() { if shasum != img.metainfo().shasum() {
bail!("Image shasum does not match metainfo"); panic!("Image shasum does not match metainfo");
} }
img.generate_verity_hashtree()?; img.generate_verity_hashtree()?;
@@ -245,44 +234,49 @@ fn install_image(arg_matches: &ArgMatches) -> Result<()> {
let filename = if metainfo.image_type() == "kernel" { let filename = if metainfo.image_type() == "kernel" {
let kernel_version = match metainfo.kernel_version() { let kernel_version = match metainfo.kernel_version() {
Some(version) => version, Some(version) => version,
None => bail!("Kernel image does not have a kernel version field in metainfo"), None => panic!("Kernel image does not have a kernel version field in metainfo"),
}; };
if kernel_version.chars().any(|c| c == '/') { if kernel_version.chars().any(|c| c == '/') {
bail!("Kernel version field has / char"); panic!("Kernel version field has / char");
} }
format!("citadel-kernel-{}-{}.img", kernel_version, metainfo.version()) format!("citadel-kernel-{}-{:03}.img", kernel_version, metainfo.version())
} else { } else {
format!("citadel-extra-{}.img", metainfo.version()) format!("citadel-extra-{:03}.img", metainfo.version())
}; };
if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) { if !metainfo.channel().chars().all(|c| c.is_ascii_lowercase()) {
bail!("Refusing to build path from strange channel name {}", metainfo.channel()); panic!(
"Refusing to build path from strange channel name {}",
metainfo.channel()
);
} }
let image_dir = Path::new("/storage/resources").join(metainfo.channel()); let image_dir = Path::new("/storage/resources").join(metainfo.channel());
let image_dest = image_dir.join(filename); let image_dest = image_dir.join(filename);
if image_dest.exists() { if image_dest.exists() {
rotate(&image_dest)?; rotate(&image_dest)?;
} }
util::rename(source, &image_dest) util::rename(source, &image_dest)?;
Ok(())
} }
fn rotate(path: &Path) -> Result<()> { fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() || path.file_name().is_none() { if !path.exists() || path.file_name().is_none() {
return Ok(()); return Ok(());
} }
let filename = path.file_name().unwrap(); let filename = path.file_name().unwrap();
let dot_zero = path.with_file_name(format!("{}.0", filename.to_string_lossy())); let dot_zero = path.with_file_name(format!("{}.0", filename.to_string_lossy()));
util::remove_file(&dot_zero)?; util::remove_file(&dot_zero)?;
util::rename(path, &dot_zero) util::rename(path, &dot_zero).unwrap();
Ok(())
} }
fn genkeys() -> Result<()> { fn genkeys() -> Result<(), Box<dyn std::error::Error>> {
let keypair = KeyPair::generate(); let keypair = KeyPair::generate();
println!("keypair = \"{}\"", keypair.to_hex()); println!("keypair = \"{}\"", keypair.to_hex());
Ok(()) Ok(())
} }
fn decompress(arg_matches: &ArgMatches) -> Result<()> { fn decompress(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = load_image(arg_matches)?; let img = load_image(arg_matches)?;
if !img.is_compressed() { if !img.is_compressed() {
info!("Image is not compressed, not decompressing."); info!("Image is not compressed, not decompressing.");
@@ -292,7 +286,7 @@ fn decompress(arg_matches: &ArgMatches) -> Result<()> {
Ok(()) Ok(())
} }
fn bless() -> Result<()> { fn bless() -> Result<(), Box<dyn std::error::Error>> {
for mut p in Partition::rootfs_partitions()? { for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.is_mounted() { if p.is_initialized() && p.is_mounted() {
p.bless()?; p.bless()?;
@@ -311,7 +305,7 @@ fn bool_to_yesno(val: bool) -> &'static str {
} }
} }
fn choose_install_partition(verbose: bool) -> Result<Partition> { fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::error::Error>> {
let partitions = Partition::rootfs_partitions()?; let partitions = Partition::rootfs_partitions()?;
if verbose { if verbose {
@@ -326,9 +320,12 @@ fn choose_install_partition(verbose: bool) -> Result<Partition> {
for p in &partitions { for p in &partitions {
if !p.is_mounted() && !p.is_initialized() { if !p.is_mounted() && !p.is_initialized() {
if verbose { if verbose {
info!("Choosing {} because it is empty and not mounted", p.path().display()); info!(
"Choosing {} because it is empty and not mounted",
p.path().display()
);
} }
return Ok(p.clone()) return Ok(p.clone());
} }
} }
for p in &partitions { for p in &partitions {
@@ -336,10 +333,10 @@ fn choose_install_partition(verbose: bool) -> Result<Partition> {
if verbose { if verbose {
info!("Choosing {} because it is not mounted", p.path().display()); info!("Choosing {} because it is not mounted", p.path().display());
info!("Header metainfo:"); info!("Header metainfo:");
print!("{}",String::from_utf8(p.header().metainfo_bytes())?); print!("{}", String::from_utf8(p.header().metainfo_bytes())?);
} }
return Ok(p.clone()) return Ok(p.clone());
} }
} }
bail!("No suitable install partition found") panic!("No suitable install partition found")
} }

View File

@@ -1,6 +1,5 @@
use std::io::{self,Write}; use std::io::{self,Write};
use std::path::Path; use std::path::Path;
use libcitadel::Result;
use super::disk::Disk; use super::disk::Disk;
use rpassword; use rpassword;
use crate::install::installer::Installer; use crate::install::installer::Installer;
@@ -8,7 +7,7 @@ use crate::install::installer::Installer;
const CITADEL_PASSPHRASE_PROMPT: &str = "Enter a password for the Citadel user (or 'q' to quit)"; const CITADEL_PASSPHRASE_PROMPT: &str = "Enter a password for the Citadel user (or 'q' to quit)";
const LUKS_PASSPHRASE_PROMPT: &str = "Enter a disk encryption passphrase (or 'q' to quit"; const LUKS_PASSPHRASE_PROMPT: &str = "Enter a disk encryption passphrase (or 'q' to quit";
pub fn run_cli_install() -> Result<bool> { pub fn run_cli_install() -> Result<bool, Box<dyn std::error::Error>> {
let disk = match choose_disk()? { let disk = match choose_disk()? {
Some(disk) => disk, Some(disk) => disk,
None => return Ok(false), None => return Ok(false),
@@ -33,7 +32,7 @@ pub fn run_cli_install() -> Result<bool> {
Ok(true) Ok(true)
} }
pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool> { pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool, Box<dyn std::error::Error>> {
let disk = find_disk_by_path(target.as_ref())?; let disk = find_disk_by_path(target.as_ref())?;
display_disk(&disk); display_disk(&disk);
@@ -55,11 +54,15 @@ pub fn run_cli_install_with<P: AsRef<Path>>(target: P) -> Result<bool> {
Ok(true) Ok(true)
} }
fn run_install(disk: Disk, citadel_passphrase: String, passphrase: String) -> Result<()> { fn run_install(
disk: Disk,
citadel_passphrase: String,
passphrase: String,
) -> Result<(), Box<dyn std::error::Error>> {
let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase); let mut install = Installer::new(disk.path(), &citadel_passphrase, &passphrase);
install.set_install_syslinux(true); install.set_install_syslinux(true);
install.verify()?; install.verify()?;
install.run() Ok(install.run()?)
} }
fn display_disk(disk: &Disk) { fn display_disk(disk: &Disk) {
@@ -70,22 +73,22 @@ fn display_disk(disk: &Disk) {
println!(); println!();
} }
fn find_disk_by_path(path: &Path) -> Result<Disk> { fn find_disk_by_path(path: &Path) -> Result<Disk, Box<dyn std::error::Error>> {
if !path.exists() { if !path.exists() {
bail!("Target disk path {} does not exist", path.display()); panic!("Target disk path {} does not exist", path.display());
} }
for disk in Disk::probe_all()? { for disk in Disk::probe_all()? {
if disk.path() == path { if disk.path() == path {
return Ok(disk.clone()); return Ok(disk.clone());
} }
} }
bail!("installation target {} is not a valid disk", path.display()) panic!("installation target {} is not a valid disk", path.display())
} }
fn choose_disk() -> Result<Option<Disk>> { fn choose_disk() -> Result<Option<Disk>, Box<dyn std::error::Error>> {
let disks = Disk::probe_all()?; let disks = Disk::probe_all()?;
if disks.is_empty() { if disks.is_empty() {
bail!("no disks found."); panic!("no disks found.");
} }
loop { loop {
@@ -96,7 +99,7 @@ fn choose_disk() -> Result<Option<Disk>> {
} }
if let Ok(n) = line.parse::<usize>() { if let Ok(n) = line.parse::<usize>() {
if n > 0 && n <= disks.len() { if n > 0 && n <= disks.len() {
return Ok(Some(disks[n-1].clone())); return Ok(Some(disks[n - 1].clone()));
} }
} }
} }
@@ -111,7 +114,7 @@ fn prompt_choose_disk(disks: &[Disk]) {
let _ = io::stdout().flush(); let _ = io::stdout().flush();
} }
fn read_line() -> Result<String> { fn read_line() -> Result<String, Box<dyn std::error::Error>> {
let mut input = String::new(); let mut input = String::new();
io::stdin().read_line(&mut input) io::stdin().read_line(&mut input)
.map_err(context!("error reading line from stdin"))?; .map_err(context!("error reading line from stdin"))?;
@@ -125,7 +128,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
loop { loop {
println!("{}", prompt); println!("{}", prompt);
println!(); println!();
let passphrase = rpassword::prompt_password(" Passphrase : ")?; let passphrase = rpassword::read_password_from_tty(Some(" Passphrase : "))?;
if passphrase.is_empty() { if passphrase.is_empty() {
println!("Passphrase cannot be empty"); println!("Passphrase cannot be empty");
continue; continue;
@@ -133,7 +136,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::prompt_password(" Confirm : ")?; let confirm = rpassword::read_password_from_tty(Some(" Confirm : "))?;
if confirm == "q" || confirm == "Q" { if confirm == "q" || confirm == "Q" {
return Ok(None); return Ok(None);
} }
@@ -146,7 +149,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
} }
} }
fn confirm_install(disk: &Disk) -> Result<bool> { fn confirm_install(disk: &Disk) -> Result<bool, Box<dyn std::error::Error>> {
println!("Are you sure you want to completely erase this this device?"); println!("Are you sure you want to completely erase this this device?");
println!(); println!();
println!(" Device: {}", disk.path().display()); println!(" Device: {}", disk.path().display());
@@ -158,4 +161,3 @@ fn confirm_install(disk: &Disk) -> Result<bool> {
let answer = read_line()?; let answer = read_line()?;
Ok(answer == "YES") Ok(answer == "YES")
} }

View File

@@ -9,7 +9,6 @@ use pwhash::sha512_crypt;
use libcitadel::util; use libcitadel::util;
use libcitadel::RealmFS; use libcitadel::RealmFS;
use libcitadel::Result;
use libcitadel::OsRelease; use libcitadel::OsRelease;
use libcitadel::KeyRing; use libcitadel::KeyRing;
use libcitadel::terminal::Base16Scheme; use libcitadel::terminal::Base16Scheme;
@@ -183,7 +182,7 @@ impl Installer {
self.install_syslinux = val; self.install_syslinux = val;
} }
pub fn verify(&self) -> Result<()> { pub fn verify(&self) -> Result<(), Box<dyn std::error::Error>> {
let kernel_img = self.kernel_imagename(); let kernel_img = self.kernel_imagename();
let bzimage = format!("bzImage-{}", self.kernel_version()); let bzimage = format!("bzImage-{}", self.kernel_version());
let artifacts = vec![ let artifacts = vec![
@@ -192,26 +191,29 @@ impl Installer {
]; ];
if !self.target().exists() { if !self.target().exists() {
bail!("target device {:?} does not exist", self.target()); panic!("target device {:?} does not exist", self.target());
} }
for a in artifacts { for a in artifacts {
if !self.artifact_path(a).exists() { if !self.artifact_path(a).exists() {
bail!("required install artifact {} does not exist in {}", a, self.artifact_directory); panic!(
"required install artifact {} does not exist in {}",
a, self.artifact_directory
);
} }
} }
Ok(()) Ok(())
} }
pub fn run(&self) -> Result<()> { pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
match self._type { match self._type {
InstallType::Install => self.run_install(), InstallType::Install => self.run_install(),
InstallType::LiveSetup => self.run_live_setup(), InstallType::LiveSetup => self.run_live_setup(),
} }
} }
pub fn run_install(&self) -> Result<()> { pub fn run_install(&self) -> Result<(), Box<dyn std::error::Error>> {
let start = Instant::now(); let start = Instant::now();
self.partition_disk()?; self.partition_disk()?;
self.setup_luks()?; self.setup_luks()?;
@@ -224,7 +226,7 @@ impl Installer {
Ok(()) Ok(())
} }
pub fn run_live_setup(&self) -> Result<()> { pub fn run_live_setup(&self) -> Result<(), Box<dyn std::error::Error>> {
self.cmd_list(&[ self.cmd_list(&[
"/bin/mount -t tmpfs var-tmpfs /sysroot/var", "/bin/mount -t tmpfs var-tmpfs /sysroot/var",
"/bin/mount -t tmpfs home-tmpfs /sysroot/home", "/bin/mount -t tmpfs home-tmpfs /sysroot/home",
@@ -242,8 +244,7 @@ impl Installer {
Ok(()) Ok(())
} }
fn setup_live_realm(&self) -> Result<()> { fn setup_live_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
let realmfs_dir = self.storage().join("realms/realmfs-images"); let realmfs_dir = self.storage().join("realms/realmfs-images");
let base_realmfs = realmfs_dir.join("base-realmfs.img"); let base_realmfs = realmfs_dir.join("base-realmfs.img");
@@ -261,14 +262,14 @@ impl Installer {
Ok(()) Ok(())
} }
pub fn partition_disk(&self) -> Result<()> { pub fn partition_disk(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Partitioning target disk")?; self.header("Partitioning target disk")?;
self.cmd_list(PARTITION_COMMANDS, &[ self.cmd_list(PARTITION_COMMANDS, &[
("$TARGET", self.target_str()) ("$TARGET", self.target_str())
]) ])
} }
pub fn setup_luks(&self) -> Result<()> { pub fn setup_luks(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up LUKS disk encryption")?; self.header("Setting up LUKS disk encryption")?;
util::create_dir(INSTALL_MOUNT)?; util::create_dir(INSTALL_MOUNT)?;
util::write_file(LUKS_PASSPHRASE_FILE, self.passphrase().as_bytes())?; util::write_file(LUKS_PASSPHRASE_FILE, self.passphrase().as_bytes())?;
@@ -281,15 +282,16 @@ impl Installer {
("$LUKS_PASSFILE", LUKS_PASSPHRASE_FILE), ("$LUKS_PASSFILE", LUKS_PASSPHRASE_FILE),
])?; ])?;
util::remove_file(LUKS_PASSPHRASE_FILE) util::remove_file(LUKS_PASSPHRASE_FILE)?;
Ok(())
} }
pub fn setup_lvm(&self) -> Result<()> { pub fn setup_lvm(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up LVM volumes")?; self.header("Setting up LVM volumes")?;
self.cmd_list(LVM_COMMANDS, &[]) self.cmd_list(LVM_COMMANDS, &[])
} }
pub fn setup_boot(&self) -> Result<()> { pub fn setup_boot(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up /boot partition")?; self.header("Setting up /boot partition")?;
let boot_partition = self.target_partition(1); let boot_partition = self.target_partition(1);
self.cmd(format!("/sbin/mkfs.vfat -F 32 {}", boot_partition))?; self.cmd(format!("/sbin/mkfs.vfat -F 32 {}", boot_partition))?;
@@ -323,11 +325,11 @@ impl Installer {
Ok(()) Ok(())
} }
fn setup_syslinux(&self) -> Result<()> { fn setup_syslinux(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Installing syslinux")?; self.header("Installing syslinux")?;
let syslinux_src = self.artifact_path("syslinux"); let syslinux_src = self.artifact_path("syslinux");
if !syslinux_src.exists() { if !syslinux_src.exists() {
bail!("no syslinux directory found in artifact directory, cannot install syslinux"); panic!("no syslinux directory found in artifact directory, cannot install syslinux");
} }
let dst = Path::new(INSTALL_MOUNT).join("syslinux"); let dst = Path::new(INSTALL_MOUNT).join("syslinux");
util::create_dir(&dst)?; util::create_dir(&dst)?;
@@ -345,17 +347,17 @@ impl Installer {
self.cmd(format!("/sbin/extlinux --install {}", dst.display())) self.cmd(format!("/sbin/extlinux --install {}", dst.display()))
} }
fn setup_syslinux_post_umount(&self) -> Result<()> { fn setup_syslinux_post_umount(&self) -> Result<(), Box<dyn std::error::Error>> {
let mbrbin = self.artifact_path("syslinux/gptmbr.bin"); let mbrbin = self.artifact_path("syslinux/gptmbr.bin");
if !mbrbin.exists() { if !mbrbin.exists() {
bail!("could not find MBR image: {:?}", mbrbin); panic!("could not find MBR image: {:?}", mbrbin);
} }
self.cmd(format!("/bin/dd bs=440 count=1 conv=notrunc if={} of={}", mbrbin.display(), self.target().display()))?; self.cmd(format!("/bin/dd bs=440 count=1 conv=notrunc if={} of={}", mbrbin.display(), self.target().display()))?;
self.cmd(format!("/sbin/parted -s {} set 1 legacy_boot on", self.target_str())) self.cmd(format!("/sbin/parted -s {} set 1 legacy_boot on", self.target_str()))
} }
pub fn create_storage(&self) -> Result<()> { pub fn create_storage(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Setting up /storage partition")?; self.header("Setting up /storage partition")?;
self.cmd_list(CREATE_STORAGE_COMMANDS, self.cmd_list(CREATE_STORAGE_COMMANDS,
@@ -365,7 +367,7 @@ impl Installer {
self.cmd(format!("/bin/umount {}", INSTALL_MOUNT)) self.cmd(format!("/bin/umount {}", INSTALL_MOUNT))
} }
fn setup_storage(&self) -> Result<()> { fn setup_storage(&self) -> Result<(), Box<dyn std::error::Error>> {
if self._type == InstallType::Install { if self._type == InstallType::Install {
self.create_keyring()?; self.create_keyring()?;
self.setup_storage_resources()?; self.setup_storage_resources()?;
@@ -389,33 +391,33 @@ impl Installer {
Ok(()) Ok(())
} }
fn create_keyring(&self) -> Result<()> { fn create_keyring(&self) -> Result<(), Box<dyn std::error::Error>> {
self.info("Creating initial keyring")?; self.info("Creating initial keyring")?;
let keyring = KeyRing::create_new(); let keyring = KeyRing::create_new();
keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap()) Ok(keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap())?)
} }
fn setup_base_realmfs(&self) -> Result<(), Box<dyn std::error::Error>> {
fn setup_base_realmfs(&self) -> Result<()> {
let realmfs_dir = self.storage().join("realms/realmfs-images"); let realmfs_dir = self.storage().join("realms/realmfs-images");
util::create_dir(&realmfs_dir)?; util::create_dir(&realmfs_dir)?;
self.sparse_copy_artifact("base-realmfs.img", &realmfs_dir)?; self.sparse_copy_artifact("base-realmfs.img", &realmfs_dir)?;
self.cmd(format!("/usr/bin/citadel-image decompress {}/base-realmfs.img", realmfs_dir.display())) self.cmd(format!("/usr/bin/citadel-image decompress {}/base-realmfs.img", realmfs_dir.display()))
} }
fn setup_realm_skel(&self) -> Result<()> { fn setup_realm_skel(&self) -> Result<(), Box<dyn std::error::Error>> {
let realm_skel = self.storage().join("realms/skel"); let realm_skel = self.storage().join("realms/skel");
util::create_dir(&realm_skel)?; util::create_dir(&realm_skel)?;
util::copy_tree_with_chown(&self.skel(), &realm_skel, (1000,1000)) util::copy_tree_with_chown(&self.skel(), &realm_skel, (1000, 1000))?;
Ok(())
} }
fn create_realmlock(&self, dir: &Path) -> Result<()> { fn create_realmlock(&self, dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
fs::File::create(dir.join(".realmlock")) fs::File::create(dir.join(".realmlock"))
.map_err(context!("failed to create {:?}/.realmlock file", dir))?; .map_err(context!("failed to create {:?}/.realmlock file", dir))?;
Ok(()) Ok(())
} }
fn setup_main_realm(&self) -> Result<()> { fn setup_main_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Creating main realm")?; self.header("Creating main realm")?;
let realm = self.storage().join("realms/realm-main"); let realm = self.storage().join("realms/realm-main");
@@ -440,7 +442,7 @@ impl Installer {
self.create_realmlock(&realm) self.create_realmlock(&realm)
} }
fn setup_apt_cacher_realm(&self) -> Result<()> { fn setup_apt_cacher_realm(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Creating apt-cacher realm")?; self.header("Creating apt-cacher realm")?;
let realm_base = self.storage().join("realms/realm-apt-cacher"); let realm_base = self.storage().join("realms/realm-apt-cacher");
@@ -460,7 +462,7 @@ impl Installer {
self.create_realmlock(&realm_base) self.create_realmlock(&realm_base)
} }
fn setup_storage_resources(&self) -> Result<()> { fn setup_storage_resources(&self) -> Result<(), Box<dyn std::error::Error>> {
let channel = match OsRelease::citadel_channel() { let channel = match OsRelease::citadel_channel() {
Some(channel) => channel, Some(channel) => channel,
None => "dev", None => "dev",
@@ -474,7 +476,7 @@ impl Installer {
self.sparse_copy_artifact(&kernel_img, &resources) self.sparse_copy_artifact(&kernel_img, &resources)
} }
fn setup_citadel_passphrase(&self) -> Result<()> { fn setup_citadel_passphrase(&self) -> Result<(), Box<dyn std::error::Error>> {
if self._type == InstallType::LiveSetup { if self._type == InstallType::LiveSetup {
self.info("Creating temporary citadel passphrase file for live mode")?; self.info("Creating temporary citadel passphrase file for live mode")?;
let path = self.storage().join("citadel-state/passwd"); let path = self.storage().join("citadel-state/passwd");
@@ -497,17 +499,15 @@ impl Installer {
Ok(()) Ok(())
} }
pub fn install_rootfs_partitions(&self) -> Result<()> { pub fn install_rootfs_partitions(&self) -> Result<(), Box<dyn std::error::Error>> {
self.header("Installing rootfs partitions")?; self.header("Installing rootfs partitions")?;
let rootfs = self.artifact_path("citadel-rootfs.img"); let rootfs = self.artifact_path("citadel-rootfs.img");
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha {}", rootfs.display()))?; self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha {}", rootfs.display()))?;
self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha --no-prefer {}", rootfs.display())) self.cmd(format!("/usr/bin/citadel-image install-rootfs --skip-sha --no-prefer {}", rootfs.display()))
} }
pub fn finish_install(&self) -> Result<()> { pub fn finish_install(&self) -> Result<(), Box<dyn std::error::Error>> {
self.cmd_list(FINISH_COMMANDS, &[ self.cmd_list(FINISH_COMMANDS, &[("$TARGET", self.target_str())])
("$TARGET", self.target_str())
])
} }
fn global_realm_config(&self) -> &str { fn global_realm_config(&self) -> &str {
@@ -546,16 +546,33 @@ impl Installer {
Path::new(&self.artifact_directory).join(filename) Path::new(&self.artifact_directory).join(filename)
} }
fn copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P) -> Result<()> { fn copy_artifact<P: AsRef<Path>>(
&self,
filename: &str,
target: P,
) -> Result<(), Box<dyn std::error::Error>> {
self._copy_artifact(filename, target, false) self._copy_artifact(filename, target, false)
} }
fn sparse_copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P) -> Result<()> { fn sparse_copy_artifact<P: AsRef<Path>>(
&self,
filename: &str,
target: P,
) -> Result<(), Box<dyn std::error::Error>> {
self._copy_artifact(filename, target, true) self._copy_artifact(filename, target, true)
} }
fn _copy_artifact<P: AsRef<Path>>(&self, filename: &str, target: P, sparse: bool) -> Result<()> { fn _copy_artifact<P: AsRef<Path>>(
self.info(format!("Copying {} to {}", filename, target.as_ref().display()))?; &self,
filename: &str,
target: P,
sparse: bool,
) -> Result<(), Box<dyn std::error::Error>> {
self.info(format!(
"Copying {} to {}",
filename,
target.as_ref().display()
))?;
let src = self.artifact_path(filename); let src = self.artifact_path(filename);
let target = target.as_ref(); let target = target.as_ref();
util::create_dir(target)?; util::create_dir(target)?;
@@ -568,20 +585,19 @@ impl Installer {
Ok(()) Ok(())
} }
fn header<S: AsRef<str>>(&self, s: S) -> Result<()> { fn header<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
self.output(format!("\n[+] {}\n", s.as_ref())) self.output(format!("\n[+] {}\n", s.as_ref()))
} }
fn info<S: AsRef<str>>(&self, s: S) -> Result<()> { fn info<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
self.output(format!(" [>] {}", s.as_ref())) self.output(format!(" [>] {}", s.as_ref()))
} }
fn output<S: AsRef<str>>(&self, s: S) -> Result<(), Box<dyn std::error::Error>> {
fn output<S: AsRef<str>>(&self, s: S) -> Result<()> { self.write_output(s.as_ref())
self.write_output(s.as_ref()).map_err(context!("error writing output"))
} }
fn write_output(&self, s: &str) -> io::Result<()> { fn write_output(&self, s: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", s); println!("{}", s);
io::stdout().flush()?; io::stdout().flush()?;
@@ -592,7 +608,11 @@ impl Installer {
Ok(()) Ok(())
} }
fn cmd_list<I: IntoIterator<Item=S>, S: AsRef<str>>(&self, cmd_lines: I, subs: &[(&str,&str)]) -> Result<()> { fn cmd_list<I: IntoIterator<Item = S>, S: AsRef<str>>(
&self,
cmd_lines: I,
subs: &[(&str, &str)],
) -> Result<(), Box<dyn std::error::Error>> {
for line in cmd_lines { for line in cmd_lines {
let line = line.as_ref(); let line = line.as_ref();
let line = subs.iter().fold(line.to_string(), |acc, (from,to)| acc.replace(from,to)); let line = subs.iter().fold(line.to_string(), |acc, (from,to)| acc.replace(from,to));
@@ -602,12 +622,12 @@ impl Installer {
Ok(()) Ok(())
} }
fn cmd<S: AsRef<str>>(&self, args: S) -> Result<()> { fn cmd<S: AsRef<str>>(&self, args: S) -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>(); let args: Vec<&str> = args.as_ref().split_whitespace().collect::<Vec<_>>();
self.run_cmd(args, false) self.run_cmd(args, false)
} }
fn run_cmd(&self, args: Vec<&str>, as_user: bool) -> Result<()> { fn run_cmd(&self, args: Vec<&str>, as_user: bool) -> Result<(), Box<dyn std::error::Error>> {
self.output(format!(" # {}", args.join(" ")))?; self.output(format!(" # {}", args.join(" ")))?;
let mut command = Command::new(args[0]); let mut command = Command::new(args[0]);
@@ -632,8 +652,8 @@ impl Installer {
if !result.status.success() { if !result.status.success() {
match result.status.code() { match result.status.code() {
Some(code) => bail!("command {} failed with exit code: {}", args[0], code), Some(code) => panic!("command {} failed with exit code: {}", args[0], code),
None => bail!("command {} failed with no exit code", args[0]), None => panic!("command {} failed with no exit code", args[0]),
} }
} }
Ok(()) Ok(())

View File

@@ -4,7 +4,7 @@ pub(crate) mod installer;
mod cli; mod cli;
mod disk; mod disk;
pub fn main(args: Vec<String>) { pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let mut args = args.iter().skip(1); let mut args = args.iter().skip(1);
let result = if let Some(dev) = args.next() { let result = if let Some(dev) = args.next() {
cli::run_cli_install_with(dev) cli::run_cli_install_with(dev)
@@ -17,10 +17,11 @@ pub fn main(args: Vec<String>) {
Err(ref err) => { Err(ref err) => {
println!("Install failed: {}", err); println!("Install failed: {}", err);
exit(1); exit(1);
}, }
}; };
if !ok { if !ok {
println!("Install cancelled..."); println!("Install cancelled...");
} }
}
Ok(())
}

View File

@@ -7,7 +7,6 @@ use std::sync::mpsc::{Sender};
use dbus::tree::{self, Factory, MTFn, MethodResult, Tree}; use dbus::tree::{self, Factory, MTFn, MethodResult, Tree};
use dbus::{Message}; use dbus::{Message};
use dbus::blocking::LocalConnection; use dbus::blocking::LocalConnection;
use libcitadel::{Result};
// Use local version of disk.rs since we added some methods // Use local version of disk.rs since we added some methods
use crate::install_backend::disk::*; use crate::install_backend::disk::*;
use crate::install::installer::*; use crate::install::installer::*;
@@ -15,7 +14,6 @@ use std::fmt;
type MethodInfo<'a> = tree::MethodInfo<'a, MTFn<TData>, TData>; type MethodInfo<'a> = tree::MethodInfo<'a, MTFn<TData>, TData>;
const OBJECT_PATH: &str = "/com/subgraph/installer"; const OBJECT_PATH: &str = "/com/subgraph/installer";
const INTERFACE_NAME: &str = "com.subgraph.installer.Manager"; const INTERFACE_NAME: &str = "com.subgraph.installer.Manager";
const BUS_NAME: &str = "com.subgraph.installer"; const BUS_NAME: &str = "com.subgraph.installer";
@@ -37,8 +35,7 @@ pub struct DbusServer {
} }
impl DbusServer { impl DbusServer {
pub fn connect() -> Result<DbusServer, Box<dyn std::error::Error>> {
pub fn connect() -> Result<DbusServer> {
let connection = LocalConnection::new_system() let connection = LocalConnection::new_system()
.map_err(|e| format_err!("Failed to connect to DBUS system bus: {}", e))?; .map_err(|e| format_err!("Failed to connect to DBUS system bus: {}", e))?;
let connection = Arc::new(connection); let connection = Arc::new(connection);
@@ -80,7 +77,7 @@ impl DbusServer {
Ok(vec![m.msg.method_return().append1(list)]) Ok(vec![m.msg.method_return().append1(list)])
} }
fn run_install(path: String, citadel_passphrase: String, luks_passphrase: String, sender: Sender<Msg>) -> Result<()> { fn run_install(path: String, citadel_passphrase: String, luks_passphrase: String, sender: Sender<Msg>) -> Result<(), Box<dyn std::error::Error>> {
let mut install = Installer::new(path, &citadel_passphrase, &luks_passphrase); let mut install = Installer::new(path, &citadel_passphrase, &luks_passphrase);
install.set_install_syslinux(true); install.set_install_syslinux(true);
install.verify()?; install.verify()?;
@@ -174,12 +171,12 @@ impl DbusServer {
Message::signal(&path, &iface, &member).append1(text) Message::signal(&path, &iface, &member).append1(text)
} }
pub fn start(&self) -> Result<()> { pub fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
let (sender, receiver) = mpsc::channel::<Msg>(); let (sender, receiver) = mpsc::channel::<Msg>();
let sender_clone = sender.clone(); let sender_clone = sender.clone();
let tree = self.build_tree(sender); let tree = self.build_tree(sender);
if let Err(_err) = self.connection.request_name(BUS_NAME, false, true, false) { if let Err(_err) = self.connection.request_name(BUS_NAME, false, true, false) {
bail!("Failed to request name"); panic!("Failed to request name");
} }
tree.start_receive(self.connection.as_ref()); tree.start_receive(self.connection.as_ref());
@@ -231,7 +228,6 @@ impl DbusServer {
} }
} }
} }
} }
#[derive(Clone)] #[derive(Clone)]
@@ -243,7 +239,6 @@ impl TreeData {
TreeData {} TreeData {}
} }
fn disks(&self) -> HashMap<String, Vec<String>> { fn disks(&self) -> HashMap<String, Vec<String>> {
let disks = Disk::probe_all().unwrap(); let disks = Disk::probe_all().unwrap();
@@ -257,7 +252,6 @@ impl TreeData {
} }
disk_map disk_map
} }
} }
impl fmt::Debug for TreeData { impl fmt::Debug for TreeData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

View File

@@ -1,24 +1,20 @@
use libcitadel::Result;
use std::process::exit; use std::process::exit;
mod disk; mod disk;
mod dbus; mod dbus;
use libcitadel::CommandLine; use libcitadel::CommandLine;
pub fn main() { pub fn main() -> Result<(), Box<dyn std::error::Error>> {
if CommandLine::install_mode() { if CommandLine::install_mode() {
if let Err(e) = run_dbus_server() { run_dbus_server()
warn!("Error: {}", e);
}
} else { } else {
println!("Citadel installer backend will only run in install or live mode"); println!("Citadel installer backend will only run in install or live mode");
exit(1); exit(1);
} }
} }
fn run_dbus_server() -> Result<()> { fn run_dbus_server() -> Result<(), Box<dyn std::error::Error>> {
let server = dbus::DbusServer::connect()?; let server = dbus::DbusServer::connect()?;
server.start()?; server.start()?;
Ok(()) Ok(())
} }

View File

@@ -17,68 +17,74 @@ mod realmfs;
mod sync; mod sync;
mod update; mod update;
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
let exe = match env::current_exe() { let exe = env::current_exe()?;
Ok(path) => path,
Err(_e) => {
return;
},
};
let args = env::args().collect::<Vec<String>>(); let args = env::args().collect::<Vec<String>>();
if exe == Path::new("/usr/libexec/citadel-boot") { if exe == Path::new("/usr/libexec/citadel-boot") {
boot::main(args); boot::main(args)
} else if exe == Path::new("/usr/libexec/citadel-install") { } else if exe == Path::new("/usr/libexec/citadel-install") {
install::main(args); install::main(args)
} 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(); image::main(args)
} else if exe == Path::new("/usr/bin/citadel-realmfs") { } else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main(); realmfs::main(args)
} 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") {
sync::main(args); sync::main(args)
} else if exe == Path::new("/usr/libexec/citadel-run") { } else if exe == Path::new("/usr/libexec/citadel-run") {
do_citadel_run(args); do_citadel_run(args)
} else if exe.file_name() == Some(OsStr::new("citadel-mkimage")) { } else if exe.file_name() == Some(OsStr::new("citadel-mkimage")) {
mkimage::main(args); mkimage::main(args)
} else if exe.file_name() == Some(OsStr::new("citadel-tool")) { } else if exe.file_name() == Some(OsStr::new("citadel-tool")) {
dispatch_command(args); dispatch_command(args)
} else { } else {
println!("Error: unknown executable {}", exe.display()); Result::Err(format!("Error: unknown executable {}", exe.display()).into())
} }
} }
fn dispatch_command(args: Vec<String>) { fn dispatch_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if let Some(command) = args.get(1) { if let Some(command) = args.get(1) {
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(), "image" => image::main(rebuild_args("citadel-image", args)),
"realmfs" => realmfs::main(), "realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"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)),
"run" => do_citadel_run(rebuild_args("citadel-run", args)), "run" => do_citadel_run(rebuild_args("citadel-run", args)),
_ => println!("Error: unknown command {}", command), _ => throw_err(command),
} }
} else { } else {
println!("Must provide an argument"); Result::Err("Must provide an argument".into())
} }
} }
fn throw_err(command: &String) -> Result<(), Box<dyn std::error::Error>> {
Result::Err(format!("Error: unknown command {}", command).into())
}
fn rebuild_args(command: &str, args: Vec<String>) -> Vec<String> { fn rebuild_args(command: &str, args: Vec<String>) -> Vec<String> {
iter::once(command.to_string()) iter::once(command.to_string())
.chain(args.into_iter().skip(2)) .chain(args.into_iter().skip(2))
.collect() .collect()
} }
fn do_citadel_run(args: Vec<String>) { fn do_citadel_run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if let Err(e) = RealmManager::run_in_current(&args[1..], true) { if let Err(e) = RealmManager::run_in_current(&args[1..], true) {
println!("RealmManager::run_in_current({:?}) failed: {}", &args[1..], e); return Result::Err(
format!(
"RealmManager::run_in_current({:?}) failed: {}",
&args[1..],
e
)
.into(),
);
} }
Ok(())
} }

View File

@@ -38,15 +38,15 @@ impl UpdateBuilder {
} }
fn target_filename(&self) -> String { fn target_filename(&self) -> String {
format!("citadel-{}-{}-{}.img", self.config.img_name(), self.config.channel(), self.config.version()) format!("citadel-{}-{}-{:03}.img", self.config.img_name(), self.config.channel(), self.config.version())
} }
fn build_filename(config: &BuildConfig) -> String { fn build_filename(config: &BuildConfig) -> String {
format!("citadel-{}-{}-{}", config.image_type(), config.channel(), config.version()) format!("citadel-{}-{}-{:03}", config.image_type(), config.channel(), config.version())
} }
fn verity_filename(&self) -> String { fn verity_filename(&self) -> String {
format!("verity-hash-{}-{}", self.config.image_type(), self.config.version()) format!("verity-hash-{}-{:03}", self.config.image_type(), self.config.version())
} }
pub fn build(&mut self) -> Result<()> { pub fn build(&mut self) -> Result<()> {
@@ -154,7 +154,7 @@ impl UpdateBuilder {
bail!("failed to compress {:?}: {}", self.image(), err); bail!("failed to compress {:?}: {}", self.image(), err);
} }
// Rename back to original image_data filename // Rename back to original image_data filename
util::rename(util::append_to_path(self.image(), ".xz"), self.image())?; util::rename(self.image().with_extension("xz"), self.image())?;
} }
Ok(()) Ok(())
} }
@@ -217,7 +217,7 @@ impl UpdateBuilder {
writeln!(v, "realmfs-name = \"{}\"", name)?; writeln!(v, "realmfs-name = \"{}\"", name)?;
} }
writeln!(v, "channel = \"{}\"", self.config.channel())?; writeln!(v, "channel = \"{}\"", self.config.channel())?;
writeln!(v, "version = \"{}\"", self.config.version())?; writeln!(v, "version = {}", self.config.version())?;
writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?; writeln!(v, "timestamp = \"{}\"", self.config.timestamp())?;
writeln!(v, "nblocks = {}", self.nblocks.unwrap())?; writeln!(v, "nblocks = {}", self.nblocks.unwrap())?;
writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?; writeln!(v, "shasum = \"{}\"", self.shasum.as_ref().unwrap())?;

View File

@@ -9,7 +9,7 @@ pub struct BuildConfig {
#[serde(rename = "image-type")] #[serde(rename = "image-type")]
image_type: String, image_type: String,
channel: String, channel: String,
version: String, version: usize,
timestamp: String, timestamp: String,
source: String, source: String,
#[serde(default)] #[serde(default)]
@@ -102,8 +102,8 @@ impl BuildConfig {
self.realmfs_name.as_ref().map(|s| s.as_str()) self.realmfs_name.as_ref().map(|s| s.as_str())
} }
pub fn version(&self) -> &str { pub fn version(&self) -> usize {
&self.version self.version
} }
pub fn channel(&self) -> &str { pub fn channel(&self) -> &str {

View File

@@ -1,13 +1,10 @@
use std::process::exit; use std::process::exit;
use libcitadel::Result;
mod config; mod config;
mod build; mod build;
pub fn main(args: Vec<String>) { pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let config_path = match args.get(1) { let config_path = match args.get(1) {
Some(arg) => arg, Some(arg) => arg,
None => { None => {
@@ -21,11 +18,11 @@ pub fn main(args: Vec<String>) {
exit(1); exit(1);
} }
Ok(())
} }
fn build_image(config_path: &str) -> Result<()> { fn build_image(config_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let conf = config::BuildConfig::load(config_path)?; let conf = config::BuildConfig::load(config_path)?;
let mut builder = build::UpdateBuilder::new(conf); let mut builder = build::UpdateBuilder::new(conf);
builder.build() Ok(builder.build()?)
} }

View File

@@ -1,24 +1,26 @@
use clap::{command, Command}; use clap::App;
use clap::{Arg, ArgMatches}; use clap::ArgMatches;
use libcitadel::{Result,RealmFS,Logger,LogLevel}; use libcitadel::{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;
pub fn main() {
pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
Logger::set_log_level(LogLevel::Debug); Logger::set_log_level(LogLevel::Debug);
let matches = command!() let app = App::new("citadel-realmfs")
.about("citadel-realmfs") .about("Citadel realmfs image tool")
.arg_required_else_help(true) .settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs])
.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::new("image") .arg(Arg::with_name("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::new("size") .arg(Arg::with_name("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 \
@@ -31,66 +33,63 @@ is the final absolute size of the image.")
.required(true))) .required(true)))
.subcommand(Command::new("fork") .subcommand(SubCommand::with_name("fork")
.about("Create a new RealmFS image as an unsealed copy of an existing image") .about("Create a new RealmFS image as an unsealed copy of an existing image")
.arg(Arg::new("image") .arg(Arg::with_name("image")
.help("Path or name of RealmFS image to fork") .help("Path or name of RealmFS image to fork")
.required(true)) .required(true))
.arg(Arg::new("forkname") .arg(Arg::with_name("forkname")
.help("Name of new image to create") .help("Name of new image to create")
.required(true))) .required(true)))
.subcommand(Command::new("autoresize") .subcommand(SubCommand::with_name("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::new("image") .arg(Arg::with_name("image")
.help("Path or name of RealmFS image") .help("Path or name of RealmFS image")
.required(true))) .required(true)))
.subcommand(Command::new("update") .subcommand(SubCommand::with_name("update")
.about("Open an update shell on the image") .about("Open an update shell on the image")
.arg(Arg::new("image") .arg(Arg::with_name("image")
.help("Path or name of RealmFS image") .help("Path or name of RealmFS image")
.required(true))) .required(true)))
.subcommand(Command::new("activate") .subcommand(SubCommand::with_name("activate")
.about("Activate a RealmFS by creating a block device for the image and mounting it.") .about("Activate a RealmFS by creating a block device for the image and mounting it.")
.arg(Arg::new("image") .arg(Arg::with_name("image")
.help("Path or name of RealmFS image to activate") .help("Path or name of RealmFS image to activate")
.required(true))) .required(true)))
.subcommand(Command::new("deactivate") .subcommand(SubCommand::with_name("deactivate")
.about("Deactivate a RealmFS by unmounting it and removing block device created during activation.") .about("Deactivate a RealmFS by unmounting it and removing block device created during activation.")
.arg(Arg::new("image") .arg(Arg::with_name("image")
.help("Path or name of RealmFS image to deactivate") .help("Path or name of RealmFS image to deactivate")
.required(true))) .required(true)))
.arg(Arg::new("image") .arg(Arg::with_name("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() {
Some(("resize", m)) => resize(m), ("resize", Some(m)) => resize(m),
Some(("autoresize", m)) => autoresize(m), ("autoresize", Some(m)) => autoresize(m),
Some(("fork", m)) => fork(m), ("fork", Some(m)) => fork(m),
Some(("update", m)) => update(m), ("update", Some(m)) => update(m),
Some(("activate", m)) => activate(m), ("activate", Some(m)) => activate(m),
Some(("deactivate", m)) => deactivate(m), ("deactivate", Some(m)) => deactivate(m),
_ => image_info(&matches), _ => image_info(&matches),
}; }?;
if let Err(ref e) = result { Ok(())
eprintln!("Error: {}", e);
exit(1);
}
} }
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> { fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS, Box<dyn std::error::Error>> {
let image = match arg_matches.get_one::<String>("image") { let image = match arg_matches.value_of("image") {
Some(s) => s, Some(s) => s,
None => bail!("Image argument required."), None => panic!("Image argument required."),
}; };
let realmfs = if RealmFS::is_valid_name(image) { let realmfs = if RealmFS::is_valid_name(image) {
@@ -98,41 +97,45 @@ fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
} else if RealmFS::is_valid_realmfs_image(image) { } else if RealmFS::is_valid_realmfs_image(image) {
RealmFS::load_from_path(image)? RealmFS::load_from_path(image)?
} else { } else {
bail!("Not a valid realmfs name or path to realmfs image file: {}", image); panic!(
"Not a valid realmfs name or path to realmfs image file: {}",
image
);
}; };
Ok(realmfs) Ok(realmfs)
} }
fn image_info(arg_matches: &ArgMatches) -> Result<()> { fn image_info(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?; let img = realmfs_image(arg_matches)?;
print!("{}", String::from_utf8(img.header().metainfo_bytes())?); print!("{}", String::from_utf8(img.header().metainfo_bytes())?);
Ok(()) Ok(())
} }
fn parse_resize_size(s: &str) -> Result<ResizeSize> { fn parse_resize_size(s: &str) -> Result<ResizeSize, Box<dyn std::error::Error>> {
let unit = s.chars().last().filter(|c| c.is_alphabetic()); let unit = s.chars().last().filter(|c| c.is_alphabetic());
let skip = if s.starts_with('+') { 1 } else { 0 }; let skip = if s.starts_with('+') { 1 } else { 0 };
let size = s.chars() let size = s
.chars()
.skip(skip) .skip(skip)
.take_while(|c| c.is_numeric()) .take_while(|c| c.is_numeric())
.collect::<String>() .collect::<String>()
.parse::<usize>() .parse::<usize>()
.map_err(|_| format_err!("Unable to parse size value '{}'",s))?; .map_err(|_| format_err!("Unable to parse size value '{}'", s))?;
let sz = match unit { let sz = match unit {
Some('g') | Some('G') => ResizeSize::gigs(size), Some('g') | Some('G') => ResizeSize::gigs(size),
Some('m') | Some('M') => ResizeSize::megs(size), Some('m') | Some('M') => ResizeSize::megs(size),
Some(c) => bail!("Unknown size unit '{}'", c), Some(c) => panic!("Unknown size unit '{}'", c),
None => ResizeSize::blocks(size), None => ResizeSize::blocks(size),
}; };
Ok(sz) Ok(sz)
} }
fn resize(arg_matches: &ArgMatches) -> Result<()> { fn resize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
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.get_one::<String>("size") { let size_arg = match arg_matches.value_of("size") {
Some(size) => size, Some(size) => size,
None => "No size argument", None => "No size argument",
}; };
@@ -141,51 +144,55 @@ fn resize(arg_matches: &ArgMatches) -> Result<()> {
let size = parse_resize_size(size_arg)?; let size = parse_resize_size(size_arg)?;
if mode_add { if mode_add {
img.resize_grow_by(size) img.resize_grow_by(size)?;
} else { } else {
img.resize_grow_to(size) img.resize_grow_to(size)?;
} }
Ok(())
} }
fn autoresize(arg_matches: &ArgMatches) -> Result<()> { fn autoresize(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?; let img = realmfs_image(arg_matches)?;
if let Some(size) = img.auto_resize_size() { if let Some(size) = img.auto_resize_size() {
img.resize_grow_to(size) img.resize_grow_to(size)?;
} else { } else {
info!("RealmFS image {} has sufficient free space, doing nothing", img.path().display()); info!(
Ok(()) "RealmFS image {} has sufficient free space, doing nothing",
img.path().display()
);
} }
Ok(())
} }
fn fork(arg_matches: &ArgMatches) -> Result<()> { fn fork(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?; let img = realmfs_image(arg_matches)?;
let forkname = match arg_matches.get_one::<String>("forkname") { let forkname = match arg_matches.value_of("forkname") {
Some(name) => name, Some(name) => name,
None => bail!("No fork name argument"), None => panic!("No fork name argument"),
}; };
if !RealmFS::is_valid_name(forkname) { if !RealmFS::is_valid_name(forkname) {
bail!("Not a valid RealmFS image name '{}'", forkname); panic!("Not a valid RealmFS image name '{}'", forkname);
} }
if RealmFS::named_image_exists(forkname) { if RealmFS::named_image_exists(forkname) {
bail!("A RealmFS image named '{}' already exists", forkname); panic!("A RealmFS image named '{}' already exists", forkname);
} }
img.fork(forkname)?; img.fork(forkname)?;
Ok(()) Ok(())
} }
fn update(arg_matches: &ArgMatches) -> Result<()> { fn update(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
if !is_euid_root() { if !is_euid_root() {
bail!("RealmFS updates must be run as root"); panic!("RealmFS updates must be run as root");
} }
let img = realmfs_image(arg_matches)?; let img = realmfs_image(arg_matches)?;
img.interactive_update(Some("icy"))?; img.interactive_update(Some("icy"))?;
Ok(()) Ok(())
} }
fn activate(arg_matches: &ArgMatches) -> Result<()> { fn activate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?; let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.get_one::<String>("image").unwrap(); let img_arg = arg_matches.value_of("image").unwrap();
if img.is_activated() { if img.is_activated() {
info!("RealmFS image {} is already activated", img_arg); info!("RealmFS image {} is already activated", img_arg);
@@ -196,9 +203,9 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> {
Ok(()) Ok(())
} }
fn deactivate(arg_matches: &ArgMatches) -> Result<()> { fn deactivate(arg_matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let img = realmfs_image(arg_matches)?; let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.get_one::<String>("image").unwrap(); let img_arg = arg_matches.value_of("image").unwrap();
if !img.is_activated() { 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

@@ -1,9 +1,9 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::{Path,PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
use libcitadel::{Realm, Realms, Result, util}; use libcitadel::{Realm, Realms, util};
use crate::sync::parser::DesktopFileParser; 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;
@@ -17,14 +17,13 @@ pub struct DesktopFileSync {
icons: Option<IconSync>, icons: Option<IconSync>,
} }
#[derive(Eq,PartialEq,Hash)] #[derive(Eq, PartialEq, Hash)]
struct DesktopItem { struct DesktopItem {
path: PathBuf, path: PathBuf,
mtime: SystemTime, mtime: SystemTime,
} }
impl DesktopItem { impl DesktopItem {
fn new(path: PathBuf, mtime: SystemTime) -> Self { fn new(path: PathBuf, mtime: SystemTime) -> Self {
DesktopItem { path, mtime } DesktopItem { path, mtime }
} }
@@ -46,7 +45,7 @@ impl DesktopItem {
impl DesktopFileSync { impl DesktopFileSync {
pub const CITADEL_APPLICATIONS: &'static str = "/home/citadel/.local/share/applications"; pub const CITADEL_APPLICATIONS: &'static str = "/home/citadel/.local/share/applications";
pub fn sync_active_realms() -> Result<()> { pub fn sync_active_realms() -> Result<(), Box<dyn std::error::Error>> {
let realms = Realms::load()?; let realms = Realms::load()?;
for realm in realms.active(true) { for realm in realms.active(true) {
let mut sync = DesktopFileSync::new(realm); let mut sync = DesktopFileSync::new(realm);
@@ -72,7 +71,7 @@ impl DesktopFileSync {
DesktopFileSync { realm, items: HashSet::new(), icons } DesktopFileSync { realm, items: HashSet::new(), icons }
} }
pub fn run_sync(&mut self, clear: bool) -> Result<()> { pub fn run_sync(&mut self, clear: bool) -> Result<(), Box<dyn std::error::Error>> {
IconSync::ensure_theme_index_exists()?; IconSync::ensure_theme_index_exists()?;
@@ -98,8 +97,8 @@ impl DesktopFileSync {
Ok(()) Ok(())
} }
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<()> { fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
let mut directory = self.realm.run_path().join(directory.as_ref()); let mut directory = Realms::current_realm_symlink().join(directory.as_ref());
directory.push("share/applications"); directory.push("share/applications");
if directory.exists() { if directory.exists() {
util::read_directory(&directory, |dent| { util::read_directory(&directory, |dent| {
@@ -119,20 +118,16 @@ impl DesktopFileSync {
} }
} }
pub fn clear_target_files() -> Result<()> { pub fn clear_target_files() -> Result<(), Box<dyn std::error::Error>> {
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| { Ok(util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
util::remove_file(dent.path()) util::remove_file(dent.path())
}) })?)
} }
fn remove_missing_target_files(&mut self) -> Result<()> { fn remove_missing_target_files(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let mut sources = self.source_filenames(); let 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| { Ok(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() {
if filename.starts_with(&prefix) && !sources.contains(filename) { if filename.starts_with(&prefix) && !sources.contains(filename) {
let path = dent.path(); let path = dent.path();
@@ -141,7 +136,7 @@ impl DesktopFileSync {
} }
} }
Ok(()) Ok(())
}) })?)
} }
fn mtime(path: &Path) -> Option<SystemTime> { fn mtime(path: &Path) -> Option<SystemTime> {
@@ -160,7 +155,7 @@ impl DesktopFileSync {
.collect() .collect()
} }
fn synchronize_items(&self) -> Result<()> { fn synchronize_items(&self) -> Result<(), Box<dyn std::error::Error>> {
for item in &self.items { for item in &self.items {
let target = Path::new(Self::CITADEL_APPLICATIONS).join(item.filename()); let target = Path::new(Self::CITADEL_APPLICATIONS).join(item.filename());
if item.is_newer_than(&target) { if item.is_newer_than(&target) {
@@ -184,11 +179,9 @@ impl DesktopFileSync {
} }
} }
fn sync_item(&self, item: &DesktopItem) -> Result<()> { fn sync_item(&self, item: &DesktopItem) -> Result<(), Box<dyn std::error::Error>> {
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 ")?;
// When use-flatpak is enabled a gnome-software desktop file will be generated if dfp.is_showable() {
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))?;
} else { } else {

View File

@@ -4,6 +4,7 @@ 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 std::fs;
use crate::sync::desktop_file::DesktopFile; use crate::sync::desktop_file::DesktopFile;
use crate::sync::REALM_BASE_PATHS; use crate::sync::REALM_BASE_PATHS;

View File

@@ -1,4 +1,4 @@
use libcitadel::{Result, Logger, LogLevel}; use libcitadel::{Logger, LogLevel};
mod desktop_file; mod desktop_file;
mod parser; mod parser;
@@ -14,13 +14,12 @@ fn has_arg(args: &[String], arg: &str) -> bool {
pub const REALM_BASE_PATHS:&[&str] = &[ pub const REALM_BASE_PATHS:&[&str] = &[
"rootfs/usr", "rootfs/usr",
"flatpak/exports", "rootfs/var/lib/flatpak/exports",
"home/.local", "home/.local",
"home/.local/share/flatpak/exports" "home/.local/share/flatpak/exports"
]; ];
pub fn main(args: Vec<String>) { pub fn main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if has_arg(&args, "-v") { if has_arg(&args, "-v") {
Logger::set_log_level(LogLevel::Debug); Logger::set_log_level(LogLevel::Debug);
} else { } else {
@@ -37,12 +36,14 @@ pub fn main(args: Vec<String>) {
println!("Desktop file sync failed: {}", e); println!("Desktop file sync failed: {}", e);
} }
} }
Ok(())
} }
fn sync(clear: bool) -> Result<()> { fn sync(clear: bool) -> Result<(), Box<dyn std::error::Error>> {
if let Some(mut sync) = DesktopFileSync::new_current() { if let Some(mut sync) = DesktopFileSync::new_current() {
sync.run_sync(clear) sync.run_sync(clear)?
} else { } else {
DesktopFileSync::clear_target_files() DesktopFileSync::clear_target_files()?
} }
Ok(())
} }

View File

@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use libcitadel::{Result, Partition, ResourceImage, ImageHeader, LogLevel, Logger, util}; use libcitadel::{Partition, ResourceImage, ImageHeader, LogLevel, Logger, util};
use crate::update::kernel::{KernelInstaller, KernelVersion}; use crate::update::kernel::{KernelInstaller, KernelVersion};
use std::collections::HashSet; use std::collections::HashSet;
use std::fs::{DirEntry, File}; use std::fs::{DirEntry, File};
@@ -16,7 +16,7 @@ const FLAG_QUIET: u32 = 0x04;
const RESOURCES_DIRECTORY: &str = "/storage/resources"; const RESOURCES_DIRECTORY: &str = "/storage/resources";
const TEMP_DIRECTORY: &str = "/storage/resources/tmp"; const TEMP_DIRECTORY: &str = "/storage/resources/tmp";
pub fn main(args: Vec<String>) { pub fn main(args: Vec<String>) -> std::result::Result<(), Box<dyn std::error::Error>> {
let mut args = args.iter().skip(1); let mut args = args.iter().skip(1);
let mut flags = 0; let mut flags = 0;
@@ -34,7 +34,7 @@ pub fn main(args: Vec<String>) {
Logger::set_log_level(LogLevel::Debug); Logger::set_log_level(LogLevel::Debug);
} else if arg == "--choose-rootfs" { } else if arg == "--choose-rootfs" {
let _ = choose_install_partition(true); let _ = choose_install_partition(true);
return; return Ok(())
} else { } else {
let path = Path::new(arg); let path = Path::new(arg);
if let Err(e) = install_image(path, flags) { if let Err(e) = install_image(path, flags) {
@@ -42,12 +42,13 @@ pub fn main(args: Vec<String>) {
} }
} }
} }
Ok(())
} }
// Search directory containing installed image files for an // Search directory containing installed image files for an
// image file that has an identical shasum and abort the installation // image file that has an identical shasum and abort the installation
// if a duplicate is found. // if a duplicate is found.
fn detect_duplicates(header: &ImageHeader) -> Result<()> { fn detect_duplicates(header: &ImageHeader) -> Result<(), Box<dyn std::error::Error>> {
let metainfo = header.metainfo(); let metainfo = header.metainfo();
let channel = metainfo.channel(); let channel = metainfo.channel();
let shasum = metainfo.shasum(); let shasum = metainfo.shasum();
@@ -61,17 +62,20 @@ fn detect_duplicates(header: &ImageHeader) -> Result<()> {
return Ok(()) return Ok(())
} }
util::read_directory(&resource_dir, |dent| { Ok(util::read_directory(&resource_dir, |dent| {
if let Ok(hdr) = ImageHeader::from_file(dent.path()) { if let Ok(hdr) = ImageHeader::from_file(dent.path()) {
if hdr.metainfo().shasum() == shasum { if hdr.metainfo().shasum() == shasum {
bail!("A duplicate image file with the same shasum already exists at {}", dent.path().display()); panic!(
"A duplicate image file with the same shasum already exists at {}",
dent.path().display()
);
} }
} }
Ok(()) Ok(())
}) })?)
} }
fn create_tmp_copy(path: &Path) -> Result<PathBuf> { fn create_tmp_copy(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
if !Path::new(TEMP_DIRECTORY).exists() { if !Path::new(TEMP_DIRECTORY).exists() {
util::create_dir(TEMP_DIRECTORY)?; util::create_dir(TEMP_DIRECTORY)?;
} }
@@ -93,12 +97,12 @@ fn create_tmp_copy(path: &Path) -> Result<PathBuf> {
Ok(path) Ok(path)
} }
fn install_image(path: &Path, flags: u32) -> Result<()> { fn install_image(path: &Path, flags: u32) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() || path.file_name().is_none() { if !path.exists() || path.file_name().is_none() {
bail!("file path {} does not exist", path.display()); panic!("file path {} does not exist", path.display());
} }
if !util::is_euid_root() { if !util::is_euid_root() {
bail!("Image updates must be installed by root user"); panic!("Image updates must be installed by root user");
} }
let header = ImageHeader::from_file(path)?; let header = ImageHeader::from_file(path)?;
@@ -113,14 +117,14 @@ fn install_image(path: &Path, flags: u32) -> Result<()> {
match image.metainfo().image_type() { match image.metainfo().image_type() {
"kernel" => install_kernel_image(&mut image), "kernel" => install_kernel_image(&mut image),
"extra" => install_extra_image(&image), "extra" => install_extra_image(&image),
"rootfs" => install_rootfs_image(&image, flags), "rootfs" => install_rootfs_image(&image, flags),
image_type => bail!("Unknown image type: {}", image_type), image_type => panic!("Unknown image type: {}", image_type),
} }
} }
// Prepare the image file for installation by decompressing and generating // Prepare the image file for installation by decompressing and generating
// dmverity hash tree. // dmverity hash tree.
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> { fn prepare_image(image: &ResourceImage, flags: u32) -> Result<(), Box<dyn std::error::Error>> {
if image.is_compressed() { if image.is_compressed() {
image.decompress(false)?; image.decompress(false)?;
} }
@@ -129,7 +133,7 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
info!("Verifying sha256 hash of image"); info!("Verifying sha256 hash of image");
let shasum = image.generate_shasum()?; let shasum = image.generate_shasum()?;
if shasum != image.metainfo().shasum() { if shasum != image.metainfo().shasum() {
bail!("image file does not have expected sha256 value"); panic!("image file does not have expected sha256 value");
} }
} }
@@ -139,24 +143,28 @@ fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
Ok(()) Ok(())
} }
fn install_extra_image(image: &ResourceImage) -> Result<()> { fn install_extra_image(image: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
let filename = format!("citadel-extra-{}.img", image.header().metainfo().version()); let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
install_image_file(image, filename.as_str())?; install_image_file(image, filename.as_str())?;
remove_old_extra_images(image)?; remove_old_extra_images(image)?;
Ok(()) Ok(())
} }
fn remove_old_extra_images(image: &ResourceImage) -> Result<()> { fn remove_old_extra_images(image: &ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
let new_meta = image.header().metainfo(); let new_meta = image.header().metainfo();
let shasum = new_meta.shasum(); let shasum = new_meta.shasum();
let target_dir = target_directory(image)?; let target_dir = target_directory(image)?;
util::read_directory(&target_dir, |dent| { Ok(util::read_directory(&target_dir, |dent| {
let path = dent.path(); let path = dent.path();
maybe_remove_old_extra_image(&path, shasum) maybe_remove_old_extra_image(&path, shasum).unwrap();
}) Ok(())
})?)
} }
fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> { fn maybe_remove_old_extra_image(
path: &Path,
shasum: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let header = ImageHeader::from_file(&path)?; let header = ImageHeader::from_file(&path)?;
if !header.is_magic_valid() { if !header.is_magic_valid() {
return Ok(()); return Ok(());
@@ -172,21 +180,21 @@ fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> {
Ok(()) Ok(())
} }
fn install_kernel_image(image: &mut ResourceImage) -> Result<()> { fn install_kernel_image(image: &mut ResourceImage) -> Result<(), Box<dyn std::error::Error>> {
if !Path::new("/boot/loader/loader.conf").exists() { if !Path::new("/boot/loader/loader.conf").exists() {
bail!("failed to automount /boot partition. Please manually mount correct partition."); panic!("failed to automount /boot partition. Please manually mount correct partition.");
} }
let metainfo = image.header().metainfo(); let metainfo = image.header().metainfo();
let version = metainfo.version(); let version = metainfo.version();
let kernel_version = match metainfo.kernel_version() { let kernel_version = match metainfo.kernel_version() {
Some(kv) => kv, Some(kv) => kv,
None => bail!("kernel image does not have kernel version field"), None => panic!("kernel image does not have kernel version field"),
}; };
info!("kernel version is {}", kernel_version); info!("kernel version is {}", kernel_version);
install_kernel_file(image, &kernel_version)?; install_kernel_file(image, &kernel_version)?;
let filename = format!("citadel-kernel-{}-{}.img", kernel_version, version); let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
install_image_file(image, &filename)?; install_image_file(image, &filename)?;
let all_versions = all_boot_kernel_versions()?; let all_versions = all_boot_kernel_versions()?;
@@ -194,7 +202,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
let mut remove_paths = Vec::new(); let mut remove_paths = Vec::new();
util::read_directory(&image_dir, |dent| { util::read_directory(&image_dir, |dent| {
let path = dent.path(); let path = dent.path();
if is_unused_kernel_image(&path, &all_versions)? { if is_unused_kernel_image(&path, &all_versions).unwrap() {
remove_paths.push(path); remove_paths.push(path);
} }
Ok(()) Ok(())
@@ -206,7 +214,7 @@ fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
Ok(()) Ok(())
} }
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool> { fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool, Box<dyn std::error::Error>> {
let header = ImageHeader::from_file(path)?; let header = ImageHeader::from_file(path)?;
if !header.is_magic_valid() { if !header.is_magic_valid() {
return Ok(false); return Ok(false);
@@ -226,23 +234,25 @@ fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<boo
Ok(false) Ok(false)
} }
fn install_kernel_file(image: &mut ResourceImage, kernel_version: &str) -> Result<()> { fn install_kernel_file(
image: &mut ResourceImage,
kernel_version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mountpoint = Path::new("/run/citadel/images/kernel-install.mountpoint"); let mountpoint = Path::new("/run/citadel/images/kernel-install.mountpoint");
info!("Temporarily mounting kernel resource image"); info!("Temporarily mounting kernel resource image");
let mut handle = image.mount_at(mountpoint)?; let mut handle = image.mount_at(mountpoint)?;
let kernel_path = mountpoint.join("kernel/bzImage"); let kernel_path = mountpoint.join("kernel/bzImage");
if !kernel_path.exists() { if !kernel_path.exists() {
handle.unmount()?; handle.unmount()?;
bail!("kernel not found in kernel resource image at /kernel/bzImage") panic!("kernel not found in kernel resource image at /kernel/bzImage")
} }
let result = KernelInstaller::install_kernel(&kernel_path, kernel_version); KernelInstaller::install_kernel(&kernel_path, kernel_version)?;
info!("Unmounting kernel resource image"); info!("Unmounting kernel resource image");
handle.unmount()?; Ok(handle.unmount()?)
result
} }
fn all_boot_kernel_versions() -> Result<HashSet<String>> { fn all_boot_kernel_versions() -> Result<HashSet<String>, Box<dyn std::error::Error>> {
let mut result = HashSet::new(); let mut result = HashSet::new();
util::read_directory("/boot", |dent| { util::read_directory("/boot", |dent| {
if is_kernel_dirent(&dent) { if is_kernel_dirent(&dent) {
@@ -264,7 +274,10 @@ fn is_kernel_dirent(dirent: &DirEntry) -> bool {
} }
} }
fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> { fn install_image_file(
image: &ResourceImage,
filename: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let image_dir = target_directory(image)?; let image_dir = target_directory(image)?;
let image_dest = image_dir.join(filename); let image_dest = image_dir.join(filename);
if image_dest.exists() { if image_dest.exists() {
@@ -275,14 +288,14 @@ fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> {
Ok(()) Ok(())
} }
fn target_directory(image: &ResourceImage) -> Result<PathBuf> { fn target_directory(image: &ResourceImage) -> Result<PathBuf, Box<dyn std::error::Error>> {
let metainfo = image.header().metainfo(); let metainfo = image.header().metainfo();
let channel = metainfo.channel(); let channel = metainfo.channel();
validate_channel_name(channel)?; validate_channel_name(channel)?;
Ok(Path::new("/storage/resources").join(channel)) Ok(Path::new("/storage/resources").join(channel))
} }
fn rotate(path: &Path) -> Result<()> { fn rotate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() || path.file_name().is_none() { if !path.exists() || path.file_name().is_none() {
return Ok(()); return Ok(());
} }
@@ -293,14 +306,17 @@ fn rotate(path: &Path) -> Result<()> {
Ok(()) Ok(())
} }
fn validate_channel_name(channel: &str) -> Result<()> { fn validate_channel_name(channel: &str) -> Result<(), Box<dyn std::error::Error>> {
if !channel.chars().all(|c| c.is_ascii_lowercase()) { if !channel.chars().all(|c| c.is_ascii_lowercase()) {
bail!("image has invalid channel name '{}'", channel); panic!("image has invalid channel name '{}'", channel);
} }
Ok(()) Ok(())
} }
fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> { fn install_rootfs_image(
image: &ResourceImage,
flags: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let quiet = flags & FLAG_QUIET != 0; let quiet = flags & FLAG_QUIET != 0;
let partition = choose_install_partition(!quiet)?; let partition = choose_install_partition(!quiet)?;
@@ -315,7 +331,7 @@ fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> {
Ok(()) Ok(())
} }
fn clear_prefer_boot() -> Result<()> { fn clear_prefer_boot() -> Result<(), Box<dyn std::error::Error>> {
for mut p in Partition::rootfs_partitions()? { for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) { if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?; p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
@@ -332,7 +348,7 @@ fn bool_to_yesno(val: bool) -> &'static str {
} }
} }
fn choose_install_partition(verbose: bool) -> Result<Partition> { fn choose_install_partition(verbose: bool) -> Result<Partition, Box<dyn std::error::Error>> {
let partitions = Partition::rootfs_partitions()?; let partitions = Partition::rootfs_partitions()?;
if verbose { if verbose {
@@ -362,5 +378,5 @@ fn choose_install_partition(verbose: bool) -> Result<Partition> {
return Ok(p.clone()) return Ok(p.clone())
} }
} }
bail!("no suitable install partition found") panic!("no suitable install partition found")
} }

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=RealmConfig
Type=Application
Icon=org.gnome.Settings
NoDisplay=true

View File

@@ -7,12 +7,10 @@
<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

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

View File

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

View File

@@ -10,7 +10,6 @@ nix = "0.17.0"
toml = "0.5" 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"
@@ -20,7 +19,6 @@ walkdir = "2"
dbus = "0.6" dbus = "0.6"
posix-acl = "1.0.0" posix-acl = "1.0.0"
procfs = "0.12.0" procfs = "0.12.0"
semver = "1.0"
[dependencies.inotify] [dependencies.inotify]
version = "0.8" version = "0.8"

View File

@@ -78,9 +78,3 @@ impl Display for Error {
} }
} }
} }
impl From<fmt::Error> for crate::Error {
fn from(e: fmt::Error) -> Self {
format_err!("Error formatting string: {}", e).into()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -453,7 +453,7 @@ pub struct MetaInfo {
realmfs_owner: Option<String>, realmfs_owner: Option<String>,
#[serde(default)] #[serde(default)]
version: String, version: u32,
#[serde(default)] #[serde(default)]
timestamp: String, timestamp: String,
@@ -508,8 +508,8 @@ impl MetaInfo {
Self::str_ref(&self.realmfs_owner) Self::str_ref(&self.realmfs_owner)
} }
pub fn version(&self) -> &str { pub fn version(&self) -> u32 {
&self.version self.version
} }
pub fn timestamp(&self) -> &str { pub fn timestamp(&self) -> &str {

View File

@@ -21,8 +21,6 @@ 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;
@@ -34,12 +32,10 @@ pub use crate::realmfs::{RealmFS,Mountpoint};
pub use crate::keyring::{KeyRing,KernelKey}; pub use crate::keyring::{KeyRing,KernelKey};
pub use crate::exec::{Exec,FileRange}; pub use crate::exec::{Exec,FileRange};
pub use crate::realmfs::resizer::ResizeSize; pub use crate::realmfs::resizer::ResizeSize;
pub use crate::realmfs::update::RealmFSUpdate;
pub use crate::realm::overlay::RealmOverlay; pub use crate::realm::overlay::RealmOverlay;
pub use crate::realm::realm::Realm; pub use crate::realm::realm::Realm;
pub use crate::realm::pidmapper::PidLookupResult; pub use crate::realm::pidmapper::PidLookupResult;
pub use crate::realm::config::{RealmConfig,OverlayType,GLOBAL_CONFIG}; pub use crate::realm::config::{RealmConfig,OverlayType,GLOBAL_CONFIG};
pub use crate::realm::liveconfig::LiveConfig;
pub use crate::realm::events::RealmEvent; pub use crate::realm::events::RealmEvent;
pub use crate::realm::realms::Realms; pub use crate::realm::realms::Realms;
pub use crate::realm::manager::RealmManager; pub use crate::realm::manager::RealmManager;

View File

@@ -62,11 +62,6 @@ 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,9 +77,6 @@ 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>,
@@ -204,7 +201,6 @@ 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),
@@ -237,7 +233,6 @@ 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,
@@ -266,44 +261,12 @@ impl RealmConfig {
self.bool_value(|c| c.use_kvm) self.bool_value(|c| c.use_kvm)
} }
pub fn set_kvm(&mut self, value: bool) -> bool {
if self.kvm() == value {
return false;
}
self.use_kvm = Some(value);
true
}
/// If `true` device /dev/fuse will be added to realm /// If `true` device /dev/fuse will be added to realm
/// ///
pub fn fuse(&self) -> bool { pub fn fuse(&self) -> bool {
self.bool_value(|c| c.use_fuse) self.bool_value(|c| c.use_fuse)
} }
pub fn set_fuse(&mut self, value: bool) -> bool {
if self.fuse() == value {
return false;
}
self.use_fuse = Some(value);
true
}
/// 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)
}
pub fn set_flatpak(&mut self, value: bool) -> bool {
if self.flatpak() == value {
return false;
}
self.use_flatpak = Some(value);
true
}
/// 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.
@@ -311,28 +274,12 @@ impl RealmConfig {
self.bool_value(|c| c.use_gpu) self.bool_value(|c| c.use_gpu)
} }
pub fn set_gpu(&mut self, value: bool) -> bool {
if self.gpu() == value {
return false;
}
self.use_gpu = Some(value);
true
}
/// If `true` and `self.gpu()` is also true, privileged device /dev/dri/card0 will be /// If `true` and `self.gpu()` is also true, privileged device /dev/dri/card0 will be
/// added to realm. /// added to realm.
pub fn gpu_card0(&self) -> bool { pub fn gpu_card0(&self) -> bool {
self.bool_value(|c| c.use_gpu_card0) self.bool_value(|c| c.use_gpu_card0)
} }
pub fn set_gpu_card0(&mut self, value: bool) -> bool {
if self.gpu_card0() == value {
return false;
}
self.use_gpu_card0 = Some(value);
true
}
/// If `true` the /Shared directory will be mounted in home directory of realm. /// If `true` the /Shared directory will be mounted in home directory of realm.
/// ///
/// This directory is shared between all running realms and is an easy way to move files /// This directory is shared between all running realms and is an easy way to move files
@@ -341,28 +288,12 @@ impl RealmConfig {
self.bool_value(|c| c.use_shared_dir) self.bool_value(|c| c.use_shared_dir)
} }
pub fn set_shared_dir(&mut self, value: bool) -> bool {
if self.shared_dir() == value {
return false;
}
self.use_shared_dir = Some(value);
true
}
/// If `true` the mount directory for external storage devices will be bind mounted as /Media /// If `true` the mount directory for external storage devices will be bind mounted as /Media
/// ///
pub fn media_dir(&self) -> bool { pub fn media_dir(&self) -> bool {
self.bool_value(|c| c.use_media_dir) self.bool_value(|c| c.use_media_dir)
} }
pub fn set_media_dir(&mut self, value: bool) -> bool {
if self.media_dir() == value {
return false;
}
self.use_media_dir = Some(value);
true
}
/// If `true` the home directory of this realm will be set up in ephemeral mode. /// If `true` the home directory of this realm will be set up in ephemeral mode.
/// ///
/// The ephemeral home directory is set up with the following steps: /// The ephemeral home directory is set up with the following steps:
@@ -377,14 +308,6 @@ impl RealmConfig {
self.bool_value(|c| c.use_ephemeral_home) self.bool_value(|c| c.use_ephemeral_home)
} }
pub fn set_ephemeral_home(&mut self, value: bool) -> bool {
if self.ephemeral_home() == value {
return false;
}
self.use_ephemeral_home = Some(value);
true
}
/// A list of subdirectories of /realms/realm-${name}/home to bind mount into realm /// A list of subdirectories of /realms/realm-${name}/home to bind mount into realm
/// home directory when ephemeral-home is enabled. /// home directory when ephemeral-home is enabled.
pub fn ephemeral_persistent_dirs(&self) -> Vec<String> { pub fn ephemeral_persistent_dirs(&self) -> Vec<String> {
@@ -407,14 +330,6 @@ impl RealmConfig {
self.bool_value(|c| c.use_sound) self.bool_value(|c| c.use_sound)
} }
pub fn set_sound(&mut self, value: bool) -> bool {
if self.sound() == value {
return false;
}
self.use_sound = Some(value);
true
}
/// If `true` access to the X11 server will be added to realm by bind mounting /// If `true` access to the X11 server will be added to realm by bind mounting
/// directory /tmp/.X11-unix /// directory /tmp/.X11-unix
pub fn x11(&self) -> bool { pub fn x11(&self) -> bool {
@@ -423,28 +338,12 @@ impl RealmConfig {
}) })
} }
pub fn set_x11(&mut self, value: bool) -> bool {
if self.x11() == value {
return false;
}
self.use_x11 = Some(value);
true
}
/// If `true` access to Wayland display will be permitted in realm by adding /// If `true` access to Wayland display will be permitted in realm by adding
/// wayland socket /run/user/1000/wayland-0 /// wayland socket /run/user/1000/wayland-0
pub fn wayland(&self) -> bool { pub fn wayland(&self) -> bool {
self.bool_value(|c| c.use_wayland) self.bool_value(|c| c.use_wayland)
} }
pub fn set_wayland(&mut self, value: bool) -> bool {
if self.wayland() == value {
return false;
}
self.use_wayland = Some(value);
true
}
/// The name of the wayland socket to use if `self.wayland()` is `true` /// The name of the wayland socket to use if `self.wayland()` is `true`
/// defaults to wayland-0, will appear in the realm as wayland-0 regardless of value /// defaults to wayland-0, will appear in the realm as wayland-0 regardless of value
pub fn wayland_socket(&self) -> &str { pub fn wayland_socket(&self) -> &str {
@@ -457,19 +356,12 @@ impl RealmConfig {
self.bool_value(|c| c.use_network) self.bool_value(|c| c.use_network)
} }
pub fn set_network(&mut self, value: bool) -> bool {
if self.network() == value {
return false;
}
self.use_network = Some(value);
true
}
/// The name of the network zone this realm will use if `self.network()` is `true`. /// The name of the network zone this realm will use if `self.network()` is `true`.
pub fn network_zone(&self) -> &str { pub fn network_zone(&self) -> &str {
self.str_value(|c| c.network_zone.as_ref()).unwrap_or(DEFAULT_ZONE) self.str_value(|c| c.network_zone.as_ref()).unwrap_or(DEFAULT_ZONE)
} }
/// If configured, this realm uses a fixed IP address on the zone subnet. The last /// If configured, this realm uses a fixed IP address on the zone subnet. The last
/// octet of the network address for this realm will be set to the provided value. /// octet of the network address for this realm will be set to the provided value.
pub fn reserved_ip(&self) -> Option<u8> { pub fn reserved_ip(&self) -> Option<u8> {
@@ -530,6 +422,7 @@ impl RealmConfig {
self.overlay = overlay.to_str_value().map(String::from) self.overlay = overlay.to_str_value().map(String::from)
} }
pub fn netns(&self) -> Option<&str> { pub fn netns(&self) -> Option<&str> {
self.str_value(|c| c.netns.as_ref()) self.str_value(|c| c.netns.as_ref())
} }

View File

@@ -12,9 +12,7 @@ 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),
@@ -24,9 +22,7 @@ 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

@@ -1,4 +1,4 @@
use std::fmt::Write; use std::fmt::{self,Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::{Realm, Result, util, realm::network::NetworkConfig}; use crate::{Realm, Result, util, realm::network::NetworkConfig};
@@ -18,23 +18,6 @@ $EXTRA_FILE_OPTIONS
"; ";
// SYSTEMD_NSPAWN_SHARE_NS_IPC is a secret flag that allows sharing IPC namespace between
// nspawn container and host. This is needed so that the X11 MIT-SHM extension will work
// correctly. Sharing the IPC namespace is not ideal, but also not obviously harmful.
//
// If this patch was applied to Mutter, then the MIT-SHM extension could be disabled for
// XWayland which would break some applications and degrade performance of others.
//
// https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2136
//
// Setting QT_X11_NO_MITSHM=1 should at least prevent QT applications from crashing if
// extension is not available.
//
// Another approach would be to use LD_PRELOAD to disable visibility of the extension for
// applications inside of container:
//
// https://github.com/jessfraz/dockerfiles/issues/359#issuecomment-828714848
//
const REALM_SERVICE_TEMPLATE: &str = "\ const REALM_SERVICE_TEMPLATE: &str = "\
[Unit] [Unit]
Description=Application Image $REALM_NAME instance Description=Application Image $REALM_NAME instance
@@ -77,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() || config.flatpak() { if config.fuse() {
self.add_device("/dev/fuse"); self.add_device("/dev/fuse");
} }
@@ -170,10 +153,6 @@ 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)?;
@@ -252,4 +231,9 @@ impl <'a> RealmLauncher <'a> {
} }
} }
impl From<fmt::Error> for crate::Error {
fn from(e: fmt::Error) -> Self {
format_err!("Error formatting string: {}", e).into()
}
}

View File

@@ -1,200 +0,0 @@
use std::process::Command;
use crate::{Realm,Result};
pub struct LiveConfig<'a> {
realm: &'a Realm,
}
const LIVE_VARS: &[&str] = &[
"use-gpu", "use-gpu-card0", "use-wayland", "use-x11",
"use-sound", "use-shared-dir", "use-kvm", "use-media-dir",
"use-fuse", "use-flatpak"
];
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
const SYSTEMD_RUN_PATH: &str = "/usr/bin/systemd-run";
const MACHINECTL_PATH: &str = "/usr/bin/machinectl";
impl <'a> LiveConfig<'a> {
pub fn is_live_configurable(varname: &str) -> bool {
LIVE_VARS.contains(&varname)
}
pub fn new(realm: &'a Realm) -> Self {
LiveConfig {
realm
}
}
pub fn configure(&self, varname: &str, enabled: bool) -> Result<()> {
match varname {
"use-gpu" => self.configure_gpu(enabled),
"use-gpu-card0" => self.configure_gpu_card0(enabled),
"use-wayland" => self.configure_wayland(enabled),
"use-x11" => self.configure_x11(enabled),
"use-sound" => self.configure_sound(enabled),
"use-shared-dir" => self.configure_shared(enabled),
"use-kvm" => self.configure_kvm(enabled),
"use-media-dir" => self.configure_media(enabled),
"use-fuse" => self.configure_fuse(enabled),
"use-flatpak" => self.configure_flatpak(enabled),
_ => bail!("Unknown live configuration variable '{}'", varname),
}
}
fn configure_gpu(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/dri/renderD128", enabled)
}
fn configure_gpu_card0(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/dri/card0", enabled)
}
fn configure_kvm(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/kvm", enabled)
}
fn configure_fuse(&self, enabled: bool) -> Result<()> {
self.enable_device("/dev/fuse", enabled)
}
fn configure_shared(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/realms/Shared",
Some("/home/user/Shared"),
false,
enabled)
}
fn configure_media(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/run/media/citadel",
Some("/home/user/Media"),
false,
enabled)
}
fn configure_sound(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/run/user/1000/pulse",
Some("/run/user/host/pulse"),
true,
enabled)
}
fn configure_wayland(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/run/user/1000/wayland-0",
Some("/run/user/host/wayland-0"),
true,
enabled)
}
fn configure_x11(&self, enabled: bool) -> Result<()> {
self.enable_bind_mount(
"/tmp/.X11-unix",
None,
true,
enabled)
}
fn configure_flatpak(&self, enabled: bool) -> Result<()> {
let path = self.realm.base_path_file("flatpak")
.display()
.to_string();
self.enable_bind_mount(
&path,
Some("/var/lib/flatpak"),
true,
enabled)
}
fn enable_bind_mount(&self, path: &str, target: Option<&str>, readonly: bool, enabled: bool) -> Result<()> {
if enabled {
self.machinectl_bind(path, target, readonly)
} else if let Some(target) = target {
self.unmount(target, false)
} else {
self.unmount(path, false)
}
}
fn enable_device(&self, device_path: &str, enabled: bool) -> Result<()> {
if enabled {
self.machinectl_bind(device_path, None, false)?;
self.systemctl_device_allow(device_path)?;
} else {
self.unmount(device_path, true)?;
}
Ok(())
}
fn unmount(&self, path: &str, delete: bool) -> Result<()> {
// systemd-run --machine={name} umount {path}
self.systemd_run(&["umount", path])?;
if delete {
self.systemd_run(&["rm", path])?;
}
Ok(())
}
fn systemctl_device_allow(&self, device_path: &str) -> Result<()> {
let status = Command::new(SYSTEMCTL_PATH)
.arg("set-property")
.arg(format!("realm-{}.service", self.realm.name()))
.arg(format!("DeviceAllow={}", device_path))
.status()
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
if !status.success() {
bail!("'systemctl set-property realm-{}.service DeviceAllow={}' command did not complete successfully status: {:?}", self.realm.name(), device_path, status.code());
}
Ok(())
}
fn machinectl_bind(&self, path: &str, target: Option<&str>, readonly: bool) -> Result<()> {
let mut cmd = Command::new(MACHINECTL_PATH);
cmd.arg("--mkdir");
if readonly {
cmd.arg("--read-only");
}
cmd.arg("bind")
.arg(self.realm.name())
.arg(path);
if let Some(target) = target {
cmd.arg(target);
}
let status = cmd.status()
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
if !status.success() {
bail!("machinectl bind {} {} {:?} did not complete successfully status: {:?}", self.realm.name(), path, target, status.code());
}
Ok(())
}
fn systemd_run(&self, args: &[&str]) -> Result<()> {
let status = Command::new(SYSTEMD_RUN_PATH)
.arg("--quiet")
.arg(format!("--machine={}", self.realm.name()))
.args(args)
.status()
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
if !status.success() {
bail!("systemd-run --machine={} command did not complete successfully args: {:?} status: {:?}", self.realm.name(), args, status.code());
}
Ok(())
}
}

View File

@@ -4,8 +4,6 @@ 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;
@@ -23,7 +21,6 @@ struct Inner {
events: RealmEventListener, events: RealmEventListener,
realms: Realms, realms: Realms,
realmfs_set: RealmFSSet, realmfs_set: RealmFSSet,
pid_mapper: PidMapper,
} }
impl Inner { impl Inner {
@@ -31,8 +28,7 @@ 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()?;
let pid_mapper = PidMapper::new()?; Ok(Inner { events, realms, realmfs_set })
Ok(Inner { events, realms, realmfs_set, pid_mapper })
} }
} }
@@ -194,13 +190,7 @@ impl RealmManager {
return Ok(()); return Ok(());
} }
info!("Starting realm {}", realm.name()); info!("Starting realm {}", realm.name());
self.inner().events.send_event(RealmEvent::Starting(realm.clone())); self._start_realm(realm, &mut HashSet::new())?;
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)
@@ -240,10 +230,6 @@ 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)?;
@@ -282,15 +268,6 @@ impl RealmManager {
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false) 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());
@@ -298,21 +275,10 @@ 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);
if let Err(err) = self.systemd.stop_realm(realm) { 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();
@@ -369,8 +335,8 @@ impl RealmManager {
} }
pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult { pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult {
let realms = self.realm_list(); let mapper = PidMapper::new(self.active_realms(false));
self.inner_mut().pid_mapper.lookup_pid(pid as libc::pid_t, realms) mapper.lookup_pid(pid as libc::pid_t)
} }
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> { pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {

View File

@@ -1,7 +1,6 @@
pub(crate) mod overlay; pub(crate) mod overlay;
pub(crate) mod config; pub(crate) mod config;
pub(crate) mod liveconfig;
pub(crate) mod realms; pub(crate) mod realms;
pub(crate) mod manager; pub(crate) mod manager;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]

View File

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

View File

@@ -169,20 +169,6 @@ 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()
} }
@@ -293,9 +279,6 @@ 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)
} }
@@ -317,9 +300,6 @@ 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

@@ -244,7 +244,7 @@ impl Realms {
pub fn delete_realm(&mut self, name: &str, save_home: bool) -> Result<()> { pub fn delete_realm(&mut self, name: &str, save_home: bool) -> Result<()> {
let _lock = Self::realmslock()?; let _lock = Self::realmslock()?;
let realm = match self.by_name(name) { let realm = match self.realms.take(name) {
Some(realm) => realm, Some(realm) => realm,
None => bail!("Cannot remove realm '{}' because it doesn't seem to exist", name), None => bail!("Cannot remove realm '{}' because it doesn't seem to exist", name),
}; };

View File

@@ -31,28 +31,6 @@ 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(())
} }
@@ -113,7 +91,7 @@ impl Systemd {
.arg(name) .arg(name)
.status() .status()
.map(|status| status.success()) .map(|status| status.success())
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?; .map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
Ok(ok) Ok(ok)
} }

View File

@@ -1,174 +0,0 @@
use std::cell::Cell;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::realm::BridgeAllocator;
use crate::{util, Result};
const NSPAWN_FILE_TEMPLATE: &str = "\
[Exec]
Boot=true
$NETWORK_CONFIG
[Files]
BindReadOnly=/storage/citadel-state/resolv.conf:/etc/resolv.conf
$BIND_MOUNTS
";
const SERVICE_TEMPLATE: &str = "\
[Unit]
Description=Update RealmFS $MACHINE_NAME instance
[Service]
DevicePolicy=closed
ExecStart=/usr/bin/systemd-nspawn --quiet --console=passive --notify-ready=yes --keep-unit --machine=$MACHINE_NAME --link-journal=auto --directory=$ROOTFS
KillMode=mixed
Type=notify
RestartForceExitStatus=133
SuccessExitStatus=133
";
const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl";
const SYSTEMD_NSPAWN_PATH: &str = "/run/systemd/nspawn";
const SYSTEMD_UNIT_PATH: &str = "/run/systemd/system";
/// Launcher for RealmFS update containers
pub struct RealmFSUpdateLauncher {
machine_name: String,
shared_directory: bool,
running: Cell<bool>,
rootfs: PathBuf,
service_path: PathBuf,
nspawn_path: PathBuf,
}
impl RealmFSUpdateLauncher {
fn new(machine_name: &str, rootfs: &Path, shared_directory: bool) -> Self {
let machine_name = machine_name.to_string();
let running = Cell::new(false);
let rootfs = rootfs.to_owned();
let service_path = PathBuf::from(SYSTEMD_UNIT_PATH).join(format!("realmfs-{machine_name}.service"));
let nspawn_path= PathBuf::from(SYSTEMD_NSPAWN_PATH).join(format!("{machine_name}.nspawn"));
RealmFSUpdateLauncher {
machine_name, shared_directory, running, rootfs, service_path, nspawn_path,
}
}
pub fn launch_update_container(machine_name: &str, rootfs: &Path, shared_directory: bool) -> Result<Self> {
let launcher = Self::new(machine_name, rootfs, shared_directory);
launcher.start_container()?;
Ok(launcher)
}
fn systemctl_start(&self) -> Result<bool> {
self.run_systemctl("start")
}
fn systemctl_stop(&self) -> Result<bool> {
self.run_systemctl("stop")
}
fn run_systemctl(&self, op: &str) -> Result<bool> {
let service_name = format!("realmfs-{}", self.machine_name);
let ok = Command::new(SYSTEMCTL_PATH)
.arg(op)
.arg(service_name)
.status()
.map(|status| status.success())
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
Ok(ok)
}
fn start_container(&self) -> Result<()> {
self.write_launch_config_files()?;
let ok = self.systemctl_start()?;
self.running.set(ok);
Ok(())
}
pub fn stop_container(&self) -> Result<()> {
if self.running.replace(false) {
self.systemctl_stop()?;
self.remove_launch_config_files()?;
// XXX remove IP address allocation?
}
Ok(())
}
fn remove_launch_config_files(&self) -> Result<()> {
util::remove_file(&self.nspawn_path)?;
util::remove_file(&self.service_path)?;
Ok(())
}
fn generate_nspawn_file(&self) -> Result<String> {
Ok(NSPAWN_FILE_TEMPLATE
.replace("$BIND_MOUNTS", &self.generate_bind_mounts()?)
.replace("$NETWORK_CONFIG", &self.generate_network_config()?))
}
fn generate_service_file(&self) -> String {
let rootfs = self.rootfs.display().to_string();
SERVICE_TEMPLATE
.replace("$MACHINE_NAME", &self.machine_name)
.replace("$ROOTFS", &rootfs)
}
/// Write the string `content` to file `path`. If the directory does
/// not already exist, create it.
fn write_launch_config_file(&self, path: &Path, content: &str) -> Result<()> {
match path.parent() {
Some(parent) => util::create_dir(parent)?,
None => bail!("config file path {} has no parent?", path.display()),
};
util::write_file(path, content)
}
fn generate_bind_mounts(&self) -> Result<String> {
let mut s = String::new();
if self.shared_directory && Path::new("/realms/Shared").exists() {
writeln!(s, "Bind=/realms/Shared:/run/Shared")?;
}
Ok(s)
}
fn generate_network_config(&self) -> Result<String> {
let mut s = String::new();
let mut alloc = BridgeAllocator::default_bridge()?;
let addr = alloc.allocate_address_for(&self.machine_name)?;
let gw = alloc.gateway();
writeln!(s, "Environment=IFCONFIG_IP={}", addr)?;
writeln!(s, "Environment=IFCONFIG_GW={}", gw)?;
writeln!(s, "[Network]")?;
writeln!(s, "Zone=clear")?;
Ok(s)
}
fn write_launch_config_files(&self) -> Result<()> {
let nspawn_content = self.generate_nspawn_file()?;
self.write_launch_config_file(&self.nspawn_path, &nspawn_content)?;
let service_content = self.generate_service_file();
self.write_launch_config_file(&self.service_path, &service_content)?;
Ok(())
}
}
impl Drop for RealmFSUpdateLauncher {
fn drop(&mut self) {
if let Err(err) = self.stop_container() {
warn!("Error stopping RealmFS update container: {}", err);
}
}
}

View File

@@ -1,10 +1,9 @@
pub(crate) mod resizer; pub(crate) mod resizer;
mod mountpoint; mod mountpoint;
pub(crate) mod update; mod update;
pub(crate) mod realmfs_set; pub(crate) mod realmfs_set;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod realmfs; mod realmfs;
mod launcher;
pub use self::realmfs::RealmFS; pub use self::realmfs::RealmFS;
pub use self::mountpoint::Mountpoint; pub use self::mountpoint::Mountpoint;

View File

@@ -7,7 +7,7 @@ use std::sync::{Arc, Weak, RwLock};
use crate::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize}; use crate::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize};
use crate::realmfs::resizer::Superblock; use crate::realmfs::resizer::Superblock;
use crate::realmfs::update::RealmFSUpdate; use crate::realmfs::update::Update;
use super::mountpoint::Mountpoint; use super::mountpoint::Mountpoint;
// Maximum length of a RealmFS name // Maximum length of a RealmFS name
@@ -266,13 +266,8 @@ impl RealmFS {
} }
} }
pub fn update(&self) -> Result<RealmFSUpdate> {
let update = RealmFSUpdate::create(self)?;
Ok(update)
}
pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> { pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> {
let mut update = RealmFSUpdate::create(self)?; let mut update = Update::create(self)?;
update.run_interactive_update(scheme) update.run_interactive_update(scheme)
} }
@@ -393,14 +388,14 @@ impl RealmFS {
pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> { pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> {
info!("Resizing to {} blocks", size.nblocks()); info!("Resizing to {} blocks", size.nblocks());
let mut update = RealmFSUpdate::create(self)?; let mut update = Update::create(self)?;
update.grow_to(size); update.grow_to(size);
update.resize() update.resize()
} }
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> { pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
info!("Resizing to an increase of {} blocks", size.nblocks()); info!("Resizing to an increase of {} blocks", size.nblocks());
let mut update = RealmFSUpdate::create(self)?; let mut update = Update::create(self)?;
update.grow_by(size); update.grow_by(size);
update.resize() update.resize()
} }

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 has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new /// If the RealmFS needs to be resized to a larger size, returns the
/// size to resize the filesystem to and return it. Otherwise, return `None` /// recommended size.
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,37 +56,22 @@ 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() {
return None; let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
} let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
let metainfo_nblocks = realmfs.metainfo().nblocks(); let mask = grow_size - 1;
let grow_blocks = (free_blocks + mask) & !mask;
if metainfo_nblocks >= AUTO_RESIZE_INCREASE_SIZE.nblocks() { Some(ResizeSize::blocks(grow_blocks))
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 {
// Otherwise for original size under 4GB, since raising to 4GB is not enough, None
// 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

@@ -11,8 +11,6 @@ use crate::util::is_euid_root;
use crate::terminal::TerminalRestorer; use crate::terminal::TerminalRestorer;
use crate::verity::Verity; use crate::verity::Verity;
use super::launcher::RealmFSUpdateLauncher;
const BLOCK_SIZE: usize = 4096; const BLOCK_SIZE: usize = 4096;
// The maximum number of backup copies the rotate() method will create // The maximum number of backup copies the rotate() method will create
@@ -23,41 +21,36 @@ const RESIZE2FS: &str = "resize2fs";
/// Manages the process of updating or resizing a `RealmFS` image file. /// Manages the process of updating or resizing a `RealmFS` image file.
/// ///
pub struct RealmFSUpdate { pub struct Update<'a> {
realmfs: RealmFS, // RealmFS being updated realmfs: &'a RealmFS, // RealmFS being updated
name: String, // name for nspawn instance name: String, // name for nspawn instance
target: PathBuf, // Path to the update copy of realmfs image target: PathBuf, // Path to the update copy of realmfs image
mountpath: PathBuf, // Path at which update copy is mounted mountpath: PathBuf, // Path at which update copy is mounted
container: Option<RealmFSUpdateLauncher>,
_lock: FileLock, _lock: FileLock,
resize: Option<ResizeSize>, // If the image needs to be resized, the resize size is stored here resize: Option<ResizeSize>, // If the image needs to be resized, the resize size is stored here
network_allocated: bool, network_allocated: bool,
} }
impl RealmFSUpdate { impl <'a> Update<'a> {
fn new(realmfs: RealmFS, lock: FileLock) -> Self { fn new(realmfs: &'a RealmFS, lock: FileLock) -> Self {
let metainfo = realmfs.metainfo(); let metainfo = realmfs.metainfo();
let tag = metainfo.verity_tag(); let tag = metainfo.verity_tag();
let mountpath = Path::new(RealmFS::RUN_DIRECTORY) let mountpath = Path::new(RealmFS::RUN_DIRECTORY)
.join(format!("realmfs-{}-{}.update", realmfs.name(), tag)); .join(format!("realmfs-{}-{}.update", realmfs.name(), tag));
let name = format!("{}-{}-update", realmfs.name(), tag); Update {
let resize = ResizeSize::auto_resize_size(&realmfs);
let target = realmfs.path().with_extension("update");
RealmFSUpdate {
realmfs, realmfs,
name, name: format!("{}-{}-update", realmfs.name(), tag),
target, target: realmfs.path().with_extension("update"),
mountpath, mountpath,
_lock: lock, _lock: lock,
container: None, resize: ResizeSize::auto_resize_size(realmfs),
resize,
network_allocated: false, network_allocated: false,
} }
} }
pub fn create(realmfs: &RealmFS) -> Result<Self> { pub fn create(realmfs: &'a RealmFS) -> Result<Self> {
let lock = FileLock::nonblocking_acquire(realmfs.path().with_extension("lock"))? let lock = FileLock::nonblocking_acquire(realmfs.path().with_extension("lock"))?
.ok_or(format_err!("Unable to obtain file lock to update realmfs image: {}", realmfs.name()))?; .ok_or(format_err!("Unable to obtain file lock to update realmfs image: {}", realmfs.name()))?;
@@ -65,10 +58,10 @@ impl RealmFSUpdate {
bail!("Cannot seal realmfs image, no sealing keys available"); bail!("Cannot seal realmfs image, no sealing keys available");
} }
Ok(RealmFSUpdate::new(realmfs.clone(), lock)) Ok(Update::new(realmfs, lock))
} }
pub fn name(&self) -> &str { fn name(&self) -> &str {
&self.name &self.name
} }
@@ -81,11 +74,10 @@ impl RealmFSUpdate {
info!("Update file {} already exists, removing it", self.target.display()); info!("Update file {} already exists, removing it", self.target.display());
util::remove_file(&self.target)?; util::remove_file(&self.target)?;
} }
info!("Creating update copy of realmfs {} -> {}",
self.realmfs.path().display(),
self.target().display());
self.realmfs.copy_image_file(self.target())?; self.realmfs.copy_image_file(self.target())?;
self.truncate_verity()?;
self.resize_image_file()?;
Ok(()) Ok(())
} }
@@ -112,10 +104,6 @@ impl RealmFSUpdate {
} }
fn mount_update_image(&mut self) -> Result<()> { fn mount_update_image(&mut self) -> Result<()> {
info!("Loop device mounting {} at {}",
self.target().display(),
self.mountpath.display());
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| { LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
if self.resize.is_some() { if self.resize.is_some() {
self.resize_device(loopdev)?; self.resize_device(loopdev)?;
@@ -127,8 +115,9 @@ impl RealmFSUpdate {
} }
// 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() self.realmfs.metainfo().nblocks() + 1
} }
fn unmount_update_image(&mut self) { fn unmount_update_image(&mut self) {
@@ -170,8 +159,7 @@ impl RealmFSUpdate {
} }
fn set_target_len(&self, nblocks: usize) -> Result<()> { fn set_target_len(&self, nblocks: usize) -> Result<()> {
// add one block for header block let len = (nblocks * BLOCK_SIZE) as u64;
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)
@@ -183,13 +171,12 @@ impl RealmFSUpdate {
// Remove dm-verity hash tree from update copy of image file. // Remove dm-verity hash tree from update copy of image file.
fn truncate_verity(&self) -> Result<()> { fn truncate_verity(&self) -> Result<()> {
info!("Truncating dm-verity hash tree from {}", self.target().display());
let file_nblocks = self.realmfs.file_nblocks()?; let file_nblocks = self.realmfs.file_nblocks()?;
let metainfo_nblocks = self.metainfo_nblock_size(); let metainfo_nblocks = self.metainfo_nblock_size();
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 + 1) { } else if file_nblocks > metainfo_nblocks {
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set"); warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
} }
Ok(()) Ok(())
@@ -198,12 +185,10 @@ impl RealmFSUpdate {
// 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(), Some(rs) => rs.nblocks() + 1,
None => return Ok(()), None => return Ok(()),
}; };
info!("Resizing target file to {} blocks", nblocks);
if nblocks < self.metainfo_nblock_size() { if nblocks < self.metainfo_nblock_size() {
bail!("Cannot shrink image") bail!("Cannot shrink image")
} }
@@ -214,19 +199,7 @@ impl RealmFSUpdate {
self.set_target_len(nblocks) self.set_target_len(nblocks)
} }
fn shutdown_container(&mut self) -> Result<()> {
if let Some(update) = self.container.take() {
update.stop_container()?;
}
Ok(())
}
pub fn cleanup(&mut self) { pub fn cleanup(&mut self) {
// if a container was started, stop it
if let Err(err) = self.shutdown_container() {
warn!("Error shutting down update container: {}", err);
}
if self.mountpath.exists() { if self.mountpath.exists() {
self.unmount_update_image(); self.unmount_update_image();
} }
@@ -251,7 +224,7 @@ impl RealmFSUpdate {
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(), None => self.metainfo_nblock_size() - 1,
}; };
let salt = hex::encode(randombytes(32)); let salt = hex::encode(randombytes(32));
@@ -259,11 +232,20 @@ impl RealmFSUpdate {
.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");
@@ -294,21 +276,6 @@ impl RealmFSUpdate {
Ok(yes) Ok(yes)
} }
pub fn prepare_update(&mut self, shared_directory: bool) -> Result<()> {
if !is_euid_root() {
bail!("RealmFS updates must be prepared as root");
}
self.setup()?;
self.launch_update_container(shared_directory)?;
Ok(())
}
pub fn commit_update(&mut self) -> Result<()> {
let result = self.apply_update();
self.cleanup();
result
}
pub fn run_interactive_update(&mut self, scheme: Option<&str>) -> Result<()> { pub fn run_interactive_update(&mut self, scheme: Option<&str>) -> Result<()> {
if !is_euid_root() { if !is_euid_root() {
bail!("RealmFS updates must be run as root"); bail!("RealmFS updates must be run as root");
@@ -340,21 +307,6 @@ impl RealmFSUpdate {
Ok(()) Ok(())
} }
fn launch_update_container(&mut self, shared_directory: bool) -> Result<()> {
if self.container.is_some() {
bail!("Update container is already running");
}
info!("Launching update container '{}'", self.name());
let update = RealmFSUpdateLauncher::launch_update_container(self.name(), &self.mountpath, shared_directory)?;
self.container = Some(update);
Ok(())
}
pub fn run_update_shell(&mut self, command: &str) -> Result<()> { pub fn run_update_shell(&mut self, command: &str) -> Result<()> {
let mut alloc = BridgeAllocator::default_bridge()?; let mut alloc = BridgeAllocator::default_bridge()?;
@@ -404,7 +356,7 @@ impl RealmFSUpdate {
} }
} }
impl Drop for RealmFSUpdate { impl <'a> Drop for Update<'a> {
fn drop(&mut self) { fn drop(&mut self) {
self.cleanup(); self.cleanup();
} }

View File

@@ -420,10 +420,8 @@ fn compare_images(a: Option<ResourceImage>, b: ResourceImage) -> Result<Resource
None => return Ok(b), None => return Ok(b),
}; };
let bind_a = a.metainfo(); let ver_a = a.metainfo().version();
let bind_b = b.metainfo(); let ver_b = b.metainfo().version();
let ver_a = bind_a.version();
let ver_b = bind_b.version();
if ver_a > ver_b { if ver_a > ver_b {
Ok(a) Ok(a)

View File

@@ -5,5 +5,5 @@ mod uname;
pub use self::uname::UtsName; pub use self::uname::UtsName;
pub use self::loopdev::LoopDevice; pub use self::loopdev::LoopDevice;
pub use self::mounts::Mounts; pub use self::mounts::{Mounts,MountLine};
pub use self::lock::FileLock; pub use self::lock::FileLock;

View File

@@ -7,7 +7,6 @@ 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 std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -48,12 +47,6 @@ fn search_path(filename: &str) -> Result<PathBuf> {
bail!("could not find {} in $PATH", filename) bail!("could not find {} in $PATH", filename)
} }
pub fn append_to_path(p: &Path, s: &str) -> PathBuf {
let mut p_osstr = p.as_os_str().to_owned();
p_osstr.push(s);
p_osstr.into()
}
pub fn ensure_command_exists(cmd: &str) -> Result<()> { pub fn ensure_command_exists(cmd: &str) -> Result<()> {
let path = Path::new(cmd); let path = Path::new(cmd);
if !path.is_absolute() { if !path.is_absolute() {
@@ -224,8 +217,7 @@ 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();
let is_symlink = fs::symlink_metadata(path).is_ok(); if path.exists() {
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))?;
} }
@@ -376,38 +368,9 @@ pub fn touch_mtime(path: &Path) -> Result<()> {
utimes(path, meta.atime(),mtime)?; utimes(path, meta.atime(),mtime)?;
Ok(()) 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

@@ -0,0 +1,15 @@
[package]
name = "realm-config-ui"
version = "0.1.0"
authors = ["Bruce Leidl <bruce@subgraph.com>"]
edition = "2018"
description = "Realm Configuration Tool"
homepage = "https://subgraph.com"
[dependencies]
libcitadel = { path = "../libcitadel" }
rand = "0.8"
zvariant = "2.7.0"
serde = { version = "1.0", features = ["derive"] }
zbus = "=2.0.0-beta.5"
gtk = { version = "0.14.0", features = ["v3_24"] }

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ColorSchemeDialog" parent="GtkDialog">
<property name="title">Choose Terminal Colors</property>
<property name="modal">True</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="orientation">horizontal</property>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkTreeView" id="colorscheme-tree">
<property name="headers-visible">False</property>
<property name="model">treemodel</property>
<child>
<object class="GtkTreeViewColumn">
<property name="expand">True</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator">
<property name="orientation">vertical</property>
</object>
</child>
<child>
<object class="GtkLabel" id="colorscheme-label">
<property name="hexpand">True</property>
<property name="halign">fill</property>
</object>
</child>
</object>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="use-underline">1</property>
<property name="label">Cancel</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="ok_button">
<property name="use-underline">1</property>
<property name="label">_Choose</property>
<property name="can-default">True</property>
</object>
</child>
<action-widgets>
<action-widget response="cancel">cancel_button</action-widget>
<action-widget response="ok" default="true">ok_button</action-widget>
</action-widgets>
</template>
<object class="GtkTreeStore" id="treemodel">
<columns>
<column type="gchararray" />
<column type="gchararray" />
</columns>
</object>
</interface>

View File

@@ -0,0 +1,216 @@
use std::rc::Rc;
use gtk::prelude::*;
use gtk::glib;
use libcitadel::terminal::{Base16Scheme, Color};
enum RootEntry {
Scheme(Base16Scheme),
Category(String, Vec<Base16Scheme>),
}
impl RootEntry {
fn key(&self) -> &str {
match self {
RootEntry::Scheme(scheme) => scheme.slug(),
RootEntry::Category(name, _) => name.as_str(),
}
}
fn add_to_category(list: &mut Vec<RootEntry>, category: &str, scheme: &Base16Scheme) {
let scheme = scheme.clone();
for entry in list.iter_mut() {
if let RootEntry::Category(name, schemes) = entry {
if name == category {
schemes.push(scheme);
return;
}
}
}
list.push(RootEntry::Category(category.to_string(), vec![scheme]))
}
fn build_list() -> Vec<RootEntry> {
let mut list = Vec::new();
for scheme in Base16Scheme::all_schemes() {
if let Some(category) = scheme.category() {
Self::add_to_category(&mut list,category, &scheme);
} else {
list.push(RootEntry::Scheme(scheme));
}
}
list.sort_by(|a, b| a.key().cmp(b.key()));
list
}
}
#[derive(Clone)]
pub struct ColorSchemes {
entries: Rc<Vec<RootEntry>>,
}
impl ColorSchemes {
pub fn new() -> Self {
ColorSchemes {
entries: Rc::new(RootEntry::build_list()),
}
}
pub fn populate_tree_model(&self, store: &gtk::TreeStore) {
for entry in self.entries.iter() {
match entry {
RootEntry::Scheme(scheme) => {
let first = scheme.slug().to_string();
let second = scheme.name().to_string();
store.insert_with_values(None, None, &[(0, &first), (1, &second)]);
}
RootEntry::Category(name, list) => {
let first = String::new();
let second = name.to_string();
let iter = store.insert_with_values(None, None, &[(0, &first), (1, &second)]);
for scheme in list {
let first = scheme.slug().to_string();
let second = scheme.name().to_string();
store.insert_with_values(Some(&iter), None, &[(0, &first), (1, &second)]);
}
}
}
}
}
pub fn preview_scheme(&self, id: &str) -> Option<(String, Color)> {
let scheme = Base16Scheme::by_name(id)?;
let bg = scheme.terminal_background();
let text = PreviewRender::new(scheme).render_preview();
Some((text, bg))
}
}
struct PreviewRender {
buffer: String,
scheme: Base16Scheme,
}
impl PreviewRender {
fn new(scheme: &Base16Scheme) -> Self {
let scheme = scheme.clone();
PreviewRender {
buffer: String::new(),
scheme,
}
}
fn print(mut self, color_idx: usize, text: &str) -> Self {
let s = glib::markup_escape_text(text);
let color = self.scheme.terminal_palette_color(color_idx);
self.color_span(Some(color), None);
self.buffer.push_str(s.as_str());
self.end_span();
self
}
fn vtype(self, text: &str) -> Self {
self.print(3, text)
}
fn konst(self, text: &str) -> Self {
self.print(1, text)
}
fn func(self, text: &str) -> Self {
self.print(4, text)
}
fn string(self, text: &str) -> Self {
self.print(2, text)
}
fn keyword(self, text: &str) -> Self {
self.print(5, text)
}
fn comment(self, text: &str) -> Self {
self.print(8, text)
}
fn text(mut self, text: &str) -> Self {
let color = self.scheme.terminal_foreground();
self.color_span(Some(color), None);
self.buffer.push_str(text);
self.end_span();
self
}
fn color_attrib(&mut self, name: &str, color: Color) {
let (r,g,b) = color.rgb();
self.buffer.push_str(&format!(" {}='#{:02X}{:02X}{:02X}'", name, r, g, b));
}
fn color_span(&mut self, fg: Option<Color>, bg: Option<Color>) {
self.buffer.push_str("<span");
if let Some(color) = fg {
self.color_attrib("foreground", color);
}
if let Some(color) = bg {
self.color_attrib("background", color);
}
self.buffer.push_str(">");
}
fn end_span(&mut self) {
self.buffer.push_str("</span>");
}
fn nl(mut self) -> Self {
self.buffer.push_str(" \n ");
self
}
fn render_colorbar(&mut self) {
self.buffer.push_str("\n ");
let color = self.scheme.terminal_foreground();
self.color_span(Some(color), None);
for i in 0..16 {
self.buffer.push_str(&format!(" {:X} ", i));
}
self.end_span();
self.buffer.push_str(" \n ");
for i in 0..16 {
let c = self.scheme.color(i);
self.color_span(None, Some(c));
self.buffer.push_str(" ");
self.end_span();
}
self.buffer.push_str(" \n ");
for i in 8..16 {
let c = self.scheme.terminal_palette_color(i);
self.color_span(None, Some(c));
self.buffer.push_str(" ");
self.end_span();
}
self.buffer.push_str(" \n ");
}
fn render_preview(mut self) -> String {
let name = self.scheme.name().to_string();
self.render_colorbar();
self.nl()
.comment("/**").nl()
.comment(" * An example of how this color scheme").nl()
.comment(" * might look in a text editor with syntax").nl()
.comment(" * highlighting.").nl()
.comment(" */").nl()
.nl()
.func("#include ").string("<stdio.h>").nl()
.func("#include ").string("<stdlib.h>").nl()
.nl()
.vtype("static char").text(" theme[] = ").string(&format!("\"{}\"", name)).text(";").nl()
.nl()
.vtype("int").text(" main(").vtype("int").text(" argc, ").vtype("char").text(" **argv) {").nl()
.text(" printf(").string("\"Hello, ").keyword("%s").text("!").keyword("\\n").string("\"").text(", theme);").nl()
.text(" exit(").konst("0").text(");").nl()
.text("}")
.nl()
.nl().buffer
}
}

View File

@@ -0,0 +1,153 @@
use std::cell::RefCell;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use libcitadel::terminal::{Base16Scheme, Color};
use crate::colorscheme::colorschemes::ColorSchemes;
#[derive(CompositeTemplate)]
#[template(file = "colorscheme-dialog.ui")]
pub struct ColorSchemeDialog {
#[template_child(id="colorscheme-tree")]
tree: TemplateChild<gtk::TreeView>,
#[template_child]
treemodel: TemplateChild<gtk::TreeStore>,
#[template_child(id="colorscheme-label")]
preview: TemplateChild<gtk::Label>,
css_provider: gtk::CssProvider,
colorschemes: ColorSchemes,
tracker: RefCell<Option<SelectionTracker>>,
}
#[derive(Clone)]
struct SelectionTracker {
model: gtk::TreeStore,
selection: gtk::TreeSelection,
preview: gtk::Label,
colorschemes: ColorSchemes,
css_provider: gtk::CssProvider,
}
impl SelectionTracker {
fn new(dialog: &ColorSchemeDialog) -> Self {
let tracker = SelectionTracker {
model: dialog.treemodel.clone(),
selection: dialog.tree.selection(),
preview: dialog.preview.clone(),
colorschemes: dialog.colorschemes.clone(),
css_provider: dialog.css_provider.clone(),
};
tracker.selection.connect_changed(glib::clone!(@strong tracker => move |_| {
if let Some(id) = tracker.selected_id() {
if let Some((text, background)) = tracker.colorschemes.preview_scheme(&id) {
tracker.set_preview_background(background);
tracker.preview.set_markup(&text);
}
}
}));
tracker
}
fn selected_id(&self) -> Option<String> {
self.selection.selected().and_then(|(model,iter)| {
model.value(&iter, 0).get::<String>().ok()
})
}
fn set_preview_background(&self, color: Color) {
const CSS: &str =
r##"
#colorscheme-label {
background-color: $COLOR;
font-family: monospace;
font-size: 14pt;
}
"##;
let (r, g, b) = color.rgb();
let css = CSS.replace("$COLOR", &format!("#{:02x}{:02x}{:02x}", r, g, b));
if let Err(e) = self.css_provider.load_from_data(css.as_bytes()) {
warn!("Error loading CSS provider data: {}", e);
}
}
fn set_selected_id(&self, id: &str) {
self.model.foreach(glib::clone!(@strong self.selection as selection => move |model, _path, iter| {
if let Ok(ref s) = model.value(iter, 0).get::<String>() {
if s == id {
selection.select_iter(iter);
return true;
}
}
false
}))
}
}
impl ColorSchemeDialog {
pub fn set_selected_id(&self, colorscheme_id: &str) {
let tracker = self.tracker.borrow();
if let Some(tracker) = tracker.as_ref() {
tracker.set_selected_id(colorscheme_id);
}
}
pub fn get_selected_scheme (&self) -> Option<Base16Scheme> {
let tracker = self.tracker.borrow();
tracker.as_ref().and_then(|t| t.selected_id())
.and_then(|id| Base16Scheme::by_name(&id))
.cloned()
}
}
impl Default for ColorSchemeDialog {
fn default() -> Self {
ColorSchemeDialog {
tree: Default::default(),
treemodel: Default::default(),
preview: Default::default(),
css_provider: gtk::CssProvider::new(),
colorschemes: ColorSchemes::new(),
tracker: RefCell::new(None),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for ColorSchemeDialog {
const NAME: &'static str = "ColorSchemeDialog";
type Type = super::ColorSchemeDialog;
type ParentType = gtk::Dialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ColorSchemeDialog {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.preview.set_widget_name("colorscheme-label");
self.preview.style_context().add_provider(&self.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
self.colorschemes.populate_tree_model(&self.treemodel);
let tracker = SelectionTracker::new(self);
self.tracker.borrow_mut().replace(tracker);
}
}
impl DialogImpl for ColorSchemeDialog {}
impl WindowImpl for ColorSchemeDialog {}
impl BinImpl for ColorSchemeDialog {}
impl ContainerImpl for ColorSchemeDialog {}
impl WidgetImpl for ColorSchemeDialog {}

View File

@@ -0,0 +1,31 @@
use gtk::glib;
use glib::subclass::prelude::*;
use libcitadel::terminal::Base16Scheme;
mod dialog;
mod colorschemes;
glib::wrapper! {
pub struct ColorSchemeDialog(ObjectSubclass<dialog::ColorSchemeDialog>)
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
@implements gtk::Buildable;
}
impl ColorSchemeDialog {
pub fn new() -> Self {
glib::Object::new(&[("use-header-bar", &1)])
.expect("Failed to create ColorSchemeDialog")
}
fn instance(&self) -> &dialog::ColorSchemeDialog {
dialog::ColorSchemeDialog::from_instance(self)
}
pub fn get_selected_scheme(&self) -> Option<Base16Scheme> {
self.instance().get_selected_scheme()
}
pub fn set_selected_scheme(&self, id: &str) {
self.instance().set_selected_id(id);
}
}

View File

@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ConfigureDialog" parent="GtkDialog">
<property name="title">Configure Realm</property>
<property name="modal">True</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin">20</property>
<child>
<object class="GtkLabel">
<property name="label">Options</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="margin-bottom">20</property>
<child>
<!-- -->
<object class="GtkListBox" id="bool-options-box">
<property name="margin">10</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>Overlay</big></b>
Type of rootfs overlay realm is configured to use.
<b>None</b> Don't use a rootfs overlay
<b>TmpFS</b> Use a rootfs overlay stored on tmpfs
<b>Storage</b> Use a rootfs overlay stored on disk in storage partition
]]></property>
<child>
<object class="GtkLabel">
<property name="label">Overlay</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkComboBoxText" id="overlay-combo">
<property name="active">0</property>
<items>
<item id="storage">Storage</item>
<item id="tmpfs">TmpFS</item>
<item id="none">None</item>
</items>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>RealmFS</big></b>
Root filesystem image to use for realm.
]]></property>
<child>
<object class="GtkLabel">
<property name="label">RealmFS</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkComboBoxText" id="realmfs-combo">
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>Terminal Color Scheme</big></b>
Choose a color scheme to use in terminals in this realm.
]]></property>
<child>
<object class="GtkLabel">
<property name="label">Color Scheme</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkButton" id="color-scheme-button">
<property name="label">Default Dark</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="tooltip-markup"><![CDATA[<b><big>Window Frame Color</big></b>
Set a color to be used when frames are drawn around application windows for this realm.
]]></property>
<child>
<object class="GtkLabel">
<property name="label">Frame Color</property>
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkColorButton" id="frame-color-button">
<property name="color">#ffff00000000</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="use-underline">1</property>
<property name="label">Cancel</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="ok_button">
<property name="use-underline">1</property>
<property name="label">Apply</property>
<property name="can-default">True</property>
</object>
</child>
<action-widgets>
<action-widget response="cancel">cancel_button</action-widget>
<action-widget response="ok" default="true">ok_button</action-widget>
</action-widgets>
</template>
<object class="GtkSizeGroup">
<widgets>
<widget name="overlay-combo" />
<widget name="realmfs-combo" />
<widget name="color-scheme-button" />
<widget name="frame-color-button" />
</widgets>
</object>
</interface>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ConfigureOption" parent="GtkListBoxRow">
<property name="width_request">100</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="spacing">30</property>
<child>
<object class="GtkLabel" id="name">
<property name="hexpand">True</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="switch">
<property name="halign">end</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@@ -0,0 +1,203 @@
use std::cell::{Ref, RefCell};
use std::rc::Rc;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use crate::colorscheme::ColorSchemeDialog;
use crate::configure_dialog::ConfigOptions;
use crate::configure_dialog::settings::CitadelSettings;
use crate::realmsd::RealmConfig;
#[derive(CompositeTemplate)]
#[template(file = "configure-dialog.ui")]
pub struct ConfigureDialog {
#[template_child(id="bool-options-box")]
bool_option_list: TemplateChild<gtk::ListBox>,
#[template_child(id="overlay-combo")]
overlay: TemplateChild<gtk::ComboBoxText>,
#[template_child(id="realmfs-combo")]
realmfs: TemplateChild<gtk::ComboBoxText>,
#[template_child(id="color-scheme-button")]
colorscheme: TemplateChild<gtk::Button>,
#[template_child(id="frame-color-button")]
frame_color: TemplateChild<gtk::ColorButton>,
options: Rc<RefCell<ConfigOptions>>,
bool_option_rows: RefCell<Vec<super::ConfigureOption>>,
colorscheme_dialog: ColorSchemeDialog,
settings: RefCell<CitadelSettings>,
}
impl ConfigureDialog {
pub fn set_realm_name(&self, name: &str) {
let color = self.settings.borrow().get_realm_color(Some(name));
self.frame_color.set_rgba(&color);
}
pub fn reset_options(&self) {
self.options.borrow_mut().reset();
self.update_options();
}
pub fn set_config(&self, config: &RealmConfig) {
self.options.borrow_mut().configure(config);
self.realmfs.remove_all();
self.update_options();
}
pub fn changes(&self) -> Vec<(String,String)> {
self.options.borrow().changes()
}
pub fn store_settings(&self, realm_name: &str) {
let color = self.frame_color.rgba();
self.settings.borrow_mut().store_realm_color(realm_name, color);
}
pub fn options(&self) -> Ref<ConfigOptions> {
self.options.borrow()
}
fn update_realmfs(&self) {
self.realmfs.remove_all();
for realmfs in self.options().realmfs_list() {
self.realmfs.append(Some(realmfs.as_str()), realmfs.as_str());
}
let current = self.options().realmfs();
self.realmfs.set_active_id(Some(&current));
}
fn update_options(&self) {
let rows = self.bool_option_rows.borrow();
for row in rows.iter() {
row.update();
}
let overlay_id = self.options().overlay_id();
self.overlay.set_active_id(Some(&overlay_id));
self.update_realmfs();
let scheme = self.options().colorscheme();
self.colorscheme.set_label(scheme.name());
}
fn create_option_rows(&self) {
let mut rows = self.bool_option_rows.borrow_mut();
let options = self.options.borrow();
for op in options.bool_options() {
let w = super::ConfigureOption::new(op);
self.bool_option_list.add(&w);
rows.push(w);
}
}
fn setup_overlay(&self) {
let options = self.options.clone();
self.overlay.connect_changed(move |combo| {
if let Some(text) = combo.active_id() {
options.borrow_mut().set_overlay_id(text.as_str());
}
});
}
fn setup_realmfs(&self) {
let options = self.options.clone();
self.realmfs.connect_changed(move |combo| {
if let Some(text) = combo.active_text() {
options.borrow_mut().set_realmfs(text.as_str());
}
});
}
fn setup_colorscheme(&self) {
let dialog = self.colorscheme_dialog.clone();
let options = self.options.clone();
self.colorscheme.connect_clicked(move |b| {
dialog.show_all();
let scheme = options.borrow().colorscheme();
dialog.set_selected_scheme(scheme.slug());
match dialog.run() {
gtk::ResponseType::Ok => {
if let Some(scheme) = dialog.get_selected_scheme() {
options.borrow_mut().set_colorscheme_id(scheme.slug());
b.set_label(scheme.name());
}
},
_ => {},
}
dialog.hide();
});
}
fn setup_frame_color(&self) {
let color = self.settings.borrow().get_realm_color(None);
self.frame_color.set_rgba(&color);
}
fn setup_widgets(&self) {
self.create_option_rows();
self.setup_overlay();
self.setup_realmfs();
self.setup_colorscheme();
self.setup_frame_color();
}
}
impl Default for ConfigureDialog {
fn default() -> Self {
ConfigureDialog {
bool_option_list: Default::default(),
overlay: Default::default(),
realmfs: Default::default(),
colorscheme: Default::default(),
frame_color: Default::default(),
colorscheme_dialog: ColorSchemeDialog::new(),
options: Rc::new(RefCell::new(ConfigOptions::new())),
settings: RefCell::new(CitadelSettings::new()),
bool_option_rows: RefCell::new(Vec::new()),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for ConfigureDialog {
const NAME: &'static str = "ConfigureDialog";
type Type = super::ConfigureDialog;
type ParentType = gtk::Dialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ConfigureDialog {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.colorscheme_dialog.set_transient_for(Some(&self.instance()));
self.setup_widgets();
}
}
impl DialogImpl for ConfigureDialog {}
impl WindowImpl for ConfigureDialog {}
impl BinImpl for ConfigureDialog {}
impl ContainerImpl for ConfigureDialog {}
impl WidgetImpl for ConfigureDialog {}

View File

@@ -0,0 +1,78 @@
use gtk::glib;
use gtk::prelude::*;
use glib::subclass::prelude::*;
use crate::realmsd::RealmConfig;
pub use crate::configure_dialog::options::{ConfigOptions,BoolOption};
mod dialog;
mod option_row;
mod options;
mod settings;
glib::wrapper! {
pub struct ConfigureDialog(ObjectSubclass<dialog::ConfigureDialog>)
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
@implements gtk::Buildable;
}
impl ConfigureDialog {
pub fn new() -> Self {
glib::Object::new(&[("use-header-bar", &1)])
.expect("Failed to create ConfigureDialog")
}
fn instance(&self) -> &dialog::ConfigureDialog {
dialog::ConfigureDialog::from_instance(self)
}
pub fn changes(&self) -> Vec<(String,String)> {
self.instance().changes()
}
pub fn store_settings(&self, realm_name: &str) {
self.instance().store_settings(realm_name);
}
pub fn reset_options(&self) {
self.instance().reset_options();
}
pub fn set_realm_name(&self, name: &str) {
self.set_title(&format!("Configure realm-{}", name));
self.instance().set_realm_name(name);
}
pub fn set_config(&self, config: &RealmConfig) {
self.instance().set_config(config);
}
}
glib::wrapper! {
pub struct ConfigureOption(ObjectSubclass<option_row::ConfigureOption>)
@extends gtk::Widget, gtk::Bin, gtk::Container,
@implements gtk::Buildable, gtk::Actionable;
}
impl ConfigureOption {
pub fn new(option: &BoolOption) -> Self {
let widget :Self = glib::Object::new(&[])
.expect("Failed to create ConfigureOption");
widget.set_bool_option(option);
widget
}
fn instance(&self) -> &option_row::ConfigureOption {
option_row::ConfigureOption::from_instance(self)
}
pub fn update(&self) {
self.instance().update();
}
fn set_bool_option(&self, option: &BoolOption) {
self.set_tooltip_markup(Some(option.tooltip()));
self.instance().set_bool_option(option);
}
}

View File

@@ -0,0 +1,68 @@
use std::cell::RefCell;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use crate::configure_dialog::BoolOption;
#[derive(CompositeTemplate)]
#[template(file = "configure-option-switch.ui")]
pub struct ConfigureOption {
#[template_child]
pub name: TemplateChild<gtk::Label>,
#[template_child]
pub switch: TemplateChild<gtk::Switch>,
pub option: RefCell<Option<BoolOption>>,
}
impl Default for ConfigureOption {
fn default() -> Self {
ConfigureOption {
name: Default::default(),
switch: Default::default(),
option: RefCell::new(None),
}
}
}
impl ConfigureOption {
pub fn set_bool_option(&self, option: &BoolOption) {
self.name.set_text(option.description());
self.switch.set_state(option.value());
self.switch.connect_state_set(glib::clone!(@strong option => move |_b,v| {
option.set_value(v);
Inhibit(false)
}));
self.option.borrow_mut().replace(option.clone());
}
pub fn update(&self) {
let option = self.option.borrow();
if let Some(option) = option.as_ref() {
self.switch.set_state(option.value());
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for ConfigureOption {
const NAME: &'static str = "ConfigureOption";
type Type = super::ConfigureOption;
type ParentType = gtk::ListBoxRow;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ConfigureOption {}
impl WidgetImpl for ConfigureOption {}
impl ContainerImpl for ConfigureOption {}
impl BinImpl for ConfigureOption {}
impl ListBoxRowImpl for ConfigureOption {}

View File

@@ -0,0 +1,384 @@
use std::cell::Cell;
use std::rc::Rc;
use libcitadel::OverlayType;
use libcitadel::terminal::Base16Scheme;
use crate::realmsd::RealmConfig;
const GPU_TOOLTIP: &str = r#"If enabled the render node device <tt><b>/dev/dri/renderD128</b></tt> will be mounted into the realm container.
If privileged device <tt><b>/dev/dri/card0</b></tt> is also needed set
additional variable in realm configuration file:
<tt><b>use-gpu-card0 = true</b></tt>
"#;
const WAYLAND_TOOLTIP: &str = "\
If enabled access to Wayland display will be permitted in realm by adding wayland socket to realm.
<tt><b>/run/user/1000/wayland-0</b></tt>
";
const X11_TOOLTIP: &str = "\
If enabled access to X11 server will be added by mounting directory X11 directory into realm.
<tt><b>/tmp/.X11-unix</b></tt>
";
const SOUND_TOOLTIP: &str = r#"If enabled allows use of sound inside of realm. The following items will be added:
<tt><b>/dev/snd</b></tt>
<tt><b>/dev/shm</b></tt>
<tt><b>/run/user/1000/pulse</b></tt>
"#;
const SHARED_DIR_TOOLTIP: &str = r#"If enabled the shared directory will be mounted as <tt><b>/Shared</b></tt> in home directory of realm.
This directory is shared between all realms with this option enabled and is an easy way to move files between realms.
"#;
const NETWORK_TOOLTIP: &str = "\
If enabled the realm will have access to the network.
";
const KVM_TOOLTIP: &str = r#"If enabled device <tt><b>/dev/kvm</b></tt> will be added to realm.
This allows use of applications such as Qemu inside of realms.
"#;
const EPHERMERAL_HOME_TOOLTIP: &str = r#"If enabled the home directory of realm will be set up in ephemeral mode.
The ephemeral home directory is set up with the following steps:
1. Home directory is mounted as tmpfs filesystem
2. Any files in <tt><b>/realms/skel</b></tt> are copied into home directory
3. Any files in <tt><b>/realms/realm-$name/skel</b></tt> are copied into home directory.
4. Any directories listed in config file variable <tt><b>ephemeral_persistent_dirs</b></tt>
are bind mounted from <tt><b>/realms/realm-$name/home</b></tt> into ephemeral
home directory.
"#;
const BOOL_OPTIONS: &[(&str, &str, &str)] = &[
("use-gpu", "Use GPU in Realm", GPU_TOOLTIP),
("use-wayland", "Use Wayland in Realm", WAYLAND_TOOLTIP),
("use-x11", "Use X11 in Realm", X11_TOOLTIP),
("use-sound", "Use Sound in Realm", SOUND_TOOLTIP),
("use-shared-dir", "Mount /Shared directory in Realm", SHARED_DIR_TOOLTIP),
("use-network", "Realm has network access", NETWORK_TOOLTIP),
("use-kvm", "Use KVM (/dev/kvm) in Realm", KVM_TOOLTIP),
("use-ephemeral-home", "Use ephemeral tmpfs mount for home directory", EPHERMERAL_HOME_TOOLTIP),
];
#[derive(Clone)]
pub struct BoolOption {
id: String,
description: String,
tooltip: String,
original: Rc<Cell<bool>>,
value: Rc<Cell<bool>>,
}
impl BoolOption {
fn create_options() -> Vec<BoolOption> {
let mut bools = Vec::new();
for (id, description, tooltip) in BOOL_OPTIONS {
bools.push(BoolOption::new(id, description, tooltip));
}
bools
}
fn new(id: &str, description: &str, tooltip: &str) -> Self {
let id = id.to_string();
let description = description.to_string();
let tooltip = format!("<b><big>{}</big></b>\n\n{}", description, tooltip);
let value = Rc::new(Cell::new(false));
let original = Rc::new(Cell::new(false));
BoolOption { id, description, tooltip, original, value }
}
pub fn value(&self) -> bool {
self.value.get()
}
fn has_changed(&self) -> bool {
self.value() != self.original.get()
}
pub fn set_value(&self, v: bool) {
self.value.set(v);
}
pub fn id(&self) -> &str {
&self.id
}
pub fn description(&self) -> &str {
&self.description
}
pub fn tooltip(&self) -> &str {
&self.tooltip
}
fn configure(&self, config: &RealmConfig) {
let v = config.get_bool(self.id());
self.original.set(v);
self.value.set(v);
}
fn reset(&self) {
self.set_value(self.original.get());
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.has_changed() {
let k = self.id.clone();
let v = self.value().to_string();
result.push((k, v))
}
}
}
struct OverlayOption {
original: OverlayType,
current: OverlayType,
}
impl OverlayOption {
fn new() -> Self {
OverlayOption {
original: OverlayType::None,
current: OverlayType::None,
}
}
fn overlay_str_to_enum(str: Option<&str>) -> OverlayType {
match str {
Some("storage") => OverlayType::Storage,
Some("tmpfs") => OverlayType::TmpFS,
Some("none") => OverlayType::None,
None => OverlayType::None,
Some(s) => {
warn!("Unexpected overlay type: {}", s);
OverlayType::None
},
}
}
fn set_overlay(&mut self, overlay: &str) {
self.current = Self::overlay_str_to_enum(Some(overlay));
}
fn str_value(&self) -> String {
self.current.to_str_value()
.unwrap_or("none").to_string()
}
fn configure(&mut self, config: &RealmConfig) {
let overlay = Self::overlay_str_to_enum(config.get_string("overlay"));
self.original = overlay;
self.current = overlay;
}
fn reset(&mut self) {
self.current = self.original;
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.original != self.current {
let k = "overlay".to_string();
let v = self.str_value();
result.push((k, v));
}
}
}
struct RealmFsOption {
original: String,
current: String,
realmfs_list: Vec<String>,
}
impl RealmFsOption {
fn new() -> Self {
let base = String::from("base");
RealmFsOption {
original: base.clone(),
current: base.clone(),
realmfs_list: vec![base],
}
}
fn realmfs_list(&self) -> Vec<String> {
self.realmfs_list.clone()
}
fn current(&self) -> String {
self.current.clone()
}
fn set_current(&mut self, realmfs: &str) {
self.current = realmfs.to_string();
}
fn configure(&mut self, config: &RealmConfig) {
if let Some(realmfs) = config.get_string("realmfs") {
self.realmfs_list.clear();
self.realmfs_list.extend(config.realmfs_list().iter().cloned());
self.original = realmfs.to_string();
self.current = realmfs.to_string();
}
}
fn reset(&mut self) {
self.current = self.original.clone();
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.current.is_empty() {
return;
}
if self.current != self.original {
result.push(("realmfs".to_string(), self.current.clone()))
}
}
}
const DEFAULT_SCHEME: &str = "default-dark";
struct ColorSchemeOption {
original: Base16Scheme,
current: Base16Scheme,
}
impl ColorSchemeOption {
fn new() -> Self {
let scheme = Base16Scheme::by_name(DEFAULT_SCHEME)
.expect("default Base16Scheme");
ColorSchemeOption {
original: scheme.clone(),
current: scheme.clone(),
}
}
fn configure(&mut self, config: &RealmConfig) {
if let Some(scheme) = config.get_string("terminal-scheme") {
if let Some(scheme) = Base16Scheme::by_name(scheme) {
self.original = scheme.clone();
self.current = scheme.clone();
}
}
}
fn reset(&mut self) {
self.set_current(self.original.clone());
}
fn set_current(&mut self, scheme: Base16Scheme) {
self.current = scheme;
}
fn set_current_id(&mut self, id: &str) {
if let Some(scheme) = Base16Scheme::by_name(id) {
self.set_current(scheme.clone());
}
}
fn current(&self) -> Base16Scheme {
self.current.clone()
}
fn add_changes(&self, result: &mut Vec<(String, String)>) {
if self.original.slug() != self.current.slug() {
result.push(("terminal-scheme".to_string(), self.current.slug().to_string()));
}
}
}
pub struct ConfigOptions {
bool_options: Vec<BoolOption>,
overlay: OverlayOption,
realmfs: RealmFsOption,
colorscheme: ColorSchemeOption,
}
impl ConfigOptions {
pub fn configure(&mut self, config: &RealmConfig) {
for op in &self.bool_options {
op.configure(config);
}
self.overlay.configure(config);
self.realmfs.configure(config);
self.colorscheme.configure(config);
}
pub fn reset(&mut self) {
for op in &self.bool_options {
op.reset();
}
self.overlay.reset();
self.realmfs.reset();
self.colorscheme.reset();
}
pub fn changes(&self) -> Vec<(String,String)> {
let mut changes = Vec::new();
for op in &self.bool_options {
op.add_changes(&mut changes);
}
self.overlay.add_changes(&mut changes);
self.realmfs.add_changes(&mut changes);
self.colorscheme.add_changes(&mut changes);
changes
}
pub fn new() -> Self {
let bool_options = BoolOption::create_options();
let overlay = OverlayOption::new();
let realmfs = RealmFsOption::new();
let colorscheme = ColorSchemeOption::new();
ConfigOptions {
bool_options, overlay, realmfs, colorscheme,
}
}
pub fn bool_options(&self) -> &[BoolOption] {
&self.bool_options
}
pub fn realmfs_list(&self) -> Vec<String> {
self.realmfs.realmfs_list()
}
pub fn overlay_id(&self) -> String {
self.overlay.str_value()
}
pub fn set_overlay_id(&mut self, id: &str) {
self.overlay.set_overlay(id);
}
pub fn realmfs(&self) -> String {
self.realmfs.current()
}
pub fn set_realmfs(&mut self, realmfs: &str) {
self.realmfs.set_current(realmfs);
}
pub fn colorscheme(&self) -> Base16Scheme {
self.colorscheme.current()
}
pub fn set_colorscheme_id(&mut self, id: &str) {
self.colorscheme.set_current_id(id);
}
}

View File

@@ -0,0 +1,126 @@
use std::collections::HashSet;
use std::convert::TryFrom;
use gtk::{gdk,gio};
use gtk::gio::prelude::*;
use rand::Rng;
use libcitadel::Realm;
pub struct CitadelSettings {
settings: gio::Settings,
frame_colors: Vec<gdk::RGBA>,
realms: Vec<RealmFrameColor>,
used_colors: HashSet<gdk::RGBA>,
}
#[derive(Clone)]
struct RealmFrameColor(String,gdk::RGBA);
impl RealmFrameColor {
fn new(realm: &str, color: &gdk::RGBA) -> Self {
RealmFrameColor(realm.to_string(), color.clone())
}
fn realm(&self) -> &str {
&self.0
}
fn color(&self) -> &gdk::RGBA {
&self.1
}
fn set_color(&mut self, color: gdk::RGBA) {
self.1 = color;
}
}
impl CitadelSettings {
fn choose_random_color(&self) -> gdk::RGBA {
if !self.frame_colors.is_empty() {
let n = rand::thread_rng().gen_range(0..self.frame_colors.len());
self.frame_colors[n].clone()
} else {
gdk::RGBA::blue()
}
}
fn allocate_color(&self) -> gdk::RGBA {
self.frame_colors.iter()
.find(|&c| !self.used_colors.contains(c))
.cloned()
.unwrap_or_else(|| self.choose_random_color())
}
pub fn get_realm_color(&self, name: Option<&str>) -> gdk::RGBA {
name.and_then(|name| self.get_realm_frame_color(name))
.cloned()
.unwrap_or_else(|| self.allocate_color())
}
pub fn store_realm_color(&mut self, name: &str, color: gdk::RGBA) -> bool {
if let Some(realm) = self.realms.iter_mut().find(|r| r.realm() == name) {
realm.set_color(color);
} else {
self.realms.push(RealmFrameColor::new(name, &color));
}
let list = self.realms.iter().map(|r| r.to_string()).collect::<Vec<String>>();
let realms = list.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
self.settings.set_strv("realm-label-colors", &realms).is_ok()
}
pub fn new() -> Self {
let settings = gio::Settings::new("com.subgraph.citadel");
let realms = settings.strv("realm-label-colors")
.into_iter()
.flat_map(|gs| RealmFrameColor::try_from(gs.as_str()).ok())
.collect::<Vec<RealmFrameColor>>();
let frame_colors = settings.strv("label-color-list").into_iter()
.flat_map(|gs| gs.as_str().parse().ok())
.collect();
let used_colors = realms.iter()
.map(|rfc| rfc.1.clone()).collect();
CitadelSettings {
settings,
frame_colors,
realms,
used_colors,
}
}
fn get_realm_frame_color(&self, name: &str) -> Option<&gdk::RGBA> {
self.realms.iter()
.find(|r| r.realm() == name)
.map(|r| r.color())
}
}
impl TryFrom<&str> for RealmFrameColor {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let idx = value.find(':').ok_or(())?;
let (realm, color_str) = value.split_at(idx);
let rgba = &color_str[1..].parse::<gdk::RGBA>()
.map_err(|_| ())?;
if Realm::is_valid_name(realm) {
Ok(RealmFrameColor::new(realm, rgba))
} else {
Err(())
}
}
}
impl ToString for RealmFrameColor {
fn to_string(&self) -> String {
format!("{}:{}", self.realm(), self.color())
}
}

View File

@@ -0,0 +1,60 @@
use std::result;
use std::fmt;
use crate::error::Error::Zbus;
use std::fmt::Formatter;
use gtk::prelude::*;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Zbus(zbus::Error),
ManagerConnect,
NoSuchRealm(String),
CreateRealmFailed,
}
impl Error {
fn create_dialog(&self) -> gtk::MessageDialog {
let title = "Error";
let message = self.to_string();
gtk::MessageDialog::builder()
.message_type(gtk::MessageType::Error)
.title(title)
.text(&message)
.buttons(gtk::ButtonsType::Close)
.build()
}
pub fn error_dialog<P: IsA<gtk::Window>>(&self, parent: Option<&P>) {
let dialog = self.create_dialog();
dialog.set_transient_for(parent);
dialog.run();
dialog.close();
}
pub fn app_error_dialog(&self, app: &gtk::Application) {
let dialog = self.create_dialog();
app.add_window(&dialog);
dialog.run();
dialog.close();
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::Zbus(e) => write!(f, "ZBus error: {}", e),
Error::ManagerConnect => write!(f, "Unable to connect to Realms Manager"),
Error::NoSuchRealm(name) => write!(f, "Realm '{}' does not exist", name),
Error::CreateRealmFailed => write!(f, "Failed to create new realm"),
}
}
}
impl From<zbus::Error> for Error {
fn from(e: zbus::Error) -> Self {
Zbus(e)
}
}

120
realm-config-ui/src/main.rs Normal file
View File

@@ -0,0 +1,120 @@
#[macro_use] extern crate libcitadel;
use std::env;
use gtk::prelude::*;
use gtk::gio;
use crate::configure_dialog::ConfigureDialog;
use crate::new_realm::NewRealmDialog;
use crate::error::Result;
use crate::realmsd::{RealmConfig, RealmsManagerProxy};
mod realmsd;
mod error;
mod colorscheme;
mod configure_dialog;
mod new_realm;
fn load_realm_names() -> Result<(RealmsManagerProxy<'static>, Vec<String>, RealmConfig)> {
let manager = RealmsManagerProxy::connect()?;
let names = manager.realm_names()?;
let config = manager.default_config()?;
Ok((manager, names, config))
}
fn new_realm_ui(app: &gtk::Application) {
let (manager, realms, config) = match load_realm_names() {
Ok(v) => v,
Err(err) => {
err.app_error_dialog(app);
return;
}
};
let dialog = NewRealmDialog::new();
dialog.set_realm_names(&realms);
dialog.set_config(&config);
app.add_window(&dialog);
dialog.show_all();
if dialog.run() == gtk::ResponseType::Ok {
let realm = dialog.get_realm_name();
dialog.store_config_settings();
let changes = dialog.config_changes();
if let Err(err) = manager.create_new_realm(&realm, changes) {
err.error_dialog(Some(&dialog));
}
}
dialog.close();
}
fn load_realm_config(realm_name: &str) -> Result<(RealmsManagerProxy<'static>, RealmConfig)> {
let manager = RealmsManagerProxy::connect()?;
let config = manager.config(realm_name)?;
Ok((manager, config))
}
fn configure_realm_ui(app: &gtk::Application, name: &str) {
let (manager, config) = match load_realm_config(name) {
Ok(val) => val,
Err(err) => {
err.app_error_dialog(app);
return;
}
};
let dialog = ConfigureDialog::new();
app.add_window(&dialog);
dialog.set_config(&config);
dialog.set_realm_name(name);
dialog.show_all();
if dialog.run() == gtk::ResponseType::Ok {
dialog.store_settings(name);
let changes = dialog.changes();
if !changes.is_empty() {
if let Err(err) = manager.configure_realm(name, changes) {
err.error_dialog(Some(&dialog));
}
}
}
dialog.close();
}
fn test_ui(app: &gtk::Application) {
let config = RealmConfig::new_default(vec![String::from("main"), String::from("foo")]);
let dialog = ConfigureDialog::new();
app.add_window(&dialog);
dialog.set_config(&config);
dialog.set_title("Configure realm-testing");
dialog.show_all();
if dialog.run() == gtk::ResponseType::Ok {
let changes = dialog.changes();
println!("Changes: {:?}", changes);
}
dialog.close();
}
fn main() {
let mut args = env::args().collect::<Vec<String>>();
if args.len() > 1 {
let first = args.remove(1);
let application = gtk::Application::new(Some("com.subgraph.RealmConfig"), gio::ApplicationFlags::empty());
if first.as_str() == "--new" {
application.connect_activate(new_realm_ui);
} else if first.as_str() == "--test" {
application.connect_activate(test_ui);
} else {
application.connect_activate(move |app| {
configure_realm_ui(app, &first);
});
}
application.run_with_args(&args);
}
}

View File

@@ -0,0 +1,134 @@
use std::cell::RefCell;
use std::rc::Rc;
use gtk::glib;
use gtk::CompositeTemplate;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use crate::configure_dialog::ConfigureDialog;
use crate::new_realm::verifier::RealmNameVerifier;
use crate::realmsd::RealmConfig;
#[derive(CompositeTemplate)]
#[template(file = "new-realm-dialog.ui")]
pub struct NewRealmDialog {
#[template_child]
pub infobar: TemplateChild<gtk::InfoBar>,
#[template_child]
pub infolabel: TemplateChild<gtk::Label>,
#[template_child]
pub label: TemplateChild<gtk::Label>,
#[template_child]
entry: TemplateChild<gtk::Entry>,
#[template_child (id="config-button")]
pub config_button: TemplateChild<gtk::Button>,
pub realm_names: Rc<RefCell<Vec<String>>>,
configure_dialog: ConfigureDialog,
}
impl Default for NewRealmDialog {
fn default() -> Self {
NewRealmDialog {
infobar: Default::default(),
infolabel: Default::default(),
label: Default::default(),
entry: Default::default(),
config_button: Default::default(),
realm_names: Default::default(),
configure_dialog: ConfigureDialog::new(),
}
}
}
impl NewRealmDialog {
pub fn set_realm_names(&self, names: &[String]) {
let mut lock = self.realm_names.borrow_mut();
lock.clear();
lock.extend_from_slice(&names)
}
pub fn set_config(&self, config: &RealmConfig) {
self.configure_dialog.set_config(config);
}
pub fn get_realm_name(&self) -> String {
self.entry.text().to_string()
}
pub fn config_changes(&self) -> Vec<(String,String)> {
self.configure_dialog.changes()
}
pub fn store_config_settings(&self) {
let realm_name = self.get_realm_name();
if !realm_name.is_empty() {
self.configure_dialog.store_settings(&realm_name);
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for NewRealmDialog {
const NAME: &'static str = "NewRealmDialog";
type Type = super::NewRealmDialog;
type ParentType = gtk::Dialog;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for NewRealmDialog {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.configure_dialog.set_transient_for(Some(&self.instance()));
let verifier = Rc::new(RealmNameVerifier::new(self));
self.entry.connect_insert_text(glib::clone!(@strong verifier => move |entry, text, pos|{
if !verifier.verify_insert(entry, text, *pos) {
entry.stop_signal_emission("insert-text");
}
}));
self.entry.connect_delete_text(glib::clone!(@strong verifier => move |entry, start, end| {
if !verifier.verify_delete(entry, start, end) {
entry.stop_signal_emission("delete-text");
}
}));
self.entry.connect_changed(glib::clone!(@strong verifier => move |entry| {
verifier.changed(entry);
}));
let config_dialog = self.configure_dialog.clone();
let entry = self.entry.clone();
self.config_button.connect_clicked(move |_b| {
let name = entry.text().to_string();
config_dialog.set_title(&format!("Configure realm-{}", name));
config_dialog.show_all();
match config_dialog.run() {
gtk::ResponseType::Ok => {},
_ => config_dialog.reset_options(),
}
config_dialog.hide();
});
}
}
impl DialogImpl for NewRealmDialog {}
impl WindowImpl for NewRealmDialog {}
impl BinImpl for NewRealmDialog {}
impl ContainerImpl for NewRealmDialog {}
impl WidgetImpl for NewRealmDialog {}

View File

@@ -0,0 +1,44 @@
use gtk::glib;
use glib::subclass::prelude::*;
use crate::realmsd::RealmConfig;
mod dialog;
mod verifier;
glib::wrapper! {
pub struct NewRealmDialog(ObjectSubclass<dialog::NewRealmDialog>)
@extends gtk::Dialog, gtk::Window, gtk::Bin, gtk::Container, gtk::Widget,
@implements gtk::Buildable;
}
impl NewRealmDialog {
pub fn new() -> Self {
glib::Object::new(&[("use-header-bar", &1)])
.expect("Failed to create NewRealmDialog")
}
fn instance(&self) -> &dialog::NewRealmDialog {
dialog::NewRealmDialog::from_instance(self)
}
pub fn set_realm_names(&self, names: &[String]) {
self.instance().set_realm_names(names);
}
pub fn set_config(&self, config: &RealmConfig) {
self.instance().set_config(config);
}
pub fn get_realm_name(&self) -> String {
self.instance().get_realm_name()
}
pub fn config_changes(&self) -> Vec<(String,String)> {
self.instance().config_changes()
}
pub fn store_config_settings(&self) {
self.instance().store_config_settings();
}
}

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="NewRealmDialog" parent="GtkDialog">
<property name="title">Create New Realm</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="orientation">vertical</property>
<!-- GtkInfoBar -->
<child>
<object class="GtkInfoBar" id="infobar">
<property name="revealed">False</property>
<property name="message-type">warning</property>
<child internal-child="content_area">
<object class="GtkBox">
<child>
<object class="GtkLabel" id="infolabel">
<property name="label">Name already exists</property>
</object>
</child>
</object>
</child>
</object>
</child>
<!-- GtkLabel -->
<child>
<object class="GtkLabel" id="label">
<property name="label">Enter name for new realm:</property>
<property name="halign">start</property>
<property name="margin-top">10</property>
<property name="margin-start">20</property>
</object>
</child>
<!-- GtkEntry-->
<child>
<object class="GtkBox">
<child>
<object class="GtkEntry" id="entry">
<property name="hexpand">True</property>
<property name="placeholder-text">Enter name of new realm</property>
<property name="margin-top">10</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
<property name="margin-end">5</property>
</object>
</child>
<!-- GtkButton -->
<child>
<object class="GtkButton" id="config-button">
<property name="sensitive">False</property>
<property name="margin-top">10</property>
<property name="margin-bottom">20</property>
<property name="margin-start">5</property>
<property name="margin-end">20</property>
<child>
<object class="GtkImage">
<property name="icon-name">emblem-system-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="use-underline">1</property>
<property name="label">Cancel</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="ok_button">
<property name="use-underline">1</property>
<property name="label">Create</property>
<property name="can-default">True</property>
</object>
</child>
<action-widgets>
<action-widget response="cancel">cancel_button</action-widget>
<action-widget response="ok" default="true">ok_button</action-widget>
</action-widgets>
</template>
</interface>

View File

@@ -0,0 +1,76 @@
use std::cell::RefCell;
use std::rc::Rc;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use libcitadel::Realm;
use crate::new_realm::dialog::NewRealmDialog;
pub struct RealmNameVerifier {
ok: gtk::Widget,
infobar: gtk::InfoBar,
infolabel: gtk::Label,
label: gtk::Label,
config: gtk::Button,
realms: Rc<RefCell<Vec<String>>>,
}
impl RealmNameVerifier {
pub fn new(dialog: &NewRealmDialog) -> Self {
let ok = dialog.instance().widget_for_response(gtk::ResponseType::Ok).expect("No Ok Widget found");
RealmNameVerifier {
ok,
infobar: dialog.infobar.clone(),
infolabel: dialog.infolabel.clone(),
label: dialog.label.clone(),
config: dialog.config_button.clone(),
realms: dialog.realm_names.clone(),
}
}
pub fn verify_insert(&self, entry: &gtk::Entry, text: &str, pos: i32) -> bool {
let mut s = entry.text().to_string();
s.insert_str(pos as usize, text);
Realm::is_valid_name(&s)
}
pub fn verify_delete(&self, entry: &gtk::Entry, start: i32, end: i32) -> bool {
let mut s = entry.text().to_string();
let start = start as usize;
let end = end as usize;
s.replace_range(start..end, "");
s.is_empty() || Realm::is_valid_name(&s)
}
fn verify_name (&self, name: &String) -> bool {
if self.realms.borrow().contains(name) {
self.infolabel.set_markup(&format!("Realm already exists with name <b>realm-{}</b>", name));
self.infobar.set_revealed(true);
false
} else {
self.infobar.set_revealed(false);
self.infolabel.set_markup("");
!name.is_empty()
}
}
pub fn changed(&self, entry: &gtk::Entry) {
let s = entry.text().to_string();
if self.verify_name(&s) {
self.ok.set_sensitive(true);
self.config.set_sensitive(true);
self.label.set_markup(&format!("<b>realm-{}</b>", s));
} else {
self.ok.set_sensitive(false);
self.config.set_sensitive(false);
if s.is_empty() {
self.label.set_markup("Enter name for new realm:");
} else {
self.label.set_markup("");
}
}
}
}

View File

@@ -0,0 +1,154 @@
use std::collections::HashMap;
use std::rc::Rc;
use zbus::dbus_proxy;
use zvariant::derive::Type;
use serde::{Serialize,Deserialize};
use crate::error::{Error, Result};
#[derive(Deserialize,Serialize,Type)]
pub struct RealmItem {
name: String,
description: String,
realmfs: String,
namespace: u64,
status: u8,
}
impl RealmItem {
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug,Clone)]
pub struct RealmConfig {
options: Rc<HashMap<String,String>>,
realmfs_list: Rc<Vec<String>>,
}
impl RealmConfig {
pub fn new_default(realmfs_list: Vec<String>) -> Self {
let config = libcitadel::RealmConfig::default();
let mut vars = HashMap::new();
vars.insert("use-gpu".to_string(), config.gpu().to_string());
vars.insert("use-wayland".to_string(), config.wayland().to_string());
vars.insert("use-x11".to_string(), config.x11().to_string());
vars.insert("use-sound".to_string(), config.sound().to_string());
vars.insert("use-shared-dir".to_string(), config.shared_dir().to_string());
vars.insert("use-network".to_string(), config.network().to_string());
vars.insert("use-kvm".to_string(), config.kvm().to_string());
vars.insert("use-ephemeral-home".to_string(), config.ephemeral_home().to_string());
if realmfs_list.contains(&String::from("main")) {
vars.insert("realmfs".to_string(), String::from("main"));
} else if let Some(first) = realmfs_list.first() {
vars.insert("realmfs".to_string(), first.clone());
}
Self::new(vars, realmfs_list)
}
fn new(options: HashMap<String, String>, realmfs_list: Vec<String>) -> Self {
RealmConfig {
options: Rc::new(options),
realmfs_list: Rc::new(realmfs_list),
}
}
pub fn get_string(&self, id: &str) -> Option<&str> {
self.options.get(id).map(|s| s.as_str())
}
fn parse_bool(val: &str) -> bool {
match val.parse::<bool>() {
Ok(v) => v,
_ => {
warn!("Failed to parse value '{}' as bool", val);
false
}
}
}
pub fn get_bool(&self, id: &str) -> bool {
match self.get_string(id) {
Some(val) => Self::parse_bool(val),
None => {
warn!("No value found for option '{}'", id);
false
}
}
}
pub fn realmfs_list(&self) -> &[String] {
&self.realmfs_list
}
}
#[dbus_proxy(
default_service = "com.subgraph.realms",
interface = "com.subgraph.realms.Manager",
default_path = "/com/subgraph/realms"
)]
pub trait RealmsManager {
fn get_current(&self) -> zbus::Result<String>;
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) -> zbus::Result<()>;
fn list(&self) -> zbus::Result<Vec<RealmItem>>;
fn realm_config(&self, name: &str) -> zbus::Result<HashMap<String,String>>;
fn realm_exists(&self, name: &str) -> zbus::Result<bool>;
fn list_realm_f_s(&self) -> zbus::Result<Vec<String>>;
fn create_realm(&self, name: &str) -> zbus::Result<bool>;
}
impl RealmsManagerProxy<'_> {
pub fn connect() -> Result<Self> {
let connection = zbus::Connection::new_system()?;
let proxy = RealmsManagerProxy::new(&connection)
.map_err(|_| Error::ManagerConnect)?;
// Test connection
proxy.get_current().map_err(|_| Error::ManagerConnect)?;
Ok(proxy)
}
pub fn realm_names(&self) -> Result<Vec<String>> {
let realms = self.list()?;
let names = realms.iter()
.map(|r| r.name().to_string())
.collect();
Ok(names)
}
pub fn default_config(&self) -> Result<RealmConfig> {
let realmfs_list = self.list_realm_f_s()?;
Ok(RealmConfig::new_default(realmfs_list))
}
pub fn config(&self, realm: &str) -> Result<RealmConfig> {
if !self.realm_exists(realm)? {
return Err(Error::NoSuchRealm(realm.to_string()));
}
let options = self.realm_config(realm)?;
let realmfs_list = self.list_realm_f_s()?;
Ok(RealmConfig::new(options, realmfs_list))
}
pub fn configure_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> {
self.realm_set_config(realm, config)?;
Ok(())
}
pub fn create_new_realm(&self, realm: &str, config: Vec<(String, String)>) -> Result<()> {
if self.create_realm(realm)? {
if !config.is_empty() {
self.realm_set_config(realm, config)?;
}
} else {
return Err(Error::CreateRealmFailed);
}
Ok(())
}
}

View File

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

View File

@@ -1,16 +1,15 @@
use async_io::block_on; use zbus::{Connection, ObjectServer};
use zbus::blocking::Connection;
use zbus::object_server::SignalEmitter;
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) -> Self { pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
EventHandler { connection } EventHandler { connection, realms_server }
} }
pub fn handle_event(&self, ev: &RealmEvent) { pub fn handle_event(&self, ev: &RealmEvent) {
@@ -26,49 +25,44 @@ 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_signal_emitter<F>(&self, func: F) -> zbus::Result<()> fn with_server<F>(&self, func: F) -> zbus::Result<()>
where where
F: Fn(&SignalEmitter) -> zbus::Result<()>, F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
{ {
let object_server = self.connection.object_server(); let mut object_server = ObjectServer::new(&self.connection);
let iface = object_server.interface::<_, RealmsManagerServer>(REALMS_SERVER_OBJECT_PATH)?; object_server.at(REALMS_SERVER_OBJECT_PATH, self.realms_server.clone())?;
object_server.with(REALMS_SERVER_OBJECT_PATH, |iface: &RealmsManagerServer| func(iface))
let emitter = iface.signal_emitter();
func(emitter)
} }
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_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_started(ctx, realm.name(), pid_ns, status))) self.with_server(|server| server.realm_started(realm.name(), pid_ns, status))
} }
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> { fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
let status = realm_status(realm); let status = realm_status(realm);
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_stopped(ctx, realm.name(), status))) self.with_server(|server| server.realm_stopped(realm.name(), status))
} }
fn on_new(&self, realm: &Realm) -> zbus::Result<()> { 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_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_new(ctx, realm.name(), &description, status))) self.with_server(|server| server.realm_new(realm.name(), &description, status))
} }
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> { fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_removed(ctx, realm.name()))) self.with_server(|server| server.realm_removed(realm.name()))
} }
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> { fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
self.with_signal_emitter(|ctx| { self.with_server(|server| {
match realm { match realm {
Some(realm) => block_on(RealmsManagerServer::realm_current(ctx, realm.name(), realm_status(realm))), Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
None => block_on(RealmsManagerServer::realm_current(ctx, "", 0)), None => server.realm_current("", 0),
} }
}) })
} }

View File

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

View File

@@ -1,227 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serde::{Deserialize, Serialize};
use zbus::fdo;
use libcitadel::{LiveConfig, OverlayType, Realm, GLOBAL_CONFIG};
use libcitadel::terminal::Base16Scheme;
use zbus::zvariant::Type;
use crate::next::manager::failed;
const BOOL_CONFIG_VARS: &[&str] = &[
"use-gpu", "use-gpu-card0", "use-wayland", "use-x11", "use-sound",
"use-shared-dir", "use-network", "use-kvm", "use-ephemeral-home",
"use-media-dir", "use-fuse", "use-flatpak",
];
fn is_bool_config_variable(variable: &str) -> bool {
BOOL_CONFIG_VARS.iter().any(|&s| s == variable)
}
fn add_boolean_vars(config: &libcitadel::RealmConfig, vars: &mut RealmConfigVars) {
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());
}
fn set_boolean_config_var(config: &mut libcitadel::RealmConfig, var: &str, value: bool) -> bool {
match var {
"use-gpu" => config.set_gpu(value),
"use-gpu-card0" => config.set_gpu_card0(value),
"use-wayland" => config.set_wayland(value),
"use-x11" => config.set_x11(value),
"use-sound" => config.set_sound(value),
"use-shared-dir" => config.set_shared_dir(value),
"use-network" => config.set_network(value),
"use-kvm" => config.set_kvm(value),
"use-ephemeral-home" => config.set_ephemeral_home(value),
"use-media-dir" => config.set_media_dir(value),
"use-fuse" => config.set_fuse(value),
"use-flatpak" => config.set_flatpak(value),
_ => false,
}
}
#[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();
add_boolean_vars(config, &mut vars);
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| {
has_changed = set_boolean_config_var(c, var, v);
});
if has_changed {
self.mark_changed();
if self.realm.is_active() && LiveConfig::is_live_configurable(var) {
let lc = LiveConfig::new(&self.realm);
if let Err(err) = lc.configure(var, v) {
warn!("Error live setting {} = {}: {}", var, value, err);
}
}
}
Ok(())
}
fn set_overlay(&mut self, value: &str) -> fdo::Result<()> {
let val = match value {
"tmpfs" => Some("tmpfs".to_string()),
"storage" => Some("storage".to_string()),
"none" => None,
_ => return failed(format!("Invalid value '{}' for overlay config", value)),
};
if self.realm.config().overlay != val {
self.realm.with_mut_config(|c| {
c.overlay = Some(value.to_string());
});
self.mark_changed();
}
Ok(())
}
fn set_terminal_scheme(&mut self, value: &str) -> fdo::Result<()> {
if Some(value) == self.realm.config().terminal_scheme() {
return Ok(())
}
let scheme = match Base16Scheme::by_name(value) {
Some(scheme) => scheme,
None => return failed(format!("Invalid terminal color scheme '{}'", value)),
};
let manager = self.realm.manager();
if let Err(err) = scheme.apply_to_realm(&manager, &self.realm) {
return failed(format!("Error applying terminal color scheme: {}", err));
}
self.realm.with_mut_config(|c| {
c.terminal_scheme = Some(value.to_string());
});
self.mark_changed();
Ok(())
}
fn set_realmfs(&mut self, value: &str) -> fdo::Result<()> {
let manager = self.realm.manager();
if manager.realmfs_by_name(value).is_none() {
return failed(format!("Failed to set 'realmfs' config for realm-{}: RealmFS named '{}' does not exist", self.realm.name(), value));
}
if self.realm.config().realmfs() != value {
self.realm.with_mut_config(|c| {
c.realmfs = Some(value.to_string())
});
self.mark_changed();
}
Ok(())
}
pub fn save_config(&self) -> fdo::Result<()> {
if self.is_changed() {
self.realm.config()
.write()
.map_err(|err| fdo::Error::Failed(format!("Error writing config file for realm-{}: {}", self.realm.name(), err)))?;
self.changed.store(false, Ordering::Relaxed);
}
Ok(())
}
pub fn set_var(&mut self, var: &str, value: &str) -> fdo::Result<()> {
if is_bool_config_variable(var) {
self.set_bool_var(var, value)
} else if var == "overlay" {
self.set_overlay(value)
} else if var == "terminal-scheme" {
self.set_terminal_scheme(value)
} else if var == "realmfs" {
self.set_realmfs(value)
} else {
failed(format!("Unknown realm configuration variable '{}'", var))
}
}
}

View File

@@ -1,150 +0,0 @@
use std::sync::Arc;
use std::thread;
use blocking::unblock;
use event_listener::Event;
use serde::Serialize;
use serde_repr::Serialize_repr;
use zbus::blocking::fdo::DBusProxy;
use zbus::blocking::Connection;
use zbus::names::BusName;
use zbus::zvariant::Type;
use zbus::{fdo, interface};
use libcitadel::{PidLookupResult, RealmManager};
use crate::next::config::RealmConfigVars;
use crate::next::realm::RealmItemState;
use super::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() },
}
}
}
#[derive(Clone)]
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,
}
}
fn name_owner_changed_loop(&self, connection: &Connection) -> zbus::Result<()> {
let dbus = DBusProxy::new(connection)?;
for sig in dbus.receive_name_owner_changed()? {
let args = sig.args()?;
match &args.name {
BusName::Unique(unique_name) if args.new_owner().is_none() => {
self.realmfs_state.client_disconnected(unique_name);
},
_ => {},
}
}
Ok(())
}
fn listen_name_owner_changed(&self, connection: &Connection) {
let connection = connection.clone();
let server = self.clone();
thread::spawn(move || {
if let Err(e) = server.name_owner_changed_loop(&connection) {
warn!("error: {}", e);
}
});
}
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> zbus::Result<Self> {
let 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)?;
server.listen_name_owner_changed(connection);
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 remove_realm(&self, name: &str, save_home: bool) -> fdo::Result<()> {
let manager = self.manager.clone();
let realm = match manager.realm_by_name(name) {
Some(realm) => realm,
None => return Err(fdo::Error::Failed(format!("No realm named {} exists", name))),
};
unblock(move || {
manager.delete_realm(&realm, save_home)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
Ok(())
}).await
}
async fn get_global_config(&self) -> RealmConfigVars {
RealmConfigVars::new_global()
}
}

View File

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

View File

@@ -1,392 +0,0 @@
use std::collections::HashMap;
use std::convert::TryInto;
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use blocking::unblock;
use zbus::zvariant::{OwnedObjectPath, Value};
use zbus::{interface, fdo};
use zbus::blocking::Connection;
use zbus::names::{BusName, InterfaceName};
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?;
Ok(())
}
async fn open_terminal(&self) -> fdo::Result<()> {
let _res = Command::new("/usr/bin/machinectl")
.uid(1000)
.gid(1000)
.arg("-q")
.arg("-E")
.arg(format!("REALM_NAME={}", self.realm.name()))
.arg("shell")
.arg(format!("user@{}", self.realm.name()))
.arg("/usr/bin/gnome-terminal")
.spawn();
Ok(())
}
#[zbus(property, name = "RunStatus")]
fn run_status(&self) -> u32 {
self.get_run_status() as u32
}
#[zbus(property, name="IsSystemRealm")]
fn is_system_realm(&self) -> bool {
self.realm.is_system()
}
#[zbus(property, name = "Name")]
fn name(&self) -> &str {
self.realm.name()
}
#[zbus(property, name = "Description")]
fn description(&self) -> String {
self.realm.notes()
.unwrap_or(String::new())
}
#[zbus(property, name = "PidNS")]
fn pid_ns(&self) -> u64 {
self.realm.pid_ns().unwrap_or_default()
}
#[zbus(property, name = "RealmFS")]
fn realmfs(&self) -> u32 {
self.realmfs_index.load(Ordering::Relaxed)
}
#[zbus(property, name = "Timestamp")]
fn timestamp(&self) -> u64 {
self.realm.timestamp() as u64
}
}
#[derive(Clone)]
pub struct RealmItemState(Arc<Mutex<Inner>>);
struct Inner {
connection: Connection,
next_index: u32,
realms: HashMap<String, RealmItem>,
current_realm: Option<RealmItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
connection,
next_index: 1,
realms:HashMap::new(),
current_realm: None,
}
}
fn load_realms(&mut self, manager: &RealmManager) -> zbus::Result<()> {
for realm in manager.realm_list() {
self.add_realm(realm)?;
}
Ok(())
}
pub fn populate_realmfs(&mut self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
for item in self.realms.values_mut() {
if let Some(realmfs) = realmfs_state.realmfs_by_name(item.realm.config().realmfs()) {
item.realmfs_index.store(realmfs.index(), Ordering::Relaxed);
}
}
Ok(())
}
fn add_realm(&mut self, realm: Realm) -> zbus::Result<()> {
if self.realms.contains_key(realm.name()) {
warn!("Attempted to add duplicate realm '{}'", realm.name());
return Ok(())
}
let key = realm.name().to_string();
let item = RealmItem::new_from_realm(self.next_index, realm);
self.connection.object_server().at(item.path(), item.clone())?;
self.realms.insert(key, item);
self.next_index += 1;
Ok(())
}
fn remove_realm(&mut self, realm: &Realm) -> zbus::Result<()> {
if let Some(item) = self.realms.remove(realm.name()) {
self.connection.object_server().remove::<RealmItem, &str>(item.path())?;
} else {
warn!("Failed to find realm to remove with name '{}'", realm.name());
}
Ok(())
}
fn emit_property_changed(&self, object_path: OwnedObjectPath, propname: &str, value: Value<'_>) -> zbus::Result<()> {
let iface_name = InterfaceName::from_str_unchecked("com.subgraph.realms.Realm");
let changed = HashMap::from([(propname.to_string(), value)]);
let inval: &[&str] = &[];
self.connection.emit_signal(
None::<BusName<'_>>,
&object_path,
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
&(iface_name, changed, inval))?;
Ok(())
}
fn realm_status_changed(&self, realm: &Realm, transition: Option<bool>) -> zbus::Result<()> {
if let Some(realm) = self.realm_by_name(realm.name()) {
if let Some(transition) = transition {
realm.in_run_transition.store(transition, Ordering::Relaxed);
}
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "RunStatus", Value::U32(realm.get_run_status() as u32))?;
let timestamp = realm.realm.timestamp();
if realm.last_timestamp.load(Ordering::Relaxed) != realm.realm.timestamp() {
realm.last_timestamp.store(timestamp, Ordering::Relaxed);
let object_path = realm.path().try_into().unwrap();
self.emit_property_changed(object_path, "Timestamp", Value::U64(timestamp as u64))?;
}
}
Ok(())
}
fn realm_by_name(&self, name: &str) -> Option<&RealmItem> {
let res = self.realms.get(name);
if res.is_none() {
warn!("Failed to find realm with name '{}'", name);
}
res
}
fn on_starting(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(true))?;
Ok(())
}
fn on_started(&self, realm: &Realm) -> zbus::Result<()>{
self.realm_status_changed(realm, Some(false))
}
fn on_stopping(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(true))
}
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
self.realm_status_changed(realm, Some(false))
}
fn on_new(&mut self, realm: &Realm) -> zbus::Result<()> {
self.add_realm(realm.clone())?;
Ok(())
}
fn on_removed(&mut self, realm: &Realm) -> zbus::Result<()> {
self.remove_realm(&realm)?;
Ok(())
}
fn on_current(&mut self, realm: Option<&Realm>) -> zbus::Result<()> {
if let Some(r) = self.current_realm.take() {
self.realm_status_changed(&r.realm, None)?;
}
if let Some(realm) = realm {
self.realm_status_changed(realm, None)?;
if let Some(item) = self.realm_by_name(realm.name()) {
self.current_realm = Some(item.clone());
}
}
Ok(())
}
}
impl RealmItemState {
pub fn new(connection: Connection) -> Self {
RealmItemState(Arc::new(Mutex::new(Inner::new(connection))))
}
pub fn load_realms(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load_realms(manager)?;
self.add_event_handler(manager)
.map_err(|err| zbus::Error::Failure(err.to_string()))?;
Ok(())
}
pub fn populate_realmfs(&self, realmfs_state: &RealmFSState) -> zbus::Result<()> {
self.inner().populate_realmfs(realmfs_state)
}
pub fn get_current(&self) -> Option<RealmItem> {
self.inner().current_realm.clone()
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
fn add_event_handler(&self, manager: &RealmManager) -> Result<()> {
let state = self.clone();
manager.add_event_handler(move |ev| {
if let Err(err) = state.handle_event(ev) {
warn!("Error handling {}: {}", ev, err);
}
});
manager.start_event_task()?;
Ok(())
}
fn handle_event(&self, ev: &RealmEvent) -> zbus::Result<()> {
match ev {
RealmEvent::Started(realm) => self.inner().on_started(realm)?,
RealmEvent::Stopped(realm) => self.inner().on_stopped(realm)?,
RealmEvent::New(realm) => self.inner().on_new(realm)?,
RealmEvent::Removed(realm) => self.inner().on_removed(realm)?,
RealmEvent::Current(realm) => self.inner().on_current(realm.as_ref())?,
RealmEvent::Starting(realm) => self.inner().on_starting(realm)?,
RealmEvent::Stopping(realm) => self.inner().on_stopping(realm)?,
};
Ok(())
}
}

View File

@@ -1,251 +0,0 @@
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::{Arc, Mutex, MutexGuard};
use zbus::blocking::Connection;
use zbus::message::Header;
use zbus::names::UniqueName;
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
use zbus::{fdo, interface};
use libcitadel::{RealmFS, RealmManager,RealmFSUpdate};
use crate::next::REALMS2_SERVER_OBJECT_PATH;
struct UpdateState(Option<(UniqueName<'static>, RealmFSUpdate)>);
impl UpdateState {
fn new() -> Self {
UpdateState(None)
}
fn is_active(&self) -> bool {
self.0.is_some()
}
fn matches(&self, name: &UniqueName) -> bool {
match &self.0{
Some((sender, _update)) => sender == name,
None => false,
}
}
fn activate(&mut self, sender: UniqueName<'static>, update: RealmFSUpdate) {
if self.0.is_none() {
self.0 = Some((sender, update));
}
}
fn cleanup_update(&mut self) {
if let Some((_name, mut update)) = self.0.take() {
update.cleanup();
}
}
fn commit_update(&mut self) {
if let Some((_name, mut update)) = self.0.take() {
if let Err(err) = update.commit_update() {
warn!("Error committing RealmFS update: {}", err);
}
}
}
}
const BLOCK_SIZE: u64 = 4096;
#[derive(Clone)]
pub struct RealmFSItem {
update_state: Arc<Mutex<UpdateState>>,
object_path: OwnedObjectPath,
index: u32,
realmfs: RealmFS,
}
impl RealmFSItem {
fn update_state(&self) -> MutexGuard<UpdateState> {
self.update_state.lock().unwrap()
}
fn client_disconnected(&mut self, name: &UniqueName) {
//debug!("disconnect {} {}", self.object_path, name);
let mut state = self.update_state();
if state.matches(name) {
state.cleanup_update();
}
}
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 {
update_state: Arc::new(Mutex::new(UpdateState::new())),
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 {
async fn prepare_update(
&mut self,
#[zbus(header)]
hdr: Header<'_>,
shared_directory: bool,
) -> fdo::Result<String> {
let mut state = self.update_state();
if state.is_active() {
return Err(fdo::Error::Failed("An update is already in progress".to_owned()));
}
let sender = match hdr.sender() {
Some(sender) => sender,
None => todo!(),
};
let mut update = self.realmfs.update()
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
update.prepare_update(shared_directory)
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
let update_container = update.name().to_string();
debug!("Update from {}, container: {}", sender, update_container);
state.activate(sender.to_owned(), update);
Ok(update_container)
}
async fn commit_update(&mut self) -> fdo::Result<()> {
self.update_state().commit_update();
Ok(())
}
async fn abandon_update(&mut self) -> fdo::Result<()> {
self.update_state().cleanup_update();
Ok(())
}
#[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)
}
}
#[derive(Clone)]
pub struct RealmFSState(Arc<Mutex<Inner>>);
impl RealmFSState {
pub fn new(connection: Connection) -> Self {
RealmFSState(Arc::new(Mutex::new(Inner::new(connection))))
}
fn inner(&self) -> MutexGuard<Inner> {
self.0.lock().unwrap()
}
pub(crate) fn load(&self, manager: &RealmManager) -> zbus::Result<()> {
self.inner().load(manager)
}
pub fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
self.inner().realmfs_by_name(name)
}
pub fn client_disconnected(&self, client_name: &UniqueName) {
let mut lock = self.inner();
for (_,v) in &mut lock.items {
v.client_disconnected(client_name);
}
println!("client disconnected: {client_name}")
}
}
struct Inner {
connection: Connection,
next_index: u32,
items: HashMap<String, RealmFSItem>,
}
impl Inner {
fn new(connection: Connection) -> Self {
Inner {
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(())
}
fn realmfs_by_name(&self, name: &str) -> Option<RealmFSItem> {
let res = self.items.get(name).cloned();
if res.is_none() {
warn!("Failed to find RealmFS with name '{}'", name);
}
res
}
}

View File

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

View File

@@ -4,6 +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/rootfs/var/lib/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 PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/flatpak/exports/share/applications

View File

@@ -1,9 +0,0 @@
[package]
name = "update-realmfs"
version = "0.1.0"
edition = "2021"
[dependencies]
libcitadel = { path = "../libcitadel" }
zbus = "5.7.1"
anyhow = "1.0"

View File

@@ -1,139 +0,0 @@
use std::io::Write;
use std::{env, io};
use std::process::Command;
use libcitadel::terminal::{AnsiTerminal, Color};
use libcitadel::warn;
use zbus::blocking::Connection;
use anyhow::{Context, Result};
use self::realmsd::RealmFS;
mod realmsd;
fn open_update_shell(machine_name: &str, realmfs_name: &str) -> Result<()> {
let mut child = Command::new("/usr/bin/machinectl")
.env("PS1", format!("(Update realmfs-{}) # ", realmfs_name))
.arg("--quiet")
.arg("--setenv=PS1")
// dumb hack to avoid PS1 being overwritten by /etc/bash.bashrc
.arg("--setenv=SUDO_PS1=foo")
.arg("--setenv=SUDO_USER=user")
.arg("shell")
.arg(machine_name)
.spawn()?;
let _status = child.wait()?;
Ok(())
}
fn update_realmfs(name: &str, shared_dir: bool) -> Result<()> {
let connection = Connection::system()?;
let realmfs = RealmFS::lookup(&connection, name)?;
print!("\x1B[2J\x1B[1;1H");
println!();
println!("Starting update container to modify '{name}-realmfs.img'");
println!();
let machine_name = realmfs.prepare_update(shared_dir)
.context(format!("Failed to prepare update for RealmFS ({name})"))?;
println!("The root filesystem can be updated from this shell to upgrade packages,");
println!("install new software, or make other changes to the filesystem.");
println!();
if shared_dir {
println!("The Shared directory has been mounted at /run/Shared");
println!();
}
println!("Exit update shell with ctrl-d or 'exit'");
println!();
open_update_shell(&machine_name, name)?;
println!();
print!("Save changes (Y/N)? ");
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
let response = buffer.trim();
if response == "y" || response == "Y" {
println!("Saving changes");
realmfs.commit_update()?;
} else {
println!("Discarding changes");
realmfs.abandon_update()?;
}
println!("Done...");
Ok(())
}
fn realmfs_arg(args: &[String]) -> Option<&str> {
if args.len() < 2 {
None
} else {
Some(&args[args.len() - 1])
}
}
fn has_arg(args: &[String], arg: &str) -> bool {
for a in args {
if a == arg {
return true;
}
}
false
}
fn set_background() -> Result<Color> {
let mut ansi = AnsiTerminal::new()?;
let saved_bg = ansi.read_palette_bg()?;
let bg = Color::new(0xd0, 0xd0, 0xff);
ansi.set_palette_bg(bg)?;
Ok(saved_bg)
}
fn restore_background(bg: Color) -> Result<()> {
let mut ansi = AnsiTerminal::new()?;
ansi.set_palette_bg(bg)?;
Ok(())
}
fn main() -> Result<()> {
let args = env::args().collect::<Vec<_>>();
let realmfs_name = match realmfs_arg(&args) {
Some(name) => name,
None => {
println!("Need a realmfs name");
return Ok(())
}
};
let shared_dir = has_arg(&args, "--shared-dir");
println!("realmfs name is {realmfs_name}");
let saved_bg = set_background()?;
if let Err(err) = update_realmfs(&realmfs_name, shared_dir) {
warn!("{}", err);
}
restore_background(saved_bg)?;
Ok(())
}

View File

@@ -1,72 +0,0 @@
use zbus::blocking::fdo::ObjectManagerProxy;
use zbus::blocking::Connection;
use zbus::{fdo, proxy};
use anyhow::Result;
#[proxy(
interface = "com.subgraph.realms.RealmFS",
default_service = "com.subgraph.realms",
gen_async = false
)]
trait RealmFS {
fn prepare_update(&self, shared_dir: bool) -> zbus::Result<String>;
fn commit_update(&self) -> zbus::Result<()>;
fn abandon_update(&self) -> zbus::Result<()>;
#[zbus(property, name = "Name")]
fn name(&self) -> fdo::Result<String>;
#[zbus(property, name = "FreeSpace")]
fn free_space(&self) -> fdo::Result<u64> ;
#[zbus(property, name = "AllocatedSpace")]
fn allocated_space(&self) -> fdo::Result<u64>;
}
#[derive(Clone)]
pub struct RealmFS<'a> {
proxy: RealmFSProxy<'a>,
}
impl <'a> RealmFS <'a> {
pub fn lookup(connection: &Connection, name: &str) -> Result<Self> {
let obj_mgr = ObjectManagerProxy::new(
connection,
"com.subgraph.realms",
"/com/subgraph/Realms2")?;
for (path, map) in obj_mgr.get_managed_objects()? {
if map.contains_key("com.subgraph.realms.RealmFS") {
let proxy = RealmFSProxy::builder(&connection)
.path(path.to_owned())?
.build()?;
let realmfs_name = proxy.name()?;
if &realmfs_name == name {
return Ok(RealmFS{ proxy });
}
}
}
anyhow::bail!("No RealmFS named '{name}' found.");
}
pub fn prepare_update(&self, shared_dir: bool) -> Result<String> {
let name = self.proxy.prepare_update(shared_dir)?;
Ok(name)
}
pub fn abandon_update(&self) -> Result<()> {
self.proxy.abandon_update()?;
Ok(())
}
pub fn commit_update(&self) -> Result<()> {
self.proxy.commit_update()?;
Ok(())
}
}