Compare commits

...
Sign in to create a new pull request.

18 commits

Author SHA1 Message Date
David Zhong
b973481539
Merge branch 'main' into gtk-rpc 2024-09-07 18:00:12 -07:00
David Zhong
648909a906
Merge branch 'main' into gtk-rpc 2024-09-07 17:18:41 -07:00
dav
aaabca9957 Update docs and CI 2024-09-07 17:11:55 -07:00
dav
dd8cd03036 Better error handling 2024-09-07 16:43:51 -07:00
dav
21e0df60a8 Fix appimage build 2024-08-17 12:33:54 -07:00
dav
5088ab9a0e Prettier UI 2024-08-17 12:26:20 -07:00
dav
0ba1ea9237 Missing import 2024-08-17 10:57:18 -07:00
dav
c455c1fbbe Test against mock rpc server 2024-08-17 10:57:18 -07:00
dav
fec725bc52 Implement networks add delete and reoder 2024-08-17 10:57:18 -07:00
dav
54e9e0bc43 Connect network list to rpc 2024-08-17 10:57:18 -07:00
dav
dbacf93418 Create main screen with placeholder networks 2024-08-17 10:57:18 -07:00
dav
37fb4f4974 Bad workaround for local appimage daemon 2024-08-17 10:57:18 -07:00
dav
9a262a1243 Tested with mock grpc server 2024-08-17 10:57:18 -07:00
David Zhong
87cea6bf64 Remove TODO comment 2024-08-17 10:57:18 -07:00
dav
753e50f79d Have the app actually open 2024-08-17 10:57:18 -07:00
dav
74325059a7 Add proto defs into burrow-gtk 2024-08-17 10:57:18 -07:00
dav
12f595011a Integrate tunnel status streaming 2024-08-17 10:57:18 -07:00
dav
90468d5518 Have burrow gtk use new rpc 2024-08-17 10:57:18 -07:00
21 changed files with 1306 additions and 1784 deletions

1898
burrow-gtk/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,17 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
relm4 = { version = "0.6", features = ["libadwaita", "gnome_44"]} relm4 = { version = "0.6", features = ["libadwaita", "gnome_44"]}
burrow = { version = "*", path = "../burrow/" }
tokio = { version = "1.35.0", features = ["time", "sync"] } tokio = { version = "1.35.0", features = ["time", "sync"] }
gettext-rs = { version = "0.7.0", features = ["gettext-system"] } gettext-rs = { version = "0.7.0", features = ["gettext-system"] }
tonic = "0.12"
prost = "0.13"
prost-types = "0.13"
hyper-util = "0.1.6"
tower = "0.4.13"
log = "0.4.22"
colog = "1.3.0"
[build-dependencies] [build-dependencies]
anyhow = "1.0" anyhow = "1.0"
glib-build-tools = "0.18.0" glib-build-tools = "0.18.0"
tonic-build = "0.12"

View file

@ -4,7 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux && \ RUN set -eux && \
dnf update -y && \ dnf update -y && \
dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file sqlite sqlite-devel dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file sqlite-devel protobuf-compiler protobuf-devel
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"

View file

@ -2,6 +2,7 @@ use anyhow::Result;
fn main() -> Result<()> { fn main() -> Result<()> {
compile_gresources()?; compile_gresources()?;
tonic_build::compile_protos("proto/burrow.proto")?;
Ok(()) Ok(())
} }

View file

@ -0,0 +1,73 @@
syntax = "proto3";
package burrow;
import "google/protobuf/timestamp.proto";
service Tunnel {
rpc TunnelConfiguration (Empty) returns (TunnelConfigurationResponse);
rpc TunnelStart (Empty) returns (Empty);
rpc TunnelStop (Empty) returns (Empty);
rpc TunnelStatus (Empty) returns (stream TunnelStatusResponse);
}
service Networks {
rpc NetworkAdd (Empty) returns (Empty);
rpc NetworkList (Empty) returns (stream NetworkListResponse);
rpc NetworkReorder (NetworkReorderRequest) returns (Empty);
rpc NetworkDelete (NetworkDeleteRequest) returns (Empty);
}
message NetworkReorderRequest {
int32 id = 1;
int32 index = 2;
}
message WireGuardPeer {
string endpoint = 1;
repeated string subnet = 2;
}
message WireGuardNetwork {
string address = 1;
string dns = 2;
repeated WireGuardPeer peer = 3;
}
message NetworkDeleteRequest {
int32 id = 1;
}
message Network {
int32 id = 1;
NetworkType type = 2;
bytes payload = 3;
}
enum NetworkType {
WireGuard = 0;
HackClub = 1;
}
message NetworkListResponse {
repeated Network network = 1;
}
message Empty {
}
enum State {
Stopped = 0;
Running = 1;
}
message TunnelStatusResponse {
State state = 1;
optional google.protobuf.Timestamp start = 2;
}
message TunnelConfigurationResponse {
repeated string addresses = 1;
int32 mtu = 2;
}

