Arch package: start the service and let the GUI reach the agent

The control socket is now opened to the tsunagi group (unit), the Arch
package enables and starts the service on install, adds the installing user
to the group, restarts a running agent on upgrade, and depends on the
libraries the tray loads at run time. The tray looks for the agent's socket
again on every poll instead of once at start, and a missing indicator
library is reported instead of panicking. Repository URLs unified.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-02 23:29:51 +01:00
1 parent a6d9edabee
commit c62d29fa14
8 files changed
+104 -36

No files matched your search

+22 -19
View File
@@ -50,11 +50,12 @@ pub(crate) struct Snapshot {
pub generation: u64,
/// When this status was read, for computing rates between refreshes.
pub at: Option<Instant>,
/// The socket the last request went to.
pub socket: PathBuf,
}
/// Handle the UI holds: send commands, read the latest snapshot.
pub(crate) struct AgentClient {
socket: PathBuf,
tx: mpsc::UnboundedSender<Command>,
shared: Arc<Mutex<Snapshot>>,
/// A secret fetched for the clipboard, taken by the UI thread which owns
@@ -65,23 +66,20 @@ pub(crate) struct AgentClient {
impl AgentClient {
/// Starts the worker on `runtime`, reporting to `ctx` (for repaints).
pub(crate) fn spawn(
runtime: &tokio::runtime::Handle,
ctx: egui::Context,
socket: PathBuf,
) -> Self {
pub(crate) fn spawn(runtime: &tokio::runtime::Handle, ctx: egui::Context) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
let shared = Arc::new(Mutex::new(Snapshot::default()));
let shared = Arc::new(Mutex::new(Snapshot {
socket: resolve_socket(),
..Snapshot::default()
}));
let pending_copy = Arc::new(Mutex::new(None));
runtime.spawn(worker(
socket.clone(),
rx,
Arc::clone(&shared),
Arc::clone(&pending_copy),
ctx,
));
Self {
socket,
tx,
shared,
pending_copy,
@@ -93,9 +91,9 @@ impl AgentClient {
lock(&self.pending_copy).take()
}
/// The control socket this client talks to.
pub(crate) fn socket(&self) -> &std::path::Path {
&self.socket
/// The control socket this client talks to, as last resolved.
pub(crate) fn socket(&self) -> PathBuf {
lock(&self.shared).socket.clone()
}
/// Sends a command; a dropped worker simply means nothing happens.
@@ -118,33 +116,37 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
/// The worker loop: refresh on an interval and whenever a command runs.
async fn worker(
socket: PathBuf,
mut rx: mpsc::UnboundedReceiver<Command>,
shared: Arc<Mutex<Snapshot>>,
pending_copy: Arc<Mutex<Option<String>>>,
ctx: egui::Context,
) {
refresh(&socket, &shared, &ctx).await;
refresh(&shared, &ctx).await;
let mut tick = tokio::time::interval(POLL_INTERVAL);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
_ = tick.tick() => {
refresh(&socket, &shared, &ctx).await;
refresh(&shared, &ctx).await;
}
command = rx.recv() => {
let Some(command) = command else { return };
run(&socket, command, &shared, &pending_copy, &ctx).await;
refresh(&socket, &shared, &ctx).await;
run(&resolve_socket(), command, &shared, &pending_copy, &ctx).await;
refresh(&shared, &ctx).await;
}
}
}
}
/// Reads status and stores it, mapping any failure to a human string.
async fn refresh(socket: &std::path::Path, shared: &Arc<Mutex<Snapshot>>, ctx: &egui::Context) {
let status = ipc::request_status(socket)
///
/// The socket is looked for again each time: a window opened before the agent
/// was started would otherwise keep asking the per-user path for ever, while
/// the service came up at the system one.
async fn refresh(shared: &Arc<Mutex<Snapshot>>, ctx: &egui::Context) {
let socket = resolve_socket();
let status = ipc::request_status(&socket)
.await
.map_err(|err| err.to_string());
{
@@ -152,6 +154,7 @@ async fn refresh(socket: &std::path::Path, shared: &Arc<Mutex<Snapshot>>, ctx: &
guard.status = Some(status);
guard.generation = guard.generation.wrapping_add(1);
guard.at = Some(Instant::now());
guard.socket = socket;
}
ctx.request_repaint();
}
+3 -3
View File
@@ -10,7 +10,7 @@ use std::time::Duration;
use eframe::egui;
use crate::about;
use crate::agent::{AgentClient, resolve_socket};
use crate::agent::AgentClient;
use crate::devices;
use crate::stats::Traffic;
use crate::ui::{self, UiState};
@@ -38,7 +38,7 @@ impl App {
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
ctx.set_fonts(fonts);
let agent = AgentClient::spawn(&runtime, ctx, resolve_socket());
let agent = AgentClient::spawn(&runtime, ctx);
Ok(Self {
agent,
state: UiState::default(),
@@ -88,7 +88,7 @@ impl App {
egui::ViewportId::from_hash_of("about"),
builder,
|vctx, _class| {
about::show(vctx, socket, snapshot);
about::show(vctx, &socket, snapshot);
!vctx.input(|i| i.viewport().close_requested())
},
);
+21 -1
View File
@@ -81,6 +81,26 @@ fn build() -> Result<Built, String> {
})
}
/// [`build`], with a missing indicator library reported instead of panicking.
///
/// The library is loaded on first use, and its binding panics when it is not
/// installed, which reads as a bug in this program rather than a package that
/// is not there.
#[cfg(target_os = "linux")]
fn build_checked() -> Result<Built, String> {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build));
std::panic::set_hook(hook);
built.unwrap_or_else(|_| {
Err(
"the tray needs libayatana-appindicator (Arch: libayatana-appindicator, \
Debian/Ubuntu: libayatana-appindicator3-1), which is not installed"
.to_string(),
)
})
}
/// Acts on menu choices as the platform's loop delivers them.
fn handle_menu(built: &Built, window: Arc<Window>) {
let (open, quit) = (built.open.clone(), built.quit.clone());
@@ -101,7 +121,7 @@ fn handle_menu(built: &Built, window: Arc<Window>) {
#[cfg(target_os = "linux")]
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
gtk::init().map_err(|err| format!("cannot initialise GTK: {err}"))?;
let built = build()?;
let built = build_checked()?;
handle_menu(
&built,
Arc::new(Window {