fix: restore Linux AppImage updater routing and fallback port reporting (#397)

This commit is contained in:
Elie Habib
2026-02-26 10:07:59 +04:00
committed by GitHub
parent 384cc78daa
commit f912c5f15a
5 changed files with 137 additions and 11 deletions

View File

@@ -12,23 +12,28 @@ const PLATFORM_PATTERNS = {
'linux-appimage': (name) => name.endsWith('_amd64.AppImage'),
};
const VARIANT_PREFIXES = {
full: ['world-monitor'],
world: ['world-monitor'],
tech: ['tech-monitor'],
finance: ['finance-monitor'],
const VARIANT_IDENTIFIERS = {
full: ['worldmonitor'],
world: ['worldmonitor'],
tech: ['techmonitor'],
finance: ['financemonitor'],
};
function canonicalAssetName(name) {
return String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
}
function findAssetForVariant(assets, variant, platformMatcher) {
const prefixes = VARIANT_PREFIXES[variant] ?? null;
if (!prefixes) return null;
const identifiers = VARIANT_IDENTIFIERS[variant] ?? null;
if (!identifiers) return null;
return assets.find((asset) => {
const assetName = String(asset?.name || '').toLowerCase();
const hasVariantPrefix = prefixes.some((prefix) =>
assetName.startsWith(`${prefix.toLowerCase()}_`) || assetName.startsWith(`${prefix.toLowerCase()}-`)
const assetName = String(asset?.name || '');
const normalizedAssetName = canonicalAssetName(assetName);
const hasVariantIdentifier = identifiers.some((identifier) =>
normalizedAssetName.includes(identifier)
);
return hasVariantPrefix && platformMatcher(String(asset?.name || ''));
return hasVariantIdentifier && platformMatcher(assetName);
}) ?? null;
}

View File

@@ -1245,6 +1245,7 @@ export async function createLocalApiServer(options = {}) {
const address = server.address();
const boundPort = typeof address === 'object' && address?.port ? address.port : context.port;
context.port = boundPort;
const portFile = process.env.LOCAL_API_PORT_FILE;
if (portFile) {

View File

@@ -1347,3 +1347,37 @@ test('traffic log strips query strings from entries to protect privacy', async (
await localApi.cleanup();
}
});
test('service-status reports bound fallback port after EADDRINUSE recovery', async () => {
const blocker = createServer((_req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('occupied');
});
await listen(blocker, '127.0.0.1', 46123);
const localApi = await setupApiDir({});
const app = await createLocalApiServer({
port: 46123,
apiDir: localApi.apiDir,
logger: { log() {}, warn() {}, error() {} },
});
const { port } = await app.start();
try {
assert.notEqual(port, 46123);
const response = await fetch(`http://127.0.0.1:${port}/api/service-status`);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.local.port, port);
const localService = body.services.find((service) => service.id === 'local-api');
assert.equal(localService.description, `Running on 127.0.0.1:${port}`);
} finally {
await app.close();
await localApi.cleanup();
await new Promise((resolve, reject) => {
blocker.close((error) => (error ? reject(error) : resolve()));
});
}
});

View File

@@ -133,6 +133,10 @@ export class DesktopUpdater implements AppModule {
return null;
}
if (normalizedOs === 'linux') {
return normalizedArch === 'x86_64' ? 'linux-appimage' : null;
}
return null;
}

View File

@@ -0,0 +1,82 @@
import { strict as assert } from 'node:assert';
import test from 'node:test';
import handler from '../api/download.js';
const RELEASES_PAGE = 'https://github.com/koala73/worldmonitor/releases/latest';
function makeGitHubReleaseResponse(assets) {
return new Response(JSON.stringify({ assets }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
test('matches full variant for dotted World.Monitor AppImage asset names', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => makeGitHubReleaseResponse([
{
name: 'World.Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage',
},
]);
try {
const response = await handler(
new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=full')
);
assert.equal(response.status, 302);
assert.equal(
response.headers.get('location'),
'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage'
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('matches tech variant for dashed Tech-Monitor AppImage asset names', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => makeGitHubReleaseResponse([
{
name: 'Tech-Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/Tech-Monitor_2.5.7_amd64.AppImage',
},
{
name: 'World.Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage',
},
]);
try {
const response = await handler(
new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=tech')
);
assert.equal(response.status, 302);
assert.equal(
response.headers.get('location'),
'https://downloads.example/Tech-Monitor_2.5.7_amd64.AppImage'
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('falls back to release page when requested variant has no matching asset', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => makeGitHubReleaseResponse([
{
name: 'World.Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage',
},
]);
try {
const response = await handler(
new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=finance')
);
assert.equal(response.status, 302);
assert.equal(response.headers.get('location'), RELEASES_PAGE);
} finally {
globalThis.fetch = originalFetch;
}
});