Skip to content

Commit 271cc13

Browse files
committed
Fixes #4734: Add locking mechanism to prevent race condition in esbuild.mjs
- Implemented file-based locking system to prevent concurrent execution - Added process PID tracking and stale lock cleanup - Ensures sequential execution when multiple packages run bundle task - Prevents ENOTEMPTY errors during CI builds - Added proper error handling and lock cleanup on process exit
1 parent 2e2f83b commit 271cc13

File tree

1 file changed

+177
-97
lines changed

1 file changed

+177
-97
lines changed

src/esbuild.mjs

Lines changed: 177 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,74 @@ import * as console from "node:console"
77

88
import { copyPaths, copyWasms, copyLocales, setupLocaleWatcher } from "@roo-code/build"
99

10+
// Lock file to prevent concurrent execution
11+
const LOCK_FILE = path.join(process.cwd(), '.esbuild.lock')
12+
const MAX_WAIT_TIME = 30000 // 30 seconds
13+
const POLL_INTERVAL = 100 // 100ms
14+
15+
async function acquireLock() {
16+
const startTime = Date.now()
17+
18+
while (Date.now() - startTime < MAX_WAIT_TIME) {
19+
try {
20+
// Try to create lock file exclusively
21+
fs.writeFileSync(LOCK_FILE, process.pid.toString(), { flag: 'wx' })
22+
return true
23+
} catch (error) {
24+
if (error.code === 'EEXIST') {
25+
// Lock file exists, check if the process is still running
26+
try {
27+
const lockPid = fs.readFileSync(LOCK_FILE, 'utf8').trim()
28+
const pid = parseInt(lockPid, 10)
29+
30+
// Check if process is still running
31+
try {
32+
process.kill(pid, 0) // Signal 0 checks if process exists
33+
// Process is still running, wait
34+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL))
35+
continue
36+
} catch (killError) {
37+
// Process is not running, remove stale lock
38+
fs.unlinkSync(LOCK_FILE)
39+
continue
40+
}
41+
} catch (readError) {
42+
// Can't read lock file, try to remove it
43+
try {
44+
fs.unlinkSync(LOCK_FILE)
45+
} catch (unlinkError) {
46+
// Ignore unlink errors
47+
}
48+
continue
49+
}
50+
} else {
51+
throw error
52+
}
53+
}
54+
}
55+
56+
throw new Error(`Failed to acquire lock after ${MAX_WAIT_TIME}ms`)
57+
}
58+
59+
function releaseLock() {
60+
try {
61+
fs.unlinkSync(LOCK_FILE)
62+
} catch (error) {
63+
// Ignore errors when releasing lock
64+
}
65+
}
66+
67+
// Ensure lock is released on process exit
68+
process.on('exit', releaseLock)
69+
process.on('SIGINT', () => {
70+
releaseLock()
71+
process.exit(0)
72+
})
73+
process.on('SIGTERM', () => {
74+
releaseLock()
75+
process.exit(0)
76+
})
77+
1078
const __filename = fileURLToPath(import.meta.url)
1179
const __dirname = path.dirname(__filename)
1280

