fix(wsl): re-read a paired host's refused default base ref, and close the signal race (#35812)

A paired host's repo.baseRefDefault now names the stopped distro that refused
it. Clients that cannot hear that host's running signal re-ask on window
focus and every 30 s while visible, until it answers. Locally, the running
listener is attached before the first read, so a signal that lands while main
is answering triggers a re-read instead of being lost.
This commit is contained in:
Neil
2026-10-29 00:56:21 -07:00
committed by GitHub
parent def7fad965
commit 21fdaee961
4 changed files with 207 additions and 43 deletions
@@ -162,10 +162,10 @@ describe('runtime repo.baseRefDefault for a stopped WSL distro (E9)', () => {
}
})
it("returns unknown without touching the stopped distro's UNC path", async () => {
it('returns unknown, naming the refusing distro, without touching its UNC path', async () => {
const queries = new RuntimeRepositoryRefQueries({ resolveRepo: async () => makeRepo() })
await expect(queries.getDefault('id:repo-1')).resolves.toEqual(UNKNOWN)
await expect(queries.getDefault('id:repo-1')).resolves.toEqual(REFUSED)
expect(mocks.getBaseRefDefault).not.toHaveBeenCalled()
})
@@ -1,4 +1,4 @@
import type { Repo } from '../../shared/repo-types'
import type { BaseRefDefaultResult, Repo } from '../../shared/repo-types'
import type { RuntimeRepoSearchRefs } from '../../shared/runtime-types'
import { isFolderRepo } from '../../shared/repo-kind'
import {
@@ -44,9 +44,7 @@ export class RuntimeRepositoryRefQueries {
}
}
async getDefault(
repoSelector: string
): Promise<{ defaultBaseRef: string | null; remoteCount: number }> {
async getDefault(repoSelector: string): Promise<BaseRefDefaultResult> {
const repo = await this.deps.resolveRepo(repoSelector)
if (isFolderRepo(repo)) {
return { defaultBaseRef: null, remoteCount: 0 }
@@ -55,8 +53,10 @@ export class RuntimeRepositoryRefQueries {
return { defaultBaseRef: null, remoteCount: 0 }
}
// A client's Settings pane must not start this host's stopped distro over `\\wsl.localhost` (E9).
if (await blockedWslDistroForPath(repo.path)) {
return { defaultBaseRef: null, remoteCount: 0 }
const blocked = await blockedWslDistroForPath(repo.path)
if (blocked) {
// Optional, so older clients read plain unknown; newer ones re-ask while it is set.
return { defaultBaseRef: null, remoteCount: 0, refusedWslDistro: blocked.distro }
}
const [defaultBaseRef, remoteCount] = await Promise.all([
getBaseRefDefault(repo.path),
@@ -1,10 +1,16 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { watchRuntimeRepoBaseRefDefault } from './runtime-repo-client'
const rpc = vi.hoisted(() => vi.fn())
vi.mock('./runtime-rpc-client', () => ({ callRuntimeRpc: rpc }))
import { REFUSED_BASE_REF_REPOLL_MS, watchRuntimeRepoBaseRefDefault } from './runtime-repo-client'
const getBaseRefDefault = vi.fn()
const distroRunningListeners = new Set<(distro: string) => void>()
const focusListeners = new Set<() => void>()
const page = { visibilityState: 'visible' }
function emitDistroRunning(distro: string): void {
for (const listener of distroRunningListeners) {
@@ -12,30 +18,58 @@ function emitDistroRunning(distro: string): void {
}
}
beforeEach(() => {
getBaseRefDefault.mockReset()
distroRunningListeners.clear()
function focusWindow(): void {
for (const listener of focusListeners) {
listener()
}
}
type PreflightStub = {
onWslDistroRunning?: (listener: (distro: string) => void) => () => void
}
function stubWindow(preflight: PreflightStub): void {
vi.stubGlobal('window', {
api: {
repos: { getBaseRefDefault },
preflight: {
onWslDistroRunning: (listener: (distro: string) => void) => {
distroRunningListeners.add(listener)
return () => distroRunningListeners.delete(listener)
}
api: { repos: { getBaseRefDefault }, preflight },
addEventListener: (type: string, listener: () => void) => {
if (type === 'focus') {
focusListeners.add(listener)
}
},
removeEventListener: (type: string, listener: () => void) => {
if (type === 'focus') {
focusListeners.delete(listener)
}
}
})
}
const hearsRunningSignal: PreflightStub = {
onWslDistroRunning: (listener: (distro: string) => void) => {
distroRunningListeners.add(listener)
return () => distroRunningListeners.delete(listener)
}
}
beforeEach(() => {
getBaseRefDefault.mockReset()
rpc.mockReset()
distroRunningListeners.clear()
focusListeners.clear()
page.visibilityState = 'visible'
vi.stubGlobal('document', page)
stubWindow(hearsRunningSignal)
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})
const REFUSED = { defaultBaseRef: null, remoteCount: 0, refusedWslDistro: 'Ubuntu' }
const LOADED = { defaultBaseRef: 'origin/main', remoteCount: 1 }
describe('watchRuntimeRepoBaseRefDefault after a stopped-distro refusal (E9)', () => {
describe("watchRuntimeRepoBaseRefDefault on this app's repos (E9)", () => {
it('loads again once the refusing distro runs', async () => {
getBaseRefDefault.mockResolvedValueOnce(REFUSED).mockResolvedValueOnce(LOADED)
const onResult = vi.fn()
@@ -46,27 +80,53 @@ describe('watchRuntimeRepoBaseRefDefault after a stopped-distro refusal (E9)', (
await vi.waitFor(() => expect(onResult).toHaveBeenLastCalledWith(LOADED))
expect(getBaseRefDefault).toHaveBeenCalledTimes(2)
expect(distroRunningListeners.size).toBe(0)
})
it('re-asks when the distro started while main was still answering', async () => {
let answer: (value: typeof REFUSED) => void = () => {}
getBaseRefDefault
.mockReturnValueOnce(new Promise((resolve) => (answer = resolve)))
.mockResolvedValueOnce(LOADED)
const onResult = vi.fn()
watchRuntimeRepoBaseRefDefault('local', 'repo-1', { onResult, onError: vi.fn() })
emitDistroRunning('Ubuntu')
answer(REFUSED)
await vi.waitFor(() => expect(onResult).toHaveBeenLastCalledWith(LOADED))
expect(getBaseRefDefault).toHaveBeenCalledTimes(2)
})
it('ignores other distros', async () => {
getBaseRefDefault.mockResolvedValue(REFUSED)
const onResult = vi.fn()
watchRuntimeRepoBaseRefDefault('local', 'repo-1', { onResult, onError: vi.fn() })
await vi.waitFor(() => expect(distroRunningListeners.size).toBe(1))
await vi.waitFor(() => expect(onResult).toHaveBeenCalledWith(REFUSED))
emitDistroRunning('Debian')
expect(getBaseRefDefault).toHaveBeenCalledTimes(1)
})
it('does not re-ask after a result that was not refused', async () => {
getBaseRefDefault.mockResolvedValue(LOADED)
const onResult = vi.fn()
watchRuntimeRepoBaseRefDefault('local', 'repo-1', { onResult, onError: vi.fn() })
await vi.waitFor(() => expect(onResult).toHaveBeenCalledWith(LOADED))
emitDistroRunning('Ubuntu')
expect(getBaseRefDefault).toHaveBeenCalledTimes(1)
})
it('stops listening when cancelled', async () => {
getBaseRefDefault.mockResolvedValue(REFUSED)
const onResult = vi.fn()
const cancel = watchRuntimeRepoBaseRefDefault('local', 'repo-1', {
onResult: vi.fn(),
onResult,
onError: vi.fn()
})
await vi.waitFor(() => expect(distroRunningListeners.size).toBe(1))
await vi.waitFor(() => expect(onResult).toHaveBeenCalledWith(REFUSED))
cancel()
emitDistroRunning('Ubuntu')
@@ -74,13 +134,72 @@ describe('watchRuntimeRepoBaseRefDefault after a stopped-distro refusal (E9)', (
expect(distroRunningListeners.size).toBe(0)
expect(getBaseRefDefault).toHaveBeenCalledTimes(1)
})
})
it('does not listen after a result that was not refused', async () => {
getBaseRefDefault.mockResolvedValue(LOADED)
describe('watchRuntimeRepoBaseRefDefault on a paired host (E9)', () => {
const PAIRED = 'runtime:env-1'
it('re-asks on window focus while the host refuses', async () => {
rpc.mockResolvedValueOnce(REFUSED).mockResolvedValueOnce(LOADED)
const onResult = vi.fn()
watchRuntimeRepoBaseRefDefault(PAIRED, 'repo-1', { onResult, onError: vi.fn() })
await vi.waitFor(() => expect(onResult).toHaveBeenCalledWith(REFUSED))
expect(distroRunningListeners.size).toBe(0)
focusWindow()
await vi.waitFor(() => expect(onResult).toHaveBeenLastCalledWith(LOADED))
focusWindow()
expect(rpc).toHaveBeenCalledTimes(2)
})
it('re-asks every 30 s while visible, and stops once answered', async () => {
vi.useFakeTimers()
rpc.mockResolvedValueOnce(REFUSED).mockResolvedValueOnce(REFUSED).mockResolvedValueOnce(LOADED)
const onResult = vi.fn()
watchRuntimeRepoBaseRefDefault(PAIRED, 'repo-1', { onResult, onError: vi.fn() })
await vi.advanceTimersByTimeAsync(0)
page.visibilityState = 'hidden'
await vi.advanceTimersByTimeAsync(REFUSED_BASE_REF_REPOLL_MS)
expect(rpc).toHaveBeenCalledTimes(1)
page.visibilityState = 'visible'
await vi.advanceTimersByTimeAsync(REFUSED_BASE_REF_REPOLL_MS)
expect(rpc).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(REFUSED_BASE_REF_REPOLL_MS)
expect(onResult).toHaveBeenLastCalledWith(LOADED)
await vi.advanceTimersByTimeAsync(REFUSED_BASE_REF_REPOLL_MS * 3)
expect(rpc).toHaveBeenCalledTimes(3)
})
it('stops re-asking when cancelled', async () => {
vi.useFakeTimers()
rpc.mockResolvedValue(REFUSED)
const cancel = watchRuntimeRepoBaseRefDefault(PAIRED, 'repo-1', {
onResult: vi.fn(),
onError: vi.fn()
})
await vi.advanceTimersByTimeAsync(0)
cancel()
focusWindow()
await vi.advanceTimersByTimeAsync(REFUSED_BASE_REF_REPOLL_MS * 2)
expect(rpc).toHaveBeenCalledTimes(1)
expect(focusListeners.size).toBe(0)
})
it('polls a local-looking host whose preload has no running signal (web client)', async () => {
stubWindow({})
getBaseRefDefault.mockResolvedValueOnce(REFUSED).mockResolvedValueOnce(LOADED)
const onResult = vi.fn()
watchRuntimeRepoBaseRefDefault('local', 'repo-1', { onResult, onError: vi.fn() })
await vi.waitFor(() => expect(onResult).toHaveBeenCalledWith(LOADED))
await vi.waitFor(() => expect(onResult).toHaveBeenCalledWith(REFUSED))
expect(distroRunningListeners.size).toBe(0)
focusWindow()
await vi.waitFor(() => expect(onResult).toHaveBeenLastCalledWith(LOADED))
})
})
+60 -15
View File
@@ -37,9 +37,13 @@ export async function getRuntimeRepoBaseRefDefault(
)
}
/** How often a refused default is re-asked from a host whose running signal cannot reach us. */
export const REFUSED_BASE_REF_REPOLL_MS = 30_000
/**
* Loads the repo's default base ref and, when a stopped WSL distro refused it (E9), loads it again
* once main reports that distro running. Returns a cancel.
* Loads the repo's default base ref and, while a stopped WSL distro refuses it (E9), re-asks once
* that distro runs: on main's running signal for this app's repos, or for a paired host (whose
* signal never reaches this window) on window focus and every 30 s while visible. Returns a cancel.
*/
export function watchRuntimeRepoBaseRefDefault(
hostId: ExecutionHostId,
@@ -50,37 +54,78 @@ export function watchRuntimeRepoBaseRefDefault(
}
): () => void {
let cancelled = false
let stopRetry: (() => void) | null = null
const load = (): void => {
let inFlight = false
let refused: string | null = null
// Running signals that land while main is still answering would otherwise be missed.
let ranDuringRead = new Set<string>()
let repoll: ReturnType<typeof setInterval> | null = null
const onDistroRunning = window.api.preflight?.onWslDistroRunning
const hearsHostSignal =
ipcOrRuntimeTargetForHostId(hostId)?.kind === 'local' && typeof onDistroRunning === 'function'
const reaskFromPairedHost = (): void => {
if (refused && !inFlight && document.visibilityState === 'visible') {
load()
}
}
const syncRepoll = (): void => {
if (refused && !hearsHostSignal && !repoll) {
repoll = setInterval(reaskFromPairedHost, REFUSED_BASE_REF_REPOLL_MS)
} else if (!refused && repoll) {
clearInterval(repoll)
repoll = null
}
}
function load(): void {
inFlight = true
ranDuringRead = new Set()
getRuntimeRepoBaseRefDefault(hostId, repoId).then(
(result) => {
inFlight = false
if (cancelled) {
return
}
handlers.onResult(result)
const refused = result.refusedWslDistro?.toLowerCase()
const onDistroRunning = window.api.preflight?.onWslDistroRunning
if (refused && typeof onDistroRunning === 'function') {
stopRetry = onDistroRunning((distro) => {
if (distro.toLowerCase() === refused) {
stopRetry?.()
stopRetry = null
load()
}
})
refused = result.refusedWslDistro?.toLowerCase() ?? null
syncRepoll()
if (refused && ranDuringRead.has(refused)) {
load()
}
},
(error: unknown) => {
inFlight = false
if (!cancelled) {
handlers.onError(error)
}
}
)
}
// Subscribed before the first read, so no running signal can fall between answer and listener.
const stopListening =
hearsHostSignal && onDistroRunning
? onDistroRunning((distro) => {
const key = distro.toLowerCase()
if (inFlight) {
ranDuringRead.add(key)
} else if (key === refused) {
load()
}
})
: null
if (!hearsHostSignal) {
window.addEventListener('focus', reaskFromPairedHost)
}
load()
return () => {
cancelled = true
stopRetry?.()
stopListening?.()
if (!hearsHostSignal) {
window.removeEventListener('focus', reaskFromPairedHost)
}
if (repoll) {
clearInterval(repoll)
}
}
}