View file

@ -2,12 +2,12 @@ use super::*;
use anyhow::Context; use anyhow::Context;
use std::time::Duration; use std::time::Duration;
const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5); const RECONNECT_POLL_TIME: Duration = Duration::from_secs(3);
pub struct App { pub struct App {
daemon_client: Arc<Mutex<Option<DaemonClient>>>, daemon_client: Arc<Mutex<Option<Channel>>>,
settings_screen: Controller<settings_screen::SettingsScreen>, settings_screen: Controller<settings_screen::SettingsScreen>,
switch_screen: AsyncController<switch_screen::SwitchScreen>, main_screen: AsyncController<main_screen::MainScreen>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -49,7 +49,7 @@ impl AsyncComponent for App {
view! { view! {
adw::Window { adw::Window {
set_title: Some("Burrow"), set_title: Some("Burrow"),
set_default_size: (640, 480), set_default_size: (640, 800),
} }
} }
@ -58,10 +58,19 @@ impl AsyncComponent for App {
root: Self::Root, root: Self::Root,
sender: AsyncComponentSender<Self>, sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> { ) -> AsyncComponentParts<Self> {
let daemon_client = Arc::new(Mutex::new(DaemonClient::new().await.ok())); // TODO: RPC REFACTOR (Handle Error)
let mut daemon_client_connected = false;
let daemon_client = Arc::new(Mutex::new(
daemon::daemon_connect()
.await
.inspect(|_| {
daemon_client_connected = true;
})
.ok(),
));
let switch_screen = switch_screen::SwitchScreen::builder() let main_screen = main_screen::MainScreen::builder()
.launch(switch_screen::SwitchScreenInit { .launch(main_screen::MainScreenInit {
daemon_client: Arc::clone(&daemon_client), daemon_client: Arc::clone(&daemon_client),
}) })
.forward(sender.input_sender(), |_| AppMsg::None); .forward(sender.input_sender(), |_| AppMsg::None);
@ -72,10 +81,21 @@ impl AsyncComponent for App {
}) })
.forward(sender.input_sender(), |_| AppMsg::None); .forward(sender.input_sender(), |_| AppMsg::None);
if !daemon_client_connected {
main_screen
.sender()
.send(main_screen::MainScreenMsg::DaemonDisconnect)
.unwrap();
settings_screen
.sender()
.send(settings_screen::SettingsScreenMsg::DaemonStateChange)
.unwrap();
}
let widgets = view_output!(); let widgets = view_output!();
let view_stack = adw::ViewStack::new(); let view_stack = adw::ViewStack::new();
view_stack.add_titled(switch_screen.widget(), None, "Switch"); view_stack.add_titled(main_screen.widget(), None, "Burrow");
view_stack.add_titled(settings_screen.widget(), None, "Settings"); view_stack.add_titled(settings_screen.widget(), None, "Settings");
let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build(); let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build();
@ -108,7 +128,7 @@ impl AsyncComponent for App {
let model = App { let model = App {
daemon_client, daemon_client,
switch_screen, main_screen,
settings_screen, settings_screen,
}; };
@ -122,27 +142,32 @@ impl AsyncComponent for App {
_root: &Self::Root, _root: &Self::Root,
) { ) {
loop { loop {
tokio::time::sleep(RECONNECT_POLL_TIME).await;
{ {
let mut daemon_client = self.daemon_client.lock().await; let mut daemon_client = self.daemon_client.lock().await;
let mut disconnected_daemon_client = false; let mut disconnected_daemon_client = false;
if let Some(daemon_client) = daemon_client.as_mut() { if let Some(client) = daemon_client.as_mut() {
if let Err(_e) = daemon_client.send_command(DaemonCommand::ServerInfo).await { let mut client = tunnel_client::TunnelClient::new(client);
if let Ok(mut res) = client.tunnel_status(burrow_rpc::Empty {}).await {
let stream = res.get_mut();
while let Ok(Some(_)) = stream.message().await {}
}
*daemon_client = None;
disconnected_daemon_client = true; disconnected_daemon_client = true;
self.switch_screen self.main_screen
.emit(switch_screen::SwitchScreenMsg::DaemonDisconnect); .emit(main_screen::MainScreenMsg::DaemonDisconnect);
self.settings_screen self.settings_screen
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange) .emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
} }
}
if disconnected_daemon_client || daemon_client.is_none() { if disconnected_daemon_client || daemon_client.is_none() {
match DaemonClient::new().await { match daemon::daemon_connect().await {
Ok(new_daemon_client) => { Ok(new_daemon_client) => {
*daemon_client = Some(new_daemon_client); *daemon_client = Some(new_daemon_client);
self.switch_screen self.main_screen
.emit(switch_screen::SwitchScreenMsg::DaemonReconnect); .emit(main_screen::MainScreenMsg::DaemonReconnect);
self.settings_screen self.settings_screen
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange) .emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
} }
@ -152,6 +177,7 @@ impl AsyncComponent for App {
} }
} }
} }
tokio::time::sleep(RECONNECT_POLL_TIME).await;
} }
} }
} }

