Skip to content

Commit 8d4ce8f

Browse files
authored
Merge pull request #84 from CodinGame/debug-service
Debug services
2 parents 45f2835 + 59a5fd5 commit 8d4ce8f

46 files changed

Lines changed: 10944 additions & 8899 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ StandaloneServices.initialize({
4949
})
5050
```
5151

52-
Additionally, this library exposes 11 modules that include the vscode version of some services (with some glue to make it work with monaco):
52+
Additionally, this library exposes 12 modules that include the vscode version of some services (with some glue to make it work with monaco):
5353

5454
- Notifications: `vscode/service-override/notifications`
5555
- Dialogs: `vscode/service-override/dialogs`
@@ -63,6 +63,7 @@ Additionally, this library exposes 11 modules that include the vscode version of
6363
- VSCode themes: `vscode/service-override/theme`
6464
- Token classification: `vscode/service-override/tokenClassification`
6565
- Audio cue: `vscode/service-override/audioCue`
66+
- Debug: `vscode/service-override/debug`
6667

6768
Usage:
6869

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<div id="app">
1010
<h1>Editor</h1>
1111
<div id="editor" class="editor"></div>
12+
<button id="run">Run with debugger</button>
1213
<h1>Settings</h1>
1314
<div id="settings-editor" class="editor"></div>
1415
<h1>Keybindings</h1>

demo/package-lock.json

Lines changed: 1322 additions & 121 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/package.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,26 @@
99
"start": "vite --config vite.config.ts",
1010
"start:debug": "vite --config vite.config.ts --debug --force",
1111
"build": "vite --config vite.config.ts build",
12-
"build:github": "vite --config vite.github-page.config.ts build && touch dist/.nojekyll"
12+
"build:github": "vite --config vite.github-page.config.ts build && touch dist/.nojekyll",
13+
"start:debugServer": "node --loader ts-node/esm src/debugServer.ts"
1314
},
1415
"devDependencies": {
16+
"@types/dockerode": "^3.3.15",
17+
"@types/express": "^4.17.17",
1518
"@types/throttle-debounce": "~5.0.0",
16-
"typescript": "~4.9.5",
17-
"vite": "~4.1.4"
19+
"@types/ws": "^8.5.4",
20+
"ts-node": "^10.9.1",
21+
"typescript": "~5.0.2",
22+
"vite": "~4.2.1"
1823
},
1924
"dependencies": {
25+
"dockerode": "^3.3.5",
26+
"express": "^4.18.2",
27+
"monaco-editor": "^0.36.1",
2028
"throttle-debounce": "~5.0.0",
2129
"vscode": "file:../",
22-
"vscode-oniguruma": "~1.7.0"
30+
"vscode-oniguruma": "~1.7.0",
31+
"ws": "^8.13.0"
2332
},
2433
"volta": {
2534
"node": "18.14.2",

demo/src/debugServer.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import express from 'express'
2+
import { WebSocketServer } from 'ws'
3+
import Docker from 'dockerode'
4+
import * as http from 'http'
5+
import * as net from 'net'
6+
import * as fs from 'fs'
7+
import * as stream from 'stream'
8+
9+
const docker = new Docker()
10+
const image = 'ghcr.io/graalvm/graalvm-ce:21.2.0'
11+
12+
async function createContainer () {
13+
const stream = await docker.pull(image)
14+
await new Promise<void>((resolve, reject) => {
15+
docker.modem.followProgress(stream, err => err == null ? resolve() : reject(err))
16+
})
17+
const container = await docker.createContainer({
18+
name: 'graalvm-debugger',
19+
Image: image,
20+
Entrypoint: ['sleep', 'infinity'],
21+
HostConfig: {
22+
NetworkMode: 'host',
23+
Mounts: [{
24+
Type: 'bind',
25+
Target: '/tmp',
26+
Source: '/tmp'
27+
}],
28+
AutoRemove: true
29+
}
30+
})
31+
return container
32+
}
33+
34+
async function prepareContainer (container: Docker.Container) {
35+
await container.start()
36+
// eslint-disable-next-line no-console
37+
console.log('Installing node')
38+
const exec = await container.exec({
39+
Cmd: ['gu', 'install', 'nodejs'],
40+
AttachStdout: true,
41+
AttachStderr: true
42+
})
43+
const execStream = await exec.start({
44+
hijack: true
45+
})
46+
execStream.pipe(process.stdout)
47+
await new Promise(resolve => execStream.on('end', resolve))
48+
// eslint-disable-next-line no-console
49+
console.log('Node installed')
50+
}
51+
52+
// eslint-disable-next-line no-console
53+
console.log('Pulling image/starting container...')
54+
const containerPromise = createContainer()
55+
56+
async function exitHandler () {
57+
// eslint-disable-next-line no-console
58+
console.log('Exiting...')
59+
try {
60+
const container = await containerPromise
61+
await container.remove({
62+
force: true
63+
})
64+
} catch (err) {
65+
console.error(err)
66+
} finally {
67+
process.exit()
68+
}
69+
}
70+
process.on('exit', exitHandler.bind(null, { cleanup: true }))
71+
process.on('SIGINT', exitHandler.bind(null, { exit: true }))
72+
process.on('SIGUSR1', exitHandler.bind(null, { exit: true }))
73+
process.on('SIGUSR2', exitHandler.bind(null, { exit: true }))
74+
process.on('uncaughtException', exitHandler.bind(null, { exit: true }))
75+
76+
const container = await containerPromise
77+
await prepareContainer(container)
78+
79+
class DAPSocket {
80+
private socket: net.Socket
81+
private rawData = Buffer.allocUnsafe(0)
82+
private contentLength = -1
83+
constructor (private onMessage: (message: string) => void) {
84+
this.socket = new net.Socket()
85+
this.socket.on('data', this.onData)
86+
}
87+
88+
private onData = (data: Buffer) => {
89+
this.rawData = Buffer.concat([this.rawData, data])
90+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
91+
while (true) {
92+
if (this.contentLength >= 0) {
93+
if (this.rawData.length >= this.contentLength) {
94+
const message = this.rawData.toString('utf8', 0, this.contentLength)
95+
this.rawData = this.rawData.subarray(this.contentLength)
96+
this.contentLength = -1
97+
if (message.length > 0) {
98+
this.onMessage(message)
99+
}
100+
continue
101+
}
102+
} else {
103+
const idx = this.rawData.indexOf(TWO_CRLF)
104+
if (idx !== -1) {
105+
const header = this.rawData.toString('utf8', 0, idx)
106+
const lines = header.split(HEADER_LINESEPARATOR)
107+
for (const h of lines) {
108+
const kvPair = h.split(HEADER_FIELDSEPARATOR)
109+
if (kvPair[0] === 'Content-Length') {
110+
this.contentLength = Number(kvPair[1])
111+
}
112+
}
113+
this.rawData = this.rawData.subarray(idx + TWO_CRLF.length)
114+
continue
115+
}
116+
}
117+
break
118+
}
119+
}
120+
121+
public connect (port: number) {
122+
this.socket.connect(port)
123+
}
124+
125+
public sendMessage (message: string) {
126+
this.socket.write(`Content-Length: ${Buffer.byteLength(message, 'utf8')}${TWO_CRLF}${message}`, 'utf8')
127+
}
128+
}
129+
130+
const TWO_CRLF = '\r\n\r\n'
131+
const HEADER_LINESEPARATOR = /\r?\n/
132+
const HEADER_FIELDSEPARATOR = /: */
133+
134+
const PORT = 5555
135+
const app = express()
136+
137+
const server = http.createServer(app)
138+
139+
const wss = new WebSocketServer({ server })
140+
141+
async function findPortFree () {
142+
return new Promise<number>(resolve => {
143+
const srv = net.createServer()
144+
srv.listen(0, () => {
145+
const port = (srv.address() as net.AddressInfo).port
146+
srv.close(() => resolve(port))
147+
})
148+
})
149+
}
150+
151+
function sequential<T, P extends unknown[]> (fn: (...params: P) => Promise<T>): (...params: P) => Promise<T> {
152+
let promise = Promise.resolve()
153+
return (...params: P) => {
154+
const result = promise.then(() => {
155+
return fn(...params)
156+
})
157+
158+
promise = result.then(() => {}, () => {})
159+
return result
160+
}
161+
}
162+
163+
wss.on('connection', (ws) => {
164+
const socket = new DAPSocket(message => ws.send(message))
165+
166+
let initialized = false
167+
168+
ws.on('message', sequential(async (message: string) => {
169+
if (!initialized) {
170+
try {
171+
initialized = true
172+
const init: { main: string, files: Record<string, string> } = JSON.parse(message)
173+
for (const [file, content] of Object.entries(init.files)) {
174+
fs.writeFileSync(file, content)
175+
}
176+
const debuggerPort = await findPortFree()
177+
const exec = await container.exec({
178+
Cmd: ['node', `--dap=${debuggerPort}`, '--dap.WaitAttached', '--dap.Suspend=false', `${init.main}`],
179+
AttachStdout: true,
180+
AttachStderr: true
181+
})
182+
183+
const execStream = await exec.start({
184+
hijack: true
185+
})
186+
const stdout = new stream.PassThrough()
187+
const stderr = new stream.PassThrough()
188+
container.modem.demuxStream(execStream, stdout, stderr)
189+
function sendOutput (category: 'stdout' | 'stderr', output: Buffer) {
190+
ws.send(JSON.stringify({
191+
type: 'event',
192+
event: 'output',
193+
body: {
194+
category,
195+
output: output.toString()
196+
}
197+
}))
198+
}
199+
stdout.on('data', sendOutput.bind(undefined, 'stdout'))
200+
stderr.on('data', sendOutput.bind(undefined, 'stderr'))
201+
202+
execStream.on('end', () => {
203+
ws.close()
204+
})
205+
206+
await new Promise(resolve => setTimeout(resolve, 1000))
207+
socket.connect(debuggerPort)
208+
209+
return
210+
} catch (err) {
211+
console.error('Failed to initialize', err)
212+
}
213+
}
214+
socket.sendMessage(message)
215+
}))
216+
})
217+
218+
server.listen(PORT, () => {
219+
// eslint-disable-next-line no-console
220+
console.log(`Server started on port ${PORT} :)`)
221+
})

demo/src/main.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { createConfiguredEditor, getJsonSchemas, onDidChangeJsonSchema } from 'v
1717
import { debounce } from 'throttle-debounce'
1818
import * as vscode from 'vscode'
1919

20-
vscode.languages.registerHoverProvider('java', {
20+
vscode.languages.registerHoverProvider('javascript', {
2121
async provideHover (document, position) {
2222
return {
2323
contents: [
@@ -28,7 +28,7 @@ vscode.languages.registerHoverProvider('java', {
2828
}
2929
})
3030

31-
vscode.languages.registerCompletionItemProvider('java', {
31+
vscode.languages.registerCompletionItemProvider('javascript', {
3232
provideCompletionItems () {
3333
return [{
3434
label: 'Demo completion',
@@ -51,15 +51,18 @@ void vscode.window.showInformationMessage('Hello', {
5151
})
5252

5353
const model = monaco.editor.createModel(
54-
`// Your First Program
54+
`
55+
let variable = 1
56+
function inc () {
57+
variable++
58+
}
5559
56-
class HelloWorld {
57-
public static void main(String[] args) {
58-
System.out.println('Hello, World!');
59-
}
60+
while (variable < 5000) {
61+
inc()
62+
console.log('Hello world', variable);
6063
}`,
61-
'java',
62-
monaco.Uri.file('HelloWorld.java')
64+
undefined,
65+
monaco.Uri.file('/tmp/test.js')
6366
)
6467

