Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e178a6f9d |
No files matched your search
+1
-1
@@ -14,7 +14,7 @@ mainline = { path = "vendor/mainline" }
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
license = "MIT OR Apache-2.0"
|
||||
license = "WTFPL"
|
||||
repository = "https://github.com/tsunagi-net/tsunagi"
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
@@ -763,4 +763,4 @@ any of this.
|
||||
|
||||
## Licence
|
||||
|
||||
MIT OR Apache-2.0.
|
||||
[WTFPL](LICENSE): do what the fuck you want to.
|
||||
@@ -4342,6 +4342,11 @@ async fn build_report(
|
||||
state_dir: paths.state_dir.display().to_string(),
|
||||
cache_dir: paths.cache_dir.display().to_string(),
|
||||
program: program_path(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
protocols: PROTOCOLS
|
||||
.iter()
|
||||
.map(|spec| format!("{} v{}", spec.name, spec.version))
|
||||
.collect(),
|
||||
privilege: match tsunagi::overlay::probe_net_admin() {
|
||||
tsunagi::overlay::Privilege::Available => tsunagi::ipc::PrivilegeReport::Available,
|
||||
tsunagi::overlay::Privilege::Missing(reason) => {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
//! The About window: what this software is, and what it is talking to.
|
||||
//!
|
||||
//! Compact on purpose — one table for the client, one for the agent. The agent
|
||||
//! half is whatever the agent says about itself, so a client and an agent that
|
||||
//! are not the same build show up as such.
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
use tsunagi::ipc::{PrivilegeReport, StatusReport};
|
||||
|
||||
use crate::agent::Snapshot;
|
||||
use crate::format;
|
||||
|
||||
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
const AUTHOR: &str = "AB <tsunagi@hexor.cy>";
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Draws the window body into its viewport.
|
||||
pub(crate) fn show(ctx: &egui::Context, socket: &std::path::Path, snapshot: &Snapshot) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
// Long paths and a taller agent table must scroll rather than be cut.
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| body(ui, socket, snapshot));
|
||||
});
|
||||
}
|
||||
|
||||
fn body(ui: &mut egui::Ui, socket: &std::path::Path, snapshot: &Snapshot) {
|
||||
{
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("tsunagi");
|
||||
ui.label(egui::RichText::new(VERSION).small().weak());
|
||||
});
|
||||
ui.label("Peer-to-peer mesh network with no server.");
|
||||
ui.add_space(6.0);
|
||||
|
||||
section(ui, "client");
|
||||
grid(ui, "about client", |ui| {
|
||||
row(ui, "repository", |ui| {
|
||||
ui.hyperlink(REPOSITORY);
|
||||
});
|
||||
row(ui, "author", |ui| {
|
||||
format::copy_field(ui, AUTHOR, AUTHOR);
|
||||
});
|
||||
row(ui, "license", |ui| {
|
||||
ui.label(env!("CARGO_PKG_LICENSE"));
|
||||
});
|
||||
row(ui, "build", |ui| {
|
||||
ui.label(format!(
|
||||
"{} {} · {}",
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH,
|
||||
if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"release"
|
||||
}
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
section(ui, "agent");
|
||||
let socket = socket.display().to_string();
|
||||
match &snapshot.status {
|
||||
None => {
|
||||
grid(ui, "about agent", |ui| {
|
||||
row(ui, "socket", |ui| format::copy_field(ui, &socket, &socket));
|
||||
row(ui, "state", |ui| {
|
||||
ui.label("connecting…");
|
||||
});
|
||||
});
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
grid(ui, "about agent", |ui| {
|
||||
row(ui, "socket", |ui| format::copy_field(ui, &socket, &socket));
|
||||
row(ui, "state", |ui| {
|
||||
ui.colored_label(egui::Color32::from_rgb(0xd8, 0x4a, 0x3a), "unreachable");
|
||||
});
|
||||
row(ui, "reason", |ui| {
|
||||
ui.add(egui::Label::new(error).wrap());
|
||||
});
|
||||
});
|
||||
}
|
||||
Some(Ok(report)) => agent_table(ui, &socket, report),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_table(ui: &mut egui::Ui, socket: &str, report: &StatusReport) {
|
||||
grid(ui, "about agent", |ui| {
|
||||
row(ui, "socket", |ui| format::copy_field(ui, socket, socket));
|
||||
row(ui, "state", |ui| {
|
||||
ui.colored_label(egui::Color32::from_rgb(0x3c, 0xb0, 0x4a), "connected");
|
||||
});
|
||||
row(ui, "version", |ui| {
|
||||
if report.version.is_empty() {
|
||||
ui.weak("unknown");
|
||||
} else if report.version == VERSION {
|
||||
ui.label(&report.version);
|
||||
} else {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(0xd8, 0x9a, 0x2a),
|
||||
format!("{} (client is {VERSION})", report.version),
|
||||
);
|
||||
}
|
||||
});
|
||||
if !report.protocols.is_empty() {
|
||||
row(ui, "protocols", |ui| {
|
||||
ui.label(report.protocols.join(" · "));
|
||||
});
|
||||
}
|
||||
row(ui, "endpoint id", |ui| {
|
||||
format::copy_field(ui, &format::short(&report.endpoint_id), &report.endpoint_id);
|
||||
});
|
||||
row(ui, "hostname", |ui| {
|
||||
ui.label(&report.hostname);
|
||||
});
|
||||
if !report.program.is_empty() {
|
||||
row(ui, "binary", |ui| {
|
||||
format::copy_field(ui, &report.program, &report.program);
|
||||
});
|
||||
}
|
||||
if !report.state_dir.is_empty() {
|
||||
row(ui, "state dir", |ui| {
|
||||
format::copy_field(ui, &report.state_dir, &report.state_dir);
|
||||
});
|
||||
row(ui, "cache dir", |ui| {
|
||||
format::copy_field(ui, &report.cache_dir, &report.cache_dir);
|
||||
});
|
||||
}
|
||||
row(ui, "interface", |ui| match &report.privilege {
|
||||
PrivilegeReport::Unknown => {
|
||||
ui.weak("unknown");
|
||||
}
|
||||
PrivilegeReport::Available => {
|
||||
ui.label("can be managed (CAP_NET_ADMIN)");
|
||||
}
|
||||
PrivilegeReport::Missing(_) => {
|
||||
ui.colored_label(egui::Color32::from_rgb(0xd8, 0x9a, 0x2a), "no privileges");
|
||||
}
|
||||
PrivilegeReport::Unsupported => {
|
||||
ui.label("not supported on this platform");
|
||||
}
|
||||
});
|
||||
row(ui, "cache", |ui| {
|
||||
ui.label(if report.cache_healthy {
|
||||
"usable"
|
||||
} else {
|
||||
"unavailable"
|
||||
});
|
||||
});
|
||||
row(ui, "listening", |ui| {
|
||||
ui.label(report.bound_sockets.join(", "));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn section(ui: &mut egui::Ui, title: &str) {
|
||||
ui.label(egui::RichText::new(title).strong());
|
||||
}
|
||||
|
||||
fn grid(ui: &mut egui::Ui, id: &str, add: impl FnOnce(&mut egui::Ui)) {
|
||||
egui::Grid::new(id)
|
||||
.num_columns(2)
|
||||
.spacing([12.0, 3.0])
|
||||
.show(ui, add);
|
||||
}
|
||||
|
||||
/// One label/value line of a table.
|
||||
fn row(ui: &mut egui::Ui, label: &str, value: impl FnOnce(&mut egui::Ui)) {
|
||||
ui.weak(label);
|
||||
value(ui);
|
||||
ui.end_row();
|
||||
}
|
||||
@@ -21,8 +21,6 @@ const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
/// A request from the UI to the agent.
|
||||
pub(crate) enum Command {
|
||||
/// Re-read status now.
|
||||
Refresh,
|
||||
/// Stop serving a network, or start serving it again.
|
||||
SetActive { network_id: String, active: bool },
|
||||
/// Turn local broadcast participation on or off for a network.
|
||||
@@ -170,7 +168,6 @@ async fn run(
|
||||
ctx.request_repaint();
|
||||
|
||||
let outcome: Result<String, String> = match command {
|
||||
Command::Refresh => Ok(String::new()),
|
||||
Command::SetActive { network_id, active } => ipc::set_active(socket, &network_id, active)
|
||||
.await
|
||||
.map(|report| {
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::time::Duration;
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
use crate::about;
|
||||
use crate::agent::{AgentClient, resolve_socket};
|
||||
use crate::devices;
|
||||
use crate::stats::Traffic;
|
||||
@@ -59,6 +60,7 @@ impl eframe::App for App {
|
||||
|
||||
ui::draw(ctx, &self.agent, &mut self.state, &snapshot, &self.traffic);
|
||||
self.show_devices_windows(ctx, &snapshot);
|
||||
self.show_about_window(ctx, &snapshot);
|
||||
|
||||
// A secret the worker fetched for the clipboard: copy it here, on the
|
||||
// UI thread that owns the clipboard, exactly once.
|
||||
@@ -72,6 +74,29 @@ impl eframe::App for App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Renders the About window while it is open.
|
||||
fn show_about_window(&mut self, ctx: &egui::Context, snapshot: &crate::agent::Snapshot) {
|
||||
if !self.state.about_open {
|
||||
return;
|
||||
}
|
||||
let builder = egui::ViewportBuilder::default()
|
||||
.with_title("About tsunagi")
|
||||
.with_inner_size([560.0, 520.0])
|
||||
.with_min_inner_size([380.0, 280.0]);
|
||||
let socket = self.agent.socket();
|
||||
let keep = ctx.show_viewport_immediate(
|
||||
egui::ViewportId::from_hash_of("about"),
|
||||
builder,
|
||||
|vctx, _class| {
|
||||
about::show(vctx, socket, snapshot);
|
||||
!vctx.input(|i| i.viewport().close_requested())
|
||||
},
|
||||
);
|
||||
if !keep {
|
||||
self.state.about_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders one separate window per open network-devices view.
|
||||
fn show_devices_windows(&mut self, ctx: &egui::Context, snapshot: &crate::agent::Snapshot) {
|
||||
let Some(Ok(report)) = &snapshot.status else {
|
||||
|
||||
@@ -233,6 +233,54 @@ fn devices<'a>(network: &'a NetworkReport, own_id: &str) -> Vec<Device<'a>> {
|
||||
devices
|
||||
}
|
||||
|
||||
/// The widest value each column is expected to hold, so the table starts out
|
||||
/// wide enough and does not reflow as numbers grow.
|
||||
const COLUMN_SAMPLES: [&str; 8] = [
|
||||
"",
|
||||
"tcp-tls",
|
||||
"255.255.255.255",
|
||||
"23h 59m",
|
||||
"99999/99999",
|
||||
"1023.9 MB / 1023.9 MB",
|
||||
"1023.9 kB/s",
|
||||
"",
|
||||
];
|
||||
|
||||
/// Column widths that only ever grow, kept across frames.
|
||||
///
|
||||
/// A grid sizes each column to its widest cell *this frame*, so a value that
|
||||
/// gets shorter — `14.4 MB/s` giving way to `382 pkt/s` — would pull the
|
||||
/// column in and push every cell after it. Remembering the widest seen keeps
|
||||
/// the table where it is.
|
||||
fn column_widths(ui: &egui::Ui, id: egui::Id) -> [f32; 8] {
|
||||
let stored: Option<[f32; 8]> = ui.memory(|m| m.data.get_temp(id));
|
||||
stored.unwrap_or_else(|| {
|
||||
let font = egui::TextStyle::Body.resolve(ui.style());
|
||||
let mut widths = [0.0; 8];
|
||||
for (width, sample) in widths.iter_mut().zip(COLUMN_SAMPLES) {
|
||||
if !sample.is_empty() {
|
||||
*width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout_no_wrap(sample.to_owned(), font.clone(), egui::Color32::WHITE)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
}
|
||||
}
|
||||
widths
|
||||
})
|
||||
}
|
||||
|
||||
/// One cell, at least as wide as its column has ever been.
|
||||
fn cell(ui: &mut egui::Ui, column: usize, widths: &mut [f32; 8], add: impl FnOnce(&mut egui::Ui)) {
|
||||
let floor = widths[column];
|
||||
let response = ui.scope(|ui| {
|
||||
ui.set_min_width(floor);
|
||||
add(ui);
|
||||
});
|
||||
widths[column] = widths[column].max(response.response.rect.width());
|
||||
}
|
||||
|
||||
/// One table of all devices: online rows carry live counters and a plot,
|
||||
/// offline rows show what is known (name and address) and dashes.
|
||||
fn device_table(
|
||||
@@ -246,12 +294,14 @@ fn device_table(
|
||||
ui.weak("none");
|
||||
return;
|
||||
}
|
||||
let widths_id = egui::Id::new(("devices column widths", network_id));
|
||||
let mut widths = column_widths(ui, widths_id);
|
||||
egui::Grid::new("devices")
|
||||
.num_columns(8)
|
||||
.striped(true)
|
||||
.spacing([10.0, 4.0])
|
||||
.show(ui, |ui| {
|
||||
for header in [
|
||||
for (column, header) in [
|
||||
"device",
|
||||
"proto",
|
||||
"address",
|
||||
@@ -260,49 +310,112 @@ fn device_table(
|
||||
"bytes",
|
||||
"rate",
|
||||
"plot",
|
||||
] {
|
||||
ui.weak(header);
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
cell(ui, column, &mut widths, |ui| {
|
||||
ui.weak(header);
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
for device in devices {
|
||||
name_cell(ui, device);
|
||||
ui.label(device.proto);
|
||||
match &device.address {
|
||||
cell(ui, 0, &mut widths, |ui| name_cell(ui, device));
|
||||
cell(ui, 1, &mut widths, |ui| {
|
||||
ui.label(device.proto);
|
||||
});
|
||||
cell(ui, 2, &mut widths, |ui| match &device.address {
|
||||
Some(address) => format::copy_field(ui, address, address),
|
||||
None => {
|
||||
ui.weak("—");
|
||||
}
|
||||
}
|
||||
ui.label(
|
||||
device
|
||||
.handshake_secs
|
||||
.map_or_else(|| "offline".to_string(), format::age),
|
||||
);
|
||||
});
|
||||
cell(ui, 3, &mut widths, |ui| {
|
||||
ui.label(
|
||||
device
|
||||
.handshake_secs
|
||||
.map_or_else(|| "offline".to_string(), format::age),
|
||||
);
|
||||
});
|
||||
|
||||
match device.overlay.filter(|_| device.online) {
|
||||
Some(peer) => {
|
||||
ui.label(format!("{}/{}", peer.tx_packets, peer.rx_packets));
|
||||
ui.label(format!(
|
||||
"{} / {}",
|
||||
format::bytes(peer.tx_bytes as f64),
|
||||
format::bytes(peer.rx_bytes as f64)
|
||||
));
|
||||
let series = traffic.peer(network_id, &peer.public_key);
|
||||
ui.add(egui::Label::new(format::series_rate(series, unit)).truncate());
|
||||
cell(ui, 4, &mut widths, |ui| {
|
||||
ui.label(format!("{}/{}", peer.tx_packets, peer.rx_packets));
|
||||
});
|
||||
cell(ui, 5, &mut widths, |ui| {
|
||||
ui.label(format!(
|
||||
"{} / {}",
|
||||
format::bytes(peer.tx_bytes as f64),
|
||||
format::bytes(peer.rx_bytes as f64)
|
||||
));
|
||||
});
|
||||
cell(ui, 6, &mut widths, |ui| {
|
||||
ui.add(
|
||||
egui::Label::new(format::series_rate(series, unit))
|
||||
.wrap_mode(egui::TextWrapMode::Extend),
|
||||
);
|
||||
});
|
||||
ui.allocate_ui(egui::vec2(100.0, 20.0), |ui| {
|
||||
format::sparkline(ui, series, unit, 18.0);
|
||||
});
|
||||
}
|
||||
None => {
|
||||
for _ in 0..4 {
|
||||
ui.weak("—");
|
||||
for column in 4..7 {
|
||||
cell(ui, column, &mut widths, |ui| {
|
||||
ui.weak("—");
|
||||
});
|
||||
}
|
||||
ui.weak("—");
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
// Everything this agent has moved through the overlay, including
|
||||
// devices that have since gone away.
|
||||
let (mut tx_packets, mut rx_packets, mut tx_bytes, mut rx_bytes) = (0u64, 0, 0, 0);
|
||||
for peer in devices.iter().filter_map(|d| d.overlay) {
|
||||
tx_packets += peer.tx_packets;
|
||||
rx_packets += peer.rx_packets;
|
||||
tx_bytes += peer.tx_bytes;
|
||||
rx_bytes += peer.rx_bytes;
|
||||
}
|
||||
cell(ui, 0, &mut widths, |ui| {
|
||||
ui.strong("total");
|
||||
});
|
||||
for column in 1..4 {
|
||||
cell(ui, column, &mut widths, |ui| {
|
||||
ui.label("");
|
||||
});
|
||||
}
|
||||
cell(ui, 4, &mut widths, |ui| {
|
||||
ui.strong(format!("{tx_packets}/{rx_packets}"));
|
||||
});
|
||||
cell(ui, 5, &mut widths, |ui| {
|
||||
ui.strong(format!(
|
||||
"{} / {}",
|
||||
format::bytes(tx_bytes as f64),
|
||||
format::bytes(rx_bytes as f64)
|
||||
));
|
||||
});
|
||||
cell(ui, 6, &mut widths, |ui| {
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
egui::RichText::new(format::series_rate(traffic.network(network_id), unit))
|
||||
.strong(),
|
||||
)
|
||||
.wrap_mode(egui::TextWrapMode::Extend),
|
||||
);
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
ui.memory_mut(|m| {
|
||||
m.data
|
||||
.insert_temp(egui::Id::new("devices column widths"), widths)
|
||||
});
|
||||
}
|
||||
|
||||
fn yes_no(value: bool) -> &'static str {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// Nothing here is a library.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod about;
|
||||
mod agent;
|
||||
mod app;
|
||||
mod devices;
|
||||
|
||||
@@ -40,6 +40,8 @@ pub(crate) struct UiState {
|
||||
pub(crate) host_edit: Option<String>,
|
||||
/// Network ids whose devices window is open.
|
||||
pub(crate) open_devices: BTreeSet<String>,
|
||||
/// Whether the About window is open.
|
||||
pub(crate) about_open: bool,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -49,6 +51,7 @@ impl Default for UiState {
|
||||
unit: Unit::Packets,
|
||||
host_edit: None,
|
||||
open_devices: BTreeSet::new(),
|
||||
about_open: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,13 +68,14 @@ pub(crate) fn draw(
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("tsunagi");
|
||||
ui.label(
|
||||
egui::RichText::new(env!("CARGO_PKG_VERSION"))
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui
|
||||
.button(icon::ARROWS_CLOCKWISE)
|
||||
.on_hover_text("Refresh")
|
||||
.clicked()
|
||||
{
|
||||
agent.send(Command::Refresh);
|
||||
if ui.button(icon::INFO).on_hover_text("About").clicked() {
|
||||
state.about_open = true;
|
||||
}
|
||||
if snapshot.busy {
|
||||
ui.add(egui::Spinner::new());
|
||||
|
||||
@@ -366,6 +366,10 @@ pub struct StatusReport {
|
||||
/// It is the agent that creates the interface, so the client's own
|
||||
/// capabilities say nothing about it.
|
||||
pub privilege: PrivilegeReport,
|
||||
/// The version of the agent's software.
|
||||
pub version: String,
|
||||
/// The data plane protocols it has, each as `name vN` (wire version).
|
||||
pub protocols: Vec<String>,
|
||||
}
|
||||
|
||||
/// What the agent found when it checked whether it may manage an interface.
|
||||
|
||||
Vendored
+1
-2
@@ -69,8 +69,7 @@ builddate = $(date +%s)
|
||||
packager = AB <ab@hexor.cy>
|
||||
size = ${size}
|
||||
arch = ${arch}
|
||||
${relations}license = MIT
|
||||
license = Apache
|
||||
${relations}license = WTFPL
|
||||
PKGINFO
|
||||
|
||||
# Scriptlet: create the dedicated system user the service runs as, keep systemd
|
||||
|
||||
Reference in new issue
Block a user