Fix latency-probe races, stale offline menu, and byte formatting

This commit is contained in:
av
2026-07-18 15:36:40 +03:00
parent 629e031845
commit 7a8c2f238c
3 changed files with 35 additions and 8 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ function scale(n) {
// "2.4 MB", human readable with a space. // "2.4 MB", human readable with a space.
export function formatBytes(n) { export function formatBytes(n) {
const [v, i] = scale(n); const [v, i] = scale(n);
const digits = i === 0 ? 0 : v < 10 ? 1 : v < 100 ? 1 : 0; const digits = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
return `${v.toFixed(digits)} ${UNITS[i]}`; return `${v.toFixed(digits)} ${UNITS[i]}`;
} }
+29 -5
View File
@@ -35,6 +35,8 @@ class SingBoxIndicator extends PanelMenu.Button {
this._selectorItems = new Map(); this._selectorItems = new Map();
this._proxiesMap = {}; this._proxiesMap = {};
this._delays = new Map(); // node name -> ms number | 'timeout' this._delays = new Map(); // node name -> ms number | 'timeout'
this._probing = new Set(); // nodes with a latency probe in flight
this._apiUrl = '';
this._polling = false; this._polling = false;
this._cancellable = new Gio.Cancellable(); this._cancellable = new Gio.Cancellable();
@@ -131,14 +133,18 @@ class SingBoxIndicator extends PanelMenu.Button {
// ---- Settings ----------------------------------------------------- // ---- Settings -----------------------------------------------------
_onSettingsChanged() { _onSettingsChanged() {
// Latencies measured against one instance are meaningless for another.
if (this._settings.get_string('api-url') !== this._apiUrl)
this._delays.clear();
this._applySettings(); this._applySettings();
this._restartPolling(); this._restartPolling();
this._poll(); this._poll();
} }
_applySettings() { _applySettings() {
this._apiUrl = this._settings.get_string('api-url');
this._api.setEndpoint( this._api.setEndpoint(
this._settings.get_string('api-url'), this._apiUrl,
this._settings.get_string('api-secret') this._settings.get_string('api-secret')
); );
this._interval = this._settings.get_int('refresh-interval'); this._interval = this._settings.get_int('refresh-interval');
@@ -299,6 +305,9 @@ class SingBoxIndicator extends PanelMenu.Button {
entry.setOrnament(isCurrent entry.setOrnament(isCurrent
? PopupMenu.Ornament.DOT ? PopupMenu.Ornament.DOT
: PopupMenu.Ornament.NONE); : PopupMenu.Ornament.NONE);
// Don't overwrite the "…" placeholder while a probe is running.
if (this._probing.has(member))
continue;
entry.label.text = this._memberLabelText(member, map[member]); entry.label.text = this._memberLabelText(member, map[member]);
} }
} }
@@ -340,6 +349,16 @@ class SingBoxIndicator extends PanelMenu.Button {
} }
} }
// Drop menu content that would otherwise stay clickable while offline.
_clearMenuData() {
this._selectorsSection.removeAll();
this._selectorItems.clear();
this._selectorSignature = null;
this._probing.clear();
this._connItem.menu.removeAll();
this._connItem.label.text = `${_('Connections')}: —`;
}
_pickPanelGroup(selectors) { _pickPanelGroup(selectors) {
if (selectors.length === 0) if (selectors.length === 0)
return null; return null;
@@ -384,6 +403,7 @@ class SingBoxIndicator extends PanelMenu.Button {
if (!stored) if (!stored)
return; return;
for (const [member, item] of stored.members) { for (const [member, item] of stored.members) {
this._probing.add(member);
item.label.text = `${member} · …`; item.label.text = `${member} · …`;
this._api.getDelay(member, {}, this._cancellable) this._api.getDelay(member, {}, this._cancellable)
.then(res => { .then(res => {
@@ -394,9 +414,14 @@ class SingBoxIndicator extends PanelMenu.Button {
this._delays.set(member, 'timeout'); this._delays.set(member, 'timeout');
}) })
.finally(() => { .finally(() => {
this._probing.delete(member);
if (this._cancellable.is_cancelled()) if (this._cancellable.is_cancelled())
return; return;
item.label.text = this._memberLabelText(member, this._proxiesMap?.[member]); // Re-resolve the row: the menu may have been rebuilt while
// the probe was in flight, disposing the captured actor.
const cur = this._selectorItems.get(groupName)?.members.get(member);
if (cur)
cur.label.text = this._memberLabelText(member, this._proxiesMap?.[member]);
}); });
} }
} }
@@ -423,13 +448,12 @@ class SingBoxIndicator extends PanelMenu.Button {
break; break;
case STATE.OFFLINE: case STATE.OFFLINE:
this._dotFilled = false; this._dotFilled = false;
this._statusItem.label.text = error this._statusItem.label.text = _('sing-box · Unreachable');
? _('sing-box · Unreachable')
: _('sing-box · Offline');
this._outboundLabel.text = ''; this._outboundLabel.text = '';
this._speedLabel.text = ''; this._speedLabel.text = '';
this._speedItem.label.text = _('API unreachable — check URL and secret in Settings'); this._speedItem.label.text = _('API unreachable — check URL and secret in Settings');
this._prevSample = null; this._prevSample = null;
this._clearMenuData();
break; break;
default: default:
this._dotFilled = false; this._dotFilled = false;
+5 -2
View File
@@ -50,8 +50,9 @@ export default class SingBoxStatusPreferences extends ExtensionPreferences {
_entryRow(settings, key, title, subtitle) { _entryRow(settings, key, title, subtitle) {
const row = new Adw.EntryRow({ title }); const row = new Adw.EntryRow({ title });
if (subtitle && row.set_subtitle) // EntryRow gained subtitle later; guard. // Adw.EntryRow has no subtitle; surface the hint as a tooltip instead.
row.set_subtitle(subtitle); if (subtitle)
row.set_tooltip_text(subtitle);
row.text = settings.get_string(key); row.text = settings.get_string(key);
row.connect('changed', () => settings.set_string(key, row.text)); row.connect('changed', () => settings.set_string(key, row.text));
settings.connect(`changed::${key}`, () => { settings.connect(`changed::${key}`, () => {
@@ -64,6 +65,8 @@ export default class SingBoxStatusPreferences extends ExtensionPreferences {
_passwordRow(settings, key, title, subtitle) { _passwordRow(settings, key, title, subtitle) {
const row = new Adw.PasswordEntryRow({ title }); const row = new Adw.PasswordEntryRow({ title });
if (subtitle)
row.set_tooltip_text(subtitle);
row.text = settings.get_string(key); row.text = settings.get_string(key);
row.connect('changed', () => settings.set_string(key, row.text)); row.connect('changed', () => settings.set_string(key, row.text));
settings.connect(`changed::${key}`, () => { settings.connect(`changed::${key}`, () => {