Skip to content

Commit 78a88d4

Browse files
committed
(GH-412) Add StdioConnectionHandler
This commit implements StdioConnectionHandler which extends ConnectionHandler and implements the connection logic to start a LanguageServer using STDIO protocol.
1 parent b8f286f commit 78a88d4

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed

src/handlers/stdio.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
4+
import { ServerOptions, Executable } from 'vscode-languageclient';
5+
import { ConnectionHandler } from '../handler';
6+
import { ConnectionType, PuppetInstallType, IConnectionConfiguration } from '../interfaces';
7+
import { ISettings } from '../settings';
8+
import { PuppetStatusBar } from '../PuppetStatusBar';
9+
import { OutputChannelLogger } from '../logging/outputchannel';
10+
import { CommandEnvironmentHelper } from '../helpers/commandHelper';
11+
12+
export class StdioConnectionHandler extends ConnectionHandler {
13+
get connectionType(): ConnectionType {
14+
return ConnectionType.Local;
15+
}
16+
17+
constructor(
18+
context: vscode.ExtensionContext,
19+
settings: ISettings,
20+
statusBar: PuppetStatusBar,
21+
logger: OutputChannelLogger,
22+
config: IConnectionConfiguration,
23+
) {
24+
super(context, settings, statusBar, logger, config);
25+
this.logger.debug(`Configuring ${ConnectionType[this.connectionType]}::${this.protocolType} connection handler`);
26+
this.start();
27+
}
28+
29+
createServerOptions(): ServerOptions {
30+
let exe: Executable = CommandEnvironmentHelper.getRubyEnvFromConfiguration(
31+
this.context.asAbsolutePath(this.config.languageServerPath),
32+
this.settings,
33+
this.config,
34+
);
35+
36+
let logPrefix: string = '';
37+
switch (this.settings.installType) {
38+
case PuppetInstallType.PDK:
39+
logPrefix = '[getRubyEnvFromPDK] ';
40+
this.logger.debug(logPrefix + 'Using environment variable DEVKIT_BASEDIR=' + exe.options.env.DEVKIT_BASEDIR);
41+
this.logger.debug(logPrefix + 'Using environment variable GEM_HOME=' + exe.options.env.GEM_HOME);
42+
this.logger.debug(logPrefix + 'Using environment variable GEM_HOME=' + exe.options.env.GEM_HOME);
43+
break;
44+
case PuppetInstallType.PUPPET:
45+
logPrefix = '[getRubyExecFromPuppetAgent] ';
46+
this.logger.debug(logPrefix + 'Using environment variable SSL_CERT_FILE=' + exe.options.env.SSL_CERT_FILE);
47+
this.logger.debug(logPrefix + 'Using environment variable SSL_CERT_DIR=' + exe.options.env.SSL_CERT_DIR);
48+
break;
49+
}
50+
51+
this.logger.debug(logPrefix + 'Using environment variable RUBY_DIR=' + exe.options.env.RUBY_DIR);
52+
this.logger.debug(logPrefix + 'Using environment variable RUBYLIB=' + exe.options.env.RUBYLIB);
53+
this.logger.debug(logPrefix + 'Using environment variable PATH=' + exe.options.env.PATH);
54+
this.logger.debug(logPrefix + 'Using environment variable RUBYOPT=' + exe.options.env.RUBYOPT);
55+
56+
let serverOptions: ServerOptions = {
57+
run: exe,
58+
debug: exe,
59+
};
60+
61+
return serverOptions;
62+
}
63+
64+
cleanup(): void {
65+
this.logger.debug(`No cleanup needed for ${this.protocolType}`);
66+
}
67+
}

