This commit is contained in:
mjallen18
2026-03-23 17:33:28 -05:00
parent 068d6c8f94
commit bd569962ca
2 changed files with 36 additions and 8 deletions

View File

@@ -15,14 +15,24 @@ const SERVICE_NAME = 'nebula@jallen-nebula.service';
const IFACE_NAME = 'jallen-nebula'; const IFACE_NAME = 'jallen-nebula';
const POLL_INTERVAL_SECS = 5; const POLL_INTERVAL_SECS = 5;
const LOG_PREFIX = '[nebula-vpn]';
// ── helpers ────────────────────────────────────────────────────────────────── // ── helpers ──────────────────────────────────────────────────────────────────
function log(msg) {
console.log(`${LOG_PREFIX} ${msg}`);
}
function logErr(msg, err) {
console.error(`${LOG_PREFIX} ${msg}`, err ?? '');
}
/** /**
* Run a command and return { success, stdout, stderr }. * Run a command and return { success, stdout, stderr }.
* The command is passed as an argv array (no shell expansion). * The command is passed as an argv array (no shell expansion).
*/ */
function runCommand(argv) { function runCommand(argv) {
log(`runCommand: ${argv.join(' ')}`);
try { try {
const [ok, stdout, stderr, exitCode] = GLib.spawn_sync( const [ok, stdout, stderr, exitCode] = GLib.spawn_sync(
null, // working dir null, // working dir
@@ -31,21 +41,23 @@ function runCommand(argv) {
GLib.SpawnFlags.SEARCH_PATH, GLib.SpawnFlags.SEARCH_PATH,
null, null,
); );
return { const result = {
success: ok && exitCode === 0, success: ok && exitCode === 0,
stdout: ok ? new TextDecoder().decode(stdout).trim() : '', stdout: ok ? new TextDecoder().decode(stdout).trim() : '',
stderr: ok ? new TextDecoder().decode(stderr).trim() : '', stderr: ok ? new TextDecoder().decode(stderr).trim() : '',
}; };
} catch (_e) { log(`runCommand result: success=${result.success} exitCode=${exitCode} stdout="${result.stdout}" stderr="${result.stderr}"`);
return result;
} catch (e) {
logErr(`runCommand exception for [${argv.join(' ')}]:`, e);
return {success: false, stdout: '', stderr: ''}; return {success: false, stdout: '', stderr: ''};
} }
} }
/** Return true if the systemd service is currently active. */ /** Return true if the systemd service is currently active. */
function isServiceActive() { function isServiceActive() {
const r = runCommand([ const r = runCommand(['systemctl', 'is-active', '--quiet', SERVICE_NAME]);
'systemctl', 'is-active', '--quiet', SERVICE_NAME, log(`isServiceActive: ${r.success}`);
]);
return r.success; return r.success;
} }
@@ -58,6 +70,7 @@ function getIfaceInfo() {
// jallen-nebula UP 10.1.1.3/24 fe80::…/64 // jallen-nebula UP 10.1.1.3/24 fe80::…/64
const r = runCommand(['ip', '-brief', 'addr', 'show', IFACE_NAME]); const r = runCommand(['ip', '-brief', 'addr', 'show', IFACE_NAME]);
if (!r.success || r.stdout === '') { if (!r.success || r.stdout === '') {
log(`getIfaceInfo: interface not found or no output`);
return {found: false, ipv4: null, ipv6: null}; return {found: false, ipv4: null, ipv6: null};
} }
@@ -66,12 +79,14 @@ function getIfaceInfo() {
const addrs = parts.slice(2); const addrs = parts.slice(2);
const ipv4 = addrs.find(a => /^\d+\.\d+\.\d+\.\d+\//.test(a)) ?? null; const ipv4 = addrs.find(a => /^\d+\.\d+\.\d+\.\d+\//.test(a)) ?? null;
const ipv6 = addrs.find(a => a.includes(':') && !a.startsWith('fe80')) ?? null; const ipv6 = addrs.find(a => a.includes(':') && !a.startsWith('fe80')) ?? null;
log(`getIfaceInfo: found=true state=${parts[1]} ipv4=${ipv4} ipv6=${ipv6}`);
return {found: true, ipv4, ipv6}; return {found: true, ipv4, ipv6};
} }
/** Start or stop the systemd service (requires polkit / passwordless sudo). */ /** Start or stop the systemd service (requires polkit / passwordless sudo). */
function setServiceActive(enable) { function setServiceActive(enable) {
const action = enable ? 'start' : 'stop'; const action = enable ? 'start' : 'stop';
log(`setServiceActive: ${action} ${SERVICE_NAME}`);
runCommand(['systemctl', action, SERVICE_NAME]); runCommand(['systemctl', action, SERVICE_NAME]);
} }
@@ -80,6 +95,7 @@ function setServiceActive(enable) {
const NebulaIndicator = GObject.registerClass( const NebulaIndicator = GObject.registerClass(
class NebulaIndicator extends PanelMenu.Button { class NebulaIndicator extends PanelMenu.Button {
_init(extensionPath) { _init(extensionPath) {
log('NebulaIndicator._init: start');
super._init(0.0, 'Nebula VPN Status'); super._init(0.0, 'Nebula VPN Status');
this._extensionPath = extensionPath; this._extensionPath = extensionPath;
@@ -127,23 +143,28 @@ class NebulaIndicator extends PanelMenu.Button {
// ── initial state + polling ── // ── initial state + polling ──
this._active = false; this._active = false;
log('NebulaIndicator._init: running initial refresh');
this._refresh(); this._refresh();
this._timerId = GLib.timeout_add_seconds( this._timerId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT, GLib.PRIORITY_DEFAULT,
POLL_INTERVAL_SECS, POLL_INTERVAL_SECS,
() => { this._refresh(); return GLib.SOURCE_CONTINUE; }, () => { this._refresh(); return GLib.SOURCE_CONTINUE; },
); );
log(`NebulaIndicator._init: poll timer registered (id=${this._timerId})`);
} }
// ── private ────────────────────────────────────────────────────────────── // ── private ──────────────────────────────────────────────────────────────
_refresh() { _refresh() {
log('_refresh: checking service and interface');
this._active = isServiceActive(); this._active = isServiceActive();
const iface = getIfaceInfo(); const iface = getIfaceInfo();
log(`_refresh: active=${this._active} iface=${JSON.stringify(iface)}`);
this._updateUI(iface); this._updateUI(iface);
} }
_updateUI(iface) { _updateUI(iface) {
log(`_updateUI: active=${this._active}`);
if (this._active) { if (this._active) {
this._icon.icon_name = 'network-vpn-symbolic'; this._icon.icon_name = 'network-vpn-symbolic';
this._label.text = 'VPN'; this._label.text = 'VPN';
@@ -166,12 +187,15 @@ class NebulaIndicator extends PanelMenu.Button {
} }
_onToggle() { _onToggle() {
setServiceActive(!this._active); const target = !this._active;
log(`_onToggle: toggling to ${target ? 'active' : 'inactive'}`);
setServiceActive(target);
// Optimistic update while systemd races // Optimistic update while systemd races
this._active = !this._active; this._active = target;
this._updateUI(getIfaceInfo()); this._updateUI(getIfaceInfo());
// Re-poll shortly after to confirm real state // Re-poll shortly after to confirm real state
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => { GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => {
log('_onToggle: confirmation poll');
this._refresh(); this._refresh();
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
@@ -180,6 +204,7 @@ class NebulaIndicator extends PanelMenu.Button {
// ── cleanup ─────────────────────────────────────────────────────────────── // ── cleanup ───────────────────────────────────────────────────────────────
destroy() { destroy() {
log('NebulaIndicator.destroy');
if (this._timerId) { if (this._timerId) {
GLib.source_remove(this._timerId); GLib.source_remove(this._timerId);
this._timerId = null; this._timerId = null;
@@ -192,11 +217,14 @@ class NebulaIndicator extends PanelMenu.Button {
export default class NebulaVpnExtension extends Extension { export default class NebulaVpnExtension extends Extension {
enable() { enable() {
log('enable: creating indicator');
this._indicator = new NebulaIndicator(this.path); this._indicator = new NebulaIndicator(this.path);
Main.panel.addToStatusArea(this.uuid, this._indicator); Main.panel.addToStatusArea(this.uuid, this._indicator);
log('enable: indicator added to panel');
} }
disable() { disable() {
log('disable: destroying indicator');
this._indicator?.destroy(); this._indicator?.destroy();
this._indicator = null; this._indicator = null;
} }

View File

@@ -2,6 +2,6 @@
"name": "Nebula VPN Status", "name": "Nebula VPN Status",
"description": "Shows the status of the Nebula VPN in the GNOME panel with interface info and a toggle.", "description": "Shows the status of the Nebula VPN in the GNOME panel with interface info and a toggle.",
"uuid": "nebula-vpn-status@mjallen", "uuid": "nebula-vpn-status@mjallen",
"shell-version": ["45", "46", "47", "48"], "shell-version": ["45", "46", "47", "48", "49"],
"version": 1 "version": 1
} }