View file

@ -0,0 +1,9 @@
use super::*;
mod network_card;
mod networks;
mod switch;
pub use network_card::{NetworkCard, NetworkCardInit};
pub use networks::{Networks, NetworksInit};
pub use switch::{Switch, SwitchInit};

View file

@ -0,0 +1,147 @@
use super::*;
pub struct NetworkCard {
id: i32,
index: usize,
index_max: usize,
daemon_client: Arc<Mutex<Option<Channel>>>,
}
pub struct NetworkCardInit {
pub id: i32,
pub index: usize,
pub index_max: usize,
pub name: String,
pub enabled: bool,
pub daemon_client: Arc<Mutex<Option<Channel>>>,
}
#[derive(Debug)]
pub enum NetworkCardMsg {
NetworkDelete,
MoveUp,
MoveDown,
}
#[relm4::component(pub, async)]
impl AsyncComponent for NetworkCard {
type Init = NetworkCardInit;
type Input = NetworkCardMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::ListBoxRow {
set_halign: Align::Fill,
set_margin_vertical: 5,
set_margin_horizontal: 25,
set_hexpand: true,
gtk::Box {
set_halign: Align::Fill,
set_hexpand: true,
set_spacing: 10,
gtk::Box {
set_halign: Align::Start,
gtk::Switch {
set_active: init.enabled,
},
},
gtk::Box {
set_halign: Align::Center,
set_hexpand: true,
gtk::Label {
set_label: &init.name
},
},
gtk::Box {
set_halign: Align::End,
set_spacing: 5,
gtk::Button {
set_icon_name: "list-remove",
connect_clicked => NetworkCardMsg::NetworkDelete,
},
gtk::Button {
set_icon_name: "pan-up-symbolic",
connect_clicked => NetworkCardMsg::MoveUp,
},
gtk::Button {
set_icon_name: "pan-down-symbolic",
connect_clicked => NetworkCardMsg::MoveDown,
},
}
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let widgets = view_output!();
let model = NetworkCard {
id: init.id,
index: init.index,
index_max: init.index_max,
daemon_client: init.daemon_client,
};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
NetworkCardMsg::NetworkDelete => {
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
let mut client = networks_client::NetworksClient::new(daemon_client);
let _ = client
.network_delete(burrow_rpc::NetworkDeleteRequest { id: self.id })
.await;
}
}
NetworkCardMsg::MoveUp => {
if self.index.checked_sub(1).is_some() {
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
let mut client = networks_client::NetworksClient::new(daemon_client);
let _ = client
.network_reorder(burrow_rpc::NetworkReorderRequest {
id: self.id,
index: self.index as i32 - 1,
})
.await;
}
}
}
NetworkCardMsg::MoveDown => {
if self.index + 1 < self.index_max {
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
let mut client = networks_client::NetworksClient::new(daemon_client);
let _ = client
.network_reorder(burrow_rpc::NetworkReorderRequest {
id: self.id,
index: self.index as i32 + 1,
})
.await;
}
}
}
}
}
}

View file

