forked from brl/citadel-tools
Compare commits
21 Commits
error_hand
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c864490dd0 | ||
![]() |
97be5b5793 | ||
![]() |
ecefb17c82 | ||
![]() |
53f87ae338 | ||
![]() |
d44847dec6 | ||
![]() |
7d3b002dab | ||
![]() |
ba268016a6 | ||
![]() |
65aa521118 | ||
![]() |
3f38cbe099 | ||
![]() |
bbcfe9540c | ||
![]() |
533fb462f9 | ||
![]() |
0aa5c36eee | ||
![]() |
3879feb998 | ||
![]() |
87dc7b9668 | ||
![]() |
d34deab087 | ||
![]() |
af75d3ce4a | ||
![]() |
11ec3441c2 | ||
![]() |
11b3e8a016 | ||
![]() |
24f786cf75 | ||
![]() |
2dc8bf2922 | ||
2a16bd4c41 |
1896
Cargo.lock
generated
1896
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "realm-config-ui" ]
|
||||
members = ["citadel-realms", "citadel-installer-ui", "citadel-tool", "realmsd", "launch-gnome-software", "update-realmfs" ]
|
||||
resolver = "2"
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
2648
citadel-installer-ui/Cargo.lock
generated
2648
citadel-installer-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@ use cursive::vec::Vec2;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fs::File;
|
||||
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::Arc;
|
||||
use std::thread;
|
||||
@@ -49,19 +51,10 @@ pub struct Backend {
|
||||
input_receiver: Receiver<TEvent>,
|
||||
resize_receiver: Receiver<()>,
|
||||
|
||||
tty_fd: RawFd,
|
||||
tty_fd: OwnedFd,
|
||||
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<()> {
|
||||
unsafe {
|
||||
if libc::pthread_kill(tid, sig) != 0 {
|
||||
@@ -98,7 +91,7 @@ impl Backend {
|
||||
// Read input from a separate thread
|
||||
|
||||
let input = std::fs::File::open("/dev/tty").unwrap();
|
||||
let tty_fd = input.as_raw_fd();
|
||||
let tty_fd = input.as_fd().try_clone_to_owned().unwrap();
|
||||
let input_thread = thread::spawn(move || {
|
||||
let mut events = input.events();
|
||||
|
||||
@@ -127,12 +120,6 @@ impl Backend {
|
||||
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) {
|
||||
if let Err(e) = pthread_kill(self.input_thread.as_pthread_t(), libc::SIGWINCH) {
|
||||
warn!("error sending signal to input thread: {}", e);
|
||||
@@ -245,7 +232,6 @@ impl backend::Backend for Backend {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.close_tty();
|
||||
self.kill_thread();
|
||||
}
|
||||
|
||||
|
@@ -8,14 +8,14 @@ homepage = "https://subgraph.com"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
rpassword = "4.0"
|
||||
clap = "2.33"
|
||||
rpassword = "7.3"
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
lazy_static = "1.4"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
toml = "0.5"
|
||||
toml = "0.8"
|
||||
hex = "0.4"
|
||||
byteorder = "1"
|
||||
dbus = "0.8.4"
|
||||
pwhash = "0.3.1"
|
||||
pwhash = "1.0"
|
||||
tempfile = "3"
|
||||
|
@@ -1,89 +1,97 @@
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
use clap::{App,Arg,SubCommand,ArgMatches};
|
||||
use clap::AppSettings::*;
|
||||
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
use clap::{Arg,ArgMatches};
|
||||
use clap::{command, ArgAction, Command};
|
||||
use hex;
|
||||
|
||||
pub fn main(args: Vec<String>) {
|
||||
use libcitadel::{Result, ResourceImage, Logger, LogLevel, Partition, KeyPair, ImageHeader, util};
|
||||
|
||||
let app = App::new("citadel-image")
|
||||
pub fn main() {
|
||||
let matches = command!()
|
||||
.about("Citadel update image builder")
|
||||
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
|
||||
.arg_required_else_help(true)
|
||||
.disable_help_subcommand(true)
|
||||
.subcommand(
|
||||
Command::new("metainfo")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("info")
|
||||
.about("Display metainfo variables for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("generate-verity")
|
||||
.about("Generate dm-verity hash tree for an image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("verify")
|
||||
.about("Verify dm-verity hash tree for an image file")
|
||||
.arg(
|
||||
Arg::new("option")
|
||||
.long("option")
|
||||
.required(true)
|
||||
.help("Path to image file"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("install-rootfs")
|
||||
.about("Install rootfs image file to a partition")
|
||||
.arg(
|
||||
Arg::new("choose")
|
||||
.long("just-choose")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Don't install anything, just show which partition would be chosen"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("skip-sha")
|
||||
.long("skip-sha")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Skip verification of header sha256 value"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-prefer")
|
||||
.long("no-prefer")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Don't set PREFER_BOOT flag"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.required_unless_present("choose")
|
||||
.help("Path to image file"),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("genkeys").about("Generate a pair of keys"))
|
||||
.subcommand(
|
||||
Command::new("decompress")
|
||||
.about("Decompress a compressed image file")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("bless")
|
||||
.about("Mark currently mounted rootfs partition as successfully booted"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("verify-shasum")
|
||||
.about("Verify the sha256 sum of the image")
|
||||
.arg(Arg::new("path").required(true).help("Path to image file")),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
.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() {
|
||||
("metainfo", Some(m)) => metainfo(m),
|
||||
("info", Some(m)) => info(m),
|
||||
("generate-verity", Some(m)) => generate_verity(m),
|
||||
("verify", Some(m)) => verify(m),
|
||||
("sign-image", Some(m)) => sign_image(m),
|
||||
("genkeys", Some(_)) => genkeys(),
|
||||
("decompress", Some(m)) => decompress(m),
|
||||
("verify-shasum", Some(m)) => verify_shasum(m),
|
||||
("install-rootfs", Some(m)) => install_rootfs(m),
|
||||
("install", Some(m)) => install_image(m),
|
||||
("bless", Some(_)) => bless(),
|
||||
Some(("metainfo", sub_m)) => metainfo(sub_m),
|
||||
Some(("info", sub_m)) => info(sub_m),
|
||||
Some(("generate-verity", sub_m)) => generate_verity(sub_m),
|
||||
Some(("verify", sub_m)) => verify(sub_m),
|
||||
Some(("sign-image", sub_m)) => sign_image(sub_m),
|
||||
Some(("genkeys", _)) => genkeys(),
|
||||
Some(("decompress", sub_m)) => decompress(sub_m),
|
||||
Some(("verify-shasum", sub_m)) => verify_shasum(sub_m),
|
||||
Some(("install-rootfs", sub_m)) => install_rootfs(sub_m),
|
||||
Some(("install", sub_m)) => install_image(sub_m),
|
||||
Some(("bless", _)) => bless(),
|
||||
_ => Ok(()),
|
||||
};
|
||||
|
||||
@@ -159,7 +167,9 @@ fn verify_shasum(arg_matches: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
let path = arg_matches.value_of("path").expect("path argument missing");
|
||||
let path = arg_matches.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
bail!("Cannot load image {}: File does not exist", path);
|
||||
}
|
||||
@@ -171,14 +181,14 @@ fn load_image(arg_matches: &ArgMatches) -> Result<ResourceImage> {
|
||||
}
|
||||
|
||||
fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
|
||||
if arg_matches.is_present("choose") {
|
||||
if arg_matches.get_flag("choose") {
|
||||
let _ = choose_install_partition(true)?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
|
||||
if !arg_matches.is_present("skip-sha") {
|
||||
if !arg_matches.get_flag("skip-sha") {
|
||||
info!("Verifying sha256 hash of image");
|
||||
let shasum = img.generate_shasum()?;
|
||||
if shasum != img.metainfo().shasum() {
|
||||
@@ -188,7 +198,7 @@ fn install_rootfs(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
let partition = choose_install_partition(true)?;
|
||||
|
||||
if !arg_matches.is_present("no-prefer") {
|
||||
if !arg_matches.get_flag("no-prefer") {
|
||||
clear_prefer_boot()?;
|
||||
img.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
|
||||
}
|
||||
@@ -212,7 +222,9 @@ fn sign_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_image(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let source = arg_matches.value_of("path").expect("path argument missing");
|
||||
let source = arg_matches.get_one::<String>("path")
|
||||
.expect("path argument missing");
|
||||
|
||||
let img = load_image(arg_matches)?;
|
||||
let _hdr = img.header();
|
||||
let metainfo = img.metainfo();
|
||||
|
@@ -125,7 +125,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
loop {
|
||||
println!("{}", prompt);
|
||||
println!();
|
||||
let passphrase = rpassword::read_password_from_tty(Some(" Passphrase : "))?;
|
||||
let passphrase = rpassword::prompt_password(" Passphrase : ")?;
|
||||
if passphrase.is_empty() {
|
||||
println!("Passphrase cannot be empty");
|
||||
continue;
|
||||
@@ -133,7 +133,7 @@ fn read_passphrase(prompt: &str) -> io::Result<Option<String>> {
|
||||
if passphrase == "q" || passphrase == "Q" {
|
||||
return Ok(None);
|
||||
}
|
||||
let confirm = rpassword::read_password_from_tty(Some(" Confirm : "))?;
|
||||
let confirm = rpassword::prompt_password(" Confirm : ")?;
|
||||
if confirm == "q" || confirm == "Q" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@@ -34,9 +34,9 @@ fn main() {
|
||||
} else if exe == Path::new("/usr/libexec/citadel-install-backend") {
|
||||
install_backend::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-image") {
|
||||
image::main(args);
|
||||
image::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
|
||||
realmfs::main(args);
|
||||
realmfs::main();
|
||||
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||
update::main(args);
|
||||
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
||||
@@ -57,8 +57,8 @@ fn dispatch_command(args: Vec<String>) {
|
||||
match command.as_str() {
|
||||
"boot" => boot::main(rebuild_args("citadel-boot", args)),
|
||||
"install" => install::main(rebuild_args("citadel-install", args)),
|
||||
"image" => image::main(rebuild_args("citadel-image", args)),
|
||||
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
|
||||
"image" => image::main(),
|
||||
"realmfs" => realmfs::main(),
|
||||
"update" => update::main(rebuild_args("citadel-update", args)),
|
||||
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
|
||||
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
|
||||
|
@@ -1,28 +1,24 @@
|
||||
use clap::App;
|
||||
use clap::ArgMatches;
|
||||
use clap::{command, Command};
|
||||
use clap::{Arg, ArgMatches};
|
||||
|
||||
use libcitadel::{Result,RealmFS,Logger,LogLevel};
|
||||
use libcitadel::util::is_euid_root;
|
||||
use clap::SubCommand;
|
||||
use clap::AppSettings::*;
|
||||
use clap::Arg;
|
||||
use libcitadel::ResizeSize;
|
||||
use std::process::exit;
|
||||
|
||||
pub fn main(args: Vec<String>) {
|
||||
pub fn main() {
|
||||
|
||||
Logger::set_log_level(LogLevel::Debug);
|
||||
|
||||
let app = App::new("citadel-realmfs")
|
||||
.about("Citadel realmfs image tool")
|
||||
.settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs])
|
||||
|
||||
.subcommand(SubCommand::with_name("resize")
|
||||
let matches = command!()
|
||||
.about("citadel-realmfs")
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(Command::new("resize")
|
||||
.about("Resize an existing RealmFS image. If the image is currently sealed, it will also be unsealed.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to resize")
|
||||
.required(true))
|
||||
.arg(Arg::with_name("size")
|
||||
.arg(Arg::new("size")
|
||||
.help("Size to increase RealmFS image to (or by if prefixed with '+')")
|
||||
.long_help("\
|
||||
The size can be followed by a 'g' or 'm' character \
|
||||
@@ -35,53 +31,53 @@ is the final absolute size of the image.")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.subcommand(SubCommand::with_name("fork")
|
||||
.subcommand(Command::new("fork")
|
||||
.about("Create a new RealmFS image as an unsealed copy of an existing image")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to fork")
|
||||
.required(true))
|
||||
|
||||
.arg(Arg::with_name("forkname")
|
||||
.arg(Arg::new("forkname")
|
||||
.help("Name of new image to create")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("autoresize")
|
||||
.subcommand(Command::new("autoresize")
|
||||
.about("Increase size of RealmFS image if not enough free space remains")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("update")
|
||||
.subcommand(Command::new("update")
|
||||
.about("Open an update shell on the image")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("activate")
|
||||
.subcommand(Command::new("activate")
|
||||
.about("Activate a RealmFS by creating a block device for the image and mounting it.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to activate")
|
||||
.required(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("deactivate")
|
||||
.subcommand(Command::new("deactivate")
|
||||
.about("Deactivate a RealmFS by unmounting it and removing block device created during activation.")
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.help("Path or name of RealmFS image to deactivate")
|
||||
.required(true)))
|
||||
|
||||
|
||||
.arg(Arg::with_name("image")
|
||||
.arg(Arg::new("image")
|
||||
.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() {
|
||||
("resize", Some(m)) => resize(m),
|
||||
("autoresize", Some(m)) => autoresize(m),
|
||||
("fork", Some(m)) => fork(m),
|
||||
("update", Some(m)) => update(m),
|
||||
("activate", Some(m)) => activate(m),
|
||||
("deactivate", Some(m)) => deactivate(m),
|
||||
Some(("resize", m)) => resize(m),
|
||||
Some(("autoresize", m)) => autoresize(m),
|
||||
Some(("fork", m)) => fork(m),
|
||||
Some(("update", m)) => update(m),
|
||||
Some(("activate", m)) => activate(m),
|
||||
Some(("deactivate", m)) => deactivate(m),
|
||||
_ => image_info(&matches),
|
||||
};
|
||||
|
||||
@@ -92,7 +88,7 @@ is the final absolute size of the image.")
|
||||
}
|
||||
|
||||
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
|
||||
let image = match arg_matches.value_of("image") {
|
||||
let image = match arg_matches.get_one::<String>("image") {
|
||||
Some(s) => s,
|
||||
None => bail!("Image argument required."),
|
||||
};
|
||||
@@ -136,10 +132,9 @@ fn parse_resize_size(s: &str) -> Result<ResizeSize> {
|
||||
fn resize(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
info!("image is {}", img.path().display());
|
||||
let size_arg = match arg_matches.value_of("size") {
|
||||
let size_arg = match arg_matches.get_one::<String>("size") {
|
||||
Some(size) => size,
|
||||
None => "No size argument",
|
||||
|
||||
};
|
||||
info!("Size is {}", size_arg);
|
||||
let mode_add = size_arg.starts_with('+');
|
||||
@@ -165,7 +160,7 @@ fn autoresize(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
fn fork(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let forkname = match arg_matches.value_of("forkname") {
|
||||
let forkname = match arg_matches.get_one::<String>("forkname") {
|
||||
Some(name) => name,
|
||||
None => bail!("No fork name argument"),
|
||||
};
|
||||
@@ -190,7 +185,7 @@ fn update(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
fn activate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let img_arg = arg_matches.value_of("image").unwrap();
|
||||
let img_arg = arg_matches.get_one::<String>("image").unwrap();
|
||||
|
||||
if img.is_activated() {
|
||||
info!("RealmFS image {} is already activated", img_arg);
|
||||
@@ -203,7 +198,7 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
|
||||
fn deactivate(arg_matches: &ArgMatches) -> Result<()> {
|
||||
let img = realmfs_image(arg_matches)?;
|
||||
let img_arg = arg_matches.value_of("image").unwrap();
|
||||
let img_arg = arg_matches.get_one::<String>("image").unwrap();
|
||||
if !img.is_activated() {
|
||||
info!("RealmFS image {} is not activated", img_arg);
|
||||
} else if img.is_in_use() {
|
||||
|
@@ -99,7 +99,7 @@ impl DesktopFileSync {
|
||||
}
|
||||
|
||||
fn collect_source_files(&mut self, directory: impl AsRef<Path>) -> Result<()> {
|
||||
let mut directory = Realms::current_realm_symlink().join(directory.as_ref());
|
||||
let mut directory = self.realm.run_path().join(directory.as_ref());
|
||||
directory.push("share/applications");
|
||||
if directory.exists() {
|
||||
util::read_directory(&directory, |dent| {
|
||||
@@ -126,7 +126,11 @@ impl DesktopFileSync {
|
||||
}
|
||||
|
||||
fn remove_missing_target_files(&mut self) -> Result<()> {
|
||||
let sources = self.source_filenames();
|
||||
let mut sources = self.source_filenames();
|
||||
// If flatpak is enabled, don't remove the generated GNOME Software desktop file
|
||||
if self.realm.config().flatpak() {
|
||||
sources.insert(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
|
||||
}
|
||||
let prefix = format!("realm-{}.", self.realm.name());
|
||||
util::read_directory(Self::CITADEL_APPLICATIONS, |dent| {
|
||||
if let Some(filename) = dent.file_name().to_str() {
|
||||
@@ -182,7 +186,9 @@ impl DesktopFileSync {
|
||||
|
||||
fn sync_item(&self, item: &DesktopItem) -> Result<()> {
|
||||
let mut dfp = DesktopFileParser::parse_from_path(&item.path, "/usr/libexec/citadel-run ")?;
|
||||
if dfp.is_showable() {
|
||||
// When use-flatpak is enabled a gnome-software desktop file will be generated
|
||||
let flatpak_gs_hide = dfp.filename() == "org.gnome.Software.desktop" && self.realm.config().flatpak();
|
||||
if dfp.is_showable() && !flatpak_gs_hide {
|
||||
self.sync_item_icon(&mut dfp);
|
||||
dfp.write_to_dir(Self::CITADEL_APPLICATIONS, Some(&self.realm))?;
|
||||
} else {
|
||||
|
@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use libcitadel::{Result, util, Realm};
|
||||
use std::cell::{RefCell, Cell};
|
||||
use std::fs;
|
||||
use crate::sync::desktop_file::DesktopFile;
|
||||
use crate::sync::REALM_BASE_PATHS;
|
||||
|
||||
|
@@ -14,7 +14,7 @@ fn has_arg(args: &[String], arg: &str) -> bool {
|
||||
|
||||
pub const REALM_BASE_PATHS:&[&str] = &[
|
||||
"rootfs/usr",
|
||||
"rootfs/var/lib/flatpak/exports",
|
||||
"flatpak/exports",
|
||||
"home/.local",
|
||||
"home/.local/share/flatpak/exports"
|
||||
];
|
||||
|
@@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=RealmConfig
|
||||
Type=Application
|
||||
Icon=org.gnome.Settings
|
||||
NoDisplay=true
|
@@ -7,10 +7,12 @@
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="com.subgraph.realms"/>
|
||||
<allow own="com.subgraph.Realms2"/>
|
||||
</policy>
|
||||
|
||||
<policy context="default">
|
||||
<allow send_destination="com.subgraph.realms"/>
|
||||
<allow send_destination="com.subgraph.Realms2"/>
|
||||
<allow send_destination="com.subgraph.realms"
|
||||
send_interface="org.freedesktop.DBus.Properties"/>
|
||||
<allow send_destination="com.subgraph.realms"
|
||||
|
8
launch-gnome-software/Cargo.toml
Normal file
8
launch-gnome-software/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "launch-gnome-software"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
anyhow = "1.0"
|
67
launch-gnome-software/src/main.rs
Normal file
67
launch-gnome-software/src/main.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::env;
|
||||
|
||||
use libcitadel::{Logger, LogLevel, Realm, Realms, util};
|
||||
use libcitadel::flatpak::GnomeSoftwareLauncher;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
|
||||
fn realm_arg() -> Option<String> {
|
||||
let mut args = env::args();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--realm" {
|
||||
return args.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn choose_realm() -> Result<Realm> {
|
||||
let mut realms = Realms::load()?;
|
||||
if let Some(realm_name) = realm_arg() {
|
||||
match realms.by_name(&realm_name) {
|
||||
None => bail!("realm '{}' not found", realm_name),
|
||||
Some(realm) => return Ok(realm),
|
||||
}
|
||||
}
|
||||
let realm = match realms.current() {
|
||||
Some(realm) => realm,
|
||||
None => bail!("no current realm"),
|
||||
};
|
||||
|
||||
Ok(realm)
|
||||
}
|
||||
|
||||
fn has_arg(arg: &str) -> bool {
|
||||
env::args()
|
||||
.skip(1)
|
||||
.any(|s| s == arg)
|
||||
}
|
||||
|
||||
fn launch() -> Result<()> {
|
||||
let realm = choose_realm()?;
|
||||
if !util::is_euid_root() {
|
||||
bail!("Must be run with root euid");
|
||||
}
|
||||
let mut launcher = GnomeSoftwareLauncher::new(realm)?;
|
||||
|
||||
if has_arg("--quit") {
|
||||
launcher.quit()?;
|
||||
} else {
|
||||
if has_arg("--shell") {
|
||||
launcher.set_run_shell();
|
||||
}
|
||||
launcher.launch()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if has_arg("--verbose") {
|
||||
Logger::set_log_level(LogLevel::Verbose);
|
||||
}
|
||||
|
||||
if let Err(err) = launch() {
|
||||
eprintln!("Error: {}", err);
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ nix = "0.17.0"
|
||||
toml = "0.5"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "=1.0.1"
|
||||
lazy_static = "1.4"
|
||||
sodiumoxide = "0.2"
|
||||
hex = "0.4"
|
||||
|
@@ -78,3 +78,9 @@ impl Display for Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fmt::Error> for crate::Error {
|
||||
fn from(e: fmt::Error) -> Self {
|
||||
format_err!("Error formatting string: {}", e).into()
|
||||
}
|
||||
}
|
||||
|
221
libcitadel/src/flatpak/bubblewrap.rs
Normal file
221
libcitadel/src/flatpak/bubblewrap.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, io};
|
||||
use std::fs::File;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{Logger, LogLevel, Result, verbose};
|
||||
|
||||
const BWRAP_PATH: &str = "/usr/libexec/flatpak-bwrap";
|
||||
|
||||
pub struct BubbleWrap {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl BubbleWrap {
|
||||
|
||||
pub fn new() -> Self {
|
||||
BubbleWrap {
|
||||
command: Command::new(BWRAP_PATH),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
|
||||
self.command.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.add_arg(arg.as_ref());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ro_bind(&mut self, path_list: &[&str]) -> &mut Self {
|
||||
for &path in path_list {
|
||||
self.add_args(&["--ro-bind", path, path]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ro_bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
|
||||
self.add_args(&["--ro-bind", src, dest])
|
||||
}
|
||||
|
||||
pub fn bind_to(&mut self, src: &str, dest: &str) -> &mut Self {
|
||||
self.add_args(&["--bind", src, dest])
|
||||
}
|
||||
|
||||
pub fn create_dirs(&mut self, dir_list: &[&str]) -> &mut Self {
|
||||
for &dir in dir_list {
|
||||
self.add_args(&["--dir", dir]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create_symlinks(&mut self, links: &[(&str, &str)]) -> &mut Self {
|
||||
for (src,dest) in links {
|
||||
self.add_args(&["--symlink", src, dest]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount_dev(&mut self) -> &mut Self {
|
||||
self.add_args(&["--dev", "/dev"])
|
||||
}
|
||||
|
||||
pub fn mount_proc(&mut self) -> &mut Self {
|
||||
self.add_args(&["--proc", "/proc"])
|
||||
}
|
||||
|
||||
pub fn dev_bind(&mut self, path: &str) -> &mut Self {
|
||||
self.add_args(&["--dev-bind", path, path])
|
||||
}
|
||||
|
||||
pub fn clear_env(&mut self) -> &mut Self {
|
||||
self.add_arg("--clearenv")
|
||||
}
|
||||
|
||||
pub fn set_env_list(&mut self, env_list: &[&str]) -> &mut Self {
|
||||
for line in env_list {
|
||||
if let Some((k,v)) = line.split_once("=") {
|
||||
self.add_args(&["--setenv", k, v]);
|
||||
} else {
|
||||
eprintln!("Warning: environment variable '{}' does not have = delimiter. Ignoring", line);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unshare_all(&mut self) -> &mut Self {
|
||||
self.add_arg("--unshare-all")
|
||||
}
|
||||
|
||||
pub fn share_net(&mut self) -> &mut Self {
|
||||
self.add_arg("--share-net")
|
||||
}
|
||||
|
||||
pub fn log_command(&self) {
|
||||
let mut buffer = String::new();
|
||||
verbose!("{}", BWRAP_PATH);
|
||||
for arg in self.command.get_args() {
|
||||
if let Some(s) = arg.to_str() {
|
||||
if s.starts_with("-") {
|
||||
if !buffer.is_empty() {
|
||||
verbose!(" {}", buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
buffer.push(' ');
|
||||
}
|
||||
buffer.push_str(s);
|
||||
}
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
verbose!(" {}", buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_file(&mut self, status_file: &File) -> &mut Self {
|
||||
// Rust sets O_CLOEXEC when opening files so we create
|
||||
// a new descriptor that will remain open across exec()
|
||||
let dup_fd = unsafe {
|
||||
libc::dup(status_file.as_raw_fd())
|
||||
};
|
||||
if dup_fd == -1 {
|
||||
warn!("Failed to dup() status file descriptor: {}", io::Error::last_os_error());
|
||||
warn!("Skipping --json-status-fd argument");
|
||||
self
|
||||
} else {
|
||||
self.add_arg("--json-status-fd")
|
||||
.add_arg(dup_fd.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch<S: AsRef<OsStr>>(&mut self, cmd: &[S]) -> Result<()> {
|
||||
if Logger::is_log_level(LogLevel::Verbose) {
|
||||
self.log_command();
|
||||
let s = cmd.iter().map(|s| format!("{} ", s.as_ref().to_str().unwrap())).collect::<String>();
|
||||
verbose!(" {}", s)
|
||||
}
|
||||
self.add_args(cmd);
|
||||
let err = self.command.exec();
|
||||
bail!("failed to exec bubblewrap: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub struct BubbleWrapRunningStatus {
|
||||
pub child_pid: u64,
|
||||
pub cgroup_namespace: u64,
|
||||
pub ipc_namespace: u64,
|
||||
pub mnt_namespace: u64,
|
||||
pub pid_namespace: u64,
|
||||
pub uts_namespace: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize,Clone)]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub struct BubbleWrapExitStatus {
|
||||
pub exit_code: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BubbleWrapStatus {
|
||||
running: BubbleWrapRunningStatus,
|
||||
exit: Option<BubbleWrapExitStatus>,
|
||||
}
|
||||
|
||||
impl BubbleWrapStatus {
|
||||
pub fn parse_file(path: impl AsRef<Path>) -> Result<Option<Self>> {
|
||||
if !path.as_ref().exists() {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(path)
|
||||
.map_err(context!("error reading status file"))?;
|
||||
|
||||
let mut lines = s.lines();
|
||||
let running = match lines.next() {
|
||||
None => return Ok(None),
|
||||
Some(s) => serde_json::from_str::<BubbleWrapRunningStatus>(s)
|
||||
.map_err(context!("failed to parse status line ({})", s))?
|
||||
};
|
||||
|
||||
let exit = match lines.next() {
|
||||
None => None,
|
||||
Some(s) => Some(serde_json::from_str::<BubbleWrapExitStatus>(s)
|
||||
.map_err(context!("failed to parse exit line ({})", s))?)
|
||||
};
|
||||
|
||||
Ok(Some(BubbleWrapStatus {
|
||||
running,
|
||||
exit
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.exit.is_none()
|
||||
}
|
||||
|
||||
pub fn running_status(&self) -> &BubbleWrapRunningStatus {
|
||||
&self.running
|
||||
}
|
||||
|
||||
pub fn child_pid(&self) -> u64 {
|
||||
self.running.child_pid
|
||||
}
|
||||
|
||||
pub fn pid_namespace(&self) -> u64 {
|
||||
self.running.pid_namespace
|
||||
}
|
||||
|
||||
pub fn exit_status(&self) -> Option<&BubbleWrapExitStatus> {
|
||||
self.exit.as_ref()
|
||||
}
|
||||
}
|
259
libcitadel/src/flatpak/launcher.rs
Normal file
259
libcitadel/src/flatpak/launcher.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use std::{fs, io};
|
||||
use std::fs::File;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::prelude::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{Realm, Result, util};
|
||||
use crate::flatpak::{BubbleWrap, BubbleWrapStatus, SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
|
||||
use crate::flatpak::netns::NetNS;
|
||||
|
||||
|
||||
const FLATPAK_PATH: &str = "/usr/bin/flatpak";
|
||||
|
||||
const ENVIRONMENT: &[&str; 7] = &[
|
||||
"HOME=/home/citadel",
|
||||
"USER=citadel",
|
||||
"XDG_RUNTIME_DIR=/run/user/1000",
|
||||
"XDG_DATA_DIRS=/home/citadel/.local/share/flatpak/exports/share:/usr/share",
|
||||
"TERM=xterm-256color",
|
||||
"GTK_A11Y=none",
|
||||
"FLATPAK_USER_DIR=/home/citadel/realm-flatpak",
|
||||
];
|
||||
|
||||
const FLATHUB_URL: &str = "https://dl.flathub.org/repo/flathub.flatpakrepo";
|
||||
|
||||
pub struct GnomeSoftwareLauncher {
|
||||
realm: Realm,
|
||||
status: Option<BubbleWrapStatus>,
|
||||
netns: NetNS,
|
||||
run_shell: bool,
|
||||
}
|
||||
impl GnomeSoftwareLauncher {
|
||||
|
||||
pub fn new(realm: Realm) -> Result<Self> {
|
||||
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
|
||||
let status = sandbox_status.realm_status(&realm);
|
||||
let netns = NetNS::new(NetNS::GS_NETNS_NAME);
|
||||
Ok(GnomeSoftwareLauncher {
|
||||
realm,
|
||||
status,
|
||||
netns,
|
||||
run_shell: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_run_shell(&mut self) {
|
||||
self.run_shell = true;
|
||||
}
|
||||
|
||||
fn ensure_flatpak_dir(&self) -> Result<()> {
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
if !flatpak_user_dir.exists() {
|
||||
if let Err(err) = fs::create_dir(&flatpak_user_dir) {
|
||||
bail!("failed to create realm flatpak directory ({}): {}", flatpak_user_dir.display(), err);
|
||||
}
|
||||
util::chown_user(&flatpak_user_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_flathub(&self) -> Result<()> {
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
|
||||
match Command::new(FLATPAK_PATH)
|
||||
.env("FLATPAK_USER_DIR", flatpak_user_dir)
|
||||
.arg("remote-add")
|
||||
.arg("--user")
|
||||
.arg("--if-not-exists")
|
||||
.arg("flathub")
|
||||
.arg(FLATHUB_URL)
|
||||
.status() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
|
||||
} else {
|
||||
bail!("failed to add flathub repo")
|
||||
}
|
||||
},
|
||||
Err(err) => bail!("error running flatpak command: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_tmp_directory(path: &Path) -> io::Result<Option<String>> {
|
||||
for entry in fs::read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_socket() {
|
||||
if let Some(filename) = entry.path().file_name() {
|
||||
if let Some(filename) = filename.to_str() {
|
||||
if filename.starts_with("dbus-") {
|
||||
return Ok(Some(format!("/tmp/{}", filename)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn find_dbus_socket(&self) -> Result<String> {
|
||||
let pid = self.running_pid()?;
|
||||
let tmp_dir = PathBuf::from(format!("/proc/{}/root/tmp", pid));
|
||||
if !tmp_dir.is_dir() {
|
||||
bail!("no /tmp directory found for process pid={}", pid);
|
||||
}
|
||||
|
||||
if let Some(s) = Self::scan_tmp_directory(&tmp_dir)
|
||||
.map_err(context!("error reading directory {}", tmp_dir.display()))? {
|
||||
Ok(s)
|
||||
|
||||
} else {
|
||||
bail!("no dbus socket found in /tmp directory for process pid={}", pid);
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_sandbox(&self, status_file: &File) -> Result<()> {
|
||||
self.ensure_flatpak_dir()?;
|
||||
if let Err(err) = self.netns.nsenter() {
|
||||
bail!("Failed to enter 'gnome-software' network namespace: {}", err);
|
||||
}
|
||||
verbose!("Entered network namespace ({})", NetNS::GS_NETNS_NAME);
|
||||
|
||||
if let Err(err) = util::drop_privileges(1000, 1000) {
|
||||
bail!("Failed to drop privileges to uid = gid = 1000: {}", err);
|
||||
}
|
||||
verbose!("Dropped privileges (uid=1000, gid=1000)");
|
||||
|
||||
self.add_flathub()?;
|
||||
let flatpak_user_dir = self.realm.base_path_file("flatpak");
|
||||
let flatpak_user_dir = flatpak_user_dir.to_str().unwrap();
|
||||
|
||||
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
|
||||
verbose!("Running command in sandbox: {}", cmd);
|
||||
|
||||
BubbleWrap::new()
|
||||
.ro_bind(&[
|
||||
"/usr/bin",
|
||||
"/usr/lib",
|
||||
"/usr/libexec",
|
||||
"/usr/share/dbus-1",
|
||||
"/usr/share/icons",
|
||||
"/usr/share/mime",
|
||||
"/usr/share/X11",
|
||||
"/usr/share/glib-2.0",
|
||||
"/usr/share/xml",
|
||||
"/usr/share/drirc.d",
|
||||
"/usr/share/fontconfig",
|
||||
"/usr/share/fonts",
|
||||
"/usr/share/zoneinfo",
|
||||
"/usr/share/swcatalog",
|
||||
|
||||
"/etc/passwd",
|
||||
"/etc/machine-id",
|
||||
"/etc/nsswitch.conf",
|
||||
"/etc/fonts",
|
||||
"/etc/ssl",
|
||||
"/sys/dev/char", "/sys/devices",
|
||||
"/run/user/1000/wayland-0",
|
||||
])
|
||||
.ro_bind_to("/run/NetworkManager/resolv.conf", "/etc/resolv.conf")
|
||||
.bind_to(flatpak_user_dir, "/home/citadel/realm-flatpak")
|
||||
.create_symlinks(&[
|
||||
("usr/lib", "/lib64"),
|
||||
("usr/bin", "/bin"),
|
||||
("/tmp", "/var/tmp"),
|
||||
])
|
||||
.create_dirs(&[
|
||||
"/var/lib/flatpak",
|
||||
"/home/citadel",
|
||||
"/tmp",
|
||||
"/sys/block", "/sys/bus", "/sys/class",
|
||||
])
|
||||
|
||||
|
||||
.mount_dev()
|
||||
.dev_bind("/dev/dri")
|
||||
.mount_proc()
|
||||
.unshare_all()
|
||||
.share_net()
|
||||
|
||||
.clear_env()
|
||||
.set_env_list(ENVIRONMENT)
|
||||
|
||||
.status_file(status_file)
|
||||
|
||||
.launch(&["dbus-run-session", "--", cmd])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_realm_status_file(&self) -> Result<File> {
|
||||
let path = Path::new(SANDBOX_STATUS_FILE_DIRECTORY).join(self.realm.name());
|
||||
File::create(&path)
|
||||
.map_err(context!("failed to open sandbox status file {}", path.display()))
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> Result<()> {
|
||||
self.netns.ensure_exists()?;
|
||||
if self.is_running() {
|
||||
let cmd = if self.run_shell { "/usr/bin/bash" } else { "/usr/bin/gnome-software"};
|
||||
self.launch_in_running_sandbox(&[cmd])?;
|
||||
} else {
|
||||
let status_file = self.new_realm_status_file()?;
|
||||
self.ensure_flatpak_dir()?;
|
||||
self.launch_sandbox(&status_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&self) -> Result<()> {
|
||||
if self.is_running() {
|
||||
self.launch_in_running_sandbox(&["/usr/bin/gnome-software", "--quit"])?;
|
||||
} else {
|
||||
warn!("No running sandbox found for realm {}", self.realm.name());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.status.as_ref()
|
||||
.map(|s| s.is_running())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn running_pid(&self) -> Result<u64> {
|
||||
self.status.as_ref()
|
||||
.map(|s| s.child_pid())
|
||||
.ok_or(format_err!("no sandbox status available for realm '{}',", self.realm.name()))
|
||||
}
|
||||
|
||||
fn dbus_session_address(&self) -> Result<String> {
|
||||
let dbus_socket = Self::find_dbus_socket(&self)?;
|
||||
Ok(format!("unix:path={}", dbus_socket))
|
||||
}
|
||||
|
||||
fn launch_in_running_sandbox(&self, command: &[&str]) -> Result<()> {
|
||||
let dbus_address = self.dbus_session_address()?;
|
||||
|
||||
let pid = self.running_pid()?.to_string();
|
||||
let mut env = ENVIRONMENT.iter()
|
||||
.map(|s| s.split_once('=').unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
env.push(("DBUS_SESSION_BUS_ADDRESS", dbus_address.as_str()));
|
||||
|
||||
let err = Command::new("/usr/bin/nsenter")
|
||||
.env_clear()
|
||||
.envs( env )
|
||||
.args(&[
|
||||
"--all",
|
||||
"--target", pid.as_str(),
|
||||
"--setuid", "1000",
|
||||
"--setgid", "1000",
|
||||
])
|
||||
.args(command)
|
||||
.exec();
|
||||
|
||||
Err(format_err!("failed to execute nsenter: {}", err))
|
||||
}
|
||||
}
|
16
libcitadel/src/flatpak/mod.rs
Normal file
16
libcitadel/src/flatpak/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
pub(crate) mod setup;
|
||||
pub(crate) mod status;
|
||||
|
||||
pub(crate) mod bubblewrap;
|
||||
|
||||
pub(crate) mod launcher;
|
||||
|
||||
pub(crate) mod netns;
|
||||
|
||||
pub use status::SandboxStatus;
|
||||
|
||||
pub use bubblewrap::{BubbleWrap,BubbleWrapStatus,BubbleWrapRunningStatus,BubbleWrapExitStatus};
|
||||
pub use launcher::GnomeSoftwareLauncher;
|
||||
|
||||
pub const SANDBOX_STATUS_FILE_DIRECTORY: &str = "/run/citadel/realms/gs-sandbox-status";
|
132
libcitadel/src/flatpak/netns.rs
Normal file
132
libcitadel/src/flatpak/netns.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{util,Result};
|
||||
|
||||
|
||||
const BRIDGE_NAME: &str = "vz-clear";
|
||||
const VETH0: &str = "gs-veth0";
|
||||
const VETH1: &str = "gs-veth1";
|
||||
const IP_ADDRESS: &str = "172.17.0.222/24";
|
||||
const GW_ADDRESS: &str = "172.17.0.1";
|
||||
|
||||
|
||||
pub struct NetNS {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl NetNS {
|
||||
pub const GS_NETNS_NAME: &'static str = "gnome-software";
|
||||
pub fn new(name: &str) -> Self {
|
||||
NetNS {
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create(&self) -> crate::Result<()> {
|
||||
Ip::new().link_add_veth(VETH0, VETH1).run()?;
|
||||
Ip::new().link_set_netns(VETH0, &self.name).run()?;
|
||||
Ip::new().link_set_master(VETH1, BRIDGE_NAME).run()?;
|
||||
Ip::new().link_set_dev_up(VETH1).run()?;
|
||||
|
||||
Ip::new().ip_netns_exec_ip(&self.name).addr_add(IP_ADDRESS, VETH0).run()?;
|
||||
Ip::new().ip_netns_exec_ip(&self.name).link_set_dev_up(VETH0).run()?;
|
||||
Ip::new().ip_netns_exec_ip(&self.name).route_add_default(GW_ADDRESS).run()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn ensure_exists(&self) -> Result<()> {
|
||||
if Path::new(&format!("/run/netns/{}", self.name)).exists() {
|
||||
verbose!("Network namespace ({}) exists", self.name);
|
||||
return Ok(())
|
||||
}
|
||||
verbose!("Setting up network namespace ({})", self.name);
|
||||
|
||||
Ip::new().netns_add(&self.name).run()
|
||||
.map_err(context!("Failed to add network namespace '{}'", self.name))?;
|
||||
|
||||
if let Err(err) = self.create() {
|
||||
Ip::new().netns_delete(&self.name).run()?;
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nsenter(&self) -> Result<()> {
|
||||
util::nsenter_netns(&self.name)
|
||||
}
|
||||
}
|
||||
const IP_PATH: &str = "/usr/sbin/ip";
|
||||
struct Ip {
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl Ip {
|
||||
|
||||
fn new() -> Self {
|
||||
let mut command = Command::new(IP_PATH);
|
||||
command.env_clear();
|
||||
Ip { command }
|
||||
}
|
||||
|
||||
fn add_args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.command.arg(arg);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn netns_add(&mut self, name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "add", name])
|
||||
}
|
||||
|
||||
pub fn netns_delete(&mut self, name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "delete", name])
|
||||
}
|
||||
|
||||
pub fn link_add_veth(&mut self, name: &str, peer_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "add", name, "type", "veth", "peer", "name", peer_name])
|
||||
}
|
||||
|
||||
pub fn link_set_netns(&mut self, iface: &str, netns_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", iface, "netns", netns_name])
|
||||
}
|
||||
|
||||
pub fn link_set_master(&mut self, iface: &str, bridge_name: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", iface, "master", bridge_name])
|
||||
}
|
||||
|
||||
pub fn link_set_dev_up(&mut self, iface: &str) -> &mut Self {
|
||||
self.add_args(&["link", "set", "dev", iface, "up"])
|
||||
}
|
||||
|
||||
pub fn ip_netns_exec_ip(&mut self, netns_name: &str) -> &mut Self {
|
||||
self.add_args(&["netns", "exec", netns_name, IP_PATH])
|
||||
}
|
||||
|
||||
pub fn addr_add(&mut self, ip_address: &str, dev: &str) -> &mut Self {
|
||||
self.add_args(&["addr", "add", ip_address, "dev", dev])
|
||||
}
|
||||
|
||||
pub fn route_add_default(&mut self, gateway: &str) -> &mut Self {
|
||||
self.add_args(&["route", "add", "default", "via", gateway])
|
||||
}
|
||||
|
||||
fn run(&mut self) -> crate::Result<()> {
|
||||
verbose!("{:?}", self.command);
|
||||
match self.command.status() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("IP command ({:?}) did not succeeed.", self.command);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("error running ip command ({:?}): {}", self.command, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
libcitadel/src/flatpak/setup.rs
Normal file
58
libcitadel/src/flatpak/setup.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::path::Path;
|
||||
use crate::{Realm, Result, util};
|
||||
|
||||
const GNOME_SOFTWARE_DESKTOP_TEMPLATE: &str = "\
|
||||
[Desktop Entry]
|
||||
Name=Software
|
||||
Comment=Add, remove or update software on this computer
|
||||
Icon=org.gnome.Software
|
||||
Exec=/usr/libexec/launch-gnome-software --realm $REALM_NAME
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=GNOME;GTK;System;PackageManager;
|
||||
Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store;
|
||||
StartupNotify=true
|
||||
";
|
||||
|
||||
const APPLICATION_DIRECTORY: &str = "/home/citadel/.local/share/applications";
|
||||
|
||||
pub struct FlatpakSetup<'a> {
|
||||
realm: &'a Realm,
|
||||
}
|
||||
|
||||
impl <'a> FlatpakSetup<'a> {
|
||||
|
||||
pub fn new(realm: &'a Realm) -> Self {
|
||||
Self { realm }
|
||||
}
|
||||
|
||||
pub fn setup(&self) -> Result<()> {
|
||||
self.write_desktop_file()?;
|
||||
self.ensure_flatpak_directory()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn write_desktop_file(&self) -> Result<()> {
|
||||
let appdir = Path::new(APPLICATION_DIRECTORY);
|
||||
if !appdir.exists() {
|
||||
util::create_dir(appdir)?;
|
||||
if let Some(parent) = appdir.parent().and_then(|p| p.parent()) {
|
||||
util::chown_tree(parent, (1000,1000), true)?;
|
||||
}
|
||||
}
|
||||
let path = appdir.join(format!("realm-{}.org.gnome.Software.desktop", self.realm.name()));
|
||||
util::write_file(path, GNOME_SOFTWARE_DESKTOP_TEMPLATE.replace("$REALM_NAME", self.realm.name()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_flatpak_directory(&self) -> Result<()> {
|
||||
let path = self.realm.base_path_file("flatpak");
|
||||
if !path.exists() {
|
||||
util::create_dir(&path)?;
|
||||
util::chown_user(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
144
libcitadel/src/flatpak/status.rs
Normal file
144
libcitadel/src/flatpak/status.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
use crate::flatpak::bubblewrap::BubbleWrapStatus;
|
||||
use crate::{Realm, Result, util};
|
||||
|
||||
|
||||
/// Utility function to read modified time from a path.
|
||||
fn modified_time(path: &Path) -> Result<SystemTime> {
|
||||
path.metadata().and_then(|meta| meta.modified())
|
||||
.map_err(context!("failed to read modified time from '{}'", path.display()))
|
||||
}
|
||||
|
||||
/// Utility function to detect if current modified time of a path
|
||||
/// matches an earlier recorded modified time.
|
||||
fn modified_changed(path: &Path, old_modified: SystemTime) -> bool {
|
||||
if !path.exists() {
|
||||
// Path existed at some earlier point, so something changed
|
||||
return true;
|
||||
}
|
||||
|
||||
match modified_time(path) {
|
||||
Ok(modified) => old_modified != modified,
|
||||
Err(err) => {
|
||||
// Print a warning but assume change
|
||||
warn!("{}", err);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the content of single entry in a sandbox status directory.
|
||||
///
|
||||
/// The path to the status file as well as the last modified time are
|
||||
/// recorded so that changes in status of a sandbox can be detected.
|
||||
struct StatusEntry {
|
||||
status: BubbleWrapStatus,
|
||||
path: PathBuf,
|
||||
modified: SystemTime,
|
||||
}
|
||||
|
||||
|
||||
impl StatusEntry {
|
||||
|
||||
fn load_timestamp_and_status(path: &Path) -> Result<Option<(SystemTime, BubbleWrapStatus)>> {
|
||||
if path.exists() {
|
||||
let modified = modified_time(path)?;
|
||||
if let Some(status) = BubbleWrapStatus::parse_file(path)? {
|
||||
return Ok(Some((modified, status)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn load(base_dir: &Path, name: &str) -> Result<Option<Self>> {
|
||||
let path = base_dir.join(name);
|
||||
let result = StatusEntry::load_timestamp_and_status(&path)?
|
||||
.map(|(modified, status)| StatusEntry { status, path, modified });
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn is_modified(&self) -> bool {
|
||||
modified_changed(&self.path, self.modified)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Holds information about entries in a sandbox status directory.
|
||||
///
|
||||
/// Bubblewrap accepts a command line argument that asks for status
|
||||
/// information to be written as a json structure to a file descriptor.
|
||||
///
|
||||
pub struct SandboxStatus {
|
||||
base_dir: PathBuf,
|
||||
base_modified: SystemTime,
|
||||
entries: HashMap<String, StatusEntry>,
|
||||
}
|
||||
|
||||
impl SandboxStatus {
|
||||
|
||||
pub fn need_reload(&self) -> bool {
|
||||
if modified_changed(&self.base_dir, self.base_modified) {
|
||||
return true;
|
||||
}
|
||||
self.entries.values().any(|entry| entry.is_modified())
|
||||
}
|
||||
|
||||
fn process_dir_entry(&mut self, dir_entry: PathBuf) -> Result<()> {
|
||||
fn realm_name_for_path(path: &Path) -> Option<&str> {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| Realm::is_valid_name(name))
|
||||
|
||||
}
|
||||
if dir_entry.is_file() {
|
||||
if let Some(name) = realm_name_for_path(&dir_entry) {
|
||||
if let Some(entry) = StatusEntry::load(&self.base_dir, name)? {
|
||||
self.entries.insert(name.to_string(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reload(&mut self) -> Result<()> {
|
||||
self.entries.clear();
|
||||
self.base_modified = modified_time(&self.base_dir)?;
|
||||
let base_dir = self.base_dir.clone();
|
||||
util::read_directory(&base_dir, |entry| {
|
||||
self.process_dir_entry(entry.path())
|
||||
})
|
||||
}
|
||||
|
||||
fn new(base_dir: &Path) -> Result<Self> {
|
||||
let base_dir = base_dir.to_owned();
|
||||
let base_modified = modified_time(&base_dir)?;
|
||||
Ok(SandboxStatus {
|
||||
base_dir,
|
||||
base_modified,
|
||||
entries: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(directory: impl AsRef<Path>) -> Result<SandboxStatus> {
|
||||
let base_dir = directory.as_ref();
|
||||
if !base_dir.exists() {
|
||||
util::create_dir(base_dir)?;
|
||||
}
|
||||
let mut status = SandboxStatus::new(base_dir)?;
|
||||
status.reload()?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn realm_status(&self, realm: &Realm) -> Option<BubbleWrapStatus> {
|
||||
self.entries.get(realm.name()).map(|entry| entry.status.clone())
|
||||
}
|
||||
|
||||
pub fn new_realm_status_file(&self, realm: &Realm) -> Result<File> {
|
||||
let path = self.base_dir.join(realm.name());
|
||||
File::create(&path)
|
||||
.map_err(context!("failed to open sandbox status file {}", path.display()))
|
||||
}
|
||||
}
|
@@ -21,6 +21,8 @@ mod realm;
|
||||
pub mod terminal;
|
||||
mod system;
|
||||
|
||||
pub mod flatpak;
|
||||
|
||||
pub use crate::config::OsRelease;
|
||||
pub use crate::blockdev::BlockDev;
|
||||
pub use crate::cmdline::CommandLine;
|
||||
@@ -32,10 +34,12 @@ pub use crate::realmfs::{RealmFS,Mountpoint};
|
||||
pub use crate::keyring::{KeyRing,KernelKey};
|
||||
pub use crate::exec::{Exec,FileRange};
|
||||
pub use crate::realmfs::resizer::ResizeSize;
|
||||
pub use crate::realmfs::update::RealmFSUpdate;
|
||||
pub use crate::realm::overlay::RealmOverlay;
|
||||
pub use crate::realm::realm::Realm;
|
||||
pub use crate::realm::pidmapper::PidLookupResult;
|
||||
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::realms::Realms;
|
||||
pub use crate::realm::manager::RealmManager;
|
||||
|
@@ -62,6 +62,11 @@ impl Logger {
|
||||
logger.level = level;
|
||||
}
|
||||
|
||||
pub fn is_log_level(level: LogLevel) -> bool {
|
||||
let logger = LOGGER.lock().unwrap();
|
||||
logger.level >= level
|
||||
}
|
||||
|
||||
pub fn set_log_output(output: Box<dyn LogOutput>) {
|
||||
let mut logger = LOGGER.lock().unwrap();
|
||||
logger.output = output;
|
||||
|
@@ -77,6 +77,9 @@ pub struct RealmConfig {
|
||||
#[serde(rename="use-fuse")]
|
||||
pub use_fuse: Option<bool>,
|
||||
|
||||
#[serde(rename="use-flatpak")]
|
||||
pub use_flatpak: Option<bool>,
|
||||
|
||||
#[serde(rename="use-gpu")]
|
||||
pub use_gpu: Option<bool>,
|
||||
|
||||
@@ -201,6 +204,7 @@ impl RealmConfig {
|
||||
wayland_socket: Some("wayland-0".to_string()),
|
||||
use_kvm: Some(false),
|
||||
use_fuse: Some(false),
|
||||
use_flatpak: Some(false),
|
||||
use_gpu: Some(false),
|
||||
use_gpu_card0: Some(false),
|
||||
use_network: Some(true),
|
||||
@@ -233,6 +237,7 @@ impl RealmConfig {
|
||||
wayland_socket: None,
|
||||
use_kvm: None,
|
||||
use_fuse: None,
|
||||
use_flatpak: None,
|
||||
use_gpu: None,
|
||||
use_gpu_card0: None,
|
||||
use_network: None,
|
||||
@@ -261,12 +266,44 @@ impl RealmConfig {
|
||||
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
|
||||
///
|
||||
pub fn fuse(&self) -> bool {
|
||||
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.
|
||||
///
|
||||
/// This enables hardware graphics acceleration in realm.
|
||||
@@ -274,12 +311,28 @@ impl RealmConfig {
|
||||
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
|
||||
/// added to realm.
|
||||
pub fn gpu_card0(&self) -> bool {
|
||||
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.
|
||||
///
|
||||
/// This directory is shared between all running realms and is an easy way to move files
|
||||
@@ -288,12 +341,28 @@ impl RealmConfig {
|
||||
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
|
||||
///
|
||||
pub fn media_dir(&self) -> bool {
|
||||
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.
|
||||
///
|
||||
/// The ephemeral home directory is set up with the following steps:
|
||||
@@ -308,6 +377,14 @@ impl RealmConfig {
|
||||
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
|
||||
/// home directory when ephemeral-home is enabled.
|
||||
pub fn ephemeral_persistent_dirs(&self) -> Vec<String> {
|
||||
@@ -330,6 +407,14 @@ impl RealmConfig {
|
||||
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
|
||||
/// directory /tmp/.X11-unix
|
||||
pub fn x11(&self) -> bool {
|
||||
@@ -338,12 +423,28 @@ 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
|
||||
/// wayland socket /run/user/1000/wayland-0
|
||||
pub fn wayland(&self) -> bool {
|
||||
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`
|
||||
/// defaults to wayland-0, will appear in the realm as wayland-0 regardless of value
|
||||
pub fn wayland_socket(&self) -> &str {
|
||||
@@ -356,12 +457,19 @@ impl RealmConfig {
|
||||
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`.
|
||||
pub fn network_zone(&self) -> &str {
|
||||
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
|
||||
/// octet of the network address for this realm will be set to the provided value.
|
||||
pub fn reserved_ip(&self) -> Option<u8> {
|
||||
@@ -422,7 +530,6 @@ impl RealmConfig {
|
||||
self.overlay = overlay.to_str_value().map(String::from)
|
||||
}
|
||||
|
||||
|
||||
pub fn netns(&self) -> Option<&str> {
|
||||
self.str_value(|c| c.netns.as_ref())
|
||||
}
|
||||
|
@@ -12,7 +12,9 @@ use dbus::{Connection, BusType, ConnectionItem, Message, Path};
|
||||
use inotify::{Inotify, WatchMask, WatchDescriptor, Event};
|
||||
|
||||
pub enum RealmEvent {
|
||||
Starting(Realm),
|
||||
Started(Realm),
|
||||
Stopping(Realm),
|
||||
Stopped(Realm),
|
||||
New(Realm),
|
||||
Removed(Realm),
|
||||
@@ -22,7 +24,9 @@ pub enum RealmEvent {
|
||||
impl Display for RealmEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
RealmEvent::Starting(ref realm) => write!(f, "RealmStarting({})", realm.name()),
|
||||
RealmEvent::Started(ref realm) => write!(f, "RealmStarted({})", realm.name()),
|
||||
RealmEvent::Stopping(ref realm) => write!(f, "RealmStopping({})", realm.name()),
|
||||
RealmEvent::Stopped(ref realm) => write!(f, "RealmStopped({})", realm.name()),
|
||||
RealmEvent::New(ref realm) => write!(f, "RealmNew({})", realm.name()),
|
||||
RealmEvent::Removed(ref realm) => write!(f, "RealmRemoved({})", realm.name()),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use std::fmt::{self,Write};
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{Realm, Result, util, realm::network::NetworkConfig};
|
||||
@@ -18,6 +18,23 @@ $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 = "\
|
||||
[Unit]
|
||||
Description=Application Image $REALM_NAME instance
|
||||
@@ -60,7 +77,7 @@ impl <'a> RealmLauncher <'a> {
|
||||
if config.kvm() {
|
||||
self.add_device("/dev/kvm");
|
||||
}
|
||||
if config.fuse() {
|
||||
if config.fuse() || config.flatpak() {
|
||||
self.add_device("/dev/fuse");
|
||||
}
|
||||
|
||||
@@ -153,6 +170,10 @@ impl <'a> RealmLauncher <'a> {
|
||||
writeln!(s, "BindReadOnly=/run/user/1000/{}:/run/user/host/wayland-0", config.wayland_socket())?;
|
||||
}
|
||||
|
||||
if config.flatpak() {
|
||||
writeln!(s, "BindReadOnly={}:/var/lib/flatpak", self.realm.base_path_file("flatpak").display())?;
|
||||
}
|
||||
|
||||
for bind in config.extra_bindmounts() {
|
||||
if Self::is_valid_bind_item(bind) {
|
||||
writeln!(s, "Bind={}", bind)?;
|
||||
@@ -231,9 +252,4 @@ impl <'a> RealmLauncher <'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fmt::Error> for crate::Error {
|
||||
fn from(e: fmt::Error) -> Self {
|
||||
format_err!("Error formatting string: {}", e).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
200
libcitadel/src/realm/liveconfig.rs
Normal file
200
libcitadel/src/realm/liveconfig.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
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(())
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use posix_acl::{ACL_EXECUTE, ACL_READ, PosixACL, Qualifier};
|
||||
|
||||
use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util};
|
||||
use crate::flatpak::GnomeSoftwareLauncher;
|
||||
use crate::flatpak::setup::FlatpakSetup;
|
||||
use crate::realm::pidmapper::{PidLookupResult, PidMapper};
|
||||
use crate::realmfs::realmfs_set::RealmFSSet;
|
||||
|
||||
@@ -21,6 +23,7 @@ struct Inner {
|
||||
events: RealmEventListener,
|
||||
realms: Realms,
|
||||
realmfs_set: RealmFSSet,
|
||||
pid_mapper: PidMapper,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
@@ -28,7 +31,8 @@ impl Inner {
|
||||
let events = RealmEventListener::new();
|
||||
let realms = Realms::load()?;
|
||||
let realmfs_set = RealmFSSet::load()?;
|
||||
Ok(Inner { events, realms, realmfs_set })
|
||||
let pid_mapper = PidMapper::new()?;
|
||||
Ok(Inner { events, realms, realmfs_set, pid_mapper })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +194,13 @@ impl RealmManager {
|
||||
return Ok(());
|
||||
}
|
||||
info!("Starting realm {}", realm.name());
|
||||
self._start_realm(realm, &mut HashSet::new())?;
|
||||
self.inner().events.send_event(RealmEvent::Starting(realm.clone()));
|
||||
if let Err(err) = self._start_realm(realm, &mut HashSet::new()) {
|
||||
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
self.inner().events.send_event(RealmEvent::Started(realm.clone()));
|
||||
|
||||
if !Realms::is_some_realm_current() {
|
||||
self.inner_mut().realms.set_realm_current(realm)
|
||||
@@ -230,6 +240,10 @@ impl RealmManager {
|
||||
self.ensure_run_media_directory()?;
|
||||
}
|
||||
|
||||
if realm.config().flatpak() {
|
||||
FlatpakSetup::new(realm).setup()?;
|
||||
}
|
||||
|
||||
self.systemd.start_realm(realm, &rootfs)?;
|
||||
|
||||
self.create_realm_namefile(realm)?;
|
||||
@@ -268,6 +282,15 @@ impl RealmManager {
|
||||
self.run_in_realm(realm, &["/usr/bin/ln", "-s", "/run/user/host/wayland-0", "/run/user/1000/wayland-0"], false)
|
||||
}
|
||||
|
||||
fn stop_gnome_software_sandbox(&self, realm: &Realm) -> Result<()> {
|
||||
let launcher = GnomeSoftwareLauncher::new(realm.clone())?;
|
||||
if launcher.is_running() {
|
||||
info!("Stopping GNOME Software sandbox for {}", realm.name());
|
||||
launcher.quit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_realm(&self, realm: &Realm) -> Result<()> {
|
||||
if !realm.is_active() {
|
||||
info!("ignoring stop request on realm '{}' which is not running", realm.name());
|
||||
@@ -275,10 +298,21 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
info!("Stopping realm {}", realm.name());
|
||||
self.inner().events.send_event(RealmEvent::Stopping(realm.clone()));
|
||||
|
||||
if realm.config().flatpak() {
|
||||
if let Err(err) = self.stop_gnome_software_sandbox(realm) {
|
||||
warn!("Error stopping GNOME Software sandbox: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
realm.set_active(false);
|
||||
self.systemd.stop_realm(realm)?;
|
||||
if let Err(err) = self.systemd.stop_realm(realm) {
|
||||
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
realm.cleanup_rootfs();
|
||||
self.inner().events.send_event(RealmEvent::Stopped(realm.clone()));
|
||||
|
||||
if realm.is_current() {
|
||||
self.choose_some_current_realm();
|
||||
@@ -335,8 +369,8 @@ impl RealmManager {
|
||||
}
|
||||
|
||||
pub fn realm_by_pid(&self, pid: u32) -> PidLookupResult {
|
||||
let mapper = PidMapper::new(self.active_realms(false));
|
||||
mapper.lookup_pid(pid as libc::pid_t)
|
||||
let realms = self.realm_list();
|
||||
self.inner_mut().pid_mapper.lookup_pid(pid as libc::pid_t, realms)
|
||||
}
|
||||
|
||||
pub fn rescan_realms(&self) -> Result<(Vec<Realm>,Vec<Realm>)> {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
|
||||
pub(crate) mod overlay;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod liveconfig;
|
||||
pub(crate) mod realms;
|
||||
pub(crate) mod manager;
|
||||
#[allow(clippy::module_inception)]
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use std::ffi::OsStr;
|
||||
use procfs::process::Process;
|
||||
use crate::Realm;
|
||||
use crate::{Result, Realm};
|
||||
use crate::flatpak::{SANDBOX_STATUS_FILE_DIRECTORY, SandboxStatus};
|
||||
|
||||
pub enum PidLookupResult {
|
||||
Unknown,
|
||||
@@ -9,14 +10,15 @@ pub enum PidLookupResult {
|
||||
}
|
||||
|
||||
pub struct PidMapper {
|
||||
active_realms: Vec<Realm>,
|
||||
sandbox_status: SandboxStatus,
|
||||
my_pid_ns_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl PidMapper {
|
||||
pub fn new(active_realms: Vec<Realm>) -> Self {
|
||||
pub fn new() -> Result<Self> {
|
||||
let sandbox_status = SandboxStatus::load(SANDBOX_STATUS_FILE_DIRECTORY)?;
|
||||
let my_pid_ns_id = Self::self_pid_namespace_id();
|
||||
PidMapper { active_realms, my_pid_ns_id }
|
||||
Ok(PidMapper { sandbox_status, my_pid_ns_id })
|
||||
}
|
||||
|
||||
fn read_process(pid: libc::pid_t) -> Option<Process> {
|
||||
@@ -72,7 +74,30 @@ impl PidMapper {
|
||||
Self::read_process(ppid)
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&self, pid: libc::pid_t) -> PidLookupResult {
|
||||
fn refresh_sandbox_status(&mut self) -> Result<()> {
|
||||
if self.sandbox_status.need_reload() {
|
||||
self.sandbox_status.reload()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn search_sandbox_realms(&mut self, pid_ns: u64, realms: &[Realm]) -> Option<Realm> {
|
||||
if let Err(err) = self.refresh_sandbox_status() {
|
||||
warn!("error reloading sandbox status directory: {}", err);
|
||||
return None;
|
||||
}
|
||||
|
||||
for r in realms {
|
||||
if let Some(status) = self.sandbox_status.realm_status(r) {
|
||||
if status.pid_namespace() == pid_ns {
|
||||
return Some(r.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn lookup_pid(&mut self, pid: libc::pid_t, realms: Vec<Realm>) -> PidLookupResult {
|
||||
const MAX_PARENT_SEARCH: i32 = 8;
|
||||
let mut n = 0;
|
||||
|
||||
@@ -92,13 +117,17 @@ impl PidMapper {
|
||||
return PidLookupResult::Citadel;
|
||||
}
|
||||
|
||||
if let Some(realm) = self.active_realms.iter()
|
||||
.find(|r| r.has_pid_ns(pid_ns_id))
|
||||
if let Some(realm) = realms.iter()
|
||||
.find(|r| r.is_active() && r.has_pid_ns(pid_ns_id))
|
||||
.cloned()
|
||||
{
|
||||
return PidLookupResult::Realm(realm)
|
||||
}
|
||||
|
||||
if let Some(r) = self.search_sandbox_realms(pid_ns_id, &realms) {
|
||||
return PidLookupResult::Realm(r)
|
||||
}
|
||||
|
||||
proc = match Self::parent_process(proc) {
|
||||
Some(proc) => proc,
|
||||
None => return PidLookupResult::Unknown,
|
||||
@@ -108,5 +137,4 @@ impl PidMapper {
|
||||
}
|
||||
PidLookupResult::Unknown
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -169,6 +169,20 @@ impl Realm {
|
||||
self.inner.write().unwrap()
|
||||
}
|
||||
|
||||
|
||||
pub fn start(&self) -> Result<()> {
|
||||
warn!("Realm({})::start()", self.name());
|
||||
self.manager().start_realm(self)
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
self.manager().stop_realm(self)
|
||||
}
|
||||
|
||||
pub fn set_current(&self) -> Result<()> {
|
||||
self.manager().set_current_realm(self)
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.inner_mut().is_active()
|
||||
}
|
||||
@@ -279,6 +293,9 @@ impl Realm {
|
||||
symlink::write(&rootfs, self.rootfs_symlink(), false)?;
|
||||
symlink::write(mountpoint.path(), self.realmfs_mountpoint_symlink(), false)?;
|
||||
symlink::write(self.base_path().join("home"), self.run_path().join("home"), false)?;
|
||||
if self.config().flatpak() {
|
||||
symlink::write(self.base_path().join("flatpak"), self.run_path().join("flatpak"), false)?;
|
||||
}
|
||||
|
||||
Ok(rootfs)
|
||||
}
|
||||
@@ -300,6 +317,9 @@ impl Realm {
|
||||
Self::remove_symlink(self.realmfs_mountpoint_symlink());
|
||||
Self::remove_symlink(self.rootfs_symlink());
|
||||
Self::remove_symlink(self.run_path().join("home"));
|
||||
if self.config().flatpak() {
|
||||
Self::remove_symlink(self.run_path().join("flatpak"));
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_dir(self.run_path()) {
|
||||
warn!("failed to remove run directory {}: {}", self.run_path().display(), e);
|
||||
|
@@ -244,7 +244,7 @@ impl Realms {
|
||||
pub fn delete_realm(&mut self, name: &str, save_home: bool) -> Result<()> {
|
||||
let _lock = Self::realmslock()?;
|
||||
|
||||
let realm = match self.realms.take(name) {
|
||||
let realm = match self.by_name(name) {
|
||||
Some(realm) => realm,
|
||||
None => bail!("Cannot remove realm '{}' because it doesn't seem to exist", name),
|
||||
};
|
||||
|
@@ -31,6 +31,28 @@ impl Systemd {
|
||||
if realm.config().ephemeral_home() {
|
||||
self.setup_ephemeral_home(realm)?;
|
||||
}
|
||||
if realm.config().flatpak() {
|
||||
self.setup_flatpak_workaround(realm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// What even is this??
|
||||
//
|
||||
// Good question.
|
||||
//
|
||||
// https://bugzilla.redhat.com/show_bug.cgi?id=2210335#c10
|
||||
//
|
||||
fn setup_flatpak_workaround(&self, realm: &Realm) -> Result<()> {
|
||||
let commands = &[
|
||||
vec!["/usr/bin/mount", "-m", "-t","proc", "proc", "/run/flatpak-workaround/proc"],
|
||||
vec!["/usr/bin/chmod", "700", "/run/flatpak-workaround"],
|
||||
];
|
||||
|
||||
for cmd in commands {
|
||||
Self::machinectl_shell(realm, cmd, "root", false, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -91,7 +113,7 @@ impl Systemd {
|
||||
.arg(name)
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.map_err(context!("failed to execute {}", MACHINECTL_PATH))?;
|
||||
.map_err(context!("failed to execute {}", SYSTEMCTL_PATH))?;
|
||||
Ok(ok)
|
||||
}
|
||||
|
||||
|
174
libcitadel/src/realmfs/launcher.rs
Normal file
174
libcitadel/src/realmfs/launcher.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
pub(crate) mod resizer;
|
||||
mod mountpoint;
|
||||
mod update;
|
||||
pub(crate) mod update;
|
||||
pub(crate) mod realmfs_set;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod realmfs;
|
||||
mod launcher;
|
||||
|
||||
pub use self::realmfs::RealmFS;
|
||||
pub use self::mountpoint::Mountpoint;
|
||||
|
@@ -7,7 +7,7 @@ use std::sync::{Arc, Weak, RwLock};
|
||||
|
||||
use crate::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize};
|
||||
use crate::realmfs::resizer::Superblock;
|
||||
use crate::realmfs::update::Update;
|
||||
use crate::realmfs::update::RealmFSUpdate;
|
||||
use super::mountpoint::Mountpoint;
|
||||
|
||||
// Maximum length of a RealmFS name
|
||||
@@ -266,8 +266,13 @@ impl RealmFS {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self) -> Result<RealmFSUpdate> {
|
||||
let update = RealmFSUpdate::create(self)?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> {
|
||||
let mut update = Update::create(self)?;
|
||||
let mut update = RealmFSUpdate::create(self)?;
|
||||
update.run_interactive_update(scheme)
|
||||
}
|
||||
|
||||
@@ -388,14 +393,14 @@ impl RealmFS {
|
||||
|
||||
pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> {
|
||||
info!("Resizing to {} blocks", size.nblocks());
|
||||
let mut update = Update::create(self)?;
|
||||
let mut update = RealmFSUpdate::create(self)?;
|
||||
update.grow_to(size);
|
||||
update.resize()
|
||||
}
|
||||
|
||||
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
|
||||
info!("Resizing to an increase of {} blocks", size.nblocks());
|
||||
let mut update = Update::create(self)?;
|
||||
let mut update = RealmFSUpdate::create(self)?;
|
||||
update.grow_by(size);
|
||||
update.resize()
|
||||
}
|
||||
|
@@ -23,8 +23,8 @@ impl ResizeSize {
|
||||
|
||||
pub fn gigs(n: usize) -> Self {
|
||||
ResizeSize(BLOCKS_PER_GIG * n)
|
||||
|
||||
}
|
||||
|
||||
pub fn megs(n: usize) -> Self {
|
||||
ResizeSize(BLOCKS_PER_MEG * n)
|
||||
}
|
||||
@@ -45,8 +45,8 @@ impl ResizeSize {
|
||||
self.0 / BLOCKS_PER_MEG
|
||||
}
|
||||
|
||||
/// If the RealmFS needs to be resized to a larger size, returns the
|
||||
/// recommended size.
|
||||
/// If the RealmFS has less than `AUTO_RESIZE_MINIMUM_FREE` blocks free then choose a new
|
||||
/// size to resize the filesystem to and return it. Otherwise, return `None`
|
||||
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
|
||||
let sb = match Superblock::load(realmfs.path(), 4096) {
|
||||
Ok(sb) => sb,
|
||||
@@ -56,22 +56,37 @@ impl ResizeSize {
|
||||
},
|
||||
};
|
||||
|
||||
sb.free_block_count();
|
||||
let free_blocks = sb.free_block_count() as usize;
|
||||
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
|
||||
let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
|
||||
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
|
||||
let mask = grow_size - 1;
|
||||
let grow_blocks = (free_blocks + mask) & !mask;
|
||||
Some(ResizeSize::blocks(grow_blocks))
|
||||
if free_blocks >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let metainfo_nblocks = realmfs.metainfo().nblocks();
|
||||
|
||||
if metainfo_nblocks >= AUTO_RESIZE_INCREASE_SIZE.nblocks() {
|
||||
return Some(ResizeSize::blocks(metainfo_nblocks + AUTO_RESIZE_INCREASE_SIZE.nblocks()))
|
||||
}
|
||||
|
||||
// If current size is under 4GB (AUTO_RESIZE_INCREASE_SIZE) and raising size to 4GB will create more than the
|
||||
// minimum free space (1GB) then just do that.
|
||||
if free_blocks + (AUTO_RESIZE_INCREASE_SIZE.nblocks() - metainfo_nblocks) >= AUTO_RESIZE_MINIMUM_FREE.nblocks() {
|
||||
Some(AUTO_RESIZE_INCREASE_SIZE)
|
||||
} else {
|
||||
None
|
||||
// Otherwise for original size under 4GB, since raising to 4GB is not enough,
|
||||
// raise size to 8GB
|
||||
Some(ResizeSize::blocks(AUTO_RESIZE_INCREASE_SIZE.nblocks() * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERBLOCK_SIZE: usize = 1024;
|
||||
|
||||
/// An EXT4 superblock structure.
|
||||
///
|
||||
/// A class for reading the first superblock from an EXT4 filesystem
|
||||
/// and parsing the Free Block Count field. No other fields are parsed
|
||||
/// since this is the only information needed for the resize operation.
|
||||
///
|
||||
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
|
||||
|
||||
impl Superblock {
|
||||
|
@@ -11,6 +11,8 @@ use crate::util::is_euid_root;
|
||||
use crate::terminal::TerminalRestorer;
|
||||
use crate::verity::Verity;
|
||||
|
||||
use super::launcher::RealmFSUpdateLauncher;
|
||||
|
||||
const BLOCK_SIZE: usize = 4096;
|
||||
|
||||
// The maximum number of backup copies the rotate() method will create
|
||||
@@ -21,36 +23,41 @@ const RESIZE2FS: &str = "resize2fs";
|
||||
|
||||
/// Manages the process of updating or resizing a `RealmFS` image file.
|
||||
///
|
||||
pub struct Update<'a> {
|
||||
realmfs: &'a RealmFS, // RealmFS being updated
|
||||
pub struct RealmFSUpdate {
|
||||
realmfs: RealmFS, // RealmFS being updated
|
||||
name: String, // name for nspawn instance
|
||||
target: PathBuf, // Path to the update copy of realmfs image
|
||||
mountpath: PathBuf, // Path at which update copy is mounted
|
||||
container: Option<RealmFSUpdateLauncher>,
|
||||
_lock: FileLock,
|
||||
resize: Option<ResizeSize>, // If the image needs to be resized, the resize size is stored here
|
||||
network_allocated: bool,
|
||||
}
|
||||
|
||||
impl <'a> Update<'a> {
|
||||
fn new(realmfs: &'a RealmFS, lock: FileLock) -> Self {
|
||||
impl RealmFSUpdate {
|
||||
fn new(realmfs: RealmFS, lock: FileLock) -> Self {
|
||||
|
||||
let metainfo = realmfs.metainfo();
|
||||
let tag = metainfo.verity_tag();
|
||||
let mountpath = Path::new(RealmFS::RUN_DIRECTORY)
|
||||
.join(format!("realmfs-{}-{}.update", realmfs.name(), tag));
|
||||
|
||||
Update {
|
||||
let name = format!("{}-{}-update", realmfs.name(), tag);
|
||||
let resize = ResizeSize::auto_resize_size(&realmfs);
|
||||
let target = realmfs.path().with_extension("update");
|
||||
RealmFSUpdate {
|
||||
realmfs,
|
||||
name: format!("{}-{}-update", realmfs.name(), tag),
|
||||
target: realmfs.path().with_extension("update"),
|
||||
name,
|
||||
target,
|
||||
mountpath,
|
||||
_lock: lock,
|
||||
resize: ResizeSize::auto_resize_size(realmfs),
|
||||
container: None,
|
||||
resize,
|
||||
network_allocated: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(realmfs: &'a RealmFS) -> Result<Self> {
|
||||
pub fn create(realmfs: &RealmFS) -> Result<Self> {
|
||||
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()))?;
|
||||
|
||||
@@ -58,10 +65,10 @@ impl <'a> Update<'a> {
|
||||
bail!("Cannot seal realmfs image, no sealing keys available");
|
||||
}
|
||||
|
||||
Ok(Update::new(realmfs, lock))
|
||||
Ok(RealmFSUpdate::new(realmfs.clone(), lock))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
@@ -74,10 +81,11 @@ impl <'a> Update<'a> {
|
||||
info!("Update file {} already exists, removing it", self.target.display());
|
||||
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.truncate_verity()?;
|
||||
self.resize_image_file()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -104,6 +112,10 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
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| {
|
||||
if self.resize.is_some() {
|
||||
self.resize_device(loopdev)?;
|
||||
@@ -115,9 +127,8 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
// Return size of image file in blocks based on metainfo `nblocks` field.
|
||||
// Include header block in count so add one block
|
||||
fn metainfo_nblock_size(&self) -> usize {
|
||||
self.realmfs.metainfo().nblocks() + 1
|
||||
self.realmfs.metainfo().nblocks()
|
||||
}
|
||||
|
||||
fn unmount_update_image(&mut self) {
|
||||
@@ -159,7 +170,8 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
|
||||
fn set_target_len(&self, nblocks: usize) -> Result<()> {
|
||||
let len = (nblocks * BLOCK_SIZE) as u64;
|
||||
// add one block for header block
|
||||
let len = ((nblocks + 1) * BLOCK_SIZE) as u64;
|
||||
let f = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&self.target)
|
||||
@@ -171,12 +183,13 @@ impl <'a> Update<'a> {
|
||||
|
||||
// Remove dm-verity hash tree from update copy of image file.
|
||||
fn truncate_verity(&self) -> Result<()> {
|
||||
info!("Truncating dm-verity hash tree from {}", self.target().display());
|
||||
let file_nblocks = self.realmfs.file_nblocks()?;
|
||||
let metainfo_nblocks = self.metainfo_nblock_size();
|
||||
|
||||
if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
|
||||
self.set_target_len(metainfo_nblocks)?;
|
||||
} else if file_nblocks > metainfo_nblocks {
|
||||
} else if file_nblocks > (metainfo_nblocks + 1) {
|
||||
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
|
||||
}
|
||||
Ok(())
|
||||
@@ -185,10 +198,12 @@ impl <'a> Update<'a> {
|
||||
// If resize was requested, adjust size of update copy of image file.
|
||||
fn resize_image_file(&self) -> Result<()> {
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks() + 1,
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
info!("Resizing target file to {} blocks", nblocks);
|
||||
|
||||
if nblocks < self.metainfo_nblock_size() {
|
||||
bail!("Cannot shrink image")
|
||||
}
|
||||
@@ -199,7 +214,19 @@ impl <'a> Update<'a> {
|
||||
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) {
|
||||
// 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() {
|
||||
self.unmount_update_image();
|
||||
}
|
||||
@@ -224,7 +251,7 @@ impl <'a> Update<'a> {
|
||||
fn seal(&mut self) -> Result<()> {
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => self.metainfo_nblock_size() - 1,
|
||||
None => self.metainfo_nblock_size(),
|
||||
};
|
||||
|
||||
let salt = hex::encode(randombytes(32));
|
||||
@@ -232,20 +259,11 @@ impl <'a> Update<'a> {
|
||||
.map_err(context!("failed to create verity context for realmfs update image {:?}", self.target()))?;
|
||||
let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)
|
||||
.map_err(context!("failed to generate dm-verity hashtree for realmfs update image {:?}", self.target()))?;
|
||||
// XXX passes metainfo for nblocks
|
||||
//let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?;
|
||||
|
||||
let root_hash = output.root_hash()
|
||||
.ok_or_else(|| format_err!("no root hash returned from verity format operation"))?;
|
||||
info!("root hash is {}", output.root_hash().unwrap());
|
||||
|
||||
/*
|
||||
let nblocks = match self.resize {
|
||||
Some(rs) => rs.nblocks(),
|
||||
None => self.metainfo_nblock_size() - 1,
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
info!("Signing new image with user realmfs keys");
|
||||
let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash);
|
||||
let keys = self.realmfs.sealing_keys().expect("No sealing keys");
|
||||
@@ -276,6 +294,21 @@ impl <'a> Update<'a> {
|
||||
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<()> {
|
||||
if !is_euid_root() {
|
||||
bail!("RealmFS updates must be run as root");
|
||||
@@ -307,6 +340,21 @@ impl <'a> Update<'a> {
|
||||
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<()> {
|
||||
|
||||
let mut alloc = BridgeAllocator::default_bridge()?;
|
||||
@@ -356,7 +404,7 @@ impl <'a> Update<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a> Drop for Update<'a> {
|
||||
impl Drop for RealmFSUpdate {
|
||||
fn drop(&mut self) {
|
||||
self.cleanup();
|
||||
}
|
||||
|
@@ -5,5 +5,5 @@ mod uname;
|
||||
|
||||
pub use self::uname::UtsName;
|
||||
pub use self::loopdev::LoopDevice;
|
||||
pub use self::mounts::{Mounts,MountLine};
|
||||
pub use self::mounts::Mounts;
|
||||
pub use self::lock::FileLock;
|
||||
|
@@ -7,6 +7,7 @@ use std::env;
|
||||
use std::fs::{self, File, DirEntry};
|
||||
use std::ffi::CString;
|
||||
use std::io::{self, Seek, Read, BufReader, SeekFrom};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
@@ -217,7 +218,8 @@ where
|
||||
///
|
||||
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
if path.exists() {
|
||||
let is_symlink = fs::symlink_metadata(path).is_ok();
|
||||
if is_symlink || path.exists() {
|
||||
fs::remove_file(path)
|
||||
.map_err(context!("failed to remove file {:?}", path))?;
|
||||
}
|
||||
@@ -368,9 +370,38 @@ pub fn touch_mtime(path: &Path) -> Result<()> {
|
||||
|
||||
utimes(path, meta.atime(),mtime)?;
|
||||
Ok(())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn nsenter_netns(netns: &str) -> Result<()> {
|
||||
let mut path = PathBuf::from("/run/netns");
|
||||
path.push(netns);
|
||||
if !path.exists() {
|
||||
bail!("Network namespace '{}' does not exist", netns);
|
||||
}
|
||||
let f = File::open(&path)
|
||||
.map_err(context!("error opening netns file {}", path.display()))?;
|
||||
|
||||
let fd = f.as_raw_fd();
|
||||
|
||||
unsafe {
|
||||
if libc::setns(fd, libc::CLONE_NEWNET) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to setns() into network namespace '{}': {}", netns, err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn drop_privileges(uid: u32, gid: u32) -> Result<()> {
|
||||
unsafe {
|
||||
if libc::setgid(gid) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to call setgid({}): {}", gid, err);
|
||||
|
||||
} else if libc::setuid(uid) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
bail!("failed to call setuid({}): {}", uid, err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
[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"] }
|
@@ -1,77 +0,0 @@
|
||||
<?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>
|
@@ -1,216 +0,0 @@
|
||||
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: >k::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
|
||||
}
|
||||
}
|
@@ -1,153 +0,0 @@
|
||||
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 {}
|
@@ -1,31 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
<?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>
|
||||
|
@@ -1,26 +0,0 @@
|
||||
<?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>
|
@@ -1,203 +0,0 @@
|
||||
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(¤t));
|
||||
}
|
||||
|
||||
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 {}
|
@@ -1,78 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,68 +0,0 @@
|
||||
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 {}
|
@@ -1,384 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,126 +0,0 @@
|
||||
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())
|
||||
}
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
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: >k::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)
|
||||
}
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
#[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: >k::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: >k::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: >k::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);
|
||||
}
|
||||
}
|
@@ -1,134 +0,0 @@
|
||||
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 {}
|
@@ -1,44 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
<?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>
|
@@ -1,76 +0,0 @@
|
||||
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: >k::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: >k::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: >k::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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,154 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
}
|
@@ -6,8 +6,10 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
zbus = "=2.0.0-beta.5"
|
||||
zvariant = "2.7.0"
|
||||
async-io = "2.3.2"
|
||||
blocking = "1.6.1"
|
||||
event-listener = "5.3.1"
|
||||
zbus = "5.7.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1.8"
|
||||
serde_repr = "0.1.20"
|
||||
|
||||
|
@@ -1,15 +1,16 @@
|
||||
use zbus::{Connection, ObjectServer};
|
||||
use async_io::block_on;
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH, realm_status};
|
||||
use libcitadel::{RealmEvent, Realm};
|
||||
|
||||
pub struct EventHandler {
|
||||
connection: Connection,
|
||||
realms_server: RealmsManagerServer,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new(connection: Connection, realms_server: RealmsManagerServer) -> Self {
|
||||
EventHandler { connection, realms_server }
|
||||
pub fn new(connection: Connection) -> Self {
|
||||
EventHandler { connection }
|
||||
}
|
||||
|
||||
pub fn handle_event(&self, ev: &RealmEvent) {
|
||||
@@ -25,44 +26,49 @@ impl EventHandler {
|
||||
RealmEvent::New(realm) => self.on_new(realm),
|
||||
RealmEvent::Removed(realm) => self.on_removed(realm),
|
||||
RealmEvent::Current(realm) => self.on_current(realm.as_ref()),
|
||||
RealmEvent::Starting(_) => Ok(()),
|
||||
RealmEvent::Stopping(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_server<F>(&self, func: F) -> zbus::Result<()>
|
||||
fn with_signal_emitter<F>(&self, func: F) -> zbus::Result<()>
|
||||
where
|
||||
F: Fn(&RealmsManagerServer) -> zbus::Result<()>,
|
||||
F: Fn(&SignalEmitter) -> zbus::Result<()>,
|
||||
{
|
||||
let mut object_server = ObjectServer::new(&self.connection);
|
||||
object_server.at(REALMS_SERVER_OBJECT_PATH, self.realms_server.clone())?;
|
||||
object_server.with(REALMS_SERVER_OBJECT_PATH, |iface: &RealmsManagerServer| func(iface))
|
||||
let object_server = self.connection.object_server();
|
||||
let iface = object_server.interface::<_, RealmsManagerServer>(REALMS_SERVER_OBJECT_PATH)?;
|
||||
|
||||
let emitter = iface.signal_emitter();
|
||||
func(emitter)
|
||||
}
|
||||
|
||||
fn on_started(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let pid_ns = realm.pid_ns().unwrap_or(0);
|
||||
let status = realm_status(realm);
|
||||
self.with_server(|server| server.realm_started(realm.name(), pid_ns, status))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_started(ctx, realm.name(), pid_ns, status)))
|
||||
}
|
||||
|
||||
fn on_stopped(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let status = realm_status(realm);
|
||||
self.with_server(|server| server.realm_stopped(realm.name(), status))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_stopped(ctx, realm.name(), status)))
|
||||
}
|
||||
|
||||
fn on_new(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
let status = realm_status(realm);
|
||||
let description = realm.notes().unwrap_or(String::new());
|
||||
self.with_server(|server| server.realm_new(realm.name(), &description, status))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_new(ctx, realm.name(), &description, status)))
|
||||
}
|
||||
|
||||
fn on_removed(&self, realm: &Realm) -> zbus::Result<()> {
|
||||
self.with_server(|server| server.realm_removed(realm.name()))
|
||||
self.with_signal_emitter(|ctx| block_on(RealmsManagerServer::realm_removed(ctx, realm.name())))
|
||||
}
|
||||
|
||||
fn on_current(&self, realm: Option<&Realm>) -> zbus::Result<()> {
|
||||
self.with_server(|server| {
|
||||
self.with_signal_emitter(|ctx| {
|
||||
match realm {
|
||||
Some(realm) => server.realm_current(realm.name(), realm_status(realm)),
|
||||
None => server.realm_current("", 0),
|
||||
Some(realm) => block_on(RealmsManagerServer::realm_current(ctx, realm.name(), realm_status(realm))),
|
||||
None => block_on(RealmsManagerServer::realm_current(ctx, "", 0)),
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -1,14 +1,18 @@
|
||||
#[macro_use] extern crate libcitadel;
|
||||
|
||||
use zbus::{Connection, fdo};
|
||||
|
||||
use libcitadel::{Logger, LogLevel, Result};
|
||||
|
||||
use crate::realms_manager::RealmsManagerServer;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use event_listener::{Event, Listener};
|
||||
use zbus::blocking::Connection;
|
||||
use zbus::fdo::ObjectManager;
|
||||
use libcitadel::{Logger, LogLevel, Result, RealmManager};
|
||||
use crate::next::{RealmsManagerServer2, REALMS2_SERVER_OBJECT_PATH};
|
||||
use crate::realms_manager::{RealmsManagerServer, REALMS_SERVER_OBJECT_PATH};
|
||||
|
||||
mod realms_manager;
|
||||
mod events;
|
||||
|
||||
mod next;
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run_realm_manager() {
|
||||
@@ -16,24 +20,43 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_system_connection() -> zbus::Result<Connection> {
|
||||
let connection = zbus::Connection::new_system()?;
|
||||
fdo::DBusProxy::new(&connection)?.request_name("com.subgraph.realms", fdo::RequestNameFlags::AllowReplacement.into())?;
|
||||
Ok(connection)
|
||||
fn register_realms_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
|
||||
let server = RealmsManagerServer::load(&connection, realm_manager.clone(), quit_event.clone())
|
||||
.map_err(context!("Loading realms server"))?;
|
||||
connection.object_server().at(REALMS_SERVER_OBJECT_PATH, server).map_err(context!("registering realms manager object"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_realms2_manager_server(connection: &Connection, realm_manager: &Arc<RealmManager>, quit_event: &Arc<Event>) -> Result<()> {
|
||||
let server2 = RealmsManagerServer2::load(&connection, realm_manager.clone(), quit_event.clone())
|
||||
.map_err(context!("Loading realms2 server"))?;
|
||||
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, server2).map_err(context!("registering realms manager object"))?;
|
||||
connection.object_server().at(REALMS2_SERVER_OBJECT_PATH, ObjectManager).map_err(context!("registering ObjectManager"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_realm_manager() -> Result<()> {
|
||||
Logger::set_log_level(LogLevel::Verbose);
|
||||
|
||||
let connection = create_system_connection()
|
||||
.map_err(context!("ZBus Connection error"))?;
|
||||
let testing = env::args().skip(1).any(|s| s == "--testing");
|
||||
|
||||
let mut object_server = RealmsManagerServer::register(&connection)?;
|
||||
let connection = Connection::system()
|
||||
.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(())
|
||||
}
|
||||
|
227
realmsd/src/next/config.rs
Normal file
227
realmsd/src/next/config.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
150
realmsd/src/next/manager.rs
Normal file
150
realmsd/src/next/manager.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
8
realmsd/src/next/mod.rs
Normal file
8
realmsd/src/next/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
mod manager;
|
||||
mod config;
|
||||
mod realm;
|
||||
mod realmfs;
|
||||
|
||||
pub use manager::RealmsManagerServer2;
|
||||
pub const REALMS2_SERVER_OBJECT_PATH: &str = "/com/subgraph/Realms2";
|
392
realmsd/src/next/realm.rs
Normal file
392
realmsd/src/next/realm.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
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(())
|
||||
}
|
||||
}
|
251
realmsd/src/next/realmfs.rs
Normal file
251
realmsd/src/next/realmfs.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
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
|
||||
}
|
||||
}
|
@@ -1,11 +1,15 @@
|
||||
use libcitadel::{RealmManager, Realm, OverlayType, Result, PidLookupResult};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::Type;
|
||||
use std::sync::Arc;
|
||||
use zbus::{dbus_interface, ObjectServer,Connection};
|
||||
use zvariant::derive::Type;
|
||||
use zbus::blocking::Connection;
|
||||
use std::thread;
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize,Deserialize};
|
||||
use blocking::unblock;
|
||||
use event_listener::Event;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_repr::Serialize_repr;
|
||||
use zbus::interface;
|
||||
use crate::events::EventHandler;
|
||||
use libcitadel::terminal::Base16Scheme;
|
||||
|
||||
@@ -39,6 +43,7 @@ impl From<PidLookupResult> for RealmFromCitadelPid {
|
||||
#[derive(Clone)]
|
||||
pub struct RealmsManagerServer {
|
||||
manager: Arc<RealmManager>,
|
||||
quit_event: Arc<Event>,
|
||||
}
|
||||
|
||||
const BOOL_CONFIG_VARS: &[&str] = &[
|
||||
@@ -121,40 +126,40 @@ fn configure_realm(manager: &RealmManager, realm: &Realm, variable: &str, value:
|
||||
|
||||
impl RealmsManagerServer {
|
||||
|
||||
fn register_events(&self, connection: &Connection) -> Result<()> {
|
||||
let events = EventHandler::new(connection.clone(), self.clone());
|
||||
self.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||
self.manager.start_event_task()
|
||||
pub fn load(connection: &Connection, manager: Arc<RealmManager>, quit_event: Arc<Event>) -> Result<RealmsManagerServer> {
|
||||
let server = RealmsManagerServer { manager, quit_event };
|
||||
let events = EventHandler::new(connection.clone());
|
||||
server.manager.add_event_handler(move |ev| events.handle_event(ev));
|
||||
server.manager.start_event_task()?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
pub fn register(connection: &Connection) -> Result<ObjectServer> {
|
||||
let manager = RealmManager::load()?;
|
||||
let iface = RealmsManagerServer { manager };
|
||||
iface.register_events(connection)?;
|
||||
let mut object_server = ObjectServer::new(connection);
|
||||
object_server.at(REALMS_SERVER_OBJECT_PATH, iface).map_err(context!("ZBus error"))?;
|
||||
Ok(object_server)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[dbus_interface(name = "com.subgraph.realms.Manager")]
|
||||
#[interface(name = "com.subgraph.realms.Manager")]
|
||||
impl RealmsManagerServer {
|
||||
|
||||
fn set_current(&self, name: &str) {
|
||||
if let Some(realm) = self.manager.realm_by_name(name) {
|
||||
if let Err(err) = self.manager.set_current_realm(&realm) {
|
||||
warn!("set_current_realm({}) failed: {}", name, err);
|
||||
async fn set_current(&self, name: &str) {
|
||||
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
if let Some(realm) = manager.realm_by_name(&name) {
|
||||
if let Err(err) = manager.set_current_realm(&realm) {
|
||||
warn!("set_current_realm({}) failed: {}", name, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn get_current(&self) -> String {
|
||||
match self.manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
async fn get_current(&self) -> String {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
match manager.current_realm() {
|
||||
Some(realm) => realm.name().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<RealmItem> {
|
||||
@@ -249,8 +254,12 @@ impl RealmsManagerServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
self.manager.realm_by_pid(pid).into()
|
||||
async fn realm_from_citadel_pid(&self, pid: u32) -> RealmFromCitadelPid {
|
||||
let manager = self.manager.clone();
|
||||
unblock(move || {
|
||||
manager.realm_by_pid(pid).into()
|
||||
|
||||
}).await
|
||||
}
|
||||
|
||||
fn realm_config(&self, name: &str) -> RealmConfig {
|
||||
@@ -261,7 +270,7 @@ impl RealmsManagerServer {
|
||||
RealmConfig::new_from_realm(&realm)
|
||||
}
|
||||
|
||||
fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||
async fn realm_set_config(&self, name: &str, vars: Vec<(String,String)>) {
|
||||
let realm = match self.manager.realm_by_name(name) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
@@ -270,8 +279,12 @@ impl RealmsManagerServer {
|
||||
},
|
||||
};
|
||||
|
||||
for var in &vars {
|
||||
configure_realm(&self.manager, &realm, &var.0, &var.1);
|
||||
for var in vars {
|
||||
let manager = self.manager.clone();
|
||||
let realm = realm.clone();
|
||||
unblock( move || {
|
||||
configure_realm(&manager, &realm, &var.0, &var.1);
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +292,18 @@ impl RealmsManagerServer {
|
||||
Realm::is_valid_name(name) && self.manager.realm_by_name(name).is_some()
|
||||
}
|
||||
|
||||
fn create_realm(&self, name: &str) -> bool {
|
||||
if let Err(err) = self.manager.new_realm(name) {
|
||||
warn!("Error creating realm ({}): {}", name, err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
async fn create_realm(&self, name: &str) -> bool {
|
||||
|
||||
let manager = self.manager.clone();
|
||||
let name = name.to_string();
|
||||
unblock(move || {
|
||||
if let Err(err) = manager.new_realm(&name) {
|
||||
warn!("Error creating realm ({}): {}", name, err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
fn list_realm_f_s(&self) -> Vec<String> {
|
||||
@@ -299,23 +317,23 @@ impl RealmsManagerServer {
|
||||
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_started(&self, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_started(ctx: &SignalEmitter<'_>, realm: &str, pid_ns: u64, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_stopped(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_stopped(ctx: &SignalEmitter<'_>, realm: &str, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_new(&self, realm: &str, description: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_new(ctx: &SignalEmitter<'_>, realm: &str, description: &str, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_removed(&self, realm: &str) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_removed(ctx: &SignalEmitter<'_>, realm: &str) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn realm_current(&self, realm: &str, status: u8) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn realm_current(ctx: &SignalEmitter<'_>, realm: &str, status: u8) -> zbus::Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
pub fn service_started(&self) -> zbus::Result<()> { Ok(()) }
|
||||
#[zbus(signal)]
|
||||
pub async fn service_started(ctx: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,6 @@ StartLimitIntervalSec=0
|
||||
|
||||
[Path]
|
||||
PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/rootfs/var/lib/flatpak/exports/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/flatpak/exports/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications
|
||||
PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/flatpak/exports/share/applications
|
||||
|
9
update-realmfs/Cargo.toml
Normal file
9
update-realmfs/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "update-realmfs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcitadel = { path = "../libcitadel" }
|
||||
zbus = "5.7.1"
|
||||
anyhow = "1.0"
|
139
update-realmfs/src/main.rs
Normal file
139
update-realmfs/src/main.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
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(())
|
||||
}
|
72
update-realmfs/src/realmsd.rs
Normal file
72
update-realmfs/src/realmsd.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
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(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user