Skip to content

Commit 77808c1

Browse files
committed
fix(desktop): bound Windows app fallback scan
1 parent 5024fea commit 77808c1

6 files changed

Lines changed: 61 additions & 22 deletions

File tree

packages/desktop/src/app-path-cache.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { describe, expect, test } from "bun:test"
2-
import { createAppPathCache, forgetAppPath, getAppPath, rememberAppPath } from "./app-path-cache"
2+
import { APP_PATH_CACHE_LIMIT, createAppPathCache, forgetAppPath, getAppPath, rememberAppPath } from "./app-path-cache"
33

44
describe("app path cache", () => {
55
test("bounds remembered app paths without retaining evicted entries", () => {
66
const cache = createAppPathCache()
77

8-
for (const index of Array.from({ length: 33 }, (_, index) => index)) {
8+
for (const index of Array.from({ length: APP_PATH_CACHE_LIMIT + 1 }, (_, index) => index)) {
99
rememberAppPath(cache, `app-${index}`, `C:\\Tools\\app-${index}.exe`)
1010
}
1111

12-
expect(cache.keys).toHaveLength(32)
13-
expect(cache.values).toHaveLength(32)
12+
expect(cache.keys).toHaveLength(APP_PATH_CACHE_LIMIT)
13+
expect(cache.values).toHaveLength(APP_PATH_CACHE_LIMIT)
1414
expect(getAppPath(cache, "app-0")).toBeUndefined()
1515
expect(getAppPath(cache, "app-1")).toBe("C:\\Tools\\app-1.exe")
16-
expect(getAppPath(cache, "app-32")).toBe("C:\\Tools\\app-32.exe")
16+
expect(getAppPath(cache, `app-${APP_PATH_CACHE_LIMIT}`)).toBe(`C:\\Tools\\app-${APP_PATH_CACHE_LIMIT}.exe`)
1717
})
1818

1919
test("forgets stale app paths", () => {

packages/desktop/src/app-path-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const APP_PATH_CACHE_LIMIT = 32
1+
export const APP_PATH_CACHE_LIMIT = 32
22

33
// Keep storage flat under high churn; this cache is small enough that linear lookup is cheaper than Map retention.
44
type AppPathCache = {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { dirname, join } from "node:path"
3+
import { APP_PATH_CACHE_LIMIT } from "../app-path-cache"
4+
import { getWindowsFallbackSearchDirs } from "./apps"
5+
6+
describe("Windows app path resolution", () => {
7+
test("deduplicates fallback search directories from stale resolver output", () => {
8+
const paths = Array.from({ length: APP_PATH_CACHE_LIMIT }, (_, index) =>
9+
join("root", "Tools", "bin", `Missing-${index}.exe`),
10+
)
11+
const dirs = getWindowsFallbackSearchDirs(paths)
12+
const bin = dirname(paths[0])
13+
const tools = dirname(bin)
14+
const root = dirname(tools)
15+
16+
expect(dirs).toEqual([bin, tools, root])
17+
})
18+
19+
test("bounds fallback search directories from scattered stale resolver output", () => {
20+
const dirs = getWindowsFallbackSearchDirs(
21+
Array.from({ length: APP_PATH_CACHE_LIMIT + 1 }, (_, index) =>
22+
join("root", `Tools-${index}`, "bin", "Missing.exe"),
23+
),
24+
)
25+
26+
expect(dirs).toHaveLength(APP_PATH_CACHE_LIMIT)
27+
expect(new Set(dirs).size).toBe(APP_PATH_CACHE_LIMIT)
28+
})
29+
})

packages/desktop/src/main/apps.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { access, readFile, readdir } from "node:fs/promises"
22
import { dirname, extname, join } from "node:path"
3-
import { createAppPathCache, forgetAppPath, getAppPath, rememberAppPath } from "../app-path-cache"
3+
import { APP_PATH_CACHE_LIMIT, createAppPathCache, forgetAppPath, getAppPath, rememberAppPath } from "../app-path-cache"
44
import { execFileHidden } from "./child-process"
55

66
const exists = (path: string) =>
@@ -10,13 +10,24 @@ const exists = (path: string) =>
1010

1111
const windowsAppPathCache = createAppPathCache()
1212

13+
// This scan runs after direct executable and command shim resolution, so keep it at app path cache size.
14+
const WINDOWS_APP_FALLBACK_DIR_LIMIT = APP_PATH_CACHE_LIMIT
15+
1316
const searchKey = (value: string) =>
1417
value
1518
.split("")
1619
.filter((value: string) => /[a-z0-9]/i.test(value))
1720
.map((value: string) => value.toLowerCase())
1821
.join("")
1922

23+
export function getWindowsFallbackSearchDirs(paths: string[]) {
24+
return Array.from(
25+
new Set(paths.flatMap((path) => [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))])),
26+
)
27+
.filter((dir) => dir !== ".")
28+
.slice(0, WINDOWS_APP_FALLBACK_DIR_LIMIT)
29+
}
30+
2031
export function checkAppExists(appName: string) {
2132
if (process.platform === "win32") return true
2233
if (process.platform === "linux") return true
@@ -133,20 +144,17 @@ async function resolveWindowsAppPath(appName: string): Promise<string | null> {
133144
const key = searchKey(appName)
134145

135146
if (key) {
136-
for (const path of paths) {
137-
const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
138-
for (const dir of dirs) {
139-
try {
140-
for (const entry of await readdir(dir)) {
141-
const candidate = join(dir, entry)
142-
if (!hasExt(candidate, "exe")) continue
143-
const stem = entry.replace(/\.exe$/i, "")
144-
const name = searchKey(stem)
145-
if (name.includes(key) || key.includes(name)) return remember(candidate)
146-
}
147-
} catch {
148-
continue
147+
for (const dir of getWindowsFallbackSearchDirs(paths)) {
148+
try {
149+
for (const entry of await readdir(dir)) {
150+
const candidate = join(dir, entry)
151+
if (!hasExt(candidate, "exe")) continue
152+
const stem = entry.replace(/\.exe$/i, "")
153+
const name = searchKey(stem)
154+
if (name.includes(key) || key.includes(name)) return remember(candidate)
149155
}
156+
} catch {
157+
continue
150158
}
151159
}
152160
}

packages/desktop/src/main/child-process.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"
22
import fs from "node:fs/promises"
33
import os from "node:os"
44
import path from "node:path"
5+
import { APP_PATH_CACHE_LIMIT } from "../app-path-cache"
56

67
type ExecFileOptions = {
78
windowsHide?: boolean
@@ -330,7 +331,7 @@ describe("child process helpers", () => {
330331
const restore = platform("win32")
331332

332333
try {
333-
for (const index of Array.from({ length: 33 }, (_, index) => index)) {
334+
for (const index of Array.from({ length: APP_PATH_CACHE_LIMIT + 1 }, (_, index) => index)) {
334335
const file = path.join(dir, `Code-${index}.exe`)
335336
await fs.writeFile(file, "")
336337
output = `${file}\r\n`

packages/desktop/src/renderer/open-path.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, test } from "bun:test"
2+
import { APP_PATH_CACHE_LIMIT } from "../app-path-cache"
23
import { createOpenPath } from "./open-path"
34

45
function api(opts?: {
@@ -129,7 +130,7 @@ describe("renderer openPath", () => {
129130
const current = api()
130131
const openPath = createOpenPath(current.api, "windows")
131132

132-
for (const index of Array.from({ length: 33 }, (_, index) => index)) {
133+
for (const index of Array.from({ length: APP_PATH_CACHE_LIMIT + 1 }, (_, index) => index)) {
133134
await openPath("C:\\repo", `bounded-code-${index}`)
134135
}
135136
await openPath("C:\\repo", "bounded-code-0")

0 commit comments

Comments
 (0)