Skip to content

Commit ae5e6ac

Browse files
committed
(GH-100) Add Debug Adapter for Puppet Language
This commit adds a DebugAdapter typescript file, which starts the a puppet debug server via ruby and then proxies the stdin/stdout commands to the debug server over TCP. This commit also configures the debug facility in the appropriate sections in package.json. Note that this feature is experimental.
1 parent 3e9c85e commit ae5e6ac

File tree

2 files changed

+258
-3
lines changed

2 files changed

+258
-3
lines changed

client/package.json

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"Linters",
2828
"Languages",
2929
"Snippets",
30-
"Formatters"
30+
"Formatters",
31+
"Debuggers"
3132
],
3233
"keywords": [
3334
"puppet",
@@ -309,7 +310,70 @@
309310
"description": "The fully qualified path to the Puppet agent install directory. For example: 'C:\\Program Files\\Puppet Labs\\Puppet' or '/opt/puppetlabs/puppet'"
310311
}
311312
}
312-
}
313+
},
314+
"breakpoints": [
315+
{
316+
"language": "puppet"
317+
}
318+
],
319+
"debuggers": [
320+
{
321+
"type": "Puppet",
322+
"label": "Puppet Debugger",
323+
"program": "./out/src/debugAdapter.js",
324+
"runtime": "node",
325+
"languages": [
326+
"puppet"
327+
],
328+
"configurationSnippets": [
329+
{
330+
"label": "Puppet: Apply Current File",
331+
"description": "Apply current file (in active editor window) under debugger",
332+
"body": {
333+
"type": "Puppet",
334+
"request": "launch",
335+
"name": "Puppet Apply current file",
336+
"manifest": "^\"\\${file}\"",
337+
"args": [],
338+
"noop": false,
339+
"cwd": "^\"\\${file}\""
340+
}
341+
}
342+
],
343+
"configurationAttributes": {
344+
"launch": {
345+
"properties": {
346+
"program": {
347+
"type": "string",
348+
"description": "Deprecated. Please use the 'manifest' property instead to specify the absolute path to the Puppet manifest to launch under the debugger."
349+
},
350+
"manifest": {
351+
"type": "string",
352+
"description": "Optional: Absolute path to the Puppet manifest to launch under the debugger."
353+
},
354+
"noop": {
355+
"type": "boolean",
356+
"description": "Optional: Whether the the Puppet run is in NoOp mode. Default is false.",
357+
"default": false
358+
},
359+
"args": {
360+
"type": "array",
361+
"description": "Command line arguments to pass to Puppet.",
362+
"items": {
363+
"type": "string"
364+
},
365+
"default": []
366+
},
367+
"cwd": {
368+
"type": "string",
369+
"description": "Absolute path to the working directory. Default is the current workspace.",
370+
"default": "${workspaceRoot}"
371+
}
372+
}
373+
}
374+
}
375+
}
376+
]
313377
},
314378
"scripts": {
315379
"vscode:prepublish": "node node_modules/gulp/bin/gulp.js build",
@@ -333,6 +397,8 @@
333397
"dependencies": {
334398
"vscode-languageclient": "~3.3.0",
335399
"vscode-extension-telemetry": "^0.0.6",
336-
"viz.js":"~1.8.0"
400+
"viz.js":"~1.8.0",
401+
"vscode-debugprotocol": "^1.19.0",
402+
"vscode-debugadapter": "^1.19.0"
337403
}
338404
}

