diff --git a/packages/desktop/src/executors/base/AbstractExecutor.ts b/packages/desktop/src/executors/base/AbstractExecutor.ts index a8bedb2e..44527f11 100644 --- a/packages/desktop/src/executors/base/AbstractExecutor.ts +++ b/packages/desktop/src/executors/base/AbstractExecutor.ts @@ -1238,24 +1238,27 @@ export abstract class AbstractExecutor extends EventEmitter { /** Kill process tree */ protected async killProcessTree(pid: number): Promise { + // Validate pid is a safe positive integer to prevent command injection + const safePid = Math.floor(pid); + if (safePid <= 0) return false; try { if (process.platform === 'win32') { - await execAsync(`taskkill /pid ${pid} /T /F`); + await execAsync(`taskkill /pid ${safePid} /T /F`); } else { // Kill process group try { - process.kill(-pid, 'SIGTERM'); + process.kill(-safePid, 'SIGTERM'); } catch { - process.kill(pid, 'SIGTERM'); + process.kill(safePid, 'SIGTERM'); } // Force kill after timeout await new Promise(resolve => setTimeout(resolve, 1000)); try { - process.kill(-pid, 'SIGKILL'); + process.kill(-safePid, 'SIGKILL'); } catch { try { - process.kill(pid, 'SIGKILL'); + process.kill(safePid, 'SIGKILL'); } catch { // Process already dead } diff --git a/packages/desktop/src/features/session/SessionManager.ts b/packages/desktop/src/features/session/SessionManager.ts index a685686b..36c560f4 100644 --- a/packages/desktop/src/features/session/SessionManager.ts +++ b/packages/desktop/src/features/session/SessionManager.ts @@ -1700,11 +1700,14 @@ export class SessionManager extends EventEmitter { private getAllDescendantPids(parentPid: number): number[] { const descendants: number[] = []; const platform = os.platform(); - + // Validate parentPid is a safe integer to prevent command injection + const safePid = Math.floor(parentPid); + if (!Number.isInteger(safePid) || safePid <= 0) return descendants; + try { if (platform === 'win32') { // On Windows, use wmic to get process tree - const output = execSync(`wmic process where (ParentProcessId=${parentPid}) get ProcessId`, { encoding: 'utf8' }); + const output = execSync(`wmic process where (ParentProcessId=${safePid}) get ProcessId`, { encoding: 'utf8' }); const lines = output.split('\n').filter(line => line.trim()); for (let i = 1; i < lines.length; i++) { // Skip header const pid = parseInt(lines[i].trim()); @@ -1716,7 +1719,7 @@ export class SessionManager extends EventEmitter { } } else { // On Unix-like systems, use ps to get children - const output = execSync(`ps -o pid= --ppid ${parentPid}`, { encoding: 'utf8' }); + const output = execSync(`ps -o pid= --ppid ${safePid}`, { encoding: 'utf8' }); const pids = output.split('\n') .map(line => parseInt(line.trim())) .filter(pid => !isNaN(pid)); diff --git a/packages/desktop/src/index.ts b/packages/desktop/src/index.ts index 02d2ce1f..6b8bece4 100644 --- a/packages/desktop/src/index.ts +++ b/packages/desktop/src/index.ts @@ -47,6 +47,10 @@ process.on('uncaughtException', (error: NodeJS.ErrnoException) => { console.error('Uncaught Exception:', error); }); +process.on('unhandledRejection', (reason: unknown) => { + console.error('Unhandled Promise Rejection:', reason); +}); + export let mainWindow: BrowserWindow | null = null; let aboutWindow: BrowserWindow | null = null; diff --git a/packages/desktop/src/infrastructure/database/database.ts b/packages/desktop/src/infrastructure/database/database.ts index b3bf3a8c..ad51eb6e 100644 --- a/packages/desktop/src/infrastructure/database/database.ts +++ b/packages/desktop/src/infrastructure/database/database.ts @@ -56,6 +56,8 @@ export class DatabaseService { mkdirSync(dir, { recursive: true }); this.db = new Database(dbPath); + this.db.pragma('journal_mode = WAL'); + this.db.pragma('foreign_keys = ON'); } /** @@ -72,26 +74,6 @@ export class DatabaseService { return transaction(); } - /** - * Execute an async function within a database transaction with automatic rollback on error - * @param fn Async function to execute within the transaction - * @returns Promise with result of the function - * @throws Error if transaction fails - */ - private async transactionAsync(fn: () => Promise): Promise { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(() => { - fn().then(resolve).catch(reject); - }); - - try { - transaction(); - } catch (error) { - reject(error); - } - }); - } - initialize(): void { this.initializeSchema(); this.runMigrations(); diff --git a/packages/desktop/src/infrastructure/utils/mutex.ts b/packages/desktop/src/infrastructure/utils/mutex.ts index 3e7c1756..a25641f0 100644 --- a/packages/desktop/src/infrastructure/utils/mutex.ts +++ b/packages/desktop/src/infrastructure/utils/mutex.ts @@ -11,50 +11,43 @@ const logger = { * in critical sections of code. Supports named locks and timeouts. */ export class Mutex { - private locks = new Map>(); - private lockCounts = new Map(); + private queue = new Map void>>(); + private locked = new Set(); private defaultTimeout = 30000; // 30 seconds - /** - * Acquire a lock for the given resource name - * @param resourceName - Unique name for the resource to lock - * @param timeout - Optional timeout in milliseconds (default: 30 seconds) - * @returns Promise<() => void> - Release function to unlock the resource - */ async acquire(resourceName: string, timeout: number = this.defaultTimeout): Promise<() => void> { - const startTime = Date.now(); - - // If there's already a lock for this resource, wait for it - while (this.locks.has(resourceName)) { - if (Date.now() - startTime > timeout) { - throw new Error(`Mutex timeout after ${timeout}ms waiting for lock: ${resourceName}`); - } - - await new Promise(resolve => setTimeout(resolve, 10)); - } - - // Create a new lock promise - let releaseLock: (() => void) | null = null; - const lockPromise = new Promise(resolve => { - releaseLock = resolve; - }); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + // Remove from queue on timeout + const waiters = this.queue.get(resourceName); + if (waiters) { + const idx = waiters.indexOf(tryAcquire); + if (idx !== -1) waiters.splice(idx, 1); + } + reject(new Error(`Mutex timeout after ${timeout}ms waiting for lock: ${resourceName}`)); + }, timeout); - // Store the lock - this.locks.set(resourceName, lockPromise); - this.lockCounts.set(resourceName, (this.lockCounts.get(resourceName) || 0) + 1); - - const lockId = this.lockCounts.get(resourceName); + const tryAcquire = () => { + clearTimeout(timer); + this.locked.add(resourceName); + resolve(() => { + this.locked.delete(resourceName); + const waiters = this.queue.get(resourceName); + if (waiters && waiters.length > 0) { + const next = waiters.shift()!; + if (waiters.length === 0) this.queue.delete(resourceName); + next(); + } + }); + }; - // Return the release function - return () => { - if (this.locks.get(resourceName) === lockPromise) { - this.locks.delete(resourceName); + if (!this.locked.has(resourceName)) { + tryAcquire(); + } else { + if (!this.queue.has(resourceName)) this.queue.set(resourceName, []); + this.queue.get(resourceName)!.push(tryAcquire); } - - if (releaseLock) { - releaseLock(); - } - }; + }); } /** @@ -84,32 +77,21 @@ export class Mutex { * @returns boolean - True if the resource is locked */ isLocked(resourceName: string): boolean { - return this.locks.has(resourceName); + return this.locked.has(resourceName); } - /** - * Get the current number of active locks - * @returns number - Number of active locks - */ getActiveLockCount(): number { - return this.locks.size; + return this.locked.size; } - /** - * Get all currently locked resource names - * @returns string[] - Array of locked resource names - */ getLockedResources(): string[] { - return Array.from(this.locks.keys()); + return Array.from(this.locked); } - /** - * Force release all locks (use with caution) - */ releaseAll(): void { - logger.warn(`[Mutex] Force releasing all locks (${this.locks.size} active locks)`); - this.locks.clear(); - this.lockCounts.clear(); + logger.warn(`[Mutex] Force releasing all locks (${this.locked.size} active locks)`); + this.locked.clear(); + this.queue.clear(); } }