Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions packages/desktop/src/executors/base/AbstractExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,24 +1238,27 @@ export abstract class AbstractExecutor extends EventEmitter {

/** Kill process tree */
protected async killProcessTree(pid: number): Promise<boolean> {
// 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
}
Expand Down
9 changes: 6 additions & 3 deletions packages/desktop/src/features/session/SessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
22 changes: 2 additions & 20 deletions packages/desktop/src/infrastructure/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

/**
Expand All @@ -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<T>(fn: () => Promise<T>): Promise<T> {
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();
Expand Down
92 changes: 37 additions & 55 deletions packages/desktop/src/infrastructure/utils/mutex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,43 @@ const logger = {
* in critical sections of code. Supports named locks and timeouts.
*/
export class Mutex {
private locks = new Map<string, Promise<void>>();
private lockCounts = new Map<string, number>();
private queue = new Map<string, Array<() => void>>();
private locked = new Set<string>();
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<void>(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();
}
};
});
}

/**
Expand Down Expand Up @@ -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();
}
}

Expand Down
Loading