@ -0,0 +1,226 @@
use super::*;
use std::time::Duration;
const RECONNECT_POLL_TIME: Duration = Duration::from_secs(3);
pub struct Networks {
daemon_client: Arc<Mutex<Option<Channel>>>,
network_cards: Vec<AsyncController<NetworkCard>>,
networks_list_box: gtk::ListBox,
_network_state_worker: WorkerController<AsyncNetworkStateHandler>,
}
pub struct NetworksInit {
pub daemon_client: Arc<Mutex<Option<Channel>>>,
}
#[derive(Debug)]
pub enum NetworksMsg {
None,
NetworkList(Vec<burrow_rpc::Network>),
NetworkAdd,
}
#[relm4::component(pub, async)]
impl AsyncComponent for Networks {
type Init = NetworksInit;
type Input = NetworksMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 20,
set_margin_all: 5,
set_valign: Align::Fill,
set_vexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
set_margin_all: 5,
set_valign: Align::Start,
set_halign: Align::Center,
gtk::Label {
set_label: "Add Network",
},
gtk::Button {
set_icon_name: "list-add",
set_margin_all: 12,
connect_clicked => NetworksMsg::NetworkAdd,
},
},
gtk::ScrolledWindow {
set_valign: Align::Fill,
set_vexpand: true,
set_margin_bottom: 50,
set_margin_start: 50,
set_margin_end: 50,
#[name = "networks"]
gtk::ListBox {
set_vexpand: true,
},
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let widgets = view_output!();
let network_cards = vec![
NetworkCard::builder()
.launch(NetworkCardInit {
id: 0,
index: 0,
index_max: 3,
daemon_client: Arc::clone(&init.daemon_client),
name: "Hello".to_owned(),
enabled: true,
})
.forward(sender.input_sender(), |_| NetworksMsg::None),
NetworkCard::builder()
.launch(NetworkCardInit {
id: 1,
index: 1,
index_max: 3,
daemon_client: Arc::clone(&init.daemon_client),
name: "World".to_owned(),
enabled: false,
})
.forward(sender.input_sender(), |_| NetworksMsg::None),
NetworkCard::builder()
.launch(NetworkCardInit {
id: 2,
index: 2,
index_max: 3,
daemon_client: Arc::clone(&init.daemon_client),
name: "Yay".to_owned(),
enabled: false,
})
.forward(sender.input_sender(), |_| NetworksMsg::None),
NetworkCard::builder()
.launch(NetworkCardInit {
id: 2,
index: 2,
index_max: 3,
daemon_client: Arc::clone(&init.daemon_client),
name: "Yay".to_owned(),
enabled: false,
})
.forward(sender.input_sender(), |_| NetworksMsg::None),
NetworkCard::builder()
.launch(NetworkCardInit {
id: 2,
index: 2,
index_max: 3,
daemon_client: Arc::clone(&init.daemon_client),
name: "Yay".to_owned(),
enabled: false,
})
.forward(sender.input_sender(), |_| NetworksMsg::None),
];
for network_card in network_cards.iter() {
widgets.networks.append(network_card.widget());
}
// let network_cards = vec![];
let model = Networks {
daemon_client: init.daemon_client,
network_cards,
networks_list_box: widgets.networks.clone(),
_network_state_worker: AsyncNetworkStateHandler::builder()
.detach_worker(())
.forward(sender.input_sender(), |msg| msg),
};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
NetworksMsg::NetworkList(networks) => {
for network_card in self.network_cards.iter() {
self.networks_list_box
.remove(&network_card.widget().clone());
}
self.network_cards.clear();
let index_max = networks.len();
for (index, network) in networks.iter().enumerate() {
let network_card = NetworkCard::builder()
.launch(NetworkCardInit {
id: network.id,
index,
index_max,
daemon_client: Arc::clone(&self.daemon_client),
name: format!("ID: {}, TYPE: {}", network.id, network.r#type),
enabled: false,
})
.forward(sender.input_sender(), |_| NetworksMsg::None);
self.networks_list_box.append(network_card.widget());
self.network_cards.push(network_card);
}
}
NetworksMsg::NetworkAdd => {
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
let mut client = networks_client::NetworksClient::new(daemon_client);
let _ = client.network_add(burrow_rpc::Empty {}).await;
}
}
_ => {}
}
}
}
struct AsyncNetworkStateHandler;
impl Worker for AsyncNetworkStateHandler {
type Init = ();
type Input = ();
type Output = NetworksMsg;
fn init(_: Self::Init, sender: ComponentSender<Self>) -> Self {
sender.input(());
Self
}
fn update(&mut self, _: (), sender: ComponentSender<Self>) {
let rt = tokio::runtime::Runtime::new().unwrap();
let task = rt.spawn(async move {
loop {
let conn = daemon::daemon_connect().await;
if let Ok(conn) = conn {
let mut client = networks_client::NetworksClient::new(conn);
if let Ok(mut res) = client.network_list(burrow_rpc::Empty {}).await {
let stream = res.get_mut();
while let Ok(Some(msg)) = stream.message().await {
sender
.output(NetworksMsg::NetworkList(msg.network))
.unwrap();
}
}
}
tokio::time::sleep(RECONNECT_POLL_TIME).await;
}
});
rt.block_on(task).unwrap();
}
}

View file

