Skip to content

Commit 1d2cf21

Browse files
committed
Add command to find and pause a PHP process.
1 parent 132e3d5 commit 1d2cf21

File tree

3 files changed

+132
-18
lines changed

3 files changed

+132
-18
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,11 @@
641641
"enablement": "!inDebugMode",
642642
"icon": "$(play)"
643643
},
644+
{
645+
"command": "extension.php-debug.pauseXdebugControlSocket",
646+
"title": "Pause PHP process (Xdebug Control Socket)",
647+
"category": "PHP Debug"
648+
},
644649
{
645650
"command": "extension.php-debug.copyVarExport",
646651
"title": "Copy Value as var_export"

src/controlSocket.ts

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as xdebug from './xdebugConnection'
2-
import * as semver from 'semver'
32
import * as net from 'net'
3+
import * as CP from 'child_process'
4+
import { promisify } from 'util'
5+
import { supportedEngine } from './xdebugUtils'
6+
import { DOMParser } from '@xmldom/xmldom'
47

58
export class ControlSocket {
69
/**
@@ -16,36 +19,36 @@ export class ControlSocket {
1619
* @returns Returns true if the current platform and xdebug version are supporting Xdebug control socket.
1720
*/
1821
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.5.0', { loose: true }) ||
24-
initPacket.engineVersion.startsWith('3.5.0'))
25-
)
22+
return this.supportedPlatform() && supportedEngine(initPacket, '3.5.0')
2623
}
2724