6568
createConfiguredEditor(document.getElementById('editor')!, {
@@ -68,7 +71,7 @@ createConfiguredEditor(document.getElementById('editor')!, {
6871

6972
const diagnostics = vscode.languages.createDiagnosticCollection('demo')
7073
diagnostics.set(model.uri, [{
71-
range: new vscode.Range(2, 6, 2, 16),
74+
range: new vscode.Range(2, 9, 2, 12),
7275
severity: vscode.DiagnosticSeverity.Error,
7376
message: 'This is not a real error, just a demo, don\'t worry',
7477
source: 'Demo',
@@ -88,7 +91,8 @@ const settingsModel = monaco.editor.createModel(
8891
"editor.semanticHighlighting.enabled": true,
8992
"editor.bracketPairColorization.enabled": false,
9093
"editor.fontSize": 12,
91-
"audioCues.lineHasError": "on"
94+
"audioCues.lineHasError": "on",
95+
"audioCues.onDebugBreak": "on"
9296
}`, 'json', monaco.Uri.file('/settings.json'))
9397
createConfiguredEditor(document.getElementById('settings-editor')!, {
9498
model: settingsModel
@@ -147,3 +151,11 @@ setTimeout(() => {
147151
void vscode.window.showInformationMessage('The configuration was changed')
148152
})
149153
}, 1000)
154+
155+
document.querySelector('#run')!.addEventListener('click', () => {
156+
void vscode.debug.startDebugging(undefined, {
157+
name: 'Test',
158+
request: 'attach',
159+
type: 'javascript'
160+
})
161+
})

0 commit comments

Comments
 (0)