mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
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:
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user