client/src/debugAdapter.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
'use strict';
2+
3+
import fs = require('fs');
4+
import path = require('path');
5+
import net = require('net');
6+
import { DebugProtocol } from 'vscode-debugprotocol';
7+
import cp = require('child_process');
8+
import { FileLogger } from './logging/file';
9+
import { NullLogger } from './logging/null';
10+
import { ILogger } from './logging';
11+
import { RubyHelper } from './rubyHelper';
12+
import { IConnectionConfiguration, ConnectionType } from './interfaces';
13+
14+
// This code just marshalls the STDIN/STDOUT to a socket
15+
16+
// Pause the stdin buffer until we're connected to the debug server
17+
process.stdin.pause();
18+
19+
class DebugErrorResponse implements DebugProtocol.ErrorResponse {
20+
public body: { error?: DebugProtocol.Message };
21+
public request_seq: number = 1;
22+
public message?: string;
23+
public success: boolean = false;
24+
public command: string = "initialize";
25+
public seq: number = 1;
26+
public type: string = "response";
27+
28+
constructor(errorMessage: string) {
29+
this.message = errorMessage;
30+
}
31+
}
32+
33+
function sendErrorMessage(message: string) {
34+
let mesageObject = new DebugErrorResponse(message)
35+
let jsonMessage:string = JSON.stringify(mesageObject);
36+
let payloadString = `Content-Length: ${jsonMessage.length}\r\n\r\n${jsonMessage}`;
37+
38+
process.stdout.write(payloadString);
39+
}
40+
41+
class DebugConfiguration implements IConnectionConfiguration {
42+
public type: ConnectionType = ConnectionType.Local ;
43+
public host: string = "127.0.0.1";
44+
public port: number = 8082;
45+
public timeout: number = 10;
46+
public preLoadPuppet: boolean = false;
47+
public debugFilePath: string; // = "STDOUT";
48+
public puppetAgentDir: string;
49+
}
50+
51+
function startDebuggingProxy(config:DebugConfiguration, logger:ILogger, exitOnClose:boolean = false) {
52+
// Establish connection before setting up the session
53+
logger.debug("Connecting to " + config.host + ":" + config.port);
54+
55+
let isConnected = false;
56+
let debugServiceSocket = net.connect(config.port, config.host);
57+
58+
// Write any errors to the log file
59+
debugServiceSocket.on('error', (e) => {
60+
logger.error("Socket ERROR: " + e)
61+
debugServiceSocket.destroy();
62+
});
63+
64+
// Route any output from the socket through stdout
65+
debugServiceSocket.on('data', (data: Buffer) => process.stdout.write(data));
66+
67+
// Wait for the connection to complete
68+
debugServiceSocket.on('connect', () => {
69+
isConnected = true;
70+
logger.debug("Connected to Debug Server");
71+
72+
// When data comes on stdin, route it through the socket
73+
process.stdin.on('data',(data: Buffer) => debugServiceSocket.write(data));
74+
75+
// Resume the stdin stream
76+
process.stdin.resume();
77+
});
78+
79+
// When the socket closes, end the session
80+
debugServiceSocket.on('close',() => {
81+
logger.debug("Socket closed, shutting down.");
82+
debugServiceSocket.destroy();
83+
isConnected = false;
84+
if (exitOnClose) { process.exit(0); }
85+
})
86+
}
87+
88+
function startDebugServerProcess(cmd : string, args : Array<string>, config:DebugConfiguration, logger:ILogger, options : cp.SpawnOptions) {
89+
if ((config.host == undefined) || (config.host == '')) {
90+
args.push('--ip=127.0.0.1');
91+
} else {
92+
args.push('--ip=' + config.host);
93+
}
94+
args.push('--port=' + config.port);
95+
args.push('--timeout=' + config.timeout);
96+
if ((config.debugFilePath != undefined) && (config.debugFilePath != '')) {
97+
args.push('--debug=' + config.debugFilePath);
98+
}
99+
100+
logger.debug("Starting the debug server with " + cmd + " " + args.join(" "));
101+
var proc = cp.spawn(cmd, args, options)
102+
logger.debug('Debug server PID:' + proc.pid)
103+
104+
return proc;
105+
}
106+
107+
function startDebugServer(config:DebugConfiguration, debugLogger: ILogger) {
108+
let localServer = null
109+
110+
let rubyfile = path.join(__dirname,'..','..','vendor', 'languageserver', 'puppet-debugserver')
111+
if (!fs.existsSync(rubyfile)) {
112+
sendErrorMessage("Unable to find the Debug Server at " + rubyfile);
113+
process.exit(255);
114+
}
115+
116+
// TODO use argv to pass in stuff?
117+
if (localServer == null) { localServer = RubyHelper.getRubyEnvFromPuppetAgent(rubyfile, config, debugLogger); }
118+
// Commented out for the moment. This will be enabled once the configuration and exact user story is figured out.
119+
// if (localServer == null) { localServer = RubyHelper.getRubyEnvFromPDK(rubyfile, config, debugLogger); }
120+
121+
if (localServer == null) {
122+
sendErrorMessage("Unable to find a valid ruby environment");
123+
process.exit(255);
124+
}
125+
126+
var debugServerProc = startDebugServerProcess(localServer.command, localServer.args, config, debugLogger, localServer.options);
127+
128+
let debugSessionRunning = false;
129+
debugServerProc.stdout.on('data', (data) => {
130+
debugLogger.debug("OUTPUT: " + data.toString());
131+
132+
// If the language client isn't already running and it's sent the trigger text, start up a client
133+
if ( !debugSessionRunning && (data.toString().match("DEBUG SERVER RUNNING") != null) ) {
134+
debugSessionRunning = true;
135+
startDebuggingProxy(config, debugLogger);
136+
}
137+
});
138+
139+
return debugServerProc;
140+
}
141+
142+
function startDebugging(config:DebugConfiguration, debugLogger:ILogger) {
143+
var debugServerProc = startDebugServer(config, debugLogger);
144+
145+
debugServerProc.on('close', (exitCode) => {
146+
debugLogger.debug("Debug server terminated with exit code: " + exitCode);
147+
debugServerProc.kill();
148+
process.exit(exitCode);
149+
});
150+
151+
debugServerProc.on('error', (data) => {
152+
debugLogger.error(data.message);
153+
});
154+
155+
process.on('SIGTERM', () => {
156+
debugLogger.debug("Received SIGTERM");
157+
debugServerProc.kill();
158+
process.exit(0);
159+
});
160+
161+
process.on('SIGHUP', () => {
162+
debugLogger.debug("Received SIGHUP");
163+
debugServerProc.kill();
164+
process.exit(0);
165+
});
166+
167+
process.on('exit', () => {
168+
debugLogger.debug("Received Exit");
169+
debugServerProc.kill();
170+
process.exit(0);
171+
});
172+
}
173+
174+
// TODO Do we need a logger? should it be optional?
175+
var logPath = path.resolve(__dirname, "../logs");
176+
var logFile = path.resolve(logPath,"DebugAdapter.log");
177+
// let debugLogger = new FileLogger(logFile);
178+
// TODO Until we figure out the logging, just use the null logger
179+
let debugLogger = new NullLogger();
180+
181+
debugLogger.normal("args = " + process.argv);
182+
183+
let config = new DebugConfiguration();
184+
185+
// Launch command
186+
startDebugging(config, debugLogger);
187+
188+
// Attach command
189+
// startDebuggingProxy(config, debugLogger, true);

0 commit comments

Comments
 (0)