Merge branch 'fix-CPU-blocking' into pre-release
This commit is contained in:
+38
-33
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user