src/helpers/commandHelper.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
4+
import { ISettings } from '../settings';
5+
import { Executable } from 'vscode-languageclient';
6+
import { PuppetInstallType, ProtocolType, IConnectionConfiguration } from '../interfaces';
7+
import { PathResolver } from '../configuration/pathResolver';
8+
9+
export class CommandEnvironmentHelper {
10+
public static getRubyEnvFromConfiguration(
11+
languageServerpath: string,
12+
settings: ISettings,
13+
config: IConnectionConfiguration,
14+
): Executable {
15+
let exe: Executable = {
16+
command: this.buildExecutableCommand(settings, config),
17+
args: this.buildLanguageServerArguments(languageServerpath, settings, config),
18+
options: {},
19+
};
20+
21+
// setup defaults
22+
exe.options.env = this.shallowCloneObject(process.env);
23+
exe.options.stdio = 'pipe';
24+
25+
switch (process.platform) {
26+
case 'win32':
27+
break;
28+
default:
29+
exe.options.shell = true;
30+
break;
31+
}
32+
33+
this.cleanEnvironmentPath(exe);
34+
35+
switch (settings.installType) {
36+
case PuppetInstallType.PDK:
37+
CommandEnvironmentHelper.buildPDKEnvironment(exe, config);
38+
break;
39+
case PuppetInstallType.PUPPET:
40+
CommandEnvironmentHelper.buildPuppetEnvironment(exe, config);
41+
break;
42+
}
43+
44+
// undefined or null values still appear in the child spawn environment variables
45+
// In this case these elements should be removed from the Object
46+
this.removeEmptyElements(exe.options.env);
47+
48+
return exe;
49+
}
50+
51+
private static buildExecutableCommand(settings: ISettings, config: IConnectionConfiguration) {
52+
let command: string = '';
53+
switch (settings.installType) {
54+
case PuppetInstallType.PDK:
55+
command = path.join(config.pdkRubyDir, 'bin', 'ruby');
56+
break;
57+
case PuppetInstallType.PUPPET:
58+
command = 'ruby';
59+
break;
60+
}
61+
return command;
62+
}
63+
64+
private static buildLanguageServerArguments(
65+
serverPath: string,
66+
settings: ISettings,
67+
config: IConnectionConfiguration,
68+
): string[] {
69+
let args = [serverPath];
70+
71+
switch (settings.editorService.protocol) {
72+
case ProtocolType.STDIO:
73+
args.push('--stdio');
74+
break;
75+
case ProtocolType.TCP:
76+
if (settings.editorService.tcp.address === undefined || settings.editorService.tcp.address === '') {
77+
args.push('--ip=127.0.0.1');
78+
} else {
79+
args.push('--ip=' + settings.editorService.tcp.address);
80+
}
81+
if (settings.editorService.tcp.port !== 0) {
82+
args.push('--port=' + settings.editorService.tcp.port);
83+
}
84+
break;
85+
default:
86+
break;
87+
}
88+
89+
args.push('--timeout=' + settings.editorService.timeout);
90+
if (vscode.workspace.workspaceFolders !== undefined) {
91+
args.push('--local-workspace=' + vscode.workspace.workspaceFolders[0].uri.fsPath);
92+
}
93+
if (config.enableFileCache) {
94+
args.push('--enable-file-cache');
95+
}
96+
if (settings.editorService.debugFilePath !== undefined && settings.editorService.debugFilePath !== '') {
97+
args.push('--debug=' + settings.editorService.debugFilePath);
98+
}
99+
return args;
100+
}
101+
102+
private static buildPuppetEnvironment(exe: Executable, config: IConnectionConfiguration) {
103+
exe.options.env.RUBYOPT = 'rubygems';
104+
exe.options.env.SSL_CERT_FILE = config.sslCertFile;
105+
exe.options.env.SSL_CERT_DIR = config.sslCertDir;
106+
exe.options.env.RUBY_DIR = config.rubydir;
107+
exe.options.env.PATH = this.buildPathArray([config.environmentPath, exe.options.env.PATH]);
108+
exe.options.env.RUBYLIB = this.buildPathArray([config.rubylib, exe.options.env.RUBYLIB]);
109+
}
110+
111+
private static buildPDKEnvironment(exe: Executable, config: IConnectionConfiguration) {
112+
exe.options.env.RUBYOPT = 'rubygems';
113+
exe.options.env.DEVKIT_BASEDIR = config.puppetBaseDir;
114+
exe.options.env.RUBY_DIR = config.pdkRubyDir;
115+
exe.options.env.GEM_HOME = config.pdkGemDir;
116+
exe.options.env.GEM_PATH = this.buildPathArray([config.pdkGemVerDir, config.pdkGemDir, config.pdkRubyVerDir]);
117+
exe.options.env.RUBYLIB = this.buildPathArray([config.pdkRubyLib, exe.options.env.RUBYLIB]);
118+
exe.options.env.PATH = this.buildPathArray([config.pdkBinDir, config.pdkRubyBinDir, exe.options.env.PATH]);
119+
}
120+
121+
private static buildPathArray(items: any[]) {
122+
return items.join(PathResolver.pathEnvSeparator());
123+
}
124+
125+
private static shallowCloneObject(value: Object): Object {
126+
const clone: Object = {};
127+
for (const propertyName in value) {
128+
if (value.hasOwnProperty(propertyName)) {
129+
clone[propertyName] = value[propertyName];
130+
}
131+
}
132+
return clone;
133+
}
134+
135+
private static removeEmptyElements(obj: Object) {
136+
const propNames = Object.getOwnPropertyNames(obj);
137+
for (var i = 0; i < propNames.length; i++) {
138+
const propName = propNames[i];
139+
if (obj[propName] === null || obj[propName] === undefined) {
140+
delete obj[propName];
141+
}
142+
}
143+
}
144+
145+
private static cleanEnvironmentPath(exe: Executable) {
146+
if (exe.options.env.PATH === undefined) {
147+
// It's possible that there is no PATH set but unlikely. Due to Object property names being
148+
// case sensitive it could simply be that it's called Path or path, particularly on Windows
149+
// not so much on Linux etc.. Look through all of the environment names looking for PATH in a
150+
// case insensitive way and remove the conflicting env var.
151+
let envPath: string = '';
152+
Object.keys(exe.options.env).forEach(function (keyname) {
153+
if (keyname.match(/^PATH$/i)) {
154+
envPath = exe.options.env[keyname];
155+
exe.options.env[keyname] = undefined;
156+
}
157+
});
158+
exe.options.env.PATH = envPath;
159+
}
160+
if (exe.options.env.RUBYLIB === undefined) {
161+
exe.options.env.RUBYLIB = '';
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)