Skip to content

Commit 116a185

Browse files
authored
refactor: eliminate synchronous I/O and process spawn bottlenecks
Merged after functional review: durable history, command log, mesh ledger, pending events, and upgrade logs remain synchronous; async batching is limited to daemon debug logs. Verified daemon-core typecheck and full daemon-core tests locally.
2 parents 73867a4 + f66f768 commit 116a185

14 files changed

Lines changed: 262 additions & 102 deletions

File tree

packages/daemon-core/src/cli-adapters/provider-cli-shared.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -484,17 +484,25 @@ export function findBinary(name: string): string {
484484
return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
485485
}
486486
const isWin = os.platform() === 'win32';
487-
try {
488-
const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
489-
return execSync(cmd, {
490-
encoding: 'utf-8',
491-
timeout: 5000,
492-
stdio: ['pipe', 'pipe', 'pipe'],
493-
...(isWin ? { windowsHide: true } : {}),
494-
}).trim().split('\n')[0].trim();
495-
} catch {
496-
return isWin ? `${trimmed}.cmd` : trimmed;
487+
const paths = (process.env.PATH || '').split(path.delimiter);
488+
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
489+
490+
for (const p of paths) {
491+
if (!p) continue;
492+
for (const ext of exes) {
493+
const fullPath = path.join(p, trimmed + ext);
494+
try {
495+
const fs = require('fs');
496+
if (fs.existsSync(fullPath)) {
497+
const stat = fs.statSync(fullPath);
498+
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
499+
return fullPath;
500+
}
501+
}
502+
} catch { }
503+
}
497504
}
505+
return isWin ? `${trimmed}.cmd` : trimmed;
498506
}
499507

