Skip to content

Commit

Permalink
Merge pull request #84 from CodinGame/debug-service
Browse files Browse the repository at this point in the history
Debug services
  • Loading branch information
CGNonofr authored Mar 29, 2023
2 parents 45f2835 + 59a5fd5 commit 8d4ce8f
Show file tree
Hide file tree
Showing 46 changed files with 10,944 additions and 8,899 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ StandaloneServices.initialize({
})
```

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

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

Usage:

Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<div id="app">
<h1>Editor</h1>
<div id="editor" class="editor"></div>
<button id="run">Run with debugger</button>
<h1>Settings</h1>
<div id="settings-editor" class="editor"></div>
<h1>Keybindings</h1>
Expand Down
1,443 changes: 1,322 additions & 121 deletions demo/package-lock.json

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,26 @@
"start": "vite --config vite.config.ts",
"start:debug": "vite --config vite.config.ts --debug --force",
"build": "vite --config vite.config.ts build",
"build:github": "vite --config vite.github-page.config.ts build && touch dist/.nojekyll"
"build:github": "vite --config vite.github-page.config.ts build && touch dist/.nojekyll",
"start:debugServer": "node --loader ts-node/esm src/debugServer.ts"
},
"devDependencies": {
"@types/dockerode": "^3.3.15",
"@types/express": "^4.17.17",
"@types/throttle-debounce": "~5.0.0",
"typescript": "~4.9.5",
"vite": "~4.1.4"
"@types/ws": "^8.5.4",
"ts-node": "^10.9.1",
"typescript": "~5.0.2",
"vite": "~4.2.1"
},
"dependencies": {
"dockerode": "^3.3.5",
"express": "^4.18.2",
"monaco-editor": "^0.36.1",
"throttle-debounce": "~5.0.0",
"vscode": "file:../",
"vscode-oniguruma": "~1.7.0"
"vscode-oniguruma": "~1.7.0",
"ws": "^8.13.0"
},
"volta": {
"node": "18.14.2",
Expand Down
221 changes: 221 additions & 0 deletions demo/src/debugServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import express from 'express'
import { WebSocketServer } from 'ws'
import Docker from 'dockerode'
import * as http from 'http'
import * as net from 'net'
import * as fs from 'fs'
import * as stream from 'stream'

const docker = new Docker()
const image = 'ghcr.io/graalvm/graalvm-ce:21.2.0'

async function createContainer () {
const stream = await docker.pull(image)
await new Promise<void>((resolve, reject) => {
docker.modem.followProgress(stream, err => err == null ? resolve() : reject(err))
})
const container = await docker.createContainer({
name: 'graalvm-debugger',
Image: image,
Entrypoint: ['sleep', 'infinity'],
HostConfig: {
NetworkMode: 'host',
Mounts: [{
Type: 'bind',
Target: '/tmp',
Source: '/tmp'
}],
AutoRemove: true
}
})
return container
}

async function prepareContainer (container: Docker.Container) {
await container.start()
// eslint-disable-next-line no-console
console.log('Installing node')
const exec = await container.exec({
Cmd: ['gu', 'install', 'nodejs'],
AttachStdout: true,
AttachStderr: true
})
const execStream = await exec.start({
hijack: true
})
execStream.pipe(process.stdout)
await new Promise(resolve => execStream.on('end', resolve))
// eslint-disable-next-line no-console
console.log('Node installed')
}

// eslint-disable-next-line no-console
console.log('Pulling image/starting container...')
const containerPromise = createContainer()

async function exitHandler () {
// eslint-disable-next-line no-console
console.log('Exiting...')
try {
const container = await containerPromise
await container.remove({
force: true
})
} catch (err) {
console.error(err)
} finally {
process.exit()
}
}
process.on('exit', exitHandler.bind(null, { cleanup: true }))
process.on('SIGINT', exitHandler.bind(null, { exit: true }))
process.on('SIGUSR1', exitHandler.bind(null, { exit: true }))
process.on('SIGUSR2', exitHandler.bind(null, { exit: true }))
process.on('uncaughtException', exitHandler.bind(null, { exit: true }))

const container = await containerPromise
await prepareContainer(container)

class DAPSocket {
private socket: net.Socket
private rawData = Buffer.allocUnsafe(0)
private contentLength = -1
constructor (private onMessage: (message: string) => void) {
this.socket = new net.Socket()
this.socket.on('data', this.onData)
}

private onData = (data: Buffer) => {
this.rawData = Buffer.concat([this.rawData, data])
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
if (this.contentLength >= 0) {
if (this.rawData.length >= this.contentLength) {
const message = this.rawData.toString('utf8', 0, this.contentLength)
this.rawData = this.rawData.subarray(this.contentLength)
this.contentLength = -1
if (message.length > 0) {
this.onMessage(message)
}
continue
}
} else {
const idx = this.rawData.indexOf(TWO_CRLF)
if (idx !== -1) {
const header = this.rawData.toString('utf8', 0, idx)
const lines = header.split(HEADER_LINESEPARATOR)
for (const h of lines) {
const kvPair = h.split(HEADER_FIELDSEPARATOR)
if (kvPair[0] === 'Content-Length') {
this.contentLength = Number(kvPair[1])
}
}
this.rawData = this.rawData.subarray(idx + TWO_CRLF.length)
continue
}
}
break
}
}

public connect (port: number) {
this.socket.connect(port)
}

public sendMessage (message: string) {
this.socket.write(`Content-Length: ${Buffer.byteLength(message, 'utf8')}${TWO_CRLF}${message}`, 'utf8')
}
}

const TWO_CRLF = '\r\n\r\n'
const HEADER_LINESEPARATOR = /\r?\n/
const HEADER_FIELDSEPARATOR = /: */

const PORT = 5555
const app = express()

const server = http.createServer(app)

const wss = new WebSocketServer({ server })

async function findPortFree () {
return new Promise<number>(resolve => {
const srv = net.createServer()
srv.listen(0, () => {
const port = (srv.address() as net.AddressInfo).port
srv.close(() => resolve(port))
})
})
}

function sequential<T, P extends unknown[]> (fn: (...params: P) => Promise<T>): (...params: P) => Promise<T> {
let promise = Promise.resolve()
return (...params: P) => {
const result = promise.then(() => {
return fn(...params)
})

promise = result.then(() => {}, () => {})
return result
}
}

wss.on('connection', (ws) => {
const socket = new DAPSocket(message => ws.send(message))

let initialized = false

ws.on('message', sequential(async (message: string) => {
if (!initialized) {
try {
initialized = true
const init: { main: string, files: Record<string, string> } = JSON.parse(message)
for (const [file, content] of Object.entries(init.files)) {
fs.writeFileSync(file, content)
}
const debuggerPort = await findPortFree()
const exec = await container.exec({
Cmd: ['node', `--dap=${debuggerPort}`, '--dap.WaitAttached', '--dap.Suspend=false', `${init.main}`],
AttachStdout: true,
AttachStderr: true
})

const execStream = await exec.start({
hijack: true
})
const stdout = new stream.PassThrough()
const stderr = new stream.PassThrough()
container.modem.demuxStream(execStream, stdout, stderr)
function sendOutput (category: 'stdout' | 'stderr', output: Buffer) {
ws.send(JSON.stringify({
type: 'event',
event: 'output',
body: {
category,
output: output.toString()
}
}))
}
stdout.on('data', sendOutput.bind(undefined, 'stdout'))
stderr.on('data', sendOutput.bind(undefined, 'stderr'))

execStream.on('end', () => {
ws.close()
})

await new Promise(resolve => setTimeout(resolve, 1000))
socket.connect(debuggerPort)

return
} catch (err) {
console.error('Failed to initialize', err)
}
}
socket.sendMessage(message)
}))
})

server.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`Server started on port ${PORT} :)`)
})
34 changes: 23 additions & 11 deletions demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { createConfiguredEditor, getJsonSchemas, onDidChangeJsonSchema } from 'v
import { debounce } from 'throttle-debounce'
import * as vscode from 'vscode'

vscode.languages.registerHoverProvider('java', {
vscode.languages.registerHoverProvider('javascript', {
async provideHover (document, position) {
return {
contents: [
Expand All @@ -28,7 +28,7 @@ vscode.languages.registerHoverProvider('java', {
}
})

vscode.languages.registerCompletionItemProvider('java', {
vscode.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems () {
return [{
label: 'Demo completion',
Expand All @@ -51,15 +51,18 @@ void vscode.window.showInformationMessage('Hello', {
})

const model = monaco.editor.createModel(
`// Your First Program
`
let variable = 1
function inc () {
variable++
}
class HelloWorld {
public static void main(String[] args) {
System.out.println('Hello, World!');
}
while (variable < 5000) {
inc()
console.log('Hello world', variable);
}`,
'java',
monaco.Uri.file('HelloWorld.java')
undefined,
monaco.Uri.file('/tmp/test.js')
)

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