@@ -17,117 +85,129 @@ async function main() {
1785
const minify = production
1886
const sourcemap = !production
1987

20-
/**
21-
* @type {import('esbuild').BuildOptions}
22-
*/
23-
const buildOptions = {
24-
bundle: true,
25-
minify,
26-
sourcemap,
27-
logLevel: "silent",
28-
format: "cjs",
29-
sourcesContent: false,
30-
platform: "node",
31-
}
88+
// Acquire lock to prevent concurrent execution
89+
console.log(`[${name}] Acquiring build lock...`)
90+
await acquireLock()
91+
console.log(`[${name}] Build lock acquired`)
3292

33-
const srcDir = __dirname
34-
const buildDir = __dirname
35-
const distDir = path.join(buildDir, "dist")
93+
try {
94+
/**
95+
* @type {import('esbuild').BuildOptions}
96+
*/
97+
const buildOptions = {
98+
bundle: true,
99+
minify,
100+
sourcemap,
101+
logLevel: "silent",
102+
format: "cjs",
103+
sourcesContent: false,
104+
platform: "node",
105+
}
36106

37-
if (fs.existsSync(distDir)) {
38-
console.log(`[${name}] Cleaning dist directory: ${distDir}`)
39-
fs.rmSync(distDir, { recursive: true, force: true })
40-
}
107+
const srcDir = __dirname
108+
const buildDir = __dirname
109+
const distDir = path.join(buildDir, "dist")
110+
111+
if (fs.existsSync(distDir)) {
112+
console.log(`[${name}] Cleaning dist directory: ${distDir}`)
113+
fs.rmSync(distDir, { recursive: true, force: true })
114+
}
41115

42-
/**
43-
* @type {import('esbuild').Plugin[]}
44-
*/
45-
const plugins = [
46-
{
47-
name: "copyFiles",
48-
setup(build) {
49-
build.onEnd(() => {
50-
copyPaths(
51-
[
52-
["../README.md", "README.md"],
53-
["../CHANGELOG.md", "CHANGELOG.md"],
54-
["../LICENSE", "LICENSE"],
55-
["../.env", ".env", { optional: true }],
56-
["node_modules/vscode-material-icons/generated", "assets/vscode-material-icons"],
57-
["../webview-ui/audio", "webview-ui/audio"],
58-
],
59-
srcDir,
60-
buildDir,
61-
)
62-
})
116+
/**
117+
* @type {import('esbuild').Plugin[]}
118+
*/
119+
const plugins = [
120+
{
121+
name: "copyFiles",
122+
setup(build) {
123+
build.onEnd(() => {
124+
copyPaths(
125+
[
126+
["../README.md", "README.md"],
127+
["../CHANGELOG.md", "CHANGELOG.md"],
128+
["../LICENSE", "LICENSE"],
129+
["../.env", ".env", { optional: true }],
130+
["node_modules/vscode-material-icons/generated", "assets/vscode-material-icons"],
131+
["../webview-ui/audio", "webview-ui/audio"],
132+
],
133+
srcDir,
134+
buildDir,
135+
)
136+
})
137+
},
63138
},
64-
},
65-
{
66-
name: "copyWasms",
67-
setup(build) {
68-
build.onEnd(() => copyWasms(srcDir, distDir))
139+
{
140+
name: "copyWasms",
141+
setup(build) {
142+
build.onEnd(() => copyWasms(srcDir, distDir))
143+
},
69144
},
70-
},
71-
{
72-
name: "copyLocales",
73-
setup(build) {
74-
build.onEnd(() => copyLocales(srcDir, distDir))
145+
{
146+
name: "copyLocales",
147+
setup(build) {
148+
build.onEnd(() => copyLocales(srcDir, distDir))
149+
},
75150
},
76-
},
77-
{
78-
name: "esbuild-problem-matcher",
79-
setup(build) {
80-
build.onStart(() => console.log("[esbuild-problem-matcher#onStart]"))
81-
build.onEnd((result) => {
82-
result.errors.forEach(({ text, location }) => {
83-
console.error(`✘ [ERROR] ${text}`)
84-
if (location && location.file) {
85-
console.error(` ${location.file}:${location.line}:${location.column}:`)
86-
}
87-
})
151+
{
152+
name: "esbuild-problem-matcher",
153+
setup(build) {
154+
build.onStart(() => console.log("[esbuild-problem-matcher#onStart]"))
155+
build.onEnd((result) => {
156+
result.errors.forEach(({ text, location }) => {
157+
console.error(`✘ [ERROR] ${text}`)
158+
if (location && location.file) {
159+
console.error(` ${location.file}:${location.line}:${location.column}:`)
160+
}
161+
})
88162

89-
console.log("[esbuild-problem-matcher#onEnd]")
90-
})
163+
console.log("[esbuild-problem-matcher#onEnd]")
164+
})
165+
},
91166
},
92-
},
93-
]
94-
95-
/**
96-
* @type {import('esbuild').BuildOptions}
97-
*/
98-
const extensionConfig = {
99-
...buildOptions,
100-
plugins,
101-
entryPoints: ["extension.ts"],
102-
outfile: "dist/extension.js",
103-
external: ["vscode"],
104-
}
167+
]
105168

106-
/**
107-
* @type {import('esbuild').BuildOptions}
108-
*/
109-
const workerConfig = {
110-
...buildOptions,
111-
entryPoints: ["workers/countTokens.ts"],
112-
outdir: "dist/workers",
113-
}
169+
/**
170+
* @type {import('esbuild').BuildOptions}
171+
*/
172+
const extensionConfig = {
173+
...buildOptions,
174+
plugins,
175+
entryPoints: ["extension.ts"],
176+
outfile: "dist/extension.js",
177+
external: ["vscode"],
178+
}
179+
180+
/**
181+
* @type {import('esbuild').BuildOptions}
182+
*/
183+
const workerConfig = {
184+
...buildOptions,
185+
entryPoints: ["workers/countTokens.ts"],
186+
outdir: "dist/workers",
187+
}
188+
189+
const [extensionCtx, workerCtx] = await Promise.all([
190+
esbuild.context(extensionConfig),
191+
esbuild.context(workerConfig),
192+
])
114193

115-
const [extensionCtx, workerCtx] = await Promise.all([
116-
esbuild.context(extensionConfig),
117-
esbuild.context(workerConfig),
118-
])
119-
120-
if (watch) {
121-
await Promise.all([extensionCtx.watch(), workerCtx.watch()])
122-
copyLocales(srcDir, distDir)
123-
setupLocaleWatcher(srcDir, distDir)
124-
} else {
125-
await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()])
126-
await Promise.all([extensionCtx.dispose(), workerCtx.dispose()])
194+
if (watch) {
195+
await Promise.all([extensionCtx.watch(), workerCtx.watch()])
196+
copyLocales(srcDir, distDir)
197+
setupLocaleWatcher(srcDir, distDir)
198+
} else {
199+
await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()])
200+
await Promise.all([extensionCtx.dispose(), workerCtx.dispose()])
201+
}
202+
} finally {
203+
// Always release the lock
204+
releaseLock()
205+
console.log(`[${name}] Build lock released`)
127206
}
128207
}
129208

130209
main().catch((e) => {
131210
console.error(e)
211+
releaseLock()
132212
process.exit(1)
133213
})

0 commit comments

Comments
 (0)