Skip to content

Commit c522202

Browse files
committed
(GH-61) Refactor code to use a connection Manager
Previously the client extension was not aware of connection state and could not manage restarting the Language Server Client, or display meaningful errors to the user. This commit: - Refactors the Language Client parts of the code into a ConnectionManager class which handles the state the connection. It can be started, stopped and restarted. - The Puppet Commands were modified to take the connection manager as input parameter instead of the Language Client directly. This means commands will be aware of the connection state and can prompt the user appropriately, for example trying to run Puppet Resource when the language server is unavailable. - Simplified the extension main method to call the connection manager - Fixed an issue where the Language Client was being started twice due to the Language Server emitting more than one line from STDOUT. In this case, the Language Client is only started if it doesn't already exist
1 parent 73c6700 commit c522202

File tree

6 files changed

+341
-163
lines changed

6 files changed

+341
-163
lines changed

client/src/commands/puppetResourceCommand.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
'use strict';
22

33
import * as vscode from 'vscode';
4-
import { LanguageClient, RequestType } from 'vscode-languageclient';
54
import { PuppetResourceRequestParams, PuppetResourceRequest } from '../messages';
5+
import { IConnectionManager, ConnectionStatus } from '../connection';
66

77
class RequestParams implements PuppetResourceRequestParams {
88
typename: string;
99
title: string;
1010
}
1111

