|
3 | 3 | import fs from "fs"; |
4 | 4 | import path from "path"; |
5 | 5 |
|
| 6 | +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); |
| 7 | + |
| 8 | +// On Windows, the uvicorn process can still hold SQLite file handles when |
| 9 | +// teardown runs. POSIX allows unlinking files with open handles; Win32 does |
| 10 | +// not, surfacing as EBUSY/EPERM. Retry with backoff, fall back to walking the |
| 11 | +// tree and removing children individually, and never throw out of teardown. |
| 12 | +async function removeWithRetry(target: string): Promise<boolean> { |
| 13 | + const attempts = 5; |
| 14 | + for (let i = 0; i < attempts; i++) { |
| 15 | + try { |
| 16 | + fs.rmSync(target, { recursive: true, force: true }); |
| 17 | + if (!fs.existsSync(target)) return true; |
| 18 | + } catch (err) { |
| 19 | + const code = (err as NodeJS.ErrnoException).code; |
| 20 | + if (code !== "EBUSY" && code !== "EPERM" && code !== "ENOTEMPTY") { |
| 21 | + throw err; |
| 22 | + } |
| 23 | + } |
| 24 | + await sleep(200 * 2 ** i); |
| 25 | + } |
| 26 | + return !fs.existsSync(target); |
| 27 | +} |
| 28 | + |
| 29 | +function removeChildrenBestEffort(target: string): string[] { |
| 30 | + const failed: string[] = []; |
| 31 | + let entries: fs.Dirent[]; |
| 32 | + try { |
| 33 | + entries = fs.readdirSync(target, { withFileTypes: true }); |
| 34 | + } catch { |
| 35 | + return failed; |
| 36 | + } |
| 37 | + for (const entry of entries) { |
| 38 | + const childPath = path.join(target, entry.name); |
| 39 | + try { |
| 40 | + fs.rmSync(childPath, { recursive: true, force: true }); |
| 41 | + } catch { |
| 42 | + failed.push(childPath); |
| 43 | + } |
| 44 | + } |
| 45 | + return failed; |
| 46 | +} |
| 47 | + |
6 | 48 | export default async () => { |
| 49 | + console.warn("Removing the temp database"); |
| 50 | + // this file is in src/frontend/tests/globalTeardown.ts |
| 51 | + // temp is in src/frontend/temp |
| 52 | + const tempDbPath = path.join(__dirname, "..", "temp"); |
| 53 | + console.warn("tempDbPath", tempDbPath); |
| 54 | + |
| 55 | + if (!fs.existsSync(tempDbPath)) { |
| 56 | + console.warn("Temp database directory does not exist, skipping removal"); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
7 | 60 | try { |
8 | | - console.warn("Removing the temp database"); |
9 | | - // Check if the file exists in the path |
10 | | - // this file is in src/frontend/tests/globalTeardown.ts |
11 | | - // temp is in src/frontend/temp |
12 | | - const tempDbPath = path.join(__dirname, "..", "temp"); |
13 | | - console.warn("tempDbPath", tempDbPath); |
14 | | - |
15 | | - // Check if the directory exists before attempting to remove it |
16 | | - if (fs.existsSync(tempDbPath)) { |
17 | | - // Remove the temp database |
18 | | - fs.rmSync(tempDbPath, { recursive: true, force: true }); |
19 | | - |
20 | | - // Check if the file is removed |
21 | | - if (!fs.existsSync(tempDbPath)) { |
22 | | - console.warn("Successfully removed the temp database"); |
23 | | - } else { |
24 | | - console.error( |
25 | | - "Error: temp database still exists after removal attempt", |
26 | | - ); |
27 | | - } |
28 | | - } else { |
29 | | - console.warn("Temp database directory does not exist, skipping removal"); |
| 61 | + if (await removeWithRetry(tempDbPath)) { |
| 62 | + console.warn("Successfully removed the temp database"); |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + const stragglers = removeChildrenBestEffort(tempDbPath); |
| 67 | + if (await removeWithRetry(tempDbPath)) { |
| 68 | + console.warn( |
| 69 | + "Successfully removed the temp database after per-file fallback", |
| 70 | + ); |
| 71 | + return; |
30 | 72 | } |
| 73 | + |
| 74 | + console.warn( |
| 75 | + `Temp database directory still present after retries; leaving it for the runner workspace cleanup. Files that resisted removal: ${stragglers.length}`, |
| 76 | + ); |
31 | 77 | } catch (error) { |
32 | 78 | console.error("Error while removing the temp database:", error); |
33 | 79 | } |
|
0 commit comments