Merge branch 'fix-CPU-blocking' into pre-release

This commit is contained in:
Zimeng Xiong
2025-11-22 22:48:51 -08:00
2 changed files with 56 additions and 33 deletions
+38 -33
View File
@@ -6,9 +6,9 @@ import fs from "fs";
import { promises as fsPromises } from "fs"; import { promises as fsPromises } from "fs";
import { createServer } from "http"; import { createServer } from "http";
import { Server } from "socket.io"; import { Server } from "socket.io";
import { Worker } from "worker_threads";
import multer from "multer"; import multer from "multer";
import archiver from "archiver"; import archiver from "archiver";
import Database from "better-sqlite3";
import { z } from "zod"; import { z } from "zod";
// @ts-ignore // @ts-ignore
import { PrismaClient } from "./generated/client"; import { PrismaClient } from "./generated/client";
@@ -287,41 +287,46 @@ const validateSqliteHeader = (filePath: string): boolean => {
return false; return false;
} }
}; };
// Non-blocking CPU check using worker threads while still verifying headers
const runIntegrityCheck = (filePath: string): boolean => { const verifyDatabaseIntegrityAsync = (filePath: string): Promise<boolean> => {
// First validate the file header to prevent RCE attacks
if (!validateSqliteHeader(filePath)) { if (!validateSqliteHeader(filePath)) {
return false; return Promise.resolve(false);
} }
let dbInstance: Database.Database | undefined; return new Promise((resolve) => {
try { const worker = new Worker(
// Use readonly mode and file locking to be more conservative with system resources path.resolve(__dirname, "./workers/db-verify.js"),
dbInstance = new Database(filePath, { {
readonly: true, workerData: { filePath },
fileMustExist: true, }
timeout: 5000, // 5 second timeout for integrity check );
let timeoutHandle: NodeJS.Timeout;
let settled = false;
const finish = (result: boolean) => {
if (settled) return;
settled = true;
clearTimeout(timeoutHandle);
resolve(result);
};
worker.on("message", (isValid: boolean) => finish(isValid));
worker.on("error", (err) => {
console.error("Worker error:", err);
finish(false);
});
worker.on("exit", (code) => {
if (code !== 0) {
finish(false);
}
}); });
// Run integrity check with timeout timeoutHandle = setTimeout(() => {
const result = dbInstance.prepare("PRAGMA integrity_check;").get(); console.warn("Integrity check worker timed out", { filePath });
return result?.integrity_check === "ok"; worker.terminate();
} catch (error) { finish(false);
console.error("Integrity check failed:", error); }, 10000); // 10 second timeout
return false; });
} finally {
// Always close database connection to free resources
if (dbInstance) {
try {
dbInstance.close();
} catch (closeError) {
console.warn(
"Failed to close database after integrity check:",
closeError
);
}
}
}
}; };
const removeFileIfExists = async (filePath?: string) => { const removeFileIfExists = async (filePath?: string) => {
@@ -843,7 +848,7 @@ app.post("/import/sqlite/verify", upload.single("db"), async (req, res) => {
} }
const stagedPath = req.file.path; const stagedPath = req.file.path;
const isValid = runIntegrityCheck(stagedPath); const isValid = await verifyDatabaseIntegrityAsync(stagedPath);
await removeFileIfExists(stagedPath); await removeFileIfExists(stagedPath);
if (!isValid) { if (!isValid) {
@@ -883,7 +888,7 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
return res.status(500).json({ error: "Failed to stage uploaded file" }); return res.status(500).json({ error: "Failed to stage uploaded file" });
} }
const isValid = runIntegrityCheck(stagedPath); const isValid = await verifyDatabaseIntegrityAsync(stagedPath);
if (!isValid) { if (!isValid) {
await removeFileIfExists(stagedPath); await removeFileIfExists(stagedPath);
return res return res
+18
View File
@@ -0,0 +1,18 @@
const { parentPort, workerData } = require('worker_threads');
const Database = require('better-sqlite3');
if (!parentPort) throw new Error("Must be run in a worker thread");
try {
const { filePath } = workerData;
const db = new Database(filePath, { readonly: true, fileMustExist: true });
// This is the CPU-heavy operation
const result = db.prepare("PRAGMA integrity_check;").get();
db.close();
parentPort.postMessage(result.integrity_check === "ok");
} catch (error) {
// Any error means invalid or corrupt DB
parentPort.postMessage(false);
}