Skip to content

Commit ec1ed3a

Browse files
authored
[XDebug Bridge] Load files in Devtools before running PHP with Xdebug enabled (#2527)
## Motivation for the change, related issues Based on the following pull request : - #2442 The first approach was to load Devtools when running a file with Xdebug enabled PHP.wasm. Unfortunately, the user experience was not great. e.g. files were loaded only when executed. The new approach opens de Devtools Source panel with the relevant loaded files ready to be manipulated. Once the breakpoints are set, the PHP file can be executed and paused with Xdebug enabled PHP.wasm. ## Implementation details - DevTools Sources Opening : Devtools Source panel is automatically opened when bridge starts. - Console Instructions : A startup message guides users on how to debug with the bridge. - Source File Loading : Relevant PHP files are preloaded in DevTools for inspection and breakpoints. - Pending Breakpoints : Breakpoints set before Xdebug init are applied once the session starts. - URI Normalization : Paths are consistently mapped between Bridge, CDP, and DBGP. - CDP Command Buffer : CDP requests are buffered until the bridge is fully initialized. - `breakOnFirstLine` Option : Allows breaking on the first executed line when no breakpoints exist. - Test Coverage ## Testing Instructions with Xdebug Bridge 1. Run the devtools ``` npx xdebug-bridge ``` 2. Connect to the devtools ``` Starting XDebug Bridge... Connect Chrome DevTools to CDP at: devtools://devtools/bundled/inspector.html?ws=localhost:9229 ``` 3. Set a breakpoint in a file <img width="1920" height="656" alt="screenshot-001" src="https://github.com/user-attachments/assets/3591ea63-7344-4a53-98ee-6ff70440dbaf" /> 4. Run the PHP script with PHP.wasm CLI and Xdebug option ``` npx php-wasm-cli test.php --xdebug ``` <img width="1920" height="651" alt="screenshot-002" src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4" /> 5. Resume script execution and repeat step 4 indefinitely <img width="1919" height="667" alt="screenshot-003" src="https://github.com/user-attachments/assets/aacfaf3d-0afb-497e-b5cd-8d3eb0ce98de" /> ## Testing Instructions with PHP.wasm CLI 1. Run the script with Xdebug and Devtools options ``` npx php-wasm-cli test.php --experimental-devtools --xdebug ``` 2. Connect to the devtools ``` Starting XDebug Bridge... Connect Chrome DevTools to CDP at: devtools://devtools/bundled/inspector.html?ws=localhost:9229 ``` 3. It will pause on the first breakable PHP code <img width="1920" height="651" alt="screenshot-002" src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4" /> ## Testing Instructions with PHP.wasm Node 1. Write the following script ``` import { PHP } from '@php-wasm/universal'; import { loadNodeRuntime } from '@php-wasm/node'; import { startBridge } from '@php-wasm/xdebug-bridge'; const php = new PHP(await loadNodeRuntime('8.4', {withXdebug: true})); const bridge = await startBridge({phpInstance: php, breakOnFirstLine: true}); bridge.start(); await php.runStream({scriptPath: `test.php`}); ``` 1. Run the script with Node ``` node script.js ``` 3. Connect to the devtools ``` Starting XDebug Bridge... Connect Chrome DevTools to CDP at: devtools://devtools/bundled/inspector.html?ws=localhost:9229 ``` 4. It will pause on the first breakable PHP code <img width="1920" height="651" alt="screenshot-002" src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4" /> ## Testing Instructions in `wordpress-playground` 1. Run the devtools ``` nx reset && nx run php-wasm-xdebug-bridge:dev --php-root /absolute/path/to/the/debuggable/directory ``` 2. Connect to the devtools ``` Starting XDebug Bridge... Connect Chrome DevTools to CDP at: devtools://devtools/bundled/inspector.html?ws=localhost:9229 ``` 3. Set a breakpoint in a file <img width="1920" height="656" alt="screenshot-001" src="https://github.com/user-attachments/assets/3591ea63-7344-4a53-98ee-6ff70440dbaf" /> 5. Run the PHP script ``` nx reset && nx run php-wasm-cli:dev /absolute/path/to/the/debuggable/directory/file.php --xdebug ``` <img width="1920" height="651" alt="screenshot-002" src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4" /> 6. Resume script execution and repeat step 4 indefinitely <img width="1919" height="667" alt="screenshot-003" src="https://github.com/user-attachments/assets/aacfaf3d-0afb-497e-b5cd-8d3eb0ce98de" />
1 parent 65029ca commit ec1ed3a

File tree

12 files changed

+316
-197
lines changed

12 files changed

+316
-197
lines changed

packages/php-wasm/cli/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ ${process.argv[0]} ${process.execArgv.join(' ')} ${process.argv[1]}
106106
useHostFilesystem(php);
107107

108108
if (hasDevtoolsOption && hasXdebugOption) {
109-
const bridge = await startBridge({});
109+
const bridge = await startBridge({ breakOnFirstLine: true });
110110

111111
bridge.start();
112112
}

packages/php-wasm/xdebug-bridge/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,9 @@ npx xdebug-bridge --help
6262
- `dbgpPort`: Port to listen for XDebug connections (default: 9003)
6363
- `phpRoot`: Root path for php files
6464
- `verbosity`: Output logs and progress messages (choices: "quiet", "normal", "debug") (default: "normal")
65-
- `remoteRoot`: Remote root path for php files
66-
- `localRoot`: Local root path for php files
6765
- `phpInstance`: PHP instance
6866
- `getPHPFile`: Custom file listing function
67+
- `breakOnFirstLine`: Breaks on the first breakable line
6968

7069
## Events
7170

@@ -74,7 +73,7 @@ The bridge listens to events for monitoring connection activity:
7473
#### From Xdebug
7574

7675
- `connected`: Xdebug Server has started
77-
- `close`: Xdebug Server has stopped
76+
- `disconnected`: Xdebug Server has stopped
7877
- `message`: Raw XDebug data received
7978
- `error`: Xdebug Server error occurred
8079

packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { type WebSocket, WebSocketServer } from 'ws';
55
export class CDPServer extends EventEmitter {
66
private wss: WebSocketServer;
77
private ws: WebSocket | null = null;
8+
private connected = false;
9+
private buffer: any[] = [];
810

911
constructor(port = 9229) {
1012
super();
@@ -28,7 +30,12 @@ export class CDPServer extends EventEmitter {
2830
} catch {
2931
return;
3032
}
31-
this.emit('message', message);
33+
34+
if (this.connected) {
35+
this.emit('message', message);
36+
} else {
37+
this.buffer.push(message);
38+
}
3239
});
3340
ws.on('close', () => {
3441
this.ws = null;
@@ -38,6 +45,26 @@ export class CDPServer extends EventEmitter {
3845
this.emit('error', err);
3946
});
4047
});
48+
49+
// When a new 'message' listener is registered,
50+
// it replays any buffered messages on the next
51+
// tick. This ensures that the listener receives
52+
// all messages that arrived before it was opened.
53+
// Once replayed, it clears the buffer and marks
54+
// the connection as established.
55+
this.on('newListener', (event) => {
56+
if (event === 'message') {
57+
process.nextTick(() => {
58+
for (const message of this.buffer) {
59+
this.emit('message', message);
60+
}
61+
62+
this.buffer = [];
63+
64+
this.connected = true;
65+
});
66+
}
67+
});
4168
}
4269