const diagnostics = vscode.languages.createDiagnosticCollection('demo')
diagnostics.set(model.uri, [{
range: new vscode.Range(2, 6, 2, 16),
range: new vscode.Range(2, 9, 2, 12),
severity: vscode.DiagnosticSeverity.Error,
message: 'This is not a real error, just a demo, don\'t worry',
source: 'Demo',
Expand All @@ -88,7 +91,8 @@ const settingsModel = monaco.editor.createModel(
"editor.semanticHighlighting.enabled": true,
"editor.bracketPairColorization.enabled": false,
"editor.fontSize": 12,
"audioCues.lineHasError": "on"
"audioCues.lineHasError": "on",
"audioCues.onDebugBreak": "on"
}`, 'json', monaco.Uri.file('/settings.json'))
createConfiguredEditor(document.getElementById('settings-editor')!, {
model: settingsModel
Expand Down Expand Up @@ -147,3 +151,11 @@ setTimeout(() => {
void vscode.window.showInformationMessage('The configuration was changed')
})
}, 1000)

document.querySelector('#run')!.addEventListener('click', () => {
void vscode.debug.startDebugging(undefined, {
name: 'Test',
request: 'attach',
type: 'javascript'
})
})
Loading

0 comments on commit 8d4ce8f

Please sign in to comment.