@ -0,0 +1,158 @@
use super::*;
use std::time::Duration;
const RECONNECT_POLL_TIME: Duration = Duration::from_secs(3);
pub struct Switch {
daemon_client: Arc<Mutex<Option<Channel>>>,
switch: gtk::Switch,
_tunnel_state_worker: WorkerController<AsyncTunnelStateHandler>,
}
pub struct SwitchInit {
pub daemon_client: Arc<Mutex<Option<Channel>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SwitchMsg {
None,
Start,
Stop,
SwitchSetStart,
SwitchSetStop,
}
#[relm4::component(pub, async)]
impl AsyncComponent for Switch {
type Init = SwitchInit;
type Input = SwitchMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: Align::Fill,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
set_valign: Align::Start,
#[name(setup_banner)]
adw::Banner {
set_title: "Burrow is not running!",
},
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
set_margin_all: 5,
set_halign: Align::Center,
gtk::Label {
set_label: "Burrow Tunnel",
},
#[name(switch)]
gtk::Switch {
set_halign: Align::Center,
set_hexpand: false,
set_vexpand: false,
connect_active_notify => move |switch|
switch_sender.input(if switch.is_active() { SwitchMsg::Start } else { SwitchMsg::Stop })
},
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let switch_sender = sender.clone();
let widgets = view_output!();
let model = Switch {
daemon_client: init.daemon_client,
switch: widgets.switch.clone(),
_tunnel_state_worker: AsyncTunnelStateHandler::builder()
.detach_worker(())
.forward(sender.input_sender(), |_| SwitchMsg::None),
};
widgets.switch.set_active(false);
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
let mut client = tunnel_client::TunnelClient::new(daemon_client);
match msg {
Self::Input::Start => {
// TODO: Figure out best way for error handling.
let _ = client.tunnel_start(burrow_rpc::Empty {}).await;
}
Self::Input::Stop => {
// TODO: Figure out best way for error handling.
let _ = client.tunnel_stop(burrow_rpc::Empty {}).await;
}
Self::Input::SwitchSetStart => {
self.switch.set_active(true);
}
Self::Input::SwitchSetStop => {
self.switch.set_active(false);
}
_ => {}
}
}
}
}
struct AsyncTunnelStateHandler;
impl Worker for AsyncTunnelStateHandler {
type Init = ();
type Input = ();
type Output = SwitchMsg;
fn init(_: Self::Init, sender: ComponentSender<Self>) -> Self {
sender.input(());
Self
}
fn update(&mut self, _: (), sender: ComponentSender<Self>) {
let rt = tokio::runtime::Runtime::new().unwrap();
let task = rt.spawn(async move {
loop {
let conn = daemon::daemon_connect().await;
if let Ok(conn) = conn {
let mut client = tunnel_client::TunnelClient::new(conn);
if let Ok(mut res) = client.tunnel_status(burrow_rpc::Empty {}).await {
let stream = res.get_mut();
while let Ok(Some(msg)) = stream.message().await {
sender
.output(match msg.state() {
burrow_rpc::State::Running => SwitchMsg::SwitchSetStart,
burrow_rpc::State::Stopped => SwitchMsg::SwitchSetStop,
})
.unwrap();
}
}
}
tokio::time::sleep(RECONNECT_POLL_TIME).await;
}
});
rt.block_on(task).unwrap();
}
}

View file

@ -0,0 +1,107 @@
use super::*;
pub struct MainScreen {
_switch: AsyncController<main::Switch>,
_networks: AsyncController<main::Networks>,
content_box: gtk::Box,
daemon_status_banner: adw::Banner,
}
pub struct MainScreenInit {
pub daemon_client: Arc<Mutex<Option<Channel>>>,
}
#[derive(Debug)]
pub enum MainScreenMsg {
None,
DaemonDisconnect,
DaemonReconnect,
}
#[relm4::component(pub, async)]
impl AsyncComponent for MainScreen {
type Init = MainScreenInit;
type Input = MainScreenMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: Align::Fill,
set_vexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
set_valign: Align::Start,
#[name(daemon_status_banner)]
adw::Banner {
set_title: "Burrow is not running!",
},
},
#[name(content)]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_margin_all: 5,
set_valign: Align::Fill,
set_vexpand: true,
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let switch = main::Switch::builder()
.launch(main::SwitchInit {
daemon_client: Arc::clone(&init.daemon_client),
})
.forward(sender.input_sender(), |_| MainScreenMsg::None);
let networks = main::Networks::builder()
.launch(main::NetworksInit {
daemon_client: Arc::clone(&init.daemon_client),
})
.forward(sender.input_sender(), |_| MainScreenMsg::None);
let widgets = view_output!();
widgets.content.append(switch.widget());
widgets.content.append(networks.widget());
let model = MainScreen {
_switch: switch,
_networks: networks,
content_box: widgets.content.clone(),
daemon_status_banner: widgets.daemon_status_banner.clone(),
};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
MainScreenMsg::DaemonDisconnect => {
self.daemon_status_banner.set_revealed(true);
self.content_box.set_sensitive(false);
}
MainScreenMsg::DaemonReconnect => {
self.daemon_status_banner.set_revealed(false);
self.content_box.set_sensitive(true);
}
_ => {}
}
}
}

View file

