feat(ui): added sandboxing feature

This commit is contained in:
Observer KRypt0n_ 2023-04-16 14:12:25 +02:00
parent d058342829
commit 1954f99783
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
3 changed files with 242 additions and 48 deletions

38
Cargo.lock generated
View file

@ -44,7 +44,8 @@ version = "1.6.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bzip2", "bzip2",
"cached", "cached 0.43.0",
"dns-lookup",
"flate2", "flate2",
"fs_extra", "fs_extra",
"kinda-virtual-fs", "kinda-virtual-fs",
@ -67,7 +68,7 @@ version = "3.4.1"
dependencies = [ dependencies = [
"anime-launcher-sdk", "anime-launcher-sdk",
"anyhow", "anyhow",
"cached", "cached 0.42.0",
"fluent-templates", "fluent-templates",
"glib-build-tools", "glib-build-tools",
"gtk4", "gtk4",
@ -89,7 +90,7 @@ version = "0.5.17"
dependencies = [ dependencies = [
"anime-game-core", "anime-game-core",
"anyhow", "anyhow",
"cached", "cached 0.43.0",
"discord-rich-presence", "discord-rich-presence",
"enum-ordinalize", "enum-ordinalize",
"lazy_static", "lazy_static",
@ -344,6 +345,25 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "cached"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc2fafddf188d13788e7099295a59b99e99b2148ab2195cae454e754cc099925"
dependencies = [
"async-trait",
"async_once",
"cached_proc_macro",
"cached_proc_macro_types",
"futures",
"hashbrown 0.13.2",
"instant",
"lazy_static",
"once_cell",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "cached_proc_macro" name = "cached_proc_macro"
version = "0.16.0" version = "0.16.0"
@ -629,6 +649,18 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "dns-lookup"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872"
dependencies = [
"cfg-if",
"libc",
"socket2",
"winapi",
]
[[package]] [[package]]
name = "doc-comment" name = "doc-comment"
version = "0.3.3" version = "0.3.3"

View file

@ -62,8 +62,8 @@ impl AsyncFactoryComponent for Variable {
pub struct EnvironmentApp { pub struct EnvironmentApp {
variables: AsyncFactoryVecDeque<Variable>, variables: AsyncFactoryVecDeque<Variable>,
name: gtk::Entry, name_entry: adw::EntryRow,
value: gtk::Entry value_entry: adw::EntryRow
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -110,20 +110,14 @@ impl SimpleAsyncComponent for EnvironmentApp {
add = &adw::PreferencesGroup { add = &adw::PreferencesGroup {
set_title: &tr("new-variable"), set_title: &tr("new-variable"),
gtk::Box { #[local_ref]
set_orientation: gtk::Orientation::Horizontal, name_entry -> adw::EntryRow {
set_spacing: 8, set_title: &tr("name")
},
#[local_ref] #[local_ref]
name -> gtk::Entry { value_entry -> adw::EntryRow {
set_placeholder_text: Some(&tr("name")) set_title: &tr("value")
},
#[local_ref]
value -> gtk::Entry {
set_placeholder_text: Some(&tr("value")),
set_hexpand: true
}
}, },
gtk::Button { gtk::Button {
@ -152,8 +146,8 @@ impl SimpleAsyncComponent for EnvironmentApp {
let mut model = Self { let mut model = Self {
variables: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()), variables: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()),
name: gtk::Entry::new(), name_entry: adw::EntryRow::new(),
value: gtk::Entry::new() value_entry: adw::EntryRow::new()
}; };
for (name, value) in &CONFIG.game.environment { for (name, value) in &CONFIG.game.environment {
@ -162,8 +156,8 @@ impl SimpleAsyncComponent for EnvironmentApp {
let variables = model.variables.widget(); let variables = model.variables.widget();
let name = &model.name; let name_entry = &model.name_entry;
let value = &model.value; let value_entry = &model.value_entry;
let widgets = view_output!(); let widgets = view_output!();
@ -174,8 +168,8 @@ impl SimpleAsyncComponent for EnvironmentApp {
match msg { match msg {
EnvironmentMsg::Add => { EnvironmentMsg::Add => {
if let Ok(mut config) = Config::get() { if let Ok(mut config) = Config::get() {
let name = self.name.text().trim().to_string(); let name = self.name_entry.text().trim().to_string();
let value = self.value.text().trim().to_string(); let value = self.value_entry.text().trim().to_string();
if !name.is_empty() && !value.is_empty() { if !name.is_empty() && !value.is_empty() {
config.game.environment.insert(name.clone(), value.clone()); config.game.environment.insert(name.clone(), value.clone());

View file

@ -10,12 +10,12 @@ use crate::i18n::tr;
use crate::*; use crate::*;
#[derive(Debug)] #[derive(Debug)]
struct Directory { struct PrivateDirectory {
path: String path: String
} }
#[relm4::factory(async)] #[relm4::factory(async)]
impl AsyncFactoryComponent for Directory { impl AsyncFactoryComponent for PrivateDirectory {
type Init = String; type Init = String;
type Input = SandboxAppMsg; type Input = SandboxAppMsg;
type Output = SandboxAppMsg; type Output = SandboxAppMsg;
@ -33,7 +33,7 @@ impl AsyncFactoryComponent for Directory {
set_valign: gtk::Align::Center, set_valign: gtk::Align::Center,
connect_clicked[sender, index] => move |_| { connect_clicked[sender, index] => move |_| {
sender.input(SandboxAppMsg::Remove(index.clone())); sender.input(SandboxAppMsg::RemovePrivate(index.clone()));
} }
} }
} }
@ -58,16 +58,76 @@ impl AsyncFactoryComponent for Directory {
} }
} }
pub struct SandboxApp { #[derive(Debug)]
directories: AsyncFactoryVecDeque<Directory>, struct SharedDirectory {
mount_from: String,
mount_to: String
}
sandboxing_path: adw::EntryRow #[relm4::factory(async)]
impl AsyncFactoryComponent for SharedDirectory {
type Init = (String, String);
type Input = SandboxAppMsg;
type Output = SandboxAppMsg;
type CommandOutput = ();
type ParentInput = SandboxAppMsg;
type ParentWidget = adw::PreferencesGroup;
view! {
root = adw::ActionRow {
set_title: &self.mount_to,
set_subtitle: &self.mount_from,
add_suffix = &gtk::Button {
set_icon_name: "user-trash-symbolic",
add_css_class: "flat",
set_valign: gtk::Align::Center,
connect_clicked[sender, index] => move |_| {
sender.input(SandboxAppMsg::RemoveShared(index.clone()));
}
}
}
}
fn output_to_parent_input(output: Self::Output) -> Option<Self::ParentInput> {
Some(output)
}
async fn init_model(
init: Self::Init,
_index: &DynamicIndex,
_sender: AsyncFactorySender<Self>,
) -> Self {
Self {
mount_from: init.0,
mount_to: init.1
}
}
async fn update(&mut self, msg: Self::Input, sender: AsyncFactorySender<Self>) {
sender.output(msg);
}
}
pub struct SandboxApp {
private_paths: AsyncFactoryVecDeque<PrivateDirectory>,
shared_paths: AsyncFactoryVecDeque<SharedDirectory>,
private_path_entry: adw::EntryRow,
shared_path_from_entry: adw::EntryRow,
shared_path_to_entry: adw::EntryRow,
read_only_switch: gtk::Switch
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SandboxAppMsg { pub enum SandboxAppMsg {
Add, AddPrivate,
Remove(DynamicIndex) RemovePrivate(DynamicIndex),
AddShared,
RemoveShared(DynamicIndex)
} }
#[relm4::component(async, pub)] #[relm4::component(async, pub)]
@ -127,15 +187,19 @@ impl SimpleAsyncComponent for SandboxApp {
} }
} }
} }
},
adw::EntryRow {
set_title: "Hostname"
} }
}, },
add = &adw::PreferencesGroup { add = &adw::PreferencesGroup {
set_title: "Sandboxed directories", set_title: "Private directories",
set_description: Some("These folders will be replaced by in-memory filesystem (tmpfs), and their original content will not be available to sandboxed game"), set_description: Some("These folders will be replaced by an empty virtual filesystem (tmpfs), and their original content will not be available to sandboxed game"),
#[local_ref] #[local_ref]
sandboxing_path -> adw::EntryRow { private_path_entry -> adw::EntryRow {
set_title: "Path" set_title: "Path"
}, },
@ -146,12 +210,50 @@ impl SimpleAsyncComponent for SandboxApp {
set_margin_top: 8, set_margin_top: 8,
set_halign: gtk::Align::Start, set_halign: gtk::Align::Start,
connect_clicked => SandboxAppMsg::Add connect_clicked => SandboxAppMsg::AddPrivate
} }
}, },
#[local_ref] #[local_ref]
add = directories -> adw::PreferencesGroup {} add = private_paths -> adw::PreferencesGroup {},
add = &adw::PreferencesGroup {
set_title: "Shared directories",
set_description: Some("These directories will be symlinked to directories in your host system"),
#[local_ref]
shared_path_from_entry -> adw::EntryRow {
set_title: "Original path"
},
#[local_ref]
shared_path_to_entry -> adw::EntryRow {
set_title: "New path"
},
adw::ActionRow {
set_title: "Read-only",
set_subtitle: "Forbid game to write any data to this directory",
#[local_ref]
add_suffix = read_only_switch -> gtk::Switch {
set_valign: gtk::Align::Center
}
},
gtk::Button {
set_label: &tr("add"),
add_css_class: "pill",
set_margin_top: 8,
set_halign: gtk::Align::Start,
connect_clicked => SandboxAppMsg::AddShared
}
},
#[local_ref]
add = shared_paths -> adw::PreferencesGroup {}
} }
} }
@ -163,17 +265,42 @@ impl SimpleAsyncComponent for SandboxApp {
tracing::info!("Initializing environment settings"); tracing::info!("Initializing environment settings");
let mut model = Self { let mut model = Self {
directories: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()), private_paths: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()),
shared_paths: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()),
sandboxing_path: adw::EntryRow::new() private_path_entry: adw::EntryRow::new(),
shared_path_from_entry: adw::EntryRow::new(),
shared_path_to_entry: adw::EntryRow::new(),
read_only_switch: gtk::Switch::new()
}; };
for path in &CONFIG.sandbox.private { for path in &CONFIG.sandbox.private {
model.directories.guard().push_back(path.trim().to_string()); model.private_paths.guard().push_back(path.trim().to_string());
} }
let directories = model.directories.widget(); for (from, to) in &CONFIG.sandbox.mounts.read_only {
let sandboxing_path = &model.sandboxing_path; model.shared_paths.guard().push_back((
from.trim().to_string(),
format!("[read-only] {}", to.trim())
));
}
for (from, to) in &CONFIG.sandbox.mounts.bind {
model.shared_paths.guard().push_back((
from.trim().to_string(),
to.trim().to_string()
));
}
let private_paths = model.private_paths.widget();
let shared_paths = model.shared_paths.widget();
let private_path_entry = &model.private_path_entry;
let shared_path_from_entry = &model.shared_path_from_entry;
let shared_path_to_entry = &model.shared_path_to_entry;
let read_only_switch = &model.read_only_switch;
let widgets = view_output!(); let widgets = view_output!();
@ -182,29 +309,70 @@ impl SimpleAsyncComponent for SandboxApp {
async fn update(&mut self, msg: Self::Input, _sender: AsyncComponentSender<Self>) { async fn update(&mut self, msg: Self::Input, _sender: AsyncComponentSender<Self>) {
match msg { match msg {
SandboxAppMsg::Add => { SandboxAppMsg::AddPrivate => {
if let Ok(mut config) = Config::get() { if let Ok(mut config) = Config::get() {
let path = self.sandboxing_path.text().trim().to_string(); let path = self.private_path_entry.text().trim().to_string();
if !path.is_empty() { if !path.is_empty() {
config.sandbox.private.push(path.clone()); config.sandbox.private.push(path.clone());
Config::update(config); Config::update(config);
self.directories.guard().push_back(path); self.private_paths.guard().push_back(path);
} }
} }
} }
SandboxAppMsg::Remove(index) => { SandboxAppMsg::RemovePrivate(index) => {
if let Ok(mut config) = Config::get() { if let Ok(mut config) = Config::get() {
if let Some(var) = self.directories.guard().get(index.current_index()) { if let Some(var) = self.private_paths.guard().get(index.current_index()) {
config.sandbox.private.retain(|item| item != &var.path); config.sandbox.private.retain(|item| item != &var.path);
Config::update(config); Config::update(config);
} }
self.directories.guard().remove(index.current_index()); self.private_paths.guard().remove(index.current_index());
}
},
SandboxAppMsg::AddShared => {
if let Ok(mut config) = Config::get() {
let from = self.shared_path_from_entry.text().trim().to_string();
let to = self.shared_path_to_entry.text().trim().to_string();
let read_only = self.read_only_switch.state();
if !from.is_empty() && !to.is_empty() {
if read_only {
config.sandbox.mounts.read_only.insert(from.clone(), to.clone());
} else {
config.sandbox.mounts.bind.insert(from.clone(), to.clone());
}
Config::update(config);
self.shared_paths.guard().push_back((
from,
if read_only {
format!("[read-only] {}", to)
} else {
to
}
));
}
}
}
SandboxAppMsg::RemoveShared(index) => {
if let Ok(mut config) = Config::get() {
if let Some(var) = self.shared_paths.guard().get(index.current_index()) {
config.sandbox.mounts.read_only.remove(&var.mount_from);
config.sandbox.mounts.bind.remove(&var.mount_from);
Config::update(config);
}
self.shared_paths.guard().remove(index.current_index());
} }
} }
} }