4370
sendMessage(message: any) {

packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class DbgpSession extends EventEmitter {
2323
socket.on('data', (data: Buffer) => this.onData(data.toString()));
2424
socket.on('close', () => {
2525
this.socket = null;
26-
this.emit('close');
26+
this.emit('disconnected');
2727
});
2828
socket.on('error', (err) => {
2929
// Forward error events if needed

packages/php-wasm/xdebug-bridge/src/lib/run-cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ Usage: xdebug-bridge [options]
4343
.option('php-root', {
4444
type: 'string',
4545
description: 'Path to PHP root directory',
46-
default: './',
4746
})
4847
.option('verbosity', {
4948
type: 'string',

packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { logger } from '@php-wasm/logger';
22
import type { PHP } from '@php-wasm/universal';
33
import { readdirSync, readFileSync, lstatSync } from 'fs';
4-
import { join } from 'path';
4+
import path from 'path';
55
import { CDPServer } from './cdp-server';
66
import { DbgpSession } from './dbgp-session';
77
import { XdebugCDPBridge } from './xdebug-cdp-bridge';
@@ -11,21 +11,21 @@ export type StartBridgeConfig = {
1111
cdpHost?: string;
1212
dbgpPort?: number;
1313
phpRoot?: string;
14-
remoteRoot?: string;
15-
localRoot?: string;
1614
phpInstance?: PHP;
1715
getPHPFile?: (path: string) => string | Promise<string>;
16+
breakOnFirstLine?: boolean;
1817
};
1918

2019
export async function startBridge(config: StartBridgeConfig) {
2120
const cdpPort = config.cdpPort ?? 9229;
2221
const dbgpPort = config.dbgpPort ?? 9003;
2322
const cdpHost = config.cdpHost ?? 'localhost';
24-
const phpRoot = config.phpRoot ?? import.meta.dirname;
23+
const phpRoot = config.phpRoot ?? process.cwd();
24+
const breakOnFirstLine = config.breakOnFirstLine ?? false;
2525

2626
logger.log('Starting XDebug Bridge...');
2727

28-
// index.ts - Entry point to start the service
28+
// Entry point to start the service
2929
const cdpServer = new CDPServer(cdpPort);
3030

3131
logger.log('Connect Chrome DevTools to CDP at:');
@@ -48,13 +48,13 @@ export async function startBridge(config: StartBridgeConfig) {
4848
const results: string[] = [];
4949
const list = readdirSync(dir);
5050
for (const file of list) {
51-
const filePath = join(dir, file);
51+
const filePath = path.join(dir, file);
5252
// lstat avoids crashes when encountering symlinks
5353
const stat = lstatSync(filePath);
5454
if (stat && stat.isDirectory()) {
5555
results.push(...getPhpFiles(filePath));
5656
} else if (file.endsWith('.php')) {
57-
results.push(`file://${filePath}`);
57+
results.push(filePath);
5858
}
5959
}
6060
return results;
@@ -64,20 +64,13 @@ export async function startBridge(config: StartBridgeConfig) {
6464
? (path: string) => config.phpInstance!.readFileAsText(path)
6565
: config.getPHPFile
6666
? config.getPHPFile
67-
: (path: string) => {
68-
// Default implementation: read from filesystem
69-
// Convert file:/// URLs to local paths
70-
const localPath = path.startsWith('file://')
71-
? path.replace('file://', '')
72-
: path;
73-
return readFileSync(localPath, 'utf-8');
74-
};
67+
: (path: string) => readFileSync(path, 'utf-8');
7568

7669
const phpFiles = getPhpFiles(phpRoot);
7770
return new XdebugCDPBridge(dbgpSession, cdpServer, {
7871
knownScriptUrls: phpFiles,
79-
remoteRoot: config.remoteRoot,
80-
localRoot: config.localRoot,
72+
phpRoot,
8173
getPHPFile,
74+
breakOnFirstLine,
8275
});
8376
}

0 commit comments

Comments
 (0)