fix: eagerly prune deleted LSP roots
This commit is contained in:
parent
88ce43bf58
commit
3aae65f44d
3 changed files with 90 additions and 13 deletions
|
|
@ -2,6 +2,7 @@ import { BusEvent } from "@/bus/bus-event"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { LSPClient } from "./client"
|
import { LSPClient } from "./client"
|
||||||
|
import { watch as fswatch, type FSWatcher } from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { pathToFileURL, fileURLToPath } from "url"
|
import { pathToFileURL, fileURLToPath } from "url"
|
||||||
import { LSPServer } from "./server"
|
import { LSPServer } from "./server"
|
||||||
|
|
@ -137,7 +138,10 @@ export namespace LSP {
|
||||||
clients: LSPClient.Info[]
|
clients: LSPClient.Info[]
|
||||||
servers: Record<string, LSPServer.Info>
|
servers: Record<string, LSPServer.Info>
|
||||||
broken: Set<string>
|
broken: Set<string>
|
||||||
|
pruning: Promise<void> | undefined
|
||||||
spawning: Map<string, Promise<LSPClient.Info | undefined>>
|
spawning: Map<string, Promise<LSPClient.Info | undefined>>
|
||||||
|
subs: Map<string, FSWatcher>
|
||||||
|
timer: ReturnType<typeof setTimeout> | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
|
@ -212,11 +216,18 @@ export namespace LSP {
|
||||||
clients: [],
|
clients: [],
|
||||||
servers,
|
servers,
|
||||||
broken: new Set(),
|
broken: new Set(),
|
||||||
|
pruning: undefined,
|
||||||
spawning: new Map(),
|
spawning: new Map(),
|
||||||
|
subs: new Map(),
|
||||||
|
timer: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
Effect.promise(async () => {
|
Effect.promise(async () => {
|
||||||
|
if (s.timer) clearTimeout(s.timer)
|
||||||
|
for (const sub of s.subs.values()) {
|
||||||
|
sub.close()
|
||||||
|
}
|
||||||
await Promise.all(s.clients.map((client) => client.shutdown()))
|
await Promise.all(s.clients.map((client) => client.shutdown()))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -269,6 +280,7 @@ export namespace LSP {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.clients.push(client)
|
s.clients.push(client)
|
||||||
|
sync(s)
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,22 +330,74 @@ export namespace LSP {
|
||||||
return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x))))
|
return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x))))
|
||||||
})
|
})
|
||||||
|
|
||||||
const trim = Effect.fnUntraced(function* () {
|
function sync(s: State) {
|
||||||
const s = yield* InstanceState.get(state)
|
const next = new Set(s.clients.map((client) => path.dirname(client.root)))
|
||||||
const dead = yield* Effect.promise(async () => {
|
|
||||||
|
for (const [dir, sub] of s.subs) {
|
||||||
|
if (next.has(dir)) continue
|
||||||
|
s.subs.delete(dir)
|
||||||
|
sub.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of next) {
|
||||||
|
if (s.subs.has(dir)) continue
|
||||||
|
try {
|
||||||
|
const sub = fswatch(
|
||||||
|
dir,
|
||||||
|
{ persistent: false },
|
||||||
|
Instance.bind(() => {
|
||||||
|
kick(s)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
sub.on(
|
||||||
|
"error",
|
||||||
|
Instance.bind(() => {
|
||||||
|
if (s.subs.get(dir) !== sub) return
|
||||||
|
s.subs.delete(dir)
|
||||||
|
sub.close()
|
||||||
|
kick(s)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
s.subs.set(dir, sub)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function kick(s: State) {
|
||||||
|
if (s.timer) clearTimeout(s.timer)
|
||||||
|
s.timer = setTimeout(() => {
|
||||||
|
s.timer = undefined
|
||||||
|
void scan(s)
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scan(s: State) {
|
||||||
|
if (s.pruning) return s.pruning
|
||||||
|
|
||||||
|
const task = (async () => {
|
||||||
const dead = (
|
const dead = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
s.clients.map(async (client) => ((await Filesystem.exists(client.root)) ? undefined : client)),
|
s.clients.map(async (client) => ((await Filesystem.exists(client.root)) ? undefined : client)),
|
||||||
)
|
)
|
||||||
).filter((client): client is LSPClient.Info => Boolean(client))
|
).filter((client): client is LSPClient.Info => Boolean(client))
|
||||||
if (!dead.length) return [] as LSPClient.Info[]
|
if (!dead.length) return
|
||||||
|
|
||||||
const ids = new Set(dead.map((client) => `${client.serverID}:${client.root}`))
|
const ids = new Set(dead.map((client) => `${client.serverID}:${client.root}`))
|
||||||
s.clients = s.clients.filter((client) => !ids.has(`${client.serverID}:${client.root}`))
|
s.clients = s.clients.filter((client) => !ids.has(`${client.serverID}:${client.root}`))
|
||||||
|
sync(s)
|
||||||
await Promise.all(dead.map((client) => client.shutdown().catch(() => undefined)))
|
await Promise.all(dead.map((client) => client.shutdown().catch(() => undefined)))
|
||||||
return dead
|
await Bus.publish(Event.Updated, {})
|
||||||
|
})().finally(() => {
|
||||||
|
if (s.pruning === task) s.pruning = undefined
|
||||||
})
|
})
|
||||||
if (dead.length) Bus.publish(Event.Updated, {})
|
|
||||||
|
s.pruning = task
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
const trim = Effect.fnUntraced(function* () {
|
||||||
|
const s = yield* InstanceState.get(state)
|
||||||
|
yield* Effect.promise(() => scan(s))
|
||||||
})
|
})
|
||||||
|
|
||||||
const runAll = Effect.fnUntraced(function* <T>(fn: (client: LSPClient.Info) => Promise<T>) {
|
const runAll = Effect.fnUntraced(function* <T>(fn: (client: LSPClient.Info) => Promise<T>) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ process.on("SIGINT", () => {
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setInterval(() => {}, 1000)
|
||||||
|
|
||||||
let nextId = 1
|
let nextId = 1
|
||||||
|
|
||||||
function encode(message) {
|
function encode(message) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { afterEach, describe, expect } from "bun:test"
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Deferred, Effect, Layer } from "effect"
|
||||||
|
import { Bus } from "../../src/bus"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
|
|
@ -42,15 +43,25 @@ describe("LSP cleanup", () => {
|
||||||
yield* LSP.Service.use((svc) => svc.touchFile(file))
|
yield* LSP.Service.use((svc) => svc.touchFile(file))
|
||||||
expect(yield* LSP.Service.use((svc) => svc.status())).toHaveLength(1)
|
expect(yield* LSP.Service.use((svc) => svc.status())).toHaveLength(1)
|
||||||
|
|
||||||
|
const done = yield* Deferred.make<void>()
|
||||||
|
const off = Bus.subscribe(LSP.Event.Updated, () => {
|
||||||
|
Deferred.doneUnsafe(done, Effect.void)
|
||||||
|
})
|
||||||
|
yield* Effect.addFinalizer(() => Effect.sync(off))
|
||||||
|
|
||||||
yield* fs.remove(dir, { recursive: true, force: true })
|
yield* fs.remove(dir, { recursive: true, force: true })
|
||||||
|
yield* Deferred.await(done).pipe(Effect.timeout("2 seconds"))
|
||||||
|
|
||||||
|
const stopped = yield* Effect.promise(async () => {
|
||||||
|
for (const _ of Array.from({ length: 20 })) {
|
||||||
|
if (await fs.exists(mark)) return true
|
||||||
|
await sleep(50)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(stopped).toBe(true)
|
||||||
expect(yield* LSP.Service.use((svc) => svc.status())).toHaveLength(0)
|
expect(yield* LSP.Service.use((svc) => svc.status())).toHaveLength(0)
|
||||||
|
|
||||||
for (const _ of Array.from({ length: 20 })) {
|
|
||||||
if (yield* fs.exists(mark)) return
|
|
||||||
yield* Effect.promise(() => sleep(50))
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("fake lsp server did not exit")
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue