Skip to content

Commit a2ed5bc

Browse files
committed
fix: handle stale locks and missing ui_messages.json on startup
- Add stale lock detection on extension activation - Automatically recover by removing stale locks and creating empty ui_messages.json - Update safeWriteJson to create file before lock acquisition - Add comprehensive tests for stale lock recovery Fixes #6022
1 parent 9fce90b commit a2ed5bc

File tree

5 files changed

+635
-0
lines changed

5 files changed

+635
-0
lines changed

src/extension.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
CodeActionProvider,
4040
} from "./activate"
4141
import { initializeI18n } from "./i18n"
42+
import { performStartupStaleLockRecovery } from "./utils/staleLockRecovery"
4243

4344
/**
4445
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -86,6 +87,18 @@ export async function activate(context: vscode.ExtensionContext) {
8687
// Initialize i18n for internationalization support
8788
initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language))
8889

90+
// Perform stale lock recovery on startup
91+
try {
92+
await performStartupStaleLockRecovery(context.globalStorageUri.fsPath, {
93+
maxLockAge: 10 * 60 * 1000, // 10 minutes
94+
autoRecover: true,
95+
})
96+
} catch (error) {
97+
outputChannel.appendLine(
98+
`[StaleLockRecovery] Error during startup recovery: ${error instanceof Error ? error.message : String(error)}`,
99+
)
100+
}
101+
89102
// Initialize terminal shell execution handlers.
90103
TerminalRegistry.initialize()
91104

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
2+
import * as fs from "fs/promises"
3+
import * as path from "path"
4+
import * as os from "os"
5+
import { safeWriteJson } from "../safeWriteJson"
6+
7+
describe("safeWriteJson - stale lock handling", () => {
8+
let tempDir: string
9+
let testFilePath: string
10+
11+
beforeEach(async () => {
12+
// Create a temporary directory for testing
13+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "safe-write-json-stale-lock-test-"))
14+
testFilePath = path.join(tempDir, "test.json")
15+
})
16+
17+
afterEach(async () => {
18+
// Clean up temporary directory
19+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {})
20+
// Clean up any stray lock files
21+
try {
22+
await fs.unlink(`${testFilePath}.lock`)
23+
} catch {
24+
// Ignore if doesn't exist
25+
}
26+
})
27+
28+
it("should create file before acquiring lock if file doesn't exist", async () => {
29+
// Ensure file doesn't exist
30+
await expect(fs.access(testFilePath)).rejects.toThrow()
31+
32+
// Write data
33+
const testData = { test: "data" }
34+
await safeWriteJson(testFilePath, testData)
35+
36+
// Verify file was created with correct content
37+
const content = await fs.readFile(testFilePath, "utf8")
38+
expect(JSON.parse(content)).toEqual(testData)
39+
})
40+
41+
it("should handle missing target file by creating it first", async () => {
42+
// Ensure file doesn't exist
43+
await expect(fs.access(testFilePath)).rejects.toThrow()
44+
45+
// Write data - should succeed by creating the file first
46+
const testData = { test: "data for missing file" }
47+
await safeWriteJson(testFilePath, testData)
48+
49+
// Verify file was created with correct content
50+
const content = await fs.readFile(testFilePath, "utf8")
51+
expect(JSON.parse(content)).toEqual(testData)
52+
})
53+
54+
it("should work normally when file already exists", async () => {
55+
// Create file first
56+
const initialData = { initial: "content" }
57+
await fs.writeFile(testFilePath, JSON.stringify(initialData))
58+
59+
// Write new data
60+
const newData = { updated: "content" }
61+
await safeWriteJson(testFilePath, newData)
62+
63+
// Verify file was updated
64+
const content = await fs.readFile(testFilePath, "utf8")
65+
expect(JSON.parse(content)).toEqual(newData)
66+
})
67+
68+
it("should handle concurrent writes correctly", async () => {
69+
// Create file first
70+
await fs.writeFile(testFilePath, JSON.stringify({ initial: "data" }))
71+
72+
// Attempt concurrent writes
73+
const writes = []
74+
for (let i = 0; i < 5; i++) {
75+
writes.push(safeWriteJson(testFilePath, { count: i }))
76+
}
77+
78+
// All writes should complete without error
79+
await expect(Promise.all(writes)).resolves.toBeDefined()
80+
81+
// File should contain data from one of the writes
82+
const content = await fs.readFile(testFilePath, "utf8")
83+
const data = JSON.parse(content)
84+
expect(data).toHaveProperty("count")
85+
expect(typeof data.count).toBe("number")
86+
})
87+
88+
it("should handle temporary files correctly", async () => {
89+
// Create initial file
90+
const initialData = { initial: "data", important: true }
91+
await fs.writeFile(testFilePath, JSON.stringify(initialData))
92+
93+
// Write new data multiple times to ensure temp files are cleaned up
94+
for (let i = 0; i < 3; i++) {
95+
const newData = { updated: "content", iteration: i }
96+
await safeWriteJson(testFilePath, newData)
97+
98+
// Verify file was updated
99+
const content = await fs.readFile(testFilePath, "utf8")
100+
expect(JSON.parse(content)).toEqual(newData)
101+
}
102+
103+
// Check that no temp files remain in the directory
104+
const files = await fs.readdir(tempDir)
105+
const tempFiles = files.filter((f) => f.includes(".tmp") || f.includes(".bak"))
106+
expect(tempFiles).toHaveLength(0)
107+
})
108+
})

0 commit comments

Comments
 (0)