1212
export class puppetResourceCommand {
13-
private _langServer: LanguageClient = undefined;
13+
private _connectionManager: IConnectionManager = undefined;
1414

15-
constructor(
16-
private langServer: LanguageClient
17-
) {
18-
this._langServer = langServer;
15+
constructor(connMgr: IConnectionManager) {
16+
this._connectionManager = connMgr;
1917
}
2018

2119
private pickPuppetResource(): Thenable<string> {
@@ -28,6 +26,13 @@ export class puppetResourceCommand {
2826
}
2927

3028
public run() {
29+
var thisCommand = this
30+
31+
if (thisCommand._connectionManager.status != ConnectionStatus.Running ) {
32+
vscode.window.showInformationMessage("Puppet Resource is not available as the Language Server is not ready");
33+
return
34+
}
35+
3136
this.pickPuppetResource().then((moduleName) => {
3237
if (moduleName) {
3338

@@ -38,7 +43,7 @@ export class puppetResourceCommand {
3843
let requestParams = new RequestParams;
3944
requestParams.typename = moduleName;
4045

41-
this._langServer
46+
thisCommand._connectionManager.languageClient
4247
.sendRequest(PuppetResourceRequest.type, requestParams)
4348
.then( (resourceResult) => {
4449
if (resourceResult.error != undefined && resourceResult.error.length > 0) {

client/src/connection.ts

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import net = require('net');
2+
import path = require('path');
3+
import vscode = require('vscode');
4+
import cp = require('child_process');
5+
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient';
6+
import { setupPuppetCommands } from '../src/puppetcommands';
7+
import * as messages from '../src/messages';
8+
9+
const langID = 'puppet'; // don't change this
10+
11+
export enum ConnectionStatus {
12+
NotStarted,
13+
Starting,
14+
Running,
15+
Stopping,
16+
Failed
17+
}
18+
19+
export enum ConnectionType {
20+
Unknown,
21+
Local,
22+
Remote
23+
}
24+
25+
export interface IConnectionConfiguration {
26+
type: ConnectionType;
27+
host: string;
28+
port: number;
29+
stopOnClientExit: string;
30+
timeout: string;
31+
preLoadPuppet: string;
32+
}
33+
34+
export interface IConnectionManager {
35+
status: ConnectionStatus;
36+
languageClient: LanguageClient;
37+
}
38+
39+
export class ConnectionManager implements IConnectionManager {
40+
private connectionStatus: ConnectionStatus;
41+
private statusBarItem: vscode.StatusBarItem;
42+
private connectionConfiguration: IConnectionConfiguration;
43+
private languageServerClient: LanguageClient = undefined;
44+
private languageServerProcess = undefined;
45+
private puppetOutputChannel = undefined;
46+
private extensionContext = undefined;
47+
private commandsRegistered = false;
48+
49+
public get status() : ConnectionStatus {
50+
return this.connectionStatus;
51+
}
52+
public get languageClient() : LanguageClient {
53+
return this.languageServerClient;
54+
}
55+
56+
constructor(context: vscode.ExtensionContext) {
57+
this.puppetOutputChannel = vscode.window.createOutputChannel('Puppet');
58+
59+
this.extensionContext = context;
60+
}
61+
62+
public start(connectionConfig: IConnectionConfiguration) {
63+
// Setup the configuration
64+
this.connectionConfiguration = connectionConfig;
65+
this.connectionConfiguration.type = ConnectionType.Unknown;
66+
var contextPath = this.extensionContext.asAbsolutePath(path.join('vendor', 'languageserver', 'puppet-languageserver'));
67+
68+
if (!this.commandsRegistered) {
69+
this.puppetOutputChannel.appendLine('Configuring commands');
70+
console.log('Configuring commands');
71+
72+
setupPuppetCommands(langID, this, this.extensionContext);
73+
this.commandsRegistered = true;
74+
}
75+
76+
this.createStatusBarItem();
77+
78+
if (this.connectionConfiguration.host == '127.0.0.1' ||
79+
this.connectionConfiguration.host == 'localhost' ||
80+
this.connectionConfiguration.host == '') {
81+
this.connectionConfiguration.type = ConnectionType.Local
82+
} else {
83+
this.connectionConfiguration.type = ConnectionType.Remote
84+
}
85+
86+
this.languageServerClient = undefined
87+
this.languageServerProcess = undefined
88+
this.setConnectionStatus("Starting Puppet...", ConnectionStatus.Starting);
89+
90+
if (this.connectionConfiguration.type == ConnectionType.Local) {
91+
this.languageServerProcess = this.createLanguageServerProcess(contextPath, this.puppetOutputChannel);
92+
if (this.languageServerProcess == undefined) { throw new Error('Unable to start the server process'); }
93+
94+
this.languageServerProcess.stdout.on('data', (data) => {
95+
console.log("OUTPUT: " + data.toString());
96+
this.puppetOutputChannel.appendLine("OUTPUT: " + data.toString());
97+
98+
// If the language client isn't already running and it's sent the trigger text, start up a client
99+
if ( (this.languageServerClient == undefined) && (data.toString().match("LANGUAGE SERVER RUNNING") != null) ) {
100+
this.languageServerClient = this.startLangClientTCP();
101+
this.extensionContext.subscriptions.push(this.languageServerClient.start());
102+
}
103+
});
104+
105+
this.languageServerProcess.on('close', (exitCode) => {
106+
console.log("SERVER terminated with exit code: " + exitCode);
107+
this.puppetOutputChannel.appendLine("SERVER terminated with exit code: " + exitCode);
108+
});
109+
}
110+
else {
111+
this.languageServerClient = this.startLangClientTCP();
112+
this.extensionContext.subscriptions.push(this.languageServerClient.start());
113+
}
114+
115+
console.log('Congratulations, your extension "vscode-puppet" is now active!');
116+
this.puppetOutputChannel.appendLine('Congratulations, your extension "vscode-puppet" is now active!');
117+
}
118+
119+
public stop() {
120+
console.log('Stopping...');
121+
this.puppetOutputChannel.appendLine('Stopping...')
122+
123+
if (this.connectionStatus === ConnectionStatus.Failed) {
124+
this.languageServerClient = undefined;
125+
this.languageServerProcess = undefined;
126+
}
127+
128+
this.connectionStatus = ConnectionStatus.Stopping;
129+
130+
// Close the language server client
131+
if (this.languageServerClient !== undefined) {
132+
this.languageServerClient.stop();
133+
this.languageServerClient = undefined;
134+
}
135+
136+
// Kill the language server process we spawned
137+
if (this.languageServerProcess !== undefined) {
138+
// Terminate Language Server process
139+
// TODO: May not be functional on Windows.
140+
// Perhaps send the exit command and wait for process to exit?
141+
this.languageServerProcess.kill();
142+
this.languageServerProcess = undefined;
143+
}
144+
145+
this.connectionStatus = ConnectionStatus.NotStarted;
146+
147+
console.log('Stopped');
148+
this.puppetOutputChannel.appendLine('Stopped');
149+
}
150+
151+
public dispose() : void {
152+
console.log('Disposing...');
153+
this.puppetOutputChannel.appendLine('Disposing...');
154+
// Stop the current session
155+
this.stop();
156+
157+
// Dispose of any subscriptions
158+
this.extensionContext.subscriptions.forEach(item => { item.dispose(); });
159+
this.extensionContext.subscriptions.clear();
160+
}
161+
162+
private createLanguageServerProcess(serverExe: string, myOutputChannel: vscode.OutputChannel) {
163+
myOutputChannel.appendLine('Language server found at: ' + serverExe)
164+
165+
let cmd = '';
166+
let args = [serverExe];
167+
let options = {};
168+
169+
// type Platform = 'aix'
170+
// | 'android'
171+
// | 'darwin'
172+
// | 'freebsd'
173+
// | 'linux'
174+
// | 'openbsd'
175+
// | 'sunos'
176+
// | 'win32';
177+
switch (process.platform) {
178+
case 'win32':
179+
myOutputChannel.appendLine('Windows spawn process does not work at the moment')
180+
vscode.window.showErrorMessage('Windows spawn process does not work at the moment. Functionality will be limited to syntax highlighting');
181+
break;
182+
default:
183+
myOutputChannel.appendLine('Starting language server')
184+
cmd = 'ruby'
185+
options = {
186+
shell: true,
187+
env: process.env,
188+
stdio: 'pipe',
189+
};
190+
}
191+
192+
console.log("Starting the language server with " + cmd + " " + args.join(" "));
193+
var proc = cp.spawn(cmd, args, options)
194+
console.log("ProcID = " + proc.pid);
195+
myOutputChannel.appendLine('Language server PID:' + proc.pid)
196+
197+
return proc;
198+
}
199+
200+
private startLangClientTCP(): LanguageClient {
201+
this.puppetOutputChannel.appendLine('Configuring language server options')
202+
203+
var connMgr:ConnectionManager = this;
204+
let serverOptions: ServerOptions = function () {
205+
return new Promise((resolve, reject) => {
206+
var client = new net.Socket();
207+
client.connect(connMgr.connectionConfiguration.port, connMgr.connectionConfiguration.host, function () {
208+
resolve({ reader: client, writer: client });
209+
});
210+
client.on('error', function (err) {
211+
console.log(`[Puppet Lang Server Client] ` + err);
212+
connMgr.setSessionFailure("Could not start language client: ", err.message);
213+
214+
return null;
215+
})
216+
});
217+
}
218+
219+
this.puppetOutputChannel.appendLine('Configuring language server client options')
220+
let clientOptions: LanguageClientOptions = {
221+
documentSelector: [langID],
222+
}
223+
224+
this.puppetOutputChannel.appendLine(`Starting language server client (host ${this.connectionConfiguration.host} port ${this.connectionConfiguration.port})`)
225+
var title = `tcp lang server (host ${this.connectionConfiguration.host} port ${this.connectionConfiguration.port})`;
226+
var languageServerClient = new LanguageClient(title, serverOptions, clientOptions)
227+
languageServerClient.onReady().then(() => {
228+
this.puppetOutputChannel.appendLine('Language server client started, setting puppet version')
229+
languageServerClient.sendRequest(messages.PuppetVersionRequest.type).then((versionDetails) => {
230+
this.setConnectionStatus(versionDetails.puppetVersion, ConnectionStatus.Running);
231+
});
232+
}, (reason) => {
233+
this.setSessionFailure("Could not start language service: ", reason);
234+
});
235+
236+
return languageServerClient;
237+
}
238+
239+
private restartConnection(connectionConfig?: IConnectionConfiguration) {
240+
this.stop();
241+
this.start(connectionConfig);
242+
}
243+
244+
private createStatusBarItem() {
245+
if (this.statusBarItem === undefined) {
246+
this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1);
247+
248+
// TODO: Add a command here to show the connection menu
249+
// this.statusBarItem.command = this.ShowConnectionMenuCommandName;
250+
this.statusBarItem.show();
251+
vscode.window.onDidChangeActiveTextEditor(textEditor => {
252+
if (textEditor === undefined || textEditor.document.languageId !== "puppet") {
253+
this.statusBarItem.hide();
254+
}
255+
else {
256+
this.statusBarItem.show();
257+
}
258+
})
259+
}
260+
}
261+
262+
private setConnectionStatus(statusText: string, status: ConnectionStatus): void {
263+
// Set color and icon for 'Running' by default
264+
var statusIconText = "$(terminal) ";
265+
var statusColor = "#affc74";
266+
267+
if (status == ConnectionStatus.Starting) {
268+
statusIconText = "$(sync) ";
269+
statusColor = "#f3fc74";
270+
}
271+
else if (status == ConnectionStatus.Failed) {
272+
statusIconText = "$(alert) ";
273+
statusColor = "#fcc174";
274+
}
275+
276+
this.connectionStatus = status;
277+
this.statusBarItem.color = statusColor;
278+
this.statusBarItem.text = statusIconText + statusText;
279+
}
280+
281+
private setSessionFailure(message: string, ...additionalMessages: string[]) {
282+
this.setConnectionStatus("Starting Error", ConnectionStatus.Failed);
283+
}
284+
}

0 commit comments

Comments
 (0)