@ -1,6 +1,5 @@
use super::*; use super::*;
use adw::prelude::*; use adw::prelude::*;
use burrow::{DaemonClient, DaemonCommand, DaemonResponseData};
use gtk::Align; use gtk::Align;
use relm4::{ use relm4::{
component::{ component::{
@ -8,14 +7,23 @@ use relm4::{
AsyncController, AsyncController,
}, },
prelude::*, prelude::*,
Worker, WorkerController,
}; };
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
pub mod burrow_rpc {
tonic::include_proto!("burrow");
}
use burrow_rpc::{networks_client, tunnel_client};
use tonic::transport::Channel;
mod app; mod app;
mod main;
mod main_screen;
mod settings; mod settings;
mod settings_screen; mod settings_screen;
mod switch_screen; // mod switch_screen;
pub use app::*; pub use app::*;
pub use settings::{DaemonGroupMsg, DiagGroupMsg}; pub use settings::{DaemonGroupMsg, DiagGroupMsg};

View file

@ -4,12 +4,12 @@ use std::process::Command;
#[derive(Debug)] #[derive(Debug)]
pub struct DaemonGroup { pub struct DaemonGroup {
system_setup: SystemSetup, system_setup: SystemSetup,
daemon_client: Arc<Mutex<Option<DaemonClient>>>, daemon_client: Arc<Mutex<Option<Channel>>>,
already_running: bool, already_running: bool,
} }
pub struct DaemonGroupInit { pub struct DaemonGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>, pub daemon_client: Arc<Mutex<Option<Channel>>>,
pub system_setup: SystemSetup, pub system_setup: SystemSetup,
} }
@ -48,7 +48,6 @@ impl AsyncComponent for DaemonGroup {
root: Self::Root, root: Self::Root,
sender: AsyncComponentSender<Self>, sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> { ) -> AsyncComponentParts<Self> {
// Should be impossible to panic here
let model = DaemonGroup { let model = DaemonGroup {
system_setup: init.system_setup, system_setup: init.system_setup,
daemon_client: init.daemon_client.clone(), daemon_client: init.daemon_client.clone(),
@ -68,40 +67,9 @@ impl AsyncComponent for DaemonGroup {
) { ) {
match msg { match msg {
DaemonGroupMsg::LaunchLocal => { DaemonGroupMsg::LaunchLocal => {
let burrow_original_bin = std::env::vars() if let Err(e) = launch_local() {
.find(|(k, _)| k == "APPDIR") error!("Failed to launch local daemon at: {}", e);
.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 => { DaemonGroupMsg::DaemonStateChange => {
self.already_running = self.daemon_client.lock().await.is_some(); self.already_running = self.daemon_client.lock().await.is_some();
@ -109,3 +77,49 @@ mv $TEMP /tmp/burrow-detached-daemon"#,
} }
} }
} }
fn launch_local() -> Result<()> {
const BURROW_LOCAL_DAEMON_PATH: &str = "/tmp/burrow-detached-daemon";
let burrow_original_bin = std::env::vars()
.find(|(k, _)| k == "APPDIR")
.map(|(_, v)| v + "/usr/bin/burrow")
.unwrap_or("/usr/bin/burrow".to_owned());
Command::new("cp")
.arg(&burrow_original_bin)
.arg(BURROW_LOCAL_DAEMON_PATH)
.output()
.with_context(|| {
format!(
"Copying {} to {}",
burrow_original_bin, BURROW_LOCAL_DAEMON_PATH
)
})?;
let mut burrow_bin = String::from_utf8(Command::new("mktemp").output()?.stdout)?;
burrow_bin.pop();
let privileged_spawn_script = format!(
r#"chmod +x {}
setcap CAP_NET_BIND_SERVICE,CAP_NET_ADMIN+eip {}"#,
BURROW_LOCAL_DAEMON_PATH, BURROW_LOCAL_DAEMON_PATH
)
.replace('\n', "&&");
// Need to be more careful here.
Command::new("pkexec")
.arg("sh")
.arg("-c")
.arg(privileged_spawn_script)
.arg(&burrow_bin)
.output()
.with_context(|| format!("Priviledged call to {}", burrow_bin))?;
Command::new(BURROW_LOCAL_DAEMON_PATH)
.env("RUST_LOG", "debug")
.arg("daemon")
.spawn()?;
Ok(())
}

View file

@ -2,7 +2,7 @@ use super::*;
#[derive(Debug)] #[derive(Debug)]
pub struct DiagGroup { pub struct DiagGroup {
daemon_client: Arc<Mutex<Option<DaemonClient>>>, daemon_client: Arc<Mutex<Option<Channel>>>,
system_setup: SystemSetup, system_setup: SystemSetup,
service_installed: StatusTernary, service_installed: StatusTernary,
@ -12,23 +12,23 @@ pub struct DiagGroup {
} }
pub struct DiagGroupInit { pub struct DiagGroupInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>, pub daemon_client: Arc<Mutex<Option<Channel>>>,
pub system_setup: SystemSetup, 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<Channel>>>) -> Self {
let system_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 { Self {
service_installed: system_setup.is_service_installed()?, service_installed: system_setup.is_service_installed(),
socket_installed: system_setup.is_socket_installed()?, socket_installed: system_setup.is_socket_installed(),
socket_enabled: system_setup.is_socket_enabled()?, socket_enabled: system_setup.is_socket_enabled(),
daemon_running, daemon_running,
system_setup, system_setup,
daemon_client, daemon_client,
}) }
} }
} }
@ -95,7 +95,7 @@ impl AsyncComponent for DiagGroup {
sender: AsyncComponentSender<Self>, sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> { ) -> AsyncComponentParts<Self> {
// Should be impossible to panic here // Should be impossible to panic here
let model = DiagGroup::new(init.daemon_client).await.unwrap(); let model = DiagGroup::new(init.daemon_client).await;
let widgets = view_output!(); let widgets = view_output!();
@ -111,7 +111,7 @@ impl AsyncComponent for DiagGroup {
match msg { match msg {
DiagGroupMsg::Refresh => { DiagGroupMsg::Refresh => {
// Should be impossible to panic here // Should be impossible to panic here
*self = Self::new(Arc::clone(&self.daemon_client)).await.unwrap(); *self = Self::new(Arc::clone(&self.daemon_client)).await;
} }
} }
} }

View file

@ -7,7 +7,7 @@ pub struct SettingsScreen {
} }
pub struct SettingsScreenInit { pub struct SettingsScreenInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>, pub daemon_client: Arc<Mutex<Option<Channel>>>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]

View file

@ -1,158 +0,0 @@
use super::*;
pub struct SwitchScreen {
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
switch: gtk::Switch,
switch_screen: gtk::Box,
disconnected_banner: adw::Banner,
}
pub struct SwitchScreenInit {
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SwitchScreenMsg {
DaemonReconnect,
DaemonDisconnect,
Start,
Stop,
}
#[relm4::component(pub, async)]
impl AsyncComponent for SwitchScreen {
type Init = SwitchScreenInit;
type Input = SwitchScreenMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: Align::Fill,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
set_valign: Align::Start,
#[name(setup_banner)]
adw::Banner {
set_title: "Burrow is not running!",
},
},
#[name(switch_screen)]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_margin_all: 5,
set_valign: Align::Center,
set_vexpand: true,
gtk::Label {
set_label: "Burrow Switch",
},
#[name(switch)]
gtk::Switch {
set_halign: Align::Center,
set_hexpand: false,
set_vexpand: false,
connect_active_notify => move |switch|
sender.input(if switch.is_active() { SwitchScreenMsg::Start } else { SwitchScreenMsg::Stop })
},
}
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let mut initial_switch_status = false;
let mut initial_daemon_server_down = false;
if let Some(daemon_client) = init.daemon_client.lock().await.as_mut() {
if let Ok(res) = daemon_client
.send_command(DaemonCommand::ServerInfo)
.await
.as_ref()
{
initial_switch_status = match res.result.as_ref() {
Ok(DaemonResponseData::None) => false,
Ok(DaemonResponseData::ServerInfo(_)) => true,
_ => false,
};
} else {
initial_daemon_server_down = true;
}
} else {
initial_daemon_server_down = true;
}
let widgets = view_output!();
widgets.switch.set_active(initial_switch_status);
if initial_daemon_server_down {
*init.daemon_client.lock().await = None;
widgets.switch.set_active(false);
widgets.switch_screen.set_sensitive(false);
widgets.setup_banner.set_revealed(true);
}
let model = SwitchScreen {
daemon_client: init.daemon_client,
switch: widgets.switch.clone(),
switch_screen: widgets.switch_screen.clone(),
disconnected_banner: widgets.setup_banner.clone(),
};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
let mut disconnected_daemon_client = false;
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
match msg {
Self::Input::Start => {
if let Err(_e) = daemon_client
.send_command(DaemonCommand::Start(Default::default()))
.await
{
disconnected_daemon_client = true;
}
}
Self::Input::Stop => {
if let Err(_e) = daemon_client.send_command(DaemonCommand::Stop).await {
disconnected_daemon_client = true;
}
}
_ => {}
}
} else {
disconnected_daemon_client = true;
}
if msg == Self::Input::DaemonReconnect {
self.disconnected_banner.set_revealed(false);
self.switch_screen.set_sensitive(true);
}
if disconnected_daemon_client || msg == Self::Input::DaemonDisconnect {
*self.daemon_client.lock().await = None;
self.switch.set_active(false);
self.switch_screen.set_sensitive(false);
self.disconnected_banner.set_revealed(true);
}
}
}