2825
/**
29-
*
26+
* Request the pause control socket command
3027
* @param ctrlSocket Control socket full path
3128
* @returns
3229
*/
3330
async requestPause(ctrlSocket: string): Promise<void> {
34-
let retval
31+
await this.executeCtrlCmd(ctrlSocket, 'pause')
32+
}
33+
34+
async requestPS(ctrlSocket: string): Promise<ControlPS> {
35+
const xml = await this.executeCtrlCmd(ctrlSocket, 'ps')
36+
const parser = new DOMParser()
37+
const document = <unknown>parser.parseFromString(xml, 'application/xml')
38+
return new ControlPS(<XMLDocument>document)
39+
}
40+
41+
private async executeCtrlCmd(ctrlSocket: string, cmd: string): Promise<string> {
42+
let rawCtrlSocket: string
3543
if (process.platform === 'linux') {
36-
retval = await this.executeCtrlCmd(`\0${ctrlSocket}`, 'pause')
44+
rawCtrlSocket = `\0${ctrlSocket}`
3745
} else if (process.platform === 'win32') {
38-
retval = await this.executeCtrlCmd(`\\\\.\\pipe\\${ctrlSocket}`, 'pause')
46+
rawCtrlSocket = `\\\\.\\pipe\\${ctrlSocket}`
3947
} else {
4048
throw new Error('Invalid platform for Xdebug control socket')
4149
}
42-
retval
43-
return
44-
}
45-
46-
private async executeCtrlCmd(ctrlSocket: string, cmd: string): Promise<string> {
4750
return new Promise<string>((resolve, reject) => {
48-
const s = net.createConnection(ctrlSocket, () => {
51+
const s = net.createConnection(rawCtrlSocket, () => {
4952
s.end(`${cmd}\0`)
5053
})
5154
s.setTimeout(3000)
@@ -69,4 +72,82 @@ export class ControlSocket {
6972
return
7073
})
7174
}
75+
76+
async listControlSockets(): Promise<XdebugRunningProcess[]> {
77+
let retval:XdebugRunningProcess[]
78+
if (process.platform === 'linux') {
79+
// TODO
80+
throw new Error('Invalid platform for Xdebug control socket')
81+
} else if (process.platform === 'win32') {
82+
retval = await this.listControlSocketsWin()
83+
} else {
84+
throw new Error('Invalid platform for Xdebug control socket')
85+
}
86+
87+
const retval2 = Promise.all(
88+
retval.map(async v => {
89+
try {
90+
v.ps = await this.requestPS(v.ctrlSocket)
91+
} catch {
92+
// ignore
93+
}
94+
return v
95+
})
96+
)
97+
return retval2
98+
}
99+
100+
private async listControlSocketsWin(): Promise<XdebugRunningProcess[]> {
101+
const exec = promisify(CP.exec)
102+
try {
103+
const ret = await exec('cmd /C "dir \\\\.\\pipe\\\\xdebug-ctrl* /b"')
104+
const lines = ret.stdout.split('\r\n')
105+
106+
const retval = lines
107+
.filter(v => v.length != 0)
108+
.map<XdebugRunningProcess>(v => <XdebugRunningProcess>{ ctrlSocket: v })
109+
110+
return retval
111+
112+
} catch (err) {
113+
if (err instanceof Error && (<ExecError>err).stderr == 'File Not Found\r\n') {
114+
return []
115+
}
116+
throw err
117+
}
118+
}
119+
}
120+
121+
interface ExecError extends Error {
122+
stderr: string
123+
}
124+
125+
export interface XdebugRunningProcess {
126+
readonly ctrlSocket: string
127+
ps: ControlPS
128+
// todo
129+
}
130+
131+
export class ControlPS {
132+
/** The file that was requested as a file:// URI */
133+
fileUri: string
134+
/** the version of Xdebug */
135+
engineVersion: string
136+
/** the name of the engine */
137+
engineName: string
138+
/** the internal PID */
139+
pid: string
140+
/** memory consumption */
141+
memory: number
142+
/**
143+
* @param {XMLDocument} document - An XML document to read from
144+
*/
145+
constructor(document: XMLDocument) {
146+
const documentElement = <Element>document.documentElement.firstChild
147+
this.fileUri = documentElement.getElementsByTagName('fileuri').item(0)?.textContent ?? ''
148+
this.engineVersion = documentElement.getElementsByTagName('engine').item(0)?.getAttribute('version') ?? ''
149+
this.engineName = documentElement.getElementsByTagName('engine').item(0)?.textContent ?? ''
150+
this.pid = documentElement.getElementsByTagName('pid').item(0)?.textContent ?? ''
151+
this.memory = parseInt(documentElement.getElementsByTagName('memory').item(0)?.textContent ?? '0')
152+
}
72153
}

src/extension.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EvaluateExtendedArguments, LaunchRequestArguments } from './phpDebug'
44
import * as which from 'which'
55
import * as path from 'path'
66
import { DebugProtocol } from '@vscode/debugprotocol'
7+
import { ControlSocket } from './controlSocket'
78

89
export function activate(context: vscode.ExtensionContext) {
910
context.subscriptions.push(
@@ -197,4 +198,31 @@ export function activate(context: vscode.ExtensionContext) {
197198
}
198199
)
199200
)
201+
202+
context.subscriptions.push(
203+
vscode.commands.registerCommand('extension.php-debug.pauseXdebugControlSocket', async () => {
204+
// check compat
205+
const controlSocket = new ControlSocket()
206+
const sockets = await controlSocket.listControlSockets()
207+
208+
// loading indicator
209+
if (sockets.length == 0) {
210+
await vscode.window.showInformationMessage('No Xdebug control sockets found...')
211+
return
212+
}
213+
214+
const qpi = sockets.map<vscode.QuickPickItem>(
215+
v =>
216+
<vscode.QuickPickItem>{
217+
label: v.ctrlSocket,
218+
description: v.ps ? `Mem: ${v.ps.memory}` : '',
219+
detail: v.ps ? v.ps.fileUri : '',
220+
}
221+
)
222+
const i = await vscode.window.showQuickPick(qpi, { canPickMany: false })
223+
if (i) {
224+
await controlSocket.requestPause(i.label)
225+
}
226+
})
227+
)
200228
}

0 commit comments

Comments
 (0)