Skip to content

Commit 8317fae

Browse files
committed
Add guard when debugger is paused
1 parent 65d2508 commit 8317fae

23 files changed

Lines changed: 637 additions & 212 deletions

File tree

.smithery/index.cjs

Lines changed: 208 additions & 188 deletions
Large diffs are not rendered by default.

docs/DEBUGGING_ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ processes. Tests should inject a mock `InteractiveSpawner` into `createLldbCliBa
225225
- Implementation: `src/utils/debugger/backends/dap-backend.ts`, with protocol support in
226226
`src/utils/debugger/dap/transport.ts`, `src/utils/debugger/dap/types.ts`, and adapter discovery in
227227
`src/utils/debugger/dap/adapter-discovery.ts`.
228-
- Selected via backend selection (explicit `backend`, `XCODEBUILDMCP_DEBUGGER_BACKEND=dap`).
228+
- Selected via backend selection (explicit `backend`, `XCODEBUILDMCP_DEBUGGER_BACKEND=dap`, or default when unset).
229229
- Adapter discovery uses `xcrun --find lldb-dap`; missing adapters raise a clear dependency error.
230230
- One `lldb-dap` process is spawned per session; DAP framing and request correlation are handled
231231
by `DapTransport`.

example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ public final class CalculatorService {
9494
guard let op = operation ?? lastOperation else { return }
9595
let operand = (operation != nil) ? currentNumber : lastOperand
9696

97+
if op == .add && previousNumber == 21 && operand == 21 {
98+
fatalError("Intentional crash for debugger smoke test")
99+
}
100+
97101
let result = op.calculate(previousNumber, operand)
98102

99103
// Error handling

src/mcp/tools/doctor/doctor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export async function runDoctor(
9090
debugger: {
9191
dap: {
9292
available: lldbDapAvailable,
93-
selected: selectedDebuggerBackend ?? '(default lldb-cli)',
93+
selected: selectedDebuggerBackend ?? '(default dap)',
9494
},
9595
},
9696
},

src/mcp/tools/ui-testing/button.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { log } from '../../../utils/logging/index.ts';
44
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
55
import type { CommandExecutor } from '../../../utils/execution/index.ts';
66
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
7+
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
8+
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
9+
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
710
import {
811
createAxeNotAvailableResponse,
912
getAxePath,
@@ -41,9 +44,18 @@ export async function buttonLogic(
4144
getBundledAxeEnvironment,
4245
createAxeNotAvailableResponse,
4346
},
47+
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
4448
): Promise<ToolResponse> {
4549
const toolName = 'button';
4650
const { simulatorId, buttonType, duration } = params;
51+
52+
const guard = await guardUiAutomationAgainstStoppedDebugger({
53+
debugger: debuggerManager,
54+
simulatorId,
55+
toolName,
56+
});
57+
if (guard.blockedResponse) return guard.blockedResponse;
58+
4759
const commandArgs = ['button', buttonType];
4860
if (duration !== undefined) {
4961
commandArgs.push('--duration', String(duration));
@@ -54,7 +66,11 @@ export async function buttonLogic(
5466
try {
5567
await executeAxeCommand(commandArgs, simulatorId, 'button', executor, axeHelpers);
5668
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
57-
return createTextResponse(`Hardware button '${buttonType}' pressed successfully.`);
69+
const message = `Hardware button '${buttonType}' pressed successfully.`;
70+
if (guard.warningText) {
71+
return createTextResponse(`${message}\n\n${guard.warningText}`);
72+
}
73+
return createTextResponse(message);
5874
} catch (error) {
5975
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
6076
if (error instanceof DependencyError) {

src/mcp/tools/ui-testing/describe_ui.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { createErrorResponse } from '../../../utils/responses/index.ts';
55
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
66
import type { CommandExecutor } from '../../../utils/execution/index.ts';
77
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
8+
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
9+
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
10+
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
811
import {
912
createAxeNotAvailableResponse,
1013
getAxePath,
@@ -52,11 +55,19 @@ export async function describe_uiLogic(
5255
getBundledAxeEnvironment,
5356
createAxeNotAvailableResponse,
5457
},
58+
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
5559
): Promise<ToolResponse> {
5660
const toolName = 'describe_ui';
5761
const { simulatorId } = params;
5862
const commandArgs = ['describe-ui'];
5963

64+
const guard = await guardUiAutomationAgainstStoppedDebugger({
65+
debugger: debuggerManager,
66+
simulatorId,
67+
toolName,
68+
});
69+
if (guard.blockedResponse) return guard.blockedResponse;
70+
6071
log('info', `${LOG_PREFIX}/${toolName}: Starting for ${simulatorId}`);
6172

6273
try {
@@ -72,7 +83,7 @@ export async function describe_uiLogic(
7283
recordDescribeUICall(simulatorId);
7384

7485
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
75-
return {
86+
const response: ToolResponse = {
7687
content: [
7788
{
7889
type: 'text',
@@ -89,6 +100,10 @@ export async function describe_uiLogic(
89100
},
90101
],
91102
};
103+
if (guard.warningText) {
104+
response.content.push({ type: 'text', text: guard.warningText });
105+
}
106+
return response;
92107
} catch (error) {
93108
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
94109
if (error instanceof DependencyError) {

src/mcp/tools/ui-testing/gesture.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
} from '../../../utils/responses/index.ts';
1818
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1919
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
20+
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
21+
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
22+
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
2023
import {
2124
createAxeNotAvailableResponse,
2225
getAxePath,
@@ -101,10 +104,17 @@ export async function gestureLogic(
101104
getBundledAxeEnvironment,
102105
createAxeNotAvailableResponse,
103106
},
107+
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
104108
): Promise<ToolResponse> {
105109
const toolName = 'gesture';
106110
const { simulatorId, preset, screenWidth, screenHeight, duration, delta, preDelay, postDelay } =
107111
params;
112+
const guard = await guardUiAutomationAgainstStoppedDebugger({
113+
debugger: debuggerManager,
114+
simulatorId,
115+
toolName,
116+
});
117+
if (guard.blockedResponse) return guard.blockedResponse;
108118
const commandArgs = ['gesture', preset];
109119

110120
if (screenWidth !== undefined) {
@@ -131,7 +141,11 @@ export async function gestureLogic(
131141
try {
132142
await executeAxeCommand(commandArgs, simulatorId, 'gesture', executor, axeHelpers);
133143
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
134-
return createTextResponse(`Gesture '${preset}' executed successfully.`);
144+
const message = `Gesture '${preset}' executed successfully.`;
145+
if (guard.warningText) {
146+
return createTextResponse(`${message}\n\n${guard.warningText}`);
147+
}
148+
return createTextResponse(message);
135149
} catch (error) {
136150
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
137151
if (error instanceof DependencyError) {

src/mcp/tools/ui-testing/key_press.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
} from '../../../utils/responses/index.ts';
1111
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1212
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
13+
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
14+
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
15+
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
1316
import {
1417
createAxeNotAvailableResponse,
1518
getAxePath,
@@ -46,9 +49,18 @@ export async function key_pressLogic(
4649
getBundledAxeEnvironment,
4750
createAxeNotAvailableResponse,
4851
},
52+
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
4953
): Promise<ToolResponse> {
5054
const toolName = 'key_press';
5155
const { simulatorId, keyCode, duration } = params;
56+
57+
const guard = await guardUiAutomationAgainstStoppedDebugger({
58+
debugger: debuggerManager,
59+
simulatorId,
60+
toolName,
61+
});
62+
if (guard.blockedResponse) return guard.blockedResponse;
63+
5264
const commandArgs = ['key', String(keyCode)];
5365
if (duration !== undefined) {
5466
commandArgs.push('--duration', String(duration));
@@ -59,7 +71,11 @@ export async function key_pressLogic(
5971
try {
6072
await executeAxeCommand(commandArgs, simulatorId, 'key', executor, axeHelpers);
6173
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
62-
return createTextResponse(`Key press (code: ${keyCode}) simulated successfully.`);
74+
const message = `Key press (code: ${keyCode}) simulated successfully.`;
75+
if (guard.warningText) {
76+
return createTextResponse(`${message}\n\n${guard.warningText}`);
77+
}
78+
return createTextResponse(message);
6379
} catch (error) {
6480
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
6581
if (error instanceof DependencyError) {

src/mcp/tools/ui-testing/key_sequence.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
} from '../../../utils/responses/index.ts';
1717
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1818
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
19+
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
20+
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
21+
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
1922
import {
2023
createAxeNotAvailableResponse,
2124
getAxePath,
@@ -54,9 +57,18 @@ export async function key_sequenceLogic(
5457
getBundledAxeEnvironment,
5558
createAxeNotAvailableResponse,
5659
},
60+
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
5761
): Promise<ToolResponse> {
5862
const toolName = 'key_sequence';
5963
const { simulatorId, keyCodes, delay } = params;
64+
65+
const guard = await guardUiAutomationAgainstStoppedDebugger({
66+
debugger: debuggerManager,
67+
simulatorId,
68+
toolName,
69+
});
70+
if (guard.blockedResponse) return guard.blockedResponse;
71+
6072
const commandArgs = ['key-sequence', '--keycodes', keyCodes.join(',')];
6173
if (delay !== undefined) {
6274
commandArgs.push('--delay', String(delay));
@@ -70,7 +82,11 @@ export async function key_sequenceLogic(
7082
try {
7183
await executeAxeCommand(commandArgs, simulatorId, 'key-sequence', executor, axeHelpers);
7284
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
73-
return createTextResponse(`Key sequence [${keyCodes.join(',')}] executed successfully.`);
85+
const message = `Key sequence [${keyCodes.join(',')}] executed successfully.`;
86+
if (guard.warningText) {
87+
return createTextResponse(`${message}\n\n${guard.warningText}`);
88+
}
89+
return createTextResponse(message);
7490
} catch (error) {
7591
log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
7692
if (error instanceof DependencyError) {

src/mcp/tools/ui-testing/long_press.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
} from '../../../utils/responses/index.ts';
1818
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1919
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
20+
import { getDefaultDebuggerManager } from '../../../utils/debugger/index.ts';
21+
import type { DebuggerManager } from '../../../utils/debugger/debugger-manager.ts';
22+
import { guardUiAutomationAgainstStoppedDebugger } from '../../../utils/debugger/ui-automation-guard.ts';
2023
import {
2124
createAxeNotAvailableResponse,
2225
getAxePath,
@@ -58,9 +61,18 @@ export async function long_pressLogic(
5861
getBundledAxeEnvironment,
5962
createAxeNotAvailableResponse,
6063
},
64+
debuggerManager: DebuggerManager = getDefaultDebuggerManager(),
6165
): Promise<ToolResponse> {
6266
const toolName = 'long_press';
6367
const { simulatorId, x, y, duration } = params;
68+
69+
const guard = await guardUiAutomationAgainstStoppedDebugger({
70+
debugger: debuggerManager,
71+
simulatorId,
72+
toolName,
73+
});
74+
if (guard.blockedResponse) return guard.blockedResponse;
75+
6476
// AXe uses touch command with --down, --up, and --delay for long press
6577
const delayInSeconds = Number(duration) / 1000; // Convert ms to seconds
6678
const commandArgs = [
@@ -84,11 +96,12 @@ export async function long_pressLogic(
8496
await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers);
8597
log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`);
8698

87-
const warning = getCoordinateWarning(simulatorId);
99+
const coordinateWarning = getCoordinateWarning(simulatorId);
88100
const message = `Long press at (${x}, ${y}) for ${duration}ms simulated successfully.`;
101+
const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n');
89102

90-
if (warning) {
91-
return createTextResponse(`${message}\n\n${warning}`);
103+
if (warnings) {
104+
return createTextResponse(`${message}\n\n${warnings}`);
92105
}
93106

94107
return createTextResponse(message);

0 commit comments

Comments
 (0)