mobile-debug-mcp 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ios/manage.js +106 -5
- package/dist/ios/utils.js +49 -0
- package/package.json +1 -1
- package/src/ios/manage.ts +93 -6
- package/src/ios/utils.ts +47 -0
- package/src/types.ts +1 -0
package/dist/ios/manage.js
CHANGED
|
@@ -291,16 +291,117 @@ export class iOSManage {
|
|
|
291
291
|
}
|
|
292
292
|
async startApp(bundleId, deviceId = "booted") {
|
|
293
293
|
validateBundleId(bundleId);
|
|
294
|
+
// Prepare instrumentation object upfront so it can be returned to callers
|
|
295
|
+
const instrumentation = { ts: new Date().toISOString(), action: 'startApp', cmd: 'xcrun', args: ['simctl', 'launch', deviceId, bundleId], cwd: process.cwd(), env: { PATH: process.env.PATH, XCRUN_PATH: process.env.XCRUN_PATH } };
|
|
294
296
|
try {
|
|
295
|
-
|
|
297
|
+
// Instrumentation: persist and emit to stderr for server logs
|
|
298
|
+
try {
|
|
299
|
+
await fs.appendFile('/tmp/mcp_startapp_instrument.log', JSON.stringify(instrumentation) + '\n');
|
|
300
|
+
}
|
|
301
|
+
catch (e) { }
|
|
302
|
+
try {
|
|
303
|
+
console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation));
|
|
304
|
+
}
|
|
305
|
+
catch (e) { }
|
|
306
|
+
}
|
|
307
|
+
catch { }
|
|
308
|
+
// Attempt to launch
|
|
309
|
+
let launchResult = null;
|
|
310
|
+
try {
|
|
311
|
+
launchResult = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
312
|
+
}
|
|
313
|
+
catch (launchErr) {
|
|
314
|
+
// Collect diagnostics when simctl launch fails
|
|
315
|
+
const launchDiag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
296
316
|
const device = await getIOSDeviceMetadata(deviceId);
|
|
297
|
-
|
|
317
|
+
const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId);
|
|
318
|
+
return { device, appStarted: false, launchTimeMs: 0, error: launchErr instanceof Error ? launchErr.message : String(launchErr), diagnostics: { launchDiag, post }, instrumentation };
|
|
319
|
+
}
|
|
320
|
+
// Basic success — but verify RunningBoard/installcoordination didn't mark it as placeholder
|
|
321
|
+
const device = await getIOSDeviceMetadata(deviceId);
|
|
322
|
+
// short wait to let system settle
|
|
323
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
324
|
+
let appinfo = '';
|
|
325
|
+
try {
|
|
326
|
+
const ai = await execCommand(['simctl', 'appinfo', deviceId, bundleId], deviceId);
|
|
327
|
+
appinfo = ai.output || '';
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
// capture recent runningboard/installcoordination logs
|
|
331
|
+
const logDiag = execCommandWithDiagnostics(['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--predicate', `(process == "${bundleId}" ) OR eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '1m'], deviceId);
|
|
332
|
+
const placeholderDetected = (appinfo && /isPlaceholder[:=]?\s*Y/i.test(appinfo)) || (logDiag && ((logDiag.runResult && ((logDiag.runResult.stdout || '').includes('isPlaceholder')) || (logDiag.runResult.stderr || '').includes('isPlaceholder'))));
|
|
333
|
+
if (placeholderDetected) {
|
|
334
|
+
const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId, appinfo);
|
|
335
|
+
return { device, appStarted: false, launchTimeMs: 0, diagnostics: { appinfo, logDiag, post }, instrumentation };
|
|
336
|
+
}
|
|
337
|
+
return { device, appStarted: !!(launchResult && launchResult.output), launchTimeMs: 1000, instrumentation };
|
|
338
|
+
}
|
|
339
|
+
appExecutableName(bundleId) {
|
|
340
|
+
// Best-effort executable name: prefer last component of bundleId
|
|
341
|
+
try {
|
|
342
|
+
const candidate = bundleId.split('.').pop();
|
|
343
|
+
return candidate || bundleId;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return bundleId;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Collect bundle- and system-level diagnostics after a failed or placeholder launch
|
|
350
|
+
async collectPostLaunchDiagnostics(bundleId, deviceId = "booted", appinfo) {
|
|
351
|
+
const diagnostics = { ts: new Date().toISOString(), bundleId, deviceId };
|
|
352
|
+
// gather simctl appinfo (if not provided)
|
|
353
|
+
try {
|
|
354
|
+
diagnostics.appinfo = appinfo || ((await execCommand(['simctl', 'appinfo', deviceId, bundleId], deviceId)).output || '');
|
|
298
355
|
}
|
|
299
356
|
catch (e) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
357
|
+
diagnostics.appinfoError = String(e);
|
|
358
|
+
}
|
|
359
|
+
// attempt to discover bundle path from appinfo
|
|
360
|
+
let bundlePath = null;
|
|
361
|
+
if (diagnostics.appinfo) {
|
|
362
|
+
const m = diagnostics.appinfo.match(/Path\s*=\s*"?([\S]+)"?/) || diagnostics.appinfo.match(/Container: (\/\S+)/);
|
|
363
|
+
if (m)
|
|
364
|
+
bundlePath = m[1];
|
|
365
|
+
}
|
|
366
|
+
// lipo / file / otool / codesign / xattr
|
|
367
|
+
if (bundlePath) {
|
|
368
|
+
diagnostics.bundlePath = bundlePath;
|
|
369
|
+
const execs = [
|
|
370
|
+
{ name: 'file', cmd: ['file', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
371
|
+
{ name: 'lipo', cmd: ['lipo', '-info', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
372
|
+
{ name: 'otool-L', cmd: ['otool', '-L', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
373
|
+
{ name: 'otool-load', cmd: ['otool', '-l', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
374
|
+
{ name: 'plutil', cmd: ['plutil', '-p', bundlePath + '/Info.plist'] },
|
|
375
|
+
{ name: 'codesign', cmd: ['codesign', '-dvvv', bundlePath] },
|
|
376
|
+
{ name: 'xattr', cmd: ['xattr', '-l', bundlePath] },
|
|
377
|
+
{ name: 'ls', cmd: ['ls', '-la', bundlePath] },
|
|
378
|
+
];
|
|
379
|
+
diagnostics.bundle = {};
|
|
380
|
+
for (const e of execs) {
|
|
381
|
+
try {
|
|
382
|
+
const r = execCommandWithDiagnostics(e.cmd, deviceId);
|
|
383
|
+
diagnostics.bundle[e.name] = r && r.runResult ? { stdout: r.runResult.stdout, stderr: r.runResult.stderr, code: r.runResult.exitCode } : { error: 'no-result' };
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
diagnostics.bundle[e.name] = { error: String(err) };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// collect recent system logs and a screenshot
|
|
391
|
+
try {
|
|
392
|
+
diagnostics.recentLogs = execCommandWithDiagnostics(['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--predicate', `eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '5m'], deviceId);
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
diagnostics.recentLogsError = String(e);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const shot = await execCommandWithDiagnostics(['simctl', 'io', deviceId, 'screenshot', '--type', 'png', '/tmp/mcp_post_launch_screenshot.png'], deviceId);
|
|
399
|
+
diagnostics.screenshot = { created: true, path: '/tmp/mcp_post_launch_screenshot.png', result: shot && shot.runResult };
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
diagnostics.screenshotError = String(e);
|
|
303
403
|
}
|
|
404
|
+
return diagnostics;
|
|
304
405
|
}
|
|
305
406
|
async terminateApp(bundleId, deviceId = "booted") {
|
|
306
407
|
validateBundleId(bundleId);
|
package/dist/ios/utils.js
CHANGED
|
@@ -96,7 +96,40 @@ export function validateBundleId(bundleId) {
|
|
|
96
96
|
}
|
|
97
97
|
export function execCommand(args, deviceId = "booted") {
|
|
98
98
|
return new Promise((resolve, reject) => {
|
|
99
|
+
// Instrumentation: append a JSON line with timestamp, command, args, cwd and selected env vars
|
|
100
|
+
try {
|
|
101
|
+
const mcpEnv = {};
|
|
102
|
+
for (const k of Object.keys(process.env || {})) {
|
|
103
|
+
if (k.startsWith('MCP_'))
|
|
104
|
+
mcpEnv[k] = process.env[k];
|
|
105
|
+
}
|
|
106
|
+
const instrument = {
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
command: getXcrunCmd(),
|
|
109
|
+
args,
|
|
110
|
+
cwd: process.cwd(),
|
|
111
|
+
env: {
|
|
112
|
+
PATH: process.env.PATH,
|
|
113
|
+
XCRUN_PATH: process.env.XCRUN_PATH,
|
|
114
|
+
...mcpEnv
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(instrument) + '\n');
|
|
119
|
+
}
|
|
120
|
+
catch (e) { }
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
// swallow instrumentation errors to avoid changing behavior
|
|
124
|
+
}
|
|
99
125
|
// Use spawn for better stream control and consistency with Android implementation
|
|
126
|
+
// Instrument: emit a JSON line to stderr so the MCP server stderr/stdout capture can record the exact command and env
|
|
127
|
+
try {
|
|
128
|
+
const instLine = JSON.stringify({ ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd(), PATH: process.env.PATH });
|
|
129
|
+
// Use stderr so it appears in server logs reliably
|
|
130
|
+
console.error('MCP-INSTRUMENT-EXEC', instLine);
|
|
131
|
+
}
|
|
132
|
+
catch (e) { }
|
|
100
133
|
const child = spawn(getXcrunCmd(), args);
|
|
101
134
|
let stdout = '';
|
|
102
135
|
let stderr = '';
|
|
@@ -110,6 +143,17 @@ export function execCommand(args, deviceId = "booted") {
|
|
|
110
143
|
stderr += data.toString();
|
|
111
144
|
});
|
|
112
145
|
}
|
|
146
|
+
// Additional instrumentation: write pid and env snapshot when child starts
|
|
147
|
+
try {
|
|
148
|
+
const pidInfo = { ts: new Date().toISOString(), childPid: (child.pid || null), invoked: getXcrunCmd(), args };
|
|
149
|
+
try {
|
|
150
|
+
require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(pidInfo) + '\n');
|
|
151
|
+
}
|
|
152
|
+
catch (e) { }
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
113
157
|
const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000; // env (ms) or default 30s
|
|
114
158
|
const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000; // env (ms) or default 60s
|
|
115
159
|
const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT; // choose appropriate timeout
|
|
@@ -133,6 +177,11 @@ export function execCommand(args, deviceId = "booted") {
|
|
|
133
177
|
});
|
|
134
178
|
}
|
|
135
179
|
export function execCommandWithDiagnostics(args, deviceId = "booted") {
|
|
180
|
+
try {
|
|
181
|
+
const syncInst = { ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd() };
|
|
182
|
+
require('fs').appendFileSync('/tmp/mcp_exec_instrument_sync.log', JSON.stringify(syncInst) + '\n');
|
|
183
|
+
}
|
|
184
|
+
catch (e) { }
|
|
136
185
|
// Run synchronously to capture stdout/stderr and exitCode reliably for diagnostics
|
|
137
186
|
const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000;
|
|
138
187
|
const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000;
|
package/package.json
CHANGED
package/src/ios/manage.ts
CHANGED
|
@@ -298,15 +298,102 @@ export class iOSManage {
|
|
|
298
298
|
|
|
299
299
|
async startApp(bundleId: string, deviceId: string = "booted"): Promise<StartAppResponse> {
|
|
300
300
|
validateBundleId(bundleId)
|
|
301
|
+
// Prepare instrumentation object upfront so it can be returned to callers
|
|
302
|
+
const instrumentation = { ts: new Date().toISOString(), action: 'startApp', cmd: 'xcrun', args: ['simctl','launch', deviceId, bundleId], cwd: process.cwd(), env: { PATH: process.env.PATH, XCRUN_PATH: process.env.XCRUN_PATH } }
|
|
303
|
+
|
|
301
304
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
} catch
|
|
306
|
-
|
|
305
|
+
// Instrumentation: persist and emit to stderr for server logs
|
|
306
|
+
try { await fs.appendFile('/tmp/mcp_startapp_instrument.log', JSON.stringify(instrumentation) + '\n') } catch (e) {}
|
|
307
|
+
try { console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation)) } catch (e) {}
|
|
308
|
+
} catch {}
|
|
309
|
+
|
|
310
|
+
// Attempt to launch
|
|
311
|
+
let launchResult: any = null
|
|
312
|
+
try {
|
|
313
|
+
launchResult = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
314
|
+
} catch (launchErr:any) {
|
|
315
|
+
// Collect diagnostics when simctl launch fails
|
|
316
|
+
const launchDiag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
307
317
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
308
|
-
|
|
318
|
+
const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId)
|
|
319
|
+
return { device, appStarted: false, launchTimeMs: 0, error: launchErr instanceof Error ? launchErr.message : String(launchErr), diagnostics: { launchDiag, post }, instrumentation } as any
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Basic success — but verify RunningBoard/installcoordination didn't mark it as placeholder
|
|
323
|
+
const device = await getIOSDeviceMetadata(deviceId)
|
|
324
|
+
// short wait to let system settle
|
|
325
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
326
|
+
|
|
327
|
+
let appinfo = ''
|
|
328
|
+
try {
|
|
329
|
+
const ai = await execCommand(['simctl', 'appinfo', deviceId, bundleId], deviceId)
|
|
330
|
+
appinfo = ai.output || ''
|
|
331
|
+
} catch {}
|
|
332
|
+
|
|
333
|
+
// capture recent runningboard/installcoordination logs
|
|
334
|
+
const logDiag = execCommandWithDiagnostics(['simctl','spawn',deviceId,'log','show','--style','syslog','--predicate',`(process == "${bundleId}" ) OR eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '1m'], deviceId)
|
|
335
|
+
|
|
336
|
+
const placeholderDetected = (appinfo && /isPlaceholder[:=]?\s*Y/i.test(appinfo)) || (logDiag && ((logDiag.runResult && ((logDiag.runResult.stdout || '').includes('isPlaceholder')) || (logDiag.runResult.stderr || '').includes('isPlaceholder'))))
|
|
337
|
+
|
|
338
|
+
if (placeholderDetected) {
|
|
339
|
+
const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId, appinfo)
|
|
340
|
+
return { device, appStarted: false, launchTimeMs: 0, diagnostics: { appinfo, logDiag, post }, instrumentation } as any
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { device, appStarted: !!(launchResult && launchResult.output), launchTimeMs: 1000, instrumentation }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
appExecutableName(bundleId: string) {
|
|
347
|
+
// Best-effort executable name: prefer last component of bundleId
|
|
348
|
+
try { const candidate = bundleId.split('.').pop(); return candidate || bundleId }
|
|
349
|
+
catch { return bundleId }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Collect bundle- and system-level diagnostics after a failed or placeholder launch
|
|
353
|
+
async collectPostLaunchDiagnostics(bundleId: string, deviceId: string = "booted", appinfo?: string) {
|
|
354
|
+
const diagnostics: any = { ts: new Date().toISOString(), bundleId, deviceId }
|
|
355
|
+
|
|
356
|
+
// gather simctl appinfo (if not provided)
|
|
357
|
+
try { diagnostics.appinfo = appinfo || ((await execCommand(['simctl','appinfo', deviceId, bundleId], deviceId)).output || '') } catch (e) { diagnostics.appinfoError = String(e) }
|
|
358
|
+
|
|
359
|
+
// attempt to discover bundle path from appinfo
|
|
360
|
+
let bundlePath: string | null = null
|
|
361
|
+
if (diagnostics.appinfo) {
|
|
362
|
+
const m = diagnostics.appinfo.match(/Path\s*=\s*"?([\S]+)"?/) || diagnostics.appinfo.match(/Container: (\/\S+)/)
|
|
363
|
+
if (m) bundlePath = m[1]
|
|
309
364
|
}
|
|
365
|
+
|
|
366
|
+
// lipo / file / otool / codesign / xattr
|
|
367
|
+
if (bundlePath) {
|
|
368
|
+
diagnostics.bundlePath = bundlePath
|
|
369
|
+
const execs = [
|
|
370
|
+
{ name: 'file', cmd: ['file', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
371
|
+
{ name: 'lipo', cmd: ['lipo', '-info', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
372
|
+
{ name: 'otool-L', cmd: ['otool', '-L', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
373
|
+
{ name: 'otool-load', cmd: ['otool', '-l', bundlePath + '/' + this.appExecutableName(bundleId)] },
|
|
374
|
+
{ name: 'plutil', cmd: ['plutil', '-p', bundlePath + '/Info.plist'] },
|
|
375
|
+
{ name: 'codesign', cmd: ['codesign', '-dvvv', bundlePath] },
|
|
376
|
+
{ name: 'xattr', cmd: ['xattr', '-l', bundlePath] },
|
|
377
|
+
{ name: 'ls', cmd: ['ls', '-la', bundlePath] },
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
diagnostics.bundle = {}
|
|
381
|
+
for (const e of execs) {
|
|
382
|
+
try {
|
|
383
|
+
const r = execCommandWithDiagnostics(e.cmd, deviceId)
|
|
384
|
+
diagnostics.bundle[e.name] = r && r.runResult ? { stdout: r.runResult.stdout, stderr: r.runResult.stderr, code: r.runResult.exitCode } : { error: 'no-result' }
|
|
385
|
+
} catch (err) { diagnostics.bundle[e.name] = { error: String(err) } }
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// collect recent system logs and a screenshot
|
|
390
|
+
try { diagnostics.recentLogs = execCommandWithDiagnostics(['simctl','spawn',deviceId,'log','show','--style','syslog','--predicate',`eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '5m'], deviceId) } catch (e) { diagnostics.recentLogsError = String(e) }
|
|
391
|
+
try {
|
|
392
|
+
const shot = await execCommandWithDiagnostics(['simctl','io', deviceId, 'screenshot', '--type', 'png', '/tmp/mcp_post_launch_screenshot.png'], deviceId)
|
|
393
|
+
diagnostics.screenshot = { created: true, path: '/tmp/mcp_post_launch_screenshot.png', result: shot && shot.runResult }
|
|
394
|
+
} catch (e) { diagnostics.screenshotError = String(e) }
|
|
395
|
+
|
|
396
|
+
return diagnostics
|
|
310
397
|
}
|
|
311
398
|
|
|
312
399
|
async terminateApp(bundleId: string, deviceId: string = "booted"): Promise<TerminateAppResponse> {
|
package/src/ios/utils.ts
CHANGED
|
@@ -88,7 +88,41 @@ export function validateBundleId(bundleId: string) {
|
|
|
88
88
|
|
|
89
89
|
export function execCommand(args: string[], deviceId: string = "booted"): Promise<IOSResult> {
|
|
90
90
|
return new Promise((resolve, reject) => {
|
|
91
|
+
// Instrumentation: append a JSON line with timestamp, command, args, cwd and selected env vars
|
|
92
|
+
try {
|
|
93
|
+
const mcpEnv: Record<string,string|undefined> = {}
|
|
94
|
+
for (const k of Object.keys(process.env || {})) {
|
|
95
|
+
if (k.startsWith('MCP_')) mcpEnv[k] = process.env[k]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const instrument = {
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
command: getXcrunCmd(),
|
|
101
|
+
args,
|
|
102
|
+
cwd: process.cwd(),
|
|
103
|
+
env: {
|
|
104
|
+
PATH: process.env.PATH,
|
|
105
|
+
XCRUN_PATH: process.env.XCRUN_PATH,
|
|
106
|
+
...mcpEnv
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(instrument) + '\n')
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// swallow instrumentation errors to avoid changing behavior
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
// Use spawn for better stream control and consistency with Android implementation
|
|
119
|
+
// Instrument: emit a JSON line to stderr so the MCP server stderr/stdout capture can record the exact command and env
|
|
120
|
+
try {
|
|
121
|
+
const instLine = JSON.stringify({ ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd(), PATH: process.env.PATH })
|
|
122
|
+
// Use stderr so it appears in server logs reliably
|
|
123
|
+
console.error('MCP-INSTRUMENT-EXEC', instLine)
|
|
124
|
+
} catch (e) {}
|
|
125
|
+
|
|
92
126
|
const child = spawn(getXcrunCmd(), args)
|
|
93
127
|
|
|
94
128
|
let stdout = ''
|
|
@@ -106,6 +140,14 @@ export function execCommand(args: string[], deviceId: string = "booted"): Promis
|
|
|
106
140
|
})
|
|
107
141
|
}
|
|
108
142
|
|
|
143
|
+
// Additional instrumentation: write pid and env snapshot when child starts
|
|
144
|
+
try {
|
|
145
|
+
const pidInfo = { ts: new Date().toISOString(), childPid: (child.pid || null), invoked: getXcrunCmd(), args }
|
|
146
|
+
try { require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(pidInfo) + '\n') } catch (e) {}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000 // env (ms) or default 30s
|
|
110
152
|
const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000 // env (ms) or default 60s
|
|
111
153
|
const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT // choose appropriate timeout
|
|
@@ -131,6 +173,11 @@ export function execCommand(args: string[], deviceId: string = "booted"): Promis
|
|
|
131
173
|
}
|
|
132
174
|
|
|
133
175
|
export function execCommandWithDiagnostics(args: string[], deviceId: string = "booted") {
|
|
176
|
+
try {
|
|
177
|
+
const syncInst = { ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd() }
|
|
178
|
+
require('fs').appendFileSync('/tmp/mcp_exec_instrument_sync.log', JSON.stringify(syncInst) + '\n')
|
|
179
|
+
} catch (e) {}
|
|
180
|
+
|
|
134
181
|
// Run synchronously to capture stdout/stderr and exitCode reliably for diagnostics
|
|
135
182
|
const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000
|
|
136
183
|
const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000
|