Skip to content

Commit b91d738

Browse files
committed
Refactor control socket and support win32 and linux.
1 parent 1ed80e6 commit b91d738

File tree

5 files changed

+134
-37
lines changed

5 files changed

+134
-37
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [Unreleased]
8+
9+
- Xdebug control socket support for pause on Linux and Windows.
10+
711
## [1.38.2]
812

913
- Fix problematic evaluate changes

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"@xmldom/xmldom": "^0.9.8",
4949
"buffer-crc32": "^1.0.0",
5050
"dotenv": "^17.2.1",
51-
"abstract-socket": "^2.1.1",
5251
"file-url": "^3.0.0",
5352
"iconv-lite": "^0.6.3",
5453
"minimatch": "^10.0.3",
@@ -58,6 +57,9 @@
5857
"string-replace-async": "^2.0.0",
5958
"which": "^5.0.0"
6059
},
60+
"optionalDependencies": {
61+
"abstract-socket": "^2.1.1"
62+
},
6163
"devDependencies": {
6264
"@commitlint/cli": "^19.8.1",
6365
"@commitlint/config-conventional": "^19.8.1",

src/abstract-socket.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/// <reference types="node" />
2-
declare module "abstract-socket" {
3-
export function createServer(listener: any): AbstractSocketServer;
4-
export function connect(name: any, connectListener: any): net.Socket;
5-
export function createConnection(name: any, connectListener: any): net.Socket;
2+
declare module 'abstract-socket' {
3+
export function createServer(listener: any): AbstractSocketServer
4+
export function connect(name: any, connectListener: any): net.Socket
5+
export function createConnection(name: any, connectListener: any): net.Socket
66
class AbstractSocketServer extends net.Server {
77
// constructor(listener: any);
88
// listen(path: string, listener: () => void): net.Socket;
99
// listen(path: string, listeningListener?: () => void): this;
1010
}
11-
import net = require("net");
12-
export {};
11+
import net = require('net')
12+
export {}
1313
}

src/controlSocket.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as xdebug from './xdebugConnection'
2+
import * as semver from 'semver'
3+
import * as net from 'net'
4+
5+
export class ControlSocket {
6+
/**
7+
* @returns Returns true if the current platoform is supported for Xdebug control socket (win and linux)
8+
*/
9+
supportedPlatform(): boolean {
10+
return process.platform === 'linux' || process.platform === 'win32'
11+
}
12+
13+
/**
14+
*
15+
* @param initPacket
16+
* @returns Returns true if the current platform and xdebug version are supporting Xdebug control socket.
17+
*/
18+
supportedInitPacket(initPacket: xdebug.InitPacket): boolean {
19+
return (
20+
this.supportedPlatform() &&
21+
initPacket.engineName === 'Xdebug' &&
22+
semver.valid(initPacket.engineVersion, { loose: true }) !== null &&
23+
(semver.gte(initPacket.engineVersion, '3.4.0', { loose: true }) || initPacket.engineVersion === '3.4.0-dev')
24+
)
25+
}
26+
27+
/**
28+
*
29+
* @param pid appId returned in the init packet
30+
* @returns
31+
*/
32+
async requestPause(pid: string): Promise<void> {
33+
let retval
34+
if (process.platform === 'linux') {
35+
retval = await this.executeLinux(pid, 'pause')
36+
} else if (process.platform === 'win32') {
37+
retval = await this.executeWindows(pid, 'pause')
38+
} else {
39+
throw new Error('Invalid platform for Xdebug control socket')
40+
}
41+
retval
42+
return
43+
}
44+
45+
private async executeLinux(pid: string, cmd: string): Promise<string> {
46+
const abs = await import('abstract-socket')
47+
return new Promise<string>((resolve, reject) => {
48+
const cs = `\0xdebug-ctrl.${pid}y`.padEnd(108, 'x')
49+
try {
50+
const s = abs.connect(cs, () => {
51+
s.write(`${cmd}\0`)
52+
})
53+
s.setTimeout(3000)
54+
s.on('timeout', () => {
55+
reject(new Error('Timed out while reading from Xdebug control socket'))
56+
s.end()
57+
})
58+
s.on('data', data => {
59+
resolve(data.toString())
60+
})
61+
s.on('error', error => {
62+
reject(
63+
new Error(
64+
`Cannot connect to Xdebug control socket: ${String(
65+
error instanceof Error ? error.message : error
66+
)}`
67+
)
68+
)
69+
})
70+
return
71+
} catch (error) {
72+
reject(
73+
new Error(
74+
`Cannot connect to Xdebug control socket: ${String(
75+
error instanceof Error ? error.message : error
76+
)}`
77+
)
78+
)
79+
return
80+
}
81+
})
82+
}
83+
84+
private async executeWindows(pid: string, cmd: string): Promise<string> {
85+
return new Promise<string>((resolve, reject) => {
86+
const s = net.createConnection(`\\\\.\\pipe\\xdebug-ctrl.${pid}`, () => {
87+
s.end(`${cmd}\0`)
88+
})
89+
s.setTimeout(3000)
90+
s.on('timeout', () => {
91+
reject(new Error('Timed out while reading from Xdebug control socket'))
92+
s.end()
93+
})
94+
s.on('data', data => {
95+
resolve(data.toString())
96+
})
97+
s.on('error', error => {
98+
// sadly this happens all the time - even on normal server-side-close, but luckily the promise is already resolved
99+
reject(
100+
new Error(
101+
`Cannot connect to Xdebug control socket: ${String(
102+
error instanceof Error ? error.message : error
103+
)}`
104+
)
105+
)
106+
})
107+
})
108+
}
109+
}

src/phpDebug.ts

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import { shouldIgnoreException } from './ignore'
2121
import { varExportProperty } from './varExport'
2222
import { supportedEngine } from './xdebugUtils'
2323
import { varJsonProperty } from './varJson'
24-
import * as abs from 'abstract-socket'
25-
24+
import { ControlSocket } from './controlSocket'
2625
if (process.env['VSCODE_NLS_CONFIG']) {
2726
try {
2827
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
@@ -1437,43 +1436,26 @@ class PhpDebugSession extends vscode.DebugSession {
14371436
response: VSCodeDebugProtocol.PauseResponse,
14381437
args: VSCodeDebugProtocol.PauseArguments
14391438
): Promise<void> {
1440-
// test
1441-
let connection: xdebug.Connection | undefined
1442-
connection = this._connections.get(args.threadId)
1439+
const connection = this._connections.get(args.threadId)
14431440
if (!connection) {
14441441
return this.sendErrorResponse(response, new Error(`Unknown thread ID ${args.threadId}`))
14451442
}
14461443

1447-
let initPacket = await connection.waitForInitPacket()
1448-
const supportedControl =
1449-
process.platform === 'linux' &&
1450-
initPacket.engineName === 'Xdebug' &&
1451-
semver.valid(initPacket.engineVersion, { loose: true }) &&
1452-
(semver.gte(initPacket.engineVersion, '3.4.0', { loose: true }) || initPacket.engineVersion === '3.4.0-dev')
1453-
1454-
if (supportedControl) {
1455-
let cs = `\0xdebug-ctrl.${initPacket.appId}y`.padEnd(108, 'x')
1444+
const initPacket = await connection.waitForInitPacket()
1445+
const controlSocket = new ControlSocket()
1446+
if (controlSocket.supportedInitPacket(initPacket)) {
1447+
if (this._skippingFiles.has(args.threadId)) {
1448+
this._skippingFiles.set(args.threadId, false)
1449+
}
14561450
try {
1457-
const sock = abs.connect(cs, () => {
1458-
sock.write("pause\0")
1459-
if (this._skippingFiles.has(args.threadId)) {
1460-
this._skippingFiles.set(args.threadId, false)
1461-
}
1462-
this.sendResponse(response)
1463-
})
1464-
sock.on('data', (data) => {
1465-
// todo
1466-
})
1467-
sock.on('error', (error) => {
1468-
this.sendErrorResponse(response, new Error('Cannot connect to Xdebug control socket'))
1469-
})
1451+
await controlSocket.requestPause(initPacket.appId)
1452+
this.sendResponse(response)
14701453
return
14711454
} catch (error) {
1472-
this.sendErrorResponse(response, new Error('Cannot connect to Xdebug control socket'))
1473-
return
1455+
this.sendErrorResponse(response, error as Error)
14741456
}
14751457
}
1476-
// test
1458+
14771459
if (this._skippingFiles.has(args.threadId)) {
14781460
this._skippingFiles.set(args.threadId, false)
14791461
this.sendResponse(response)

0 commit comments

Comments
 (0)