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:
1 parent
a6d9edabee
commit
c62d29fa14
8 files changed
+104
-36
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+9
-4
@@ -37,10 +37,15 @@ creates and owns its directories:
|
||||
| cache | `/var/cache/tsunagi` |
|
||||
| control socket | `/run/tsunagi/agent.sock` |
|
||||
|
||||
The polkit rule lets anyone in the `tsunagi` group configure systemd-resolved
|
||||
for the overlay zones (the service user is in that group). To run the agent as
|
||||
your own user as well, add yourself to the group: `sudo usermod -aG tsunagi
|
||||
"$USER"`.
|
||||
Installing the package enables and starts the service, and adds the user who ran
|
||||
pacman to the `tsunagi` group (log out and in again for it to apply). The
|
||||
control socket is open to that group, so members can run `tsunagi status`, the
|
||||
CLI and the tray GUI without sudo; anyone else adds themselves with `sudo
|
||||
usermod -aG tsunagi "$USER"`. The group also lets its members configure
|
||||
systemd-resolved for the overlay zones through the polkit rule.
|
||||
|
||||
The GUI package depends on `gtk3`, `libayatana-appindicator` (the tray icon),
|
||||
`xdotool` (its `libxdo`), and the usual windowing libraries.
|
||||
|
||||
## Removal
|
||||
|
||||
|
||||
Vendored
+39
-5
@@ -50,6 +50,11 @@ if [ -n "$tray" ]; then
|
||||
pkgname=tsunagi-gui
|
||||
pkgdesc="Serverless private mesh networking agent, with the tray GUI"
|
||||
relations=$'conflict = tsunagi\nprovides = tsunagi\nreplaces = tsunagi\n'
|
||||
# What the tray and its window load at run time; none of it is linked in
|
||||
# statically, so pacman has to be told.
|
||||
for dep in gtk3 libayatana-appindicator xdotool libxkbcommon wayland libx11 libglvnd; do
|
||||
relations+="depend = ${dep}"$'\n'
|
||||
done
|
||||
install -D -m 0755 "$tray" "$root/usr/bin/tsunagi-tray"
|
||||
install -D -m 0644 "$here/../linux/tsunagi-tray.desktop" \
|
||||
"$root/usr/share/applications/tsunagi-tray.desktop"
|
||||
@@ -64,7 +69,7 @@ cat > "$root/.PKGINFO" <<PKGINFO
|
||||
pkgname = ${pkgname}
|
||||
pkgver = ${pkgver}-${pkgrel}
|
||||
pkgdesc = ${pkgdesc}
|
||||
url = https://github.com/Ultradesu/tsunagi
|
||||
url = https://github.com/house-of-vanity/tsunagi
|
||||
builddate = $(date +%s)
|
||||
packager = AB <ab@hexor.cy>
|
||||
size = ${size}
|
||||
@@ -72,21 +77,50 @@ arch = ${arch}
|
||||
${relations}license = WTFPL
|
||||
PKGINFO
|
||||
|
||||
# Scriptlet: create the dedicated system user the service runs as, keep systemd
|
||||
# in step, and refresh the desktop/icon caches (a no-op without a GUI payload).
|
||||
# Scriptlet: create the dedicated system user the service runs as, start the
|
||||
# service, and let whoever ran pacman use it. The control socket belongs to the
|
||||
# `tsunagi` group, so the tray and the CLI work for any member of it; the
|
||||
# person installing is added, since the package cannot know anyone else.
|
||||
cat > "$root/.INSTALL" <<'INSTALL'
|
||||
post_install() {
|
||||
getent group tsunagi >/dev/null 2>&1 || groupadd -r tsunagi
|
||||
getent passwd tsunagi >/dev/null 2>&1 || \
|
||||
useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/bin/nologin -c "tsunagi agent" tsunagi
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
command -v update-desktop-database >/dev/null 2>&1 && \
|
||||
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
|
||||
command -v gtk-update-icon-cache >/dev/null 2>&1 && \
|
||||
gtk-update-icon-cache -q -t /usr/share/icons/hicolor >/dev/null 2>&1 || true
|
||||
|
||||
# Whoever ran pacman through sudo or pkexec (an AUR helper does too).
|
||||
installer="${SUDO_USER:-}"
|
||||
if [ -z "$installer" ] && [ -n "${PKEXEC_UID:-}" ]; then
|
||||
installer="$(getent passwd "$PKEXEC_UID" | cut -d: -f1)"
|
||||
fi
|
||||
if [ -n "$installer" ] && [ "$installer" != root ] && id "$installer" >/dev/null 2>&1; then
|
||||
usermod -aG tsunagi "$installer" >/dev/null 2>&1 || true
|
||||
echo ":: $installer was added to the tsunagi group; log out and in again so the"
|
||||
echo " tray and the CLI can reach the agent without sudo."
|
||||
else
|
||||
echo ":: add yourself to the tsunagi group to use the tray and the CLI without"
|
||||
echo " sudo: sudo usermod -aG tsunagi \$USER, then log out and in again."
|
||||
fi
|
||||
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl enable --now tsunagi.service >/dev/null 2>&1 || \
|
||||
echo ":: could not start tsunagi.service; see: systemctl status tsunagi"
|
||||
fi
|
||||
}
|
||||
post_upgrade() {
|
||||
post_install
|
||||
# Not enabled again: it may have been turned off on purpose. A running
|
||||
# agent is restarted, because the control protocol changes between builds
|
||||
# and a new client cannot talk to an old agent.
|
||||
command -v update-desktop-database >/dev/null 2>&1 && \
|
||||
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl try-restart tsunagi.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
pre_remove() {
|
||||
systemctl disable --now tsunagi.service >/dev/null 2>&1 || true
|
||||
|
||||
Vendored
+3
-1
@@ -45,6 +45,8 @@ if [ -n "$tray" ]; then
|
||||
# The GUI package is a superset of the headless one and must not coexist
|
||||
# with it (both ship /usr/bin/tsunagi).
|
||||
extra_control=$'Conflicts: tsunagi\nReplaces: tsunagi\nProvides: tsunagi\n'
|
||||
# What the tray and its window load at run time.
|
||||
extra_control+=$'Depends: libgtk-3-0 | libgtk-3-0t64, libayatana-appindicator3-1 | libappindicator3-1, libxdo3, libxkbcommon0, libwayland-client0, libx11-6, libgl1\n'
|
||||
desc_gui=" It also installs the tray GUI (tsunagi-tray) and a desktop launcher so it can be started from the applications menu."
|
||||
install -D -m 0755 "$tray" "$root/usr/bin/tsunagi-tray"
|
||||
install -D -m 0644 "$here/../linux/tsunagi-tray.desktop" \
|
||||
@@ -62,7 +64,7 @@ Architecture: ${arch}
|
||||
Maintainer: AB <ab@hexor.cy>
|
||||
Section: net
|
||||
Priority: optional
|
||||
Homepage: https://github.com/Ultradesu/tsunagi
|
||||
Homepage: https://github.com/house-of-vanity/tsunagi
|
||||
${extra_control}Description: Serverless private mesh networking agent
|
||||
tsunagi forms small private mesh IP networks between peers, with NAT
|
||||
traversal and no central server. This package installs a static,
|
||||
|
||||
Vendored
+3
-1
@@ -22,7 +22,9 @@ and `CAP_NET_BIND_SERVICE` (granted by systemd), not root. systemd creates its
|
||||
state (`/var/lib/tsunagi`), cache (`/var/cache/tsunagi`) and runtime
|
||||
(`/run/tsunagi`) directories, and the polkit rule lets it configure the
|
||||
resolver. Control it with `sudo` — the CLI finds the running service's socket at
|
||||
`/run/tsunagi/agent.sock` with no flags:
|
||||
`/run/tsunagi/agent.sock` with no flags. The socket is open to the `tsunagi`
|
||||
group, so after `sudo usermod -aG tsunagi "$USER"` (and logging in again) no
|
||||
`sudo` is needed:
|
||||
|
||||
```sh
|
||||
sudo tsunagi status
|
||||
|
||||
Vendored
+4
-2
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=tsunagi mesh agent
|
||||
Documentation=https://github.com/Ultradesu/tsunagi
|
||||
Documentation=https://github.com/house-of-vanity/tsunagi
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -16,7 +16,8 @@ CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
||||
|
||||
# systemd creates and owns these for the service user, and the agent is pointed
|
||||
# at them: state in /var/lib/tsunagi, cache in /var/cache/tsunagi, and the
|
||||
# control socket in /run/tsunagi, which stays private to the service user.
|
||||
# control socket in /run/tsunagi. The socket is opened to the `tsunagi` group,
|
||||
# so a user who joins it can run the CLI and the tray without sudo.
|
||||
StateDirectory=tsunagi
|
||||
StateDirectoryMode=0700
|
||||
CacheDirectory=tsunagi
|
||||
@@ -25,6 +26,7 @@ RuntimeDirectoryMode=0755
|
||||
Environment=TSUNAGI_STATE_DIR=%S/tsunagi
|
||||
Environment=TSUNAGI_CACHE_DIR=%C/tsunagi
|
||||
Environment=TSUNAGI_CONTROL_SOCKET=%t/tsunagi/agent.sock
|
||||
Environment=TSUNAGI_CONTROL_GROUP=tsunagi
|
||||
|
||||
# The tarball installer puts the binary here; the deb and the Arch package
|
||||
# rewrite this to /usr/bin where they install it.
|
||||
|
||||
Reference in new issue
Block a user