fix: curl fetch for Linux updater, overlay restart restore, Yomitan late

- Use /usr/bin/curl on Linux for update checks to avoid Electron net-service crashes
- Restore visible overlay on manual restart even when auto-start visibility is disabled
- Reload overlay windows after Yomitan extension loads to fix popup race on startup
This commit is contained in:
2026-05-17 22:12:38 -07:00
parent 2b13c82d69
commit 48447c2f1a
14 changed files with 344 additions and 10 deletions
+48 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createElectronNetFetch, createGlobalFetch } from './fetch-adapter';
import { createCurlFetch, createElectronNetFetch, createGlobalFetch } from './fetch-adapter';
import type { FetchResponseLike } from './release-assets';
test('createElectronNetFetch delegates updater requests to Electron net.fetch', async () => {
@@ -62,3 +62,50 @@ test('createGlobalFetch delegates updater requests to main-process fetch', async
},
]);
});
test('createCurlFetch requests updater metadata without Electron networking', async () => {
const calls: Array<{
file: string;
args: readonly string[];
options: { encoding: 'utf8' | 'buffer'; maxBuffer?: number; timeout?: number };
}> = [];
const payload = Buffer.from(JSON.stringify([{ tag_name: 'v1.2.3', assets: [] }]));
const fetch = createCurlFetch({
curlPath: '/usr/bin/curl',
execFile: (file, args, options, callback) => {
calls.push({ file, args, options });
callback(null, payload, Buffer.alloc(0));
return { kill: () => undefined };
},
});
const response = await fetch('https://api.github.com/repos/ksyasuda/SubMiner/releases', {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': 'SubMiner updater',
},
});
assert.equal(response.ok, true);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), [{ tag_name: 'v1.2.3', assets: [] }]);
assert.equal(await response.text(), '[{"tag_name":"v1.2.3","assets":[]}]');
assert.deepEqual(Buffer.from(await response.arrayBuffer()), payload);
assert.equal(calls.length, 1);
assert.equal(calls[0]?.file, '/usr/bin/curl');
assert.deepEqual(calls[0]?.args, [
'--fail',
'--location',
'--silent',
'--show-error',
'--connect-timeout',
'30',
'--header',
'Accept: application/vnd.github+json',
'--header',
'User-Agent: SubMiner updater',
'https://api.github.com/repos/ksyasuda/SubMiner/releases',
]);
assert.equal(calls[0]?.options.encoding, 'buffer');
});
+80
View File
@@ -1,4 +1,6 @@
import { execFile as defaultExecFile } from 'node:child_process';
import type { FetchLike, FetchResponseLike } from './release-assets';
import type { CurlExecFile } from './curl-http-executor';
export interface ElectronNetFetchLike {
fetch: (url: string, init?: Record<string, unknown>) => Promise<FetchResponseLike>;
@@ -20,3 +22,81 @@ function getGlobalFetch(): GlobalFetchLike {
export function createGlobalFetch(fetchImpl?: GlobalFetchLike): FetchLike {
return (url, init) => (fetchImpl ?? getGlobalFetch())(url, init as RequestInit);
}
type CurlFetchOptions = {
execFile?: CurlExecFile;
curlPath?: string;
};
function addHeaderArgs(args: string[], headers: unknown): void {
if (!headers) return;
if (Array.isArray(headers)) {
for (const header of headers) {
if (Array.isArray(header) && header.length >= 2) {
args.push('--header', `${header[0]}: ${header[1]}`);
}
}
return;
}
if (typeof headers === 'object' && 'forEach' in headers) {
(headers as { forEach: (callback: (value: string, name: string) => void) => void }).forEach(
(value, name) => {
args.push('--header', `${name}: ${value}`);
},
);
return;
}
if (typeof headers !== 'object') return;
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
if (value === undefined) continue;
const values = Array.isArray(value) ? value : [value];
for (const item of values) {
args.push('--header', `${name}: ${String(item)}`);
}
}
}
function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
const result = new ArrayBuffer(buffer.length);
new Uint8Array(result).set(buffer);
return result;
}
export function createCurlFetch(options: CurlFetchOptions = {}): FetchLike {
const execFile = options.execFile ?? (defaultExecFile as unknown as CurlExecFile);
const curlPath = options.curlPath ?? '/usr/bin/curl';
return async (url, init = {}) => {
const args = ['--fail', '--location', '--silent', '--show-error', '--connect-timeout', '30'];
addHeaderArgs(args, init.headers);
args.push(url);
const body = await new Promise<Buffer>((resolve, reject) => {
execFile(
curlPath,
args,
{
encoding: 'buffer',
maxBuffer: 600 * 1024 * 1024,
},
(error, stdout, stderr) => {
if (error) {
const stderrMessage = Buffer.isBuffer(stderr) ? stderr.toString('utf8') : stderr;
const errno = (error as NodeJS.ErrnoException).code;
const fallback = errno ? `curl failed (${errno})` : 'curl failed';
reject(new Error(stderrMessage.trim() || fallback));
return;
}
resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
},
);
});
return {
ok: true,
status: 200,
statusText: 'OK',
json: async () => JSON.parse(body.toString('utf8')),
text: async () => body.toString('utf8'),
arrayBuffer: async () => bufferToArrayBuffer(body),
};
};
}