500508
export function isScriptBinary(binaryPath: string): boolean {

packages/daemon-core/src/commands/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5101,7 +5101,7 @@ export class DaemonCommandRouter {
51015101

51025102
// 3. Kill OS process if requested
51035103
if (killProcess) {
5104-
const running = isIdeRunning(ideType);
5104+
const running = await isIdeRunning(ideType);
51055105
if (running) {
51065106
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
51075107
const killed = await killIdeProcess(ideType);

packages/daemon-core/src/detection/ide-detector.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
* Migrated from @adhdev/core — this is now the single source of truth.
88
*/
99

10-
import { execSync } from 'child_process';
11-
import { existsSync } from 'fs';
10+
import { exec } from 'child_process';
11+
import { promisify } from 'util';
12+
const execAsync = promisify(exec);
13+
import { existsSync, statSync } from 'fs';
1214
import { platform, homedir } from 'os';
1315
import * as path from 'path';
1416
import type { ProviderLoader } from '../providers/provider-loader.js';
@@ -73,25 +75,33 @@ function findCliCommand(command: string): string | null {
7375
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
7476
return existsSync(resolved) ? resolved : null;
7577
}
76-
try {
77-
const result = execSync(
78-
platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
79-
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
80-
).trim();
81-
return result.split('\n')[0] || null;
82-
} catch {
83-
return null;
78+
const isWin = platform() === 'win32';
79+
const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
80+
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
81+
for (const p of paths) {
82+
if (!p) continue;
83+
for (const ext of exes) {
84+
const fullPath = path.join(p, trimmed + ext);
85+
try {
86+
if (existsSync(fullPath)) {
87+
const stat = statSync(fullPath);
88+
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
89+
return fullPath;
90+
}
91+
}
92+
} catch { }
93+
}
8494
}
95+
return null;
8596
}
8697

87-
function getIdeVersion(cliCommand: string): string | null {
98+
async function getIdeVersion(cliCommand: string): Promise<string | null> {
8899
try {
89-
const result = execSync(`"${cliCommand}" --version`, {
100+
const { stdout } = await execAsync(`"${cliCommand}" --version`, {
90101
encoding: 'utf-8',
91102
timeout: 10000,
92-
stdio: ['pipe', 'pipe', 'pipe'],
93-
}).trim();
94-
return result.split('\n')[0] || null;
103+
});
104+
return stdout.trim().split('\n')[0] || null;
95105
} catch {
96106
return null;
97107
}
@@ -152,7 +162,7 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
152162
const installed = os === 'darwin'
153163
? !!(resolvedCli || appPath)
154164
: !!resolvedCli;
155-
const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
165+
const version = resolvedCli ? await getIdeVersion(resolvedCli) : null;
156166

157167
results.push({
158168
id: def.id,

packages/daemon-core/src/installer.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface InstallResult {
3131
/**
3232
* Check if an extension is already installed
3333
*/
34-
export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): boolean;
34+
export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): Promise<boolean>;
3535
/**
3636
* Install a single extension
3737
*/

packages/daemon-core/src/installer.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,22 @@ export interface InstallResult {
122122
/**
123123
* Check if an extension is already installed
124124
*/
125-
export function isExtensionInstalled(
125+
import { promisify } from 'util';
126+
const execAsync = promisify(exec);
127+
128+
export async function isExtensionInstalled(
126129
ide: IDEInfo,
127130
marketplaceId: string
128-
): boolean {
131+
): Promise<boolean> {
129132
if (!ide.cliCommand) return false;
130133

131134
try {
132-
const result = execSync(`"${ide.cliCommand}" --list-extensions`, {
135+
const { stdout } = await execAsync(`"${ide.cliCommand}" --list-extensions`, {
133136
encoding: 'utf-8',
134137
timeout: 15000,
135-
stdio: ['pipe', 'pipe', 'pipe'],
136138
});
137139

138-
const installed = result
140+
const installed = stdout
139141
.trim()
140142
.split('\n')
141143
.map((e) => e.trim().toLowerCase());
@@ -163,7 +165,7 @@ export async function installExtension(
163165
}
164166

165167
// Check if already installed
166-
const alreadyInstalled = isExtensionInstalled(ide, extension.marketplaceId);
168+
const alreadyInstalled = await isExtensionInstalled(ide, extension.marketplaceId);
167169
if (alreadyInstalled) {
168170
return {
169171
extensionId: extension.id,

packages/daemon-core/src/launch.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/** Kill IDE process (graceful → force) */
1919
export declare function killIdeProcess(ideId: string): Promise<boolean>;
2020
/** Check if IDE process is running */
21-
export declare function isIdeRunning(ideId: string): boolean;
21+
export declare function isIdeRunning(ideId: string): Promise<boolean>;
2222
export interface LaunchOptions {
2323
ideId?: string;
2424
workspace?: string;

packages/daemon-core/src/launch.ts

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@
1616
* adhdev launch --workspace /path — Open specific workspace
1717
*/
1818

19-
import { execSync, spawn, spawnSync } from 'child_process';
19+
import { exec, spawn, spawnSync } from 'child_process';
20+
21+
async function execQuiet(command: string, options: any = {}): Promise<string> {
22+
return new Promise((resolve) => {
23+
exec(command, options, (error, stdout) => {
24+
if (error) return resolve('');
25+
resolve(stdout.toString());
26+
});
27+
});
28+
}
2029
import * as net from 'net';
2130
import * as os from 'os';
2231
import * as path from 'path';
@@ -76,11 +85,11 @@ function getIdePathCandidates(ideId: string): string[] {
7685
return getProviderLoader().getIdePathCandidates(ideId);
7786
}
7887

79-
function getMacAppProcessPids(ideId: string): number[] {
88+
async function getMacAppProcessPids(ideId: string): Promise<number[]> {
8089
const appPaths = getIdePathCandidates(ideId);
8190
if (appPaths.length === 0) return [];
8291
try {
83-
const output = execSync('ps axww -o pid=,args=', {
92+
const output = await execQuiet('ps axww -o pid=,args=', {
8493
encoding: 'utf-8',
8594
timeout: 3000,
8695
stdio: ['pipe', 'pipe', 'pipe'],
@@ -91,8 +100,8 @@ function getMacAppProcessPids(ideId: string): number[] {
91100
}
92101
}
93102

94-
function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): boolean {
95-
const pids = getMacAppProcessPids(ideId);
103+
async function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): Promise<boolean> {
104+
const pids = (await getMacAppProcessPids(ideId));
96105
let signalled = false;
97106
for (const pid of pids) {
98107
try {
@@ -163,73 +172,73 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
163172
if (plat === 'darwin' && appName) {
164173
// macOS: graceful quit via osascript
165174
try {
166-
execSync(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
175+
await execQuiet(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
167176
timeout: 5000,
168177
});
169178
} catch {
170-
try { execSync(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
179+
try { await execQuiet(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
171180
}
172-
killMacAppPathProcesses(ideId, 'SIGTERM');
181+
await killMacAppPathProcesses(ideId, 'SIGTERM');
173182
} else if (plat === 'win32' && winProcesses) {
174183
// Windows: taskkill for each process name
175184
for (const proc of winProcesses) {
176185
try {
177-
execSync(`taskkill /IM "${proc}" /F 2>nul`, { timeout: 5000 });
186+
await execQuiet(`taskkill /IM "${proc}" /F 2>nul`, { timeout: 5000 });
178187
} catch { }
179188
}
180189
// Process name may differ, so also try via WMIC
181190
try {
182191
const exeName = winProcesses[0].replace('.exe', '');
183-
execSync(`powershell -Command "Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue | Stop-Process -Force"`, {
192+
await execQuiet(`powershell -Command "Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue | Stop-Process -Force"`, {
184193
timeout: 10000,
185194
});
186195
} catch { }
187196
} else {
188-
try { execSync(`pkill -f "${ideId}" 2>/dev/null`); } catch { }
197+
try { await execQuiet(`pkill -f "${ideId}" 2>/dev/null`); } catch { }
189198
}
190199

191200
// Wait for process kill (max 15 seconds)
192201
for (let i = 0; i < 30; i++) {
193202
await new Promise(r => setTimeout(r, 500));
194-
if (!isIdeRunning(ideId)) return true;
203+
if (!(await isIdeRunning(ideId))) return true;
195204
}
196205

197206
// Force terminate retry
198207
if (plat === 'darwin' && appName) {
199-
try { execSync(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
200-
killMacAppPathProcesses(ideId, 'SIGKILL');
208+
try { await execQuiet(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
209+
await killMacAppPathProcesses(ideId, 'SIGKILL');
201210
} else if (plat === 'win32' && winProcesses) {
202211
for (const proc of winProcesses) {
203-
try { execSync(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
212+
try { await execQuiet(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
204213
}
205214
}
206215

207216
await new Promise(r => setTimeout(r, 2000));
208-
return !isIdeRunning(ideId);
217+
return !(await isIdeRunning(ideId));
209218

210219
} catch {
211220
return false;
212221
}
213222
}
214223

215224
/** Check if IDE process is running */
216-
export function isIdeRunning(ideId: string): boolean {
225+
export async function isIdeRunning(ideId: string): Promise<boolean> {
217226
const plat = os.platform();
218227

219228
try {
220229
if (plat === 'darwin') {
221230
const appName = getMacAppIdentifiers()[ideId];
222-
if (!appName) return getMacAppProcessPids(ideId).length > 0;
231+
if (!appName) return (await getMacAppProcessPids(ideId)).length > 0;
223232
try {
224-
const result = execSync(`pgrep -x "${appName}" 2>/dev/null`, {
233+
const result = await execQuiet(`pgrep -x "${appName}" 2>/dev/null`, {
225234
encoding: 'utf-8',
226235
timeout: 3000,
227236
});
228237
if (result.trim().length > 0) return true;
229238
} catch { }
230239

231240
try {
232-
const result = execSync(
241+
const result = await execQuiet(
233242
`osascript -e 'tell application "System Events" to count (every process whose name is "${escapeForAppleScript(appName)}")'`,
234243
{
235244
encoding: 'utf-8',
@@ -240,29 +249,29 @@ export function isIdeRunning(ideId: string): boolean {
240249
if (Number.parseInt(result.trim() || '0', 10) > 0) return true;
241250
} catch { }
242251

243-
return getMacAppProcessPids(ideId).length > 0;
252+
return (await getMacAppProcessPids(ideId)).length > 0;
244253
} else if (plat === 'win32') {
245254
const winProcesses = getWinProcessNames()[ideId];
246255
if (!winProcesses) return false;
247256
// Check each process name
248257
for (const proc of winProcesses) {
249258
try {
250-
const result = execSync(`tasklist /FI "IMAGENAME eq ${proc}" /NH 2>nul`, { encoding: 'utf-8' });
259+
const result = await execQuiet(`tasklist /FI "IMAGENAME eq ${proc}" /NH 2>nul`, { encoding: 'utf-8' });
251260
if (result.includes(proc)) return true;
252261
} catch { }
253262
}
254263
// Also check via PowerShell (when tasklist cannot find)
255264
try {
256265
const exeName = winProcesses[0].replace('.exe', '');
257-
const result = execSync(
266+
const result = await execQuiet(
258267
`powershell -Command "(Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue).Count"`,
259268
{ encoding: 'utf-8', timeout: 5000 }
260269
);
261270
return parseInt(result.trim()) > 0;
262271
} catch { }
263272
return false;
264273
} else {
265-
const result = execSync(`pgrep -f "${ideId}" 2>/dev/null`, { encoding: 'utf-8' });
274+
const result = await execQuiet(`pgrep -f "${ideId}" 2>/dev/null`, { encoding: 'utf-8' });
266275
return result.trim().length > 0;
267276
}
268277
} catch {
@@ -271,14 +280,14 @@ export function isIdeRunning(ideId: string): boolean {
271280
}
272281

273282
/** Detect currently open workspace path */
274-
function detectCurrentWorkspace(ideId: string): string | undefined {
283+
async function detectCurrentWorkspace(ideId: string): Promise<string | undefined> {
275284
const plat = os.platform();
276285

277286
if (plat === 'darwin') {
278287
try {
279288
const appName = getMacAppIdentifiers()[ideId];
280289
if (!appName) return undefined;
281-
const result = execSync(
290+
const result = await execQuiet(
282291
`lsof -c "${appName}" 2>/dev/null | grep cwd | head -1 | awk '{print $NF}'`,
283292
{ encoding: 'utf-8', timeout: 3000 }
284293
);
@@ -392,8 +401,8 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
392401
}
393402

394403
// 4. Check if IDE is currently running
395-
const alreadyRunning = isIdeRunning(targetIde.id);
396-
const workspace = options.workspace || (alreadyRunning ? detectCurrentWorkspace(targetIde.id) : undefined);
404+
const alreadyRunning = await isIdeRunning(targetIde.id);
405+
const workspace = options.workspace || (alreadyRunning ? await detectCurrentWorkspace(targetIde.id) : undefined);
397406

398407
// 5. If IDE is running, terminate it
399408
if (alreadyRunning) {

0 commit comments

Comments
 (0)