fix(updates): avoid network stack traces offline

This commit is contained in:
pixelpaws
2025-09-25 14:50:06 +08:00
parent b0cc42ef1f
commit 7499570766
4 changed files with 130 additions and 7 deletions

View File

@@ -5,12 +5,16 @@ import git
import zipfile import zipfile
import shutil import shutil
import tempfile import tempfile
from aiohttp import web import asyncio
from aiohttp import web, ClientError
from typing import Dict, List from typing import Dict, List
from ..services.downloader import get_downloader from ..services.downloader import get_downloader
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
NETWORK_EXCEPTIONS = (ClientError, OSError, asyncio.TimeoutError)
class UpdateRoutes: class UpdateRoutes:
"""Routes for handling plugin update checks""" """Routes for handling plugin update checks"""
@@ -63,6 +67,12 @@ class UpdateRoutes:
'nightly': nightly 'nightly': nightly
}) })
except NETWORK_EXCEPTIONS as e:
logger.warning("Network unavailable during update check: %s", e)
return web.json_response({
'success': False,
'error': 'Network unavailable for update check'
})
except Exception as e: except Exception as e:
logger.error(f"Failed to check for updates: {e}", exc_info=True) logger.error(f"Failed to check for updates: {e}", exc_info=True)
return web.json_response({ return web.json_response({
@@ -283,6 +293,9 @@ class UpdateRoutes:
return version, changelog return version, changelog
except NETWORK_EXCEPTIONS as e:
logger.warning("Unable to reach GitHub for nightly version: %s", e)
return "main", []
except Exception as e: except Exception as e:
logger.error(f"Error fetching nightly version: {e}", exc_info=True) logger.error(f"Error fetching nightly version: {e}", exc_info=True)
return "main", [] return "main", []
@@ -448,6 +461,9 @@ class UpdateRoutes:
return version, changelog return version, changelog
except NETWORK_EXCEPTIONS as e:
logger.warning("Unable to reach GitHub for release info: %s", e)
return "v0.0.0", []
except Exception as e: except Exception as e:
logger.error(f"Error fetching remote version: {e}", exc_info=True) logger.error(f"Error fetching remote version: {e}", exc_info=True)
return "v0.0.0", [] return "v0.0.0", []

View File

@@ -82,10 +82,14 @@ export class UpdateService {
} }
} }
async checkForUpdates() { async checkForUpdates({ force = false } = {}) {
if (!force && !this.updateNotificationsEnabled) {
return;
}
// Check if we should perform an update check // Check if we should perform an update check
const now = Date.now(); const now = Date.now();
const forceCheck = this.lastCheckTime === 0; const forceCheck = force || this.lastCheckTime === 0;
if (!forceCheck && now - this.lastCheckTime < this.updateCheckInterval) { if (!forceCheck && now - this.lastCheckTime < this.updateCheckInterval) {
// If we already have update info, just update the UI // If we already have update info, just update the UI
@@ -435,8 +439,7 @@ export class UpdateService {
} }
async manualCheckForUpdates() { async manualCheckForUpdates() {
this.lastCheckTime = 0; // Reset last check time to force check await this.checkForUpdates({ force: true });
await this.checkForUpdates();
// Ensure badge visibility is updated after manual check // Ensure badge visibility is updated after manual check
this.updateBadgeVisibility(); this.updateBadgeVisibility();
} }

View File

@@ -0,0 +1,45 @@
import { describe, beforeEach, afterEach, expect, it, vi } from 'vitest';
import { UpdateService } from '../../../static/js/managers/UpdateService.js';
function createFetchResponse(payload) {
return {
json: vi.fn().mockResolvedValue(payload)
};
}
describe('UpdateService passive checks', () => {
let service;
let fetchMock;
beforeEach(() => {
fetchMock = vi.fn().mockResolvedValue(createFetchResponse({
success: true,
current_version: 'v1.0.0',
latest_version: 'v1.0.0',
git_info: { short_hash: 'abc123' }
}));
global.fetch = fetchMock;
service = new UpdateService();
service.updateNotificationsEnabled = false;
service.lastCheckTime = 0;
service.nightlyMode = false;
});
afterEach(() => {
delete global.fetch;
});
it('skips passive update checks when notifications are disabled', async () => {
await service.checkForUpdates();
expect(fetchMock).not.toHaveBeenCalled();
});
it('allows manual checks even when notifications are disabled', async () => {
await service.checkForUpdates({ force: true });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith('/api/lm/check-updates?nightly=false');
});
});

View File

@@ -0,0 +1,59 @@
import logging
from aiohttp import ClientError
import pytest
from py.routes import update_routes
class OfflineDownloader:
async def make_request(self, *_, **__):
return False, "Cannot connect to host"
class RaisingDownloader:
async def make_request(self, *_, **__):
raise ClientError("offline")
async def _stub_downloader(instance):
return instance
@pytest.mark.asyncio
async def test_get_remote_version_offline_logs_without_traceback(monkeypatch, caplog):
caplog.set_level(logging.WARNING)
monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(OfflineDownloader()))
version, changelog = await update_routes.UpdateRoutes._get_remote_version()
assert version == "v0.0.0"
assert changelog == []
assert "Failed to fetch GitHub release" in caplog.text
assert "Cannot connect to host" in caplog.text
assert "Traceback" not in caplog.text
@pytest.mark.asyncio
async def test_get_remote_version_network_error_logs_warning(monkeypatch, caplog):
caplog.set_level(logging.WARNING)
monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(RaisingDownloader()))
version, changelog = await update_routes.UpdateRoutes._get_remote_version()
assert version == "v0.0.0"
assert changelog == []
assert "Unable to reach GitHub for release info" in caplog.text
assert "Traceback" not in caplog.text
@pytest.mark.asyncio
async def test_get_nightly_version_network_error_logs_warning(monkeypatch, caplog):
caplog.set_level(logging.WARNING)
monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(RaisingDownloader()))
version, changelog = await update_routes.UpdateRoutes._get_nightly_version()
assert version == "main"
assert changelog == []
assert "Unable to reach GitHub for nightly version" in caplog.text
assert "Traceback" not in caplog.text