Fix connectivity staleness, distinct checking dot, read-only groups, poll/probe robustness
This commit is contained in:
+70
-20
@@ -35,7 +35,9 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
this._delays = new Map(); // node name -> ms number | 'timeout'
|
||||
this._probing = new Set(); // nodes with a latency probe in flight
|
||||
this._apiUrl = '';
|
||||
this._apiSecret = '';
|
||||
this._polling = false;
|
||||
this._pollAgain = false; // a refresh arrived while polling
|
||||
this._pinging = false; // connectivity probe in flight
|
||||
this._settingsDebounceId = 0;
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
@@ -59,10 +61,10 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
});
|
||||
|
||||
// Monochrome status dot in the panel's text color (adapts to theme):
|
||||
// filled disc — service up and outbound has connectivity;
|
||||
// bright ring — service up but no connectivity through the outbound;
|
||||
// dim ring — service up, connectivity not yet measured;
|
||||
// dashed dim ring — service (Clash API) unreachable / connecting.
|
||||
// filled bright disc — service up and outbound has connectivity;
|
||||
// filled dim disc — service up, connectivity not yet measured;
|
||||
// bright ring — service up but no connectivity through the outbound;
|
||||
// dashed dim ring — service (Clash API) unreachable / connecting.
|
||||
this._dot = new St.DrawingArea({
|
||||
style_class: 'sbx-dot',
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
@@ -149,8 +151,15 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
|
||||
_reconfigure() {
|
||||
// Latencies measured against one instance are meaningless for another.
|
||||
if (this._settings.get_string('api-url') !== this._apiUrl)
|
||||
// If the endpoint (URL or secret) changed, drop cached latencies and
|
||||
// rotate the cancellable so stale old-endpoint responses are discarded.
|
||||
const urlChanged = this._settings.get_string('api-url') !== this._apiUrl;
|
||||
const secretChanged = this._settings.get_string('api-secret') !== this._apiSecret;
|
||||
if (urlChanged || secretChanged) {
|
||||
this._delays.clear();
|
||||
this._cancellable.cancel();
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
}
|
||||
this._applySettings();
|
||||
this._restartPolling();
|
||||
this._restartConnectivityCheck();
|
||||
@@ -164,9 +173,10 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
|
||||
_applySettings() {
|
||||
this._apiUrl = this._settings.get_string('api-url');
|
||||
this._apiSecret = this._settings.get_string('api-secret');
|
||||
this._api.setEndpoint(
|
||||
this._apiUrl,
|
||||
this._settings.get_string('api-secret')
|
||||
this._apiSecret
|
||||
);
|
||||
this._interval = this._settings.get_int('refresh-interval');
|
||||
this._showOutbound = this._settings.get_boolean('show-outbound');
|
||||
@@ -209,8 +219,10 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
}
|
||||
|
||||
async _poll() {
|
||||
if (this._polling)
|
||||
if (this._polling) {
|
||||
this._pollAgain = true;
|
||||
return;
|
||||
}
|
||||
this._polling = true;
|
||||
const c = this._cancellable;
|
||||
try {
|
||||
@@ -236,14 +248,24 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
return;
|
||||
this._apiUp = true;
|
||||
// Note: _updateFromData is UI code — keep it out of the API try so
|
||||
// a rendering bug isn't misreported as "API unreachable".
|
||||
this._updateFromData(conn, proxies);
|
||||
// a rendering bug isn't misreported as "API unreachable". A render
|
||||
// failure is logged but must not skip status/connectivity handling.
|
||||
try {
|
||||
this._updateFromData(conn, proxies);
|
||||
} catch (e) {
|
||||
logError(e, 'sing-box-status: render failed');
|
||||
}
|
||||
this._updateStatus();
|
||||
// Resolve unknown connectivity promptly (startup, recovery, switch).
|
||||
if (this._checkConnectivity && this._connectivity === 'unknown')
|
||||
this._pingConnectivity();
|
||||
} finally {
|
||||
this._polling = false;
|
||||
// A refresh arrived mid-poll and was swallowed by the guard: run it.
|
||||
if (this._pollAgain) {
|
||||
this._pollAgain = false;
|
||||
this._poll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,11 +326,17 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
} catch (e) {
|
||||
if (c.is_cancelled() || node !== this._activeOutbound)
|
||||
return;
|
||||
// A transport/API failure is not an outbound-connectivity verdict.
|
||||
this._connectivity = this._apiUp ? 'down' : 'unknown';
|
||||
this._connectivityDelay = null;
|
||||
if (this._apiUp)
|
||||
// An HTTP error is a real outbound-failure verdict; a transport/API
|
||||
// error (unreachable, aborted) is not — don't record it as a timeout.
|
||||
const httpFail = typeof e?.message === 'string' && e.message.startsWith('HTTP');
|
||||
if (httpFail) {
|
||||
this._connectivity = 'down';
|
||||
this._connectivityDelay = null;
|
||||
this._delays.set(node, 'timeout');
|
||||
} else {
|
||||
this._connectivity = 'unknown';
|
||||
this._connectivityDelay = null;
|
||||
}
|
||||
this._updateStatus();
|
||||
} finally {
|
||||
this._pinging = false;
|
||||
@@ -393,15 +421,26 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
if (!p || !Array.isArray(p.all))
|
||||
continue;
|
||||
if (p.type === 'Selector')
|
||||
groups.push({ name, now: p.now, all: p.all, switchable: true });
|
||||
groups.push({ name, now: p.now, all: p.all, type: p.type, switchable: true });
|
||||
else if (p.type === 'URLTest' || p.type === 'Fallback')
|
||||
groups.push({ name, now: p.now, all: p.all, switchable: false });
|
||||
groups.push({ name, now: p.now, all: p.all, type: p.type, switchable: false });
|
||||
}
|
||||
|
||||
// Switchable (Selector) groups first; sort is stable within each class.
|
||||
groups.sort((a, b) => (b.switchable ? 1 : 0) - (a.switchable ? 1 : 0));
|
||||
|
||||
// Panel outbound label: chosen group's current node.
|
||||
const panelGroup = this._pickPanelGroup(groups);
|
||||
this._outboundLabel.text = panelGroup ? panelGroup.now ?? '' : '';
|
||||
this._activeOutbound = panelGroup ? panelGroup.now ?? null : null;
|
||||
const newActive = panelGroup ? panelGroup.now ?? null : null;
|
||||
// If the active outbound changed outside _selectOutbound (e.g. a
|
||||
// URLTest re-pick or an external switch), the last connectivity verdict
|
||||
// is stale — reset it so the poll-side re-ping re-probes the new node.
|
||||
if (newActive !== this._activeOutbound && this._checkConnectivity && newActive) {
|
||||
this._connectivity = 'unknown';
|
||||
this._connectivityDelay = null;
|
||||
}
|
||||
this._activeOutbound = newActive;
|
||||
|
||||
// Rebuild the group section only when the structure changes.
|
||||
const signature = JSON.stringify(groups.map(g => [g.name, g.all, g.switchable]));
|
||||
@@ -415,7 +454,7 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
const stored = this._selectorItems.get(g.name);
|
||||
if (!stored)
|
||||
continue;
|
||||
stored.subMenu.label.text = `${g.name}: ${g.now ?? '—'}`;
|
||||
stored.subMenu.label.text = this._groupTitle(g);
|
||||
for (const [member, entry] of stored.members) {
|
||||
entry.setOrnament(member === g.now
|
||||
? PopupMenu.Ornament.DOT
|
||||
@@ -442,7 +481,7 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
}
|
||||
|
||||
for (const g of groups) {
|
||||
const subMenu = new PopupMenu.PopupSubMenuMenuItem(`${g.name}: ${g.now ?? '—'}`, true);
|
||||
const subMenu = new PopupMenu.PopupSubMenuMenuItem(this._groupTitle(g), true);
|
||||
subMenu.icon.icon_name = 'network-workgroup-symbolic';
|
||||
|
||||
// Manual latency probe for all nodes in this group. Override
|
||||
@@ -479,6 +518,13 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
this._connItem.label.text = `${_('Connections')}: —`;
|
||||
}
|
||||
|
||||
// Submenu title: read-only groups also show their type (URLTest/Fallback).
|
||||
_groupTitle(g) {
|
||||
return g.switchable
|
||||
? `${g.name}: ${g.now ?? '—'}`
|
||||
: `${g.name} [${g.type}]: ${g.now ?? '—'}`;
|
||||
}
|
||||
|
||||
_pickPanelGroup(groups) {
|
||||
if (groups.length === 0)
|
||||
return null;
|
||||
@@ -524,6 +570,9 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
if (!stored)
|
||||
return;
|
||||
for (const [member, item] of stored.members) {
|
||||
// Skip nodes already being probed (manual re-click or overlap).
|
||||
if (this._probing.has(member))
|
||||
continue;
|
||||
this._probing.add(member);
|
||||
item.label.text = `${member} · …`;
|
||||
this._api.getDelay(member, { url: this._connectivityUrl || undefined }, this._cancellable)
|
||||
@@ -581,7 +630,7 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
this._outboundLabel.text = '';
|
||||
this._speedLabel.text = '';
|
||||
this._speedItem.label.text = _('API unreachable — check URL and secret in Settings');
|
||||
} else if (this._checkConnectivity && this._connectivity === 'unknown') {
|
||||
} else if (this._checkConnectivity && this._activeOutbound && this._connectivity === 'unknown') {
|
||||
// Service up, connectivity not measured yet — don't claim it works.
|
||||
dot = 'checking';
|
||||
this._statusItem.label.text = _('sing-box · Active (checking…)');
|
||||
@@ -631,8 +680,9 @@ class SingBoxIndicator extends PanelMenu.Button {
|
||||
cr.stroke();
|
||||
break;
|
||||
case 'checking':
|
||||
// Dim solid ring: service up, connectivity not measured yet.
|
||||
// Dim filled disc: service up, connectivity not measured yet.
|
||||
cr.setSourceRGBA(r, g, b, a * 0.5);
|
||||
cr.fillPreserve();
|
||||
cr.stroke();
|
||||
break;
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user