fix: time out system font listing (#3786)

This commit is contained in:
Neil 2026-05-30 10:11:48 -07:00 committed by GitHub
parent ac8146ab67
commit 25c83796d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 73 additions and 1 deletions

View File

@ -0,0 +1,47 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, killMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
killMock: vi.fn()
}))
vi.mock('child_process', () => ({
execFile: execFileMock
}))
function expectedFallbackFont(): string {
if (process.platform === 'darwin') {
return 'SF Mono'
}
if (process.platform === 'win32') {
return 'Cascadia Mono'
}
return 'JetBrains Mono'
}
describe('listSystemFontFamilies', () => {
afterEach(() => {
vi.useRealTimers()
vi.resetModules()
execFileMock.mockReset()
killMock.mockReset()
})
it('falls back when the platform font command never exits', async () => {
vi.useFakeTimers()
execFileMock.mockReturnValue({ kill: killMock })
const { listSystemFontFamilies } = await import('./system-fonts')
const fontsPromise = listSystemFontFamilies()
let resolvedFonts: string[] | null = null
fontsPromise.then((fonts) => {
resolvedFonts = fonts
})
await vi.advanceTimersByTimeAsync(60_000)
expect(resolvedFonts).not.toBeNull()
expect(resolvedFonts).toContain(expectedFallbackFont())
expect(killMock).toHaveBeenCalledOnce()
})
})

View File

@ -2,6 +2,7 @@ import { execFile } from 'child_process'
let cachedFonts: string[] | null = null
let fontsPromise: Promise<string[]> | null = null
const SYSTEM_FONT_LIST_TIMEOUT_MS = 15_000
export async function listSystemFontFamilies(): Promise<string[]> {
if (cachedFonts) {
@ -96,13 +97,37 @@ $fonts.Families | ForEach-Object { $_.Name }
function execFileText(command: string, args: string[], maxBuffer: number): Promise<string> {
return new Promise((resolve, reject) => {
execFile(command, args, { encoding: 'utf8', maxBuffer }, (error, stdout) => {
let settled = false
let timer: ReturnType<typeof setTimeout> | undefined
const child = execFile(command, args, { encoding: 'utf8', maxBuffer }, (error, stdout) => {
if (settled) {
return
}
settled = true
if (timer) {
clearTimeout(timer)
}
if (error) {
reject(error)
return
}
resolve(stdout)
})
if (!settled) {
timer = setTimeout(() => {
if (settled) {
return
}
settled = true
// Why: font discovery is a startup convenience; a stuck OS font tool
// should fall back instead of keeping settings IPC pending forever.
child.kill()
reject(new Error(`Timed out listing system fonts with ${command}`))
}, SYSTEM_FONT_LIST_TIMEOUT_MS)
if (typeof timer === 'object' && 'unref' in timer) {
timer.unref()
}
}
})
}