View file

@ -0,0 +1,39 @@
pub struct Template {}
pub struct TemplateInit {}
#[derive(Debug)]
pub enum TemplateMsg {}
#[relm4::component(pub, async)]
impl AsyncComponent for Template {
type Init = TemplateInit;
type Input = TemplateMsg;
type Output = ();
type CommandOutput = ();
view! {
gtk::Box {
}
}
async fn init(
init: Self::Init,
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let widgets = view_output!();
let model = Template {};
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
msg: Self::Input,
_: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
}
}

17
burrow-gtk/src/daemon.rs Normal file
View file

@ -0,0 +1,17 @@
use anyhow::Result;
use hyper_util::rt::TokioIo;
use tokio::net::UnixStream;
use tonic::transport::{Channel, Endpoint, Uri};
use tower::service_fn;
const BURROW_RPC_SOCKET_PATH: &str = "/run/burrow.sock";
pub async fn daemon_connect() -> Result<Channel> {
Ok(Endpoint::try_from("http://[::]:50051")?
.connect_with_connector(service_fn(|_: Uri| async {
Ok::<_, std::io::Error>(TokioIo::new(
UnixStream::connect(BURROW_RPC_SOCKET_PATH).await?,
))
}))
.await?)
}

View file

@ -4,7 +4,6 @@ use std::{fmt::Display, fs, process::Command};
const SYSTEMD_SOCKET_LOC: &str = "/etc/systemd/system/burrow.socket"; const SYSTEMD_SOCKET_LOC: &str = "/etc/systemd/system/burrow.socket";
const SYSTEMD_SERVICE_LOC: &str = "/etc/systemd/system/burrow.service"; const SYSTEMD_SERVICE_LOC: &str = "/etc/systemd/system/burrow.service";
// I don't like this type very much.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum StatusTernary { pub enum StatusTernary {
True, True,
@ -33,35 +32,41 @@ impl SystemSetup {
} }
} }
pub fn is_service_installed(&self) -> Result<StatusTernary> { pub fn is_service_installed(&self) -> StatusTernary {
match self { match self {
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()), SystemSetup::Systemd => fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into(),
SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::AppImage => StatusTernary::NA,
SystemSetup::Other => Ok(StatusTernary::NA), SystemSetup::Other => StatusTernary::NA,
} }
} }
pub fn is_socket_installed(&self) -> Result<StatusTernary> { pub fn is_socket_installed(&self) -> StatusTernary {
match self { match self {
SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()), SystemSetup::Systemd => fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into(),
SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::AppImage => StatusTernary::NA,
SystemSetup::Other => Ok(StatusTernary::NA), SystemSetup::Other => StatusTernary::NA,
} }
} }
pub fn is_socket_enabled(&self) -> Result<StatusTernary> { pub fn is_socket_enabled(&self) -> StatusTernary {
match self { match self {
SystemSetup::Systemd => { SystemSetup::Systemd => {
let output = Command::new("systemctl") let Ok(output) = Command::new("systemctl")
.arg("is-enabled") .arg("is-enabled")
.arg("burrow.socket") .arg("burrow.socket")
.output()? .output()
.stdout; .map(|o| o.stdout)
let output = String::from_utf8(output)?; .inspect_err(|e| {
Ok((output == "enabled\n").into()) error!("Failed to run `systemctl is-enabled burrow.socket` {}", e)
})
else {
return StatusTernary::NA;
};
let output = String::from_utf8(output).unwrap();
(output == "enabled\n").into()
} }
SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::AppImage => StatusTernary::NA,
SystemSetup::Other => Ok(StatusTernary::NA), SystemSetup::Other => StatusTernary::NA,
} }
} }
} }

View file

@ -1,11 +1,16 @@
use anyhow::Result; use anyhow::{Context, Result};
use log::error;
pub mod components; pub mod components;
mod daemon;
mod diag; mod diag;
// Generated using meson // Generated using meson
mod config; mod config;
fn main() { fn main() {
colog::default_builder()
.filter(None, log::LevelFilter::Error)
.init();
components::App::run(); components::App::run();
} }

View file

@ -15,7 +15,7 @@ Note that the flatpak version can compile but will not run properly!
1. Install build dependencies 1. Install build dependencies
``` ```
sudo apt install -y clang meson cmake pkg-config libgtk-4-dev libadwaita-1-dev gettext desktop-file-utils sudo apt install -y clang meson cmake pkg-config libgtk-4-dev libadwaita-1-dev gettext desktop-file-utils libsqlite3-dev protobuf-compiler libprotobuf-dev
``` ```
2. Install flatpak builder (Optional) 2. Install flatpak builder (Optional)
@ -38,7 +38,7 @@ Note that the flatpak version can compile but will not run properly!
1. Install build dependencies 1. Install build dependencies
``` ```
sudo dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib sudo dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib sqlite-devel protobuf-compiler protobuf-devel
``` ```
2. Install flatpak builder (Optional) 2. Install flatpak builder (Optional)
@ -61,7 +61,7 @@ Note that the flatpak version can compile but will not run properly!
1. Install build dependencies 1. Install build dependencies
``` ```
sudo xbps-install -Sy gcc clang meson cmake pkg-config gtk4-devel gettext desktop-file-utils gtk4-update-icon-cache appstream-glib sudo xbps-install -Sy gcc clang meson cmake pkg-config gtk4-devel gettext desktop-file-utils gtk4-update-icon-cache appstream-glib sqlite-devel protobuf protobuf-devel
``` ```
2. Install flatpak builder (Optional) 2. Install flatpak builder (Optional)