Implement launching a local daemon (#261)
Allow AppImage and non-systemd systems to launch a local burrow daemon.
This commit is contained in:
parent
c4c342dc8b
commit
c755f752a0
9 changed files with 192 additions and 27 deletions
|
|
@ -8,13 +8,14 @@ Burrow is an open source tool for burrowing through firewalls, built by teenager
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! For more information on how to contribute, please see [CONTRIBUTING.md]
|
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app.
|
||||||
|
|
||||||
The project structure is divided in the following folders:
|
The project structure is divided in the following folders:
|
||||||
|
|
||||||
```
|
```
|
||||||
Apple/ # Xcode project for burrow on macOS and iOS
|
Apple/ # Xcode project for burrow on macOS and iOS
|
||||||
burrow/ # Higher-level API library for tun and tun-async
|
burrow/ # Higher-level API library for tun and tun-async
|
||||||
|
burrow-gtk/ # GTK project for burrow on Linux
|
||||||
tun/ # Low-level interface to OS networking
|
tun/ # Low-level interface to OS networking
|
||||||
src/
|
src/
|
||||||
tokio/ # Async/Tokio code
|
tokio/ # Async/Tokio code
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ set -ex
|
||||||
BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)"
|
BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)"
|
||||||
BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage"
|
BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage"
|
||||||
LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}"
|
LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}"
|
||||||
|
BURROW_BUILD_TYPE="${BURROW_BUILD_TYPE:-"release"}"
|
||||||
|
|
||||||
if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then
|
if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then
|
||||||
echo "Make sure to cd into burrow-gtk"
|
echo "Make sure to cd into burrow-gtk"
|
||||||
|
|
@ -21,8 +22,9 @@ elif [ "$ARCHITECTURE" == "aarch64" ]; then
|
||||||
chmod a+x /tmp/linuxdeploy
|
chmod a+x /tmp/linuxdeploy
|
||||||
fi
|
fi
|
||||||
|
|
||||||
meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr
|
meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr --buildtype $BURROW_BUILD_TYPE
|
||||||
meson compile -C $BURROW_GTK_BUILD
|
meson compile -C $BURROW_GTK_BUILD
|
||||||
DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD
|
DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD
|
||||||
/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir --output appimage
|
cargo b --$BURROW_BUILD_TYPE --manifest-path=../Cargo.toml
|
||||||
|
/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir -e $BURROW_GTK_BUILD/../../target/$BURROW_BUILD_TYPE/burrow --output appimage
|
||||||
mv *.AppImage $BURROW_GTK_BUILD
|
mv *.AppImage $BURROW_GTK_BUILD
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||||
_settings_screen: Controller<settings_screen::SettingsScreen>,
|
settings_screen: Controller<settings_screen::SettingsScreen>,
|
||||||
switch_screen: AsyncController<switch_screen::SwitchScreen>,
|
switch_screen: AsyncController<switch_screen::SwitchScreen>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ impl AsyncComponent for App {
|
||||||
let model = App {
|
let model = App {
|
||||||
daemon_client,
|
daemon_client,
|
||||||
switch_screen,
|
switch_screen,
|
||||||
_settings_screen: settings_screen,
|
settings_screen,
|
||||||
};
|
};
|
||||||
|
|
||||||
AsyncComponentParts { model, widgets }
|
AsyncComponentParts { model, widgets }
|
||||||
|
|
@ -132,14 +132,23 @@ impl AsyncComponent for App {
|
||||||
disconnected_daemon_client = true;
|
disconnected_daemon_client = true;
|
||||||
self.switch_screen
|
self.switch_screen
|
||||||
.emit(switch_screen::SwitchScreenMsg::DaemonDisconnect);
|
.emit(switch_screen::SwitchScreenMsg::DaemonDisconnect);
|
||||||
|
self.settings_screen
|
||||||
|
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if disconnected_daemon_client || daemon_client.is_none() {
|
if disconnected_daemon_client || daemon_client.is_none() {
|
||||||
*daemon_client = DaemonClient::new().await.ok();
|
match DaemonClient::new().await {
|
||||||
if daemon_client.is_some() {
|
Ok(new_daemon_client) => {
|
||||||
|
*daemon_client = Some(new_daemon_client);
|
||||||
self.switch_screen
|
self.switch_screen
|
||||||
.emit(switch_screen::SwitchScreenMsg::DaemonReconnect);
|
.emit(switch_screen::SwitchScreenMsg::DaemonReconnect);
|
||||||
|
self.settings_screen
|
||||||
|
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
|
||||||
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
// TODO: Handle Error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,4 @@ mod settings_screen;
|
||||||
mod switch_screen;
|
mod switch_screen;
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
|
pub use settings::{DaemonGroupMsg, DiagGroupMsg};
|
||||||
|
|
|
||||||
111
burrow-gtk/src/components/settings/daemon_group.rs
Normal file
111
burrow-gtk/src/components/settings/daemon_group.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
use super::*;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DaemonGroup {
|
||||||
|
system_setup: SystemSetup,
|
||||||
|
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||||
|
already_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DaemonGroupInit {
|
||||||
|
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||||
|
pub system_setup: SystemSetup,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DaemonGroupMsg {
|
||||||
|
LaunchLocal,
|
||||||
|
DaemonStateChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub, async)]
|
||||||
|
impl AsyncComponent for DaemonGroup {
|
||||||
|
type Init = DaemonGroupInit;
|
||||||
|
type Input = DaemonGroupMsg;
|
||||||
|
type Output = ();
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[name(group)]
|
||||||
|
adw::PreferencesGroup {
|
||||||
|
#[watch]
|
||||||
|
set_sensitive:
|
||||||
|
(model.system_setup == SystemSetup::AppImage || model.system_setup == SystemSetup::Other) &&
|
||||||
|
!model.already_running,
|
||||||
|
set_title: "Local Daemon",
|
||||||
|
set_description: Some("Run Local Daemon"),
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: "Launch",
|
||||||
|
connect_clicked => DaemonGroupMsg::LaunchLocal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
// Should be impossible to panic here
|
||||||
|
let model = DaemonGroup {
|
||||||
|
system_setup: init.system_setup,
|
||||||
|
daemon_client: init.daemon_client.clone(),
|
||||||
|
already_running: init.daemon_client.lock().await.is_some(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
msg: Self::Input,
|
||||||
|
_sender: AsyncComponentSender<Self>,
|
||||||
|
_root: &Self::Root,
|
||||||
|
) {
|
||||||
|
match msg {
|
||||||
|
DaemonGroupMsg::LaunchLocal => {
|
||||||
|
let burrow_original_bin = std::env::vars()
|
||||||
|
.find(|(k, _)| k == "APPDIR")
|
||||||
|
.map(|(_, v)| v + "/usr/bin/burrow")
|
||||||
|
.unwrap_or("/usr/bin/burrow".to_owned());
|
||||||
|
|
||||||
|
let mut burrow_bin =
|
||||||
|
String::from_utf8(Command::new("mktemp").output().unwrap().stdout).unwrap();
|
||||||
|
burrow_bin.pop();
|
||||||
|
|
||||||
|
let privileged_spawn_script = format!(
|
||||||
|
r#"TEMP=$(mktemp -p /root)
|
||||||
|
cp {} $TEMP
|
||||||
|
chmod +x $TEMP
|
||||||
|
setcap CAP_NET_BIND_SERVICE,CAP_NET_ADMIN+eip $TEMP
|
||||||
|
mv $TEMP /tmp/burrow-detached-daemon"#,
|
||||||
|
burrow_original_bin
|
||||||
|
)
|
||||||
|
.replace('\n', "&&");
|
||||||
|
|
||||||
|
// TODO: Handle error condition
|
||||||
|
|
||||||
|
Command::new("pkexec")
|
||||||
|
.arg("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(privileged_spawn_script)
|
||||||
|
.arg(&burrow_bin)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Command::new("/tmp/burrow-detached-daemon")
|
||||||
|
.env("RUST_LOG", "debug")
|
||||||
|
.arg("daemon")
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
DaemonGroupMsg::DaemonStateChange => {
|
||||||
|
self.already_running = self.daemon_client.lock().await.is_some();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use diag::{StatusTernary, SystemSetup};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DiagGroup {
|
pub struct DiagGroup {
|
||||||
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||||
|
|
||||||
init_system: SystemSetup,
|
system_setup: SystemSetup,
|
||||||
service_installed: StatusTernary,
|
service_installed: StatusTernary,
|
||||||
socket_installed: StatusTernary,
|
socket_installed: StatusTernary,
|
||||||
socket_enabled: StatusTernary,
|
socket_enabled: StatusTernary,
|
||||||
|
|
@ -14,19 +13,20 @@ pub struct DiagGroup {
|
||||||
|
|
||||||
pub struct DiagGroupInit {
|
pub struct DiagGroupInit {
|
||||||
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||||
|
pub system_setup: SystemSetup,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagGroup {
|
impl DiagGroup {
|
||||||
async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> {
|
async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> {
|
||||||
let setup = SystemSetup::new();
|
let system_setup = SystemSetup::new();
|
||||||
let daemon_running = daemon_client.lock().await.is_some();
|
let daemon_running = daemon_client.lock().await.is_some();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
service_installed: setup.is_service_installed()?,
|
service_installed: system_setup.is_service_installed()?,
|
||||||
socket_installed: setup.is_socket_installed()?,
|
socket_installed: system_setup.is_socket_installed()?,
|
||||||
socket_enabled: setup.is_socket_enabled()?,
|
socket_enabled: system_setup.is_socket_enabled()?,
|
||||||
daemon_running,
|
daemon_running,
|
||||||
init_system: setup,
|
system_setup,
|
||||||
daemon_client,
|
daemon_client,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ impl AsyncComponent for DiagGroup {
|
||||||
|
|
||||||
adw::ActionRow {
|
adw::ActionRow {
|
||||||
#[watch]
|
#[watch]
|
||||||
set_title: &format!("Init System: {}", model.init_system)
|
set_title: &format!("System Type: {}", model.system_setup)
|
||||||
},
|
},
|
||||||
adw::ActionRow {
|
adw::ActionRow {
|
||||||
#[watch]
|
#[watch]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diag::{StatusTernary, SystemSetup};
|
||||||
|
|
||||||
|
mod daemon_group;
|
||||||
mod diag_group;
|
mod diag_group;
|
||||||
|
|
||||||
pub use diag_group::{DiagGroup, DiagGroupInit};
|
pub use daemon_group::{DaemonGroup, DaemonGroupInit, DaemonGroupMsg};
|
||||||
|
pub use diag_group::{DiagGroup, DiagGroupInit, DiagGroupMsg};
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diag::SystemSetup;
|
||||||
|
|
||||||
pub struct SettingsScreen {
|
pub struct SettingsScreen {
|
||||||
_diag_group: AsyncController<settings::DiagGroup>,
|
diag_group: AsyncController<settings::DiagGroup>,
|
||||||
|
daemon_group: AsyncController<settings::DaemonGroup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SettingsScreenInit {
|
pub struct SettingsScreenInit {
|
||||||
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum SettingsScreenMsg {
|
||||||
|
DaemonStateChange,
|
||||||
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
impl SimpleComponent for SettingsScreen {
|
impl SimpleComponent for SettingsScreen {
|
||||||
type Init = SettingsScreenInit;
|
type Init = SettingsScreenInit;
|
||||||
type Input = ();
|
type Input = SettingsScreenMsg;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
|
@ -24,21 +31,41 @@ impl SimpleComponent for SettingsScreen {
|
||||||
root: &Self::Root,
|
root: &Self::Root,
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
) -> ComponentParts<Self> {
|
) -> ComponentParts<Self> {
|
||||||
|
let system_setup = SystemSetup::new();
|
||||||
|
|
||||||
let diag_group = settings::DiagGroup::builder()
|
let diag_group = settings::DiagGroup::builder()
|
||||||
.launch(settings::DiagGroupInit {
|
.launch(settings::DiagGroupInit {
|
||||||
|
system_setup,
|
||||||
daemon_client: Arc::clone(&init.daemon_client),
|
daemon_client: Arc::clone(&init.daemon_client),
|
||||||
})
|
})
|
||||||
.forward(sender.input_sender(), |_| ());
|
.forward(sender.input_sender(), |_| {
|
||||||
|
SettingsScreenMsg::DaemonStateChange
|
||||||
|
});
|
||||||
|
|
||||||
|
let daemon_group = settings::DaemonGroup::builder()
|
||||||
|
.launch(settings::DaemonGroupInit {
|
||||||
|
system_setup,
|
||||||
|
daemon_client: Arc::clone(&init.daemon_client),
|
||||||
|
})
|
||||||
|
.forward(sender.input_sender(), |_| {
|
||||||
|
SettingsScreenMsg::DaemonStateChange
|
||||||
|
});
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
widgets.preferences.add(diag_group.widget());
|
widgets.preferences.add(diag_group.widget());
|
||||||
|
widgets.preferences.add(daemon_group.widget());
|
||||||
|
|
||||||
let model = SettingsScreen {
|
let model = SettingsScreen { diag_group, daemon_group };
|
||||||
_diag_group: diag_group,
|
|
||||||
};
|
|
||||||
|
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _: Self::Input, _sender: ComponentSender<Self>) {}
|
fn update(&mut self, _: Self::Input, _sender: ComponentSender<Self>) {
|
||||||
|
// Currently, `SettingsScreenMsg` only has one variant, so the if is ambiguous.
|
||||||
|
//
|
||||||
|
// if let SettingsScreenMsg::DaemonStateChange = msg {
|
||||||
|
self.diag_group.emit(DiagGroupMsg::Refresh);
|
||||||
|
self.daemon_group.emit(DaemonGroupMsg::DaemonStateChange);
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,18 @@ pub enum StatusTernary {
|
||||||
// Realistically, we may not explicitly "support" non-systemd platforms which would simply this
|
// Realistically, we may not explicitly "support" non-systemd platforms which would simply this
|
||||||
// code greatly.
|
// code greatly.
|
||||||
// Along with replacing [`StatusTernary`] with good old [`bool`].
|
// Along with replacing [`StatusTernary`] with good old [`bool`].
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SystemSetup {
|
pub enum SystemSetup {
|
||||||
Systemd,
|
Systemd,
|
||||||
|
AppImage,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemSetup {
|
impl SystemSetup {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
if Command::new("systemctl").arg("--version").output().is_ok() {
|
if is_appimage() {
|
||||||
|
SystemSetup::AppImage
|
||||||
|
} else if Command::new("systemctl").arg("--version").output().is_ok() {
|
||||||
SystemSetup::Systemd
|
SystemSetup::Systemd
|
||||||
} else {
|
} else {
|
||||||
SystemSetup::Other
|
SystemSetup::Other
|
||||||
|
|
@ -33,6 +36,7 @@ impl SystemSetup {
|
||||||
pub fn is_service_installed(&self) -> Result<StatusTernary> {
|
pub fn is_service_installed(&self) -> Result<StatusTernary> {
|
||||||
match self {
|
match self {
|
||||||
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()),
|
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()),
|
||||||
|
SystemSetup::AppImage => Ok(StatusTernary::NA),
|
||||||
SystemSetup::Other => Ok(StatusTernary::NA),
|
SystemSetup::Other => Ok(StatusTernary::NA),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +44,7 @@ impl SystemSetup {
|
||||||
pub fn is_socket_installed(&self) -> Result<StatusTernary> {
|
pub fn is_socket_installed(&self) -> Result<StatusTernary> {
|
||||||
match self {
|
match self {
|
||||||
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()),
|
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()),
|
||||||
|
SystemSetup::AppImage => Ok(StatusTernary::NA),
|
||||||
SystemSetup::Other => Ok(StatusTernary::NA),
|
SystemSetup::Other => Ok(StatusTernary::NA),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +60,7 @@ impl SystemSetup {
|
||||||
let output = String::from_utf8(output)?;
|
let output = String::from_utf8(output)?;
|
||||||
Ok((output == "enabled\n").into())
|
Ok((output == "enabled\n").into())
|
||||||
}
|
}
|
||||||
|
SystemSetup::AppImage => Ok(StatusTernary::NA),
|
||||||
SystemSetup::Other => Ok(StatusTernary::NA),
|
SystemSetup::Other => Ok(StatusTernary::NA),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +80,12 @@ impl Display for SystemSetup {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
SystemSetup::Systemd => "Systemd",
|
SystemSetup::Systemd => "Systemd",
|
||||||
|
SystemSetup::AppImage => "AppImage",
|
||||||
SystemSetup::Other => "Other",
|
SystemSetup::Other => "Other",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_appimage() -> bool {
|
||||||
|
std::env::vars().any(|(k, _)| k == "APPDIR")
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue