refactor(wsl): register WSL host IPC only when the WSL host service exists (#28446)

* refactor(wsl): register WSL host IPC only when the WSL host service exists

With the internal gate closed (or off Windows) the preload exposes no WSL host API, so the
seven handlers and the settings listener were registered only to refuse. Register nothing
in that case and drop the per-handler gate checks that a live service makes unreachable.

* test(wsl): cover the settings listener, re-registration and non-Windows gate-open cases
This commit is contained in:
Neil
2026-10-13 17:15:05 -07:00
committed by GitHub
parent 03bf6b49d4
commit 52432e9952
2 changed files with 75 additions and 62 deletions
+36 -14
View File
@@ -76,7 +76,7 @@ vi.mock('./runtime-environment-request-connections', async () => {
import { registerRuntimeEnvironmentHandlers } from './runtime-environments'
import { channelHandlerLookup, pairingCode } from './runtime-environments-ipc-test-harness'
import { registerWslHostHandlers } from './wsl-host-handlers'
import { registerWslHostHandlers, WSL_HOST_HANDLER_CHANNELS } from './wsl-host-handlers'
import { resolveManagedRuntimeEnvironment } from './runtime-environment-managed-tunnel'
import { getWslHostService, setWslHostService } from '../wsl/wsl-host-service-instance'
import { getSavedRuntimeAiVaultHostInfos } from '../ai-vault/runtime-session-scanner'
@@ -105,6 +105,12 @@ function persistWslHost(userDataPath: string): void {
})
}
function registeredWslHostChannels(): unknown[] {
return mocks.handle.mock.calls
.map((call) => call[0])
.filter((channel) => String(channel).startsWith('wslHosts:'))
}
function touchedWslHost(mock: { mock: { calls: unknown[][] } }): boolean {
return mock.mock.calls.some((call) => call[0] === WSL_ID)
}
@@ -154,6 +160,7 @@ describe('WSL hosts with the internal gate closed', () => {
startApp()
expect(getWslHostService()).toBeNull()
expect(store.onSettingsChanged).not.toHaveBeenCalled()
expect(mocks.createConnector).not.toHaveBeenCalled()
expect(mocks.createFakeConnector).not.toHaveBeenCalled()
expect(getRuntimeEnvironmentStatusSnapshots().map((s) => s.environmentId)).not.toContain(WSL_ID)
@@ -165,8 +172,7 @@ describe('WSL hosts with the internal gate closed', () => {
expect(
getSavedRuntimeAiVaultHostInfos(userDataPath).map((host) => host.environmentId)
).not.toContain(WSL_ID)
expect(await handler('wslHosts:listDistros')(null, undefined)).toEqual([])
expect(await handler('wslHosts:getStatuses')(null, undefined)).toEqual([])
expect(registeredWslHostChannels()).toEqual([])
expect(mocks.spawnProcess).not.toHaveBeenCalled()
expect(mocks.runProcess).not.toHaveBeenCalled()
})
@@ -174,17 +180,7 @@ describe('WSL hosts with the internal gate closed', () => {
it('refuses every WSL host operation and leaves the persisted host on disk', async () => {
startApp()
expect(await handler('wslHosts:add')(null, { distro: 'Ubuntu' })).toMatchObject({
ok: false,
kind: 'disabled',
message: GATED
})
await expect(
Promise.resolve().then(() => handler('wslHosts:connect')(null, { environmentId: WSL_ID }))
).rejects.toThrow(GATED)
await expect(
Promise.resolve().then(() => handler('wslHosts:remove')(null, { environmentId: WSL_ID }))
).rejects.toThrow(GATED)
expect(registeredWslHostChannels()).toEqual([])
await expect(
handler('runtimeEnvironments:connect')(null, { selector: WSL_ID })
).rejects.toThrow(GATED)
@@ -216,5 +212,31 @@ describe('WSL hosts with the internal gate closed', () => {
expect(getWslHostService()).not.toBeNull()
expect(mocks.createFakeConnector).toHaveBeenCalledTimes(1)
expect(registeredWslHostChannels().sort()).toEqual([...WSL_HOST_HANDLER_CHANNELS].sort())
expect(store.onSettingsChanged).toHaveBeenCalledTimes(1)
// Registering again (another window) reuses the service and its one settings listener.
mocks.handle.mockClear()
startApp()
expect(mocks.createFakeConnector).toHaveBeenCalledTimes(1)
expect(store.onSettingsChanged).toHaveBeenCalledTimes(1)
expect(registeredWslHostChannels().sort()).toEqual([...WSL_HOST_HANDLER_CHANNELS].sort())
})
it('registers nothing with the gate open on a non-Windows host without the fake', () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { value: 'linux' })
try {
startApp()
} finally {
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
}
expect(getWslHostService()).toBeNull()
expect(registeredWslHostChannels()).toEqual([])
expect(store.onSettingsChanged).not.toHaveBeenCalled()
})
})
+39 -48
View File
@@ -17,7 +17,7 @@ import {
} from '../wsl/wsl-host-e2e-fake-connector'
import { hasOrcadTemplate } from '../orcad/orcad-artifact-materializer'
import { WslHostService } from '../wsl/wsl-host-service'
import { WSL_HOSTS_GATED_OFF_MESSAGE, wslHostsGateOpen } from '../wsl/wsl-host-gate'
import { wslHostsGateOpen } from '../wsl/wsl-host-gate'
import { getWslHostService, setWslHostService } from '../wsl/wsl-host-service-instance'
import { retireRemovedRuntimeEnvironment } from './runtime-environment-removal-cleanup'
import { getRuntimeEnvironmentStatus } from './runtime-environment-status-probe'
@@ -72,11 +72,13 @@ function wslHostRuntimeAccess(getUserDataPath: () => string): WslHostRuntimeAcce
}
}
function createService(args: {
type WslHostHandlerArgs = {
store: Store
getUserDataPath: () => string
invalidateTransport: (environmentId: string) => Promise<void> | void
}): WslHostService | null {
}
function createService(args: WslHostHandlerArgs): WslHostService | null {
// Closed gate: no service, so no holder adoption, wsl.exe spawn or reconnect can start.
if (!wslHostsGateOpen()) {
return null
@@ -116,67 +118,56 @@ function createService(args: {
return created
}
export function registerWslHostHandlers(args: {
store: Store
getUserDataPath: () => string
invalidateTransport: (environmentId: string) => Promise<void> | void
}): void {
function startService(args: WslHostHandlerArgs): WslHostService | null {
const created = createService(args)
if (!created) {
return null
}
setWslHostService(created)
args.store.onSettingsChanged((updates, settings) => {
if ('experimentalWslHosts' in updates) {
void created.enabledChanged(settings.experimentalWslHosts === true).catch((error) => {
console.warn('[wsl-hosts] Could not apply the WSL hosts setting:', error)
})
}
})
void created.start().catch((error: unknown) => {
console.warn('[wsl-hosts] Could not adopt WSL distro holders:', error)
})
return created
}
export function registerWslHostHandlers(args: WslHostHandlerArgs): void {
for (const channel of WSL_HOST_HANDLER_CHANNELS) {
ipcMain.removeHandler(channel)
}
if (!getWslHostService()) {
const created = createService(args)
setWslHostService(created)
args.store.onSettingsChanged((updates, settings) => {
if ('experimentalWslHosts' in updates) {
void created?.enabledChanged(settings.experimentalWslHosts === true).catch((error) => {
console.warn('[wsl-hosts] Could not apply the WSL hosts setting:', error)
})
}
})
void created?.start().catch((error: unknown) => {
console.warn('[wsl-hosts] Could not adopt WSL distro holders:', error)
})
// Why none at all: without a service (gate closed, or not Windows) the preload exposes no API.
const wsl = getWslHostService() ?? startService(args)
if (!wsl) {
return
}
const wsl = getWslHostService()
ipcMain.handle('wslHosts:listDistros', () => wsl?.listDistros() ?? [])
ipcMain.handle('wslHosts:listDistros', () => wsl.listDistros())
ipcMain.handle(
'wslHosts:add',
(
_event,
request: { distro: string; startIfStopped?: boolean }
): Promise<WslHostAddResult> | WslHostAddResult => {
if (!wslHostsGateOpen()) {
return { ok: false, kind: 'disabled', message: WSL_HOSTS_GATED_OFF_MESSAGE }
}
return wsl
? wsl.add(request)
: { ok: false, kind: 'failed', message: 'WSL hosts are not available on this computer.' }
}
(_event, request: { distro: string; startIfStopped?: boolean }): Promise<WslHostAddResult> =>
wsl.add(request)
)
ipcMain.handle('wslHosts:remove', async (_event, request: { environmentId: string }) => {
// A closed gate leaves persisted WSL hosts untouched on disk.
if (!wslHostsGateOpen()) {
throw new Error(WSL_HOSTS_GATED_OFF_MESSAGE)
}
if (args.store.getSettings().activeRuntimeEnvironmentId === request.environmentId) {
throw new Error(
'This server is the default host for new projects. Choose another one in Advanced before removing it.'
)
}
return wsl ? wsl.remove(request.environmentId) : { stopped: true }
})
ipcMain.handle('wslHosts:connect', async (_event, request: { environmentId: string }) => {
if (!wslHostsGateOpen()) {
throw new Error(WSL_HOSTS_GATED_OFF_MESSAGE)
}
await wsl?.connect(request.environmentId)
return wsl.remove(request.environmentId)
})
ipcMain.handle('wslHosts:connect', (_event, request: { environmentId: string }) =>
wsl.connect(request.environmentId)
)
ipcMain.handle('wslHosts:getCapability', (): WslHostCapability => ({
serverFiles: wsl?.hasServerFiles() ?? false
serverFiles: wsl.hasServerFiles()
}))
ipcMain.handle('wslHosts:getStatuses', () => wsl?.getStatuses() ?? [])
ipcMain.handle('wslHosts:getStatuses', () => wsl.getStatuses())
ipcMain.handle('wslHosts:setVisibleWork', (_event, request: { environmentIds: string[] }) => {
wsl?.setVisibleWork(Array.isArray(request.environmentIds) ? request.environmentIds : [])
wsl.setVisibleWork(Array.isArray(request.environmentIds) ? request.environmentIds : [])
})
}