Skip to content

Commit a3a5930

Browse files
committed
Made changes more conservative
1 parent 2844728 commit a3a5930

File tree

15 files changed

+316
-350
lines changed

15 files changed

+316
-350
lines changed

vscode/src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ function collectClientOptions(
278278
});
279279
}
280280

281-
outputChannel.info(
281+
outputChannel.debug(
282282
`Document Selector Paths: ${JSON.stringify(documentSelector)}`,
283283
);
284284

vscode/src/common.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { promisify } from "util";
44

55
import * as vscode from "vscode";
66
import { State } from "vscode-languageclient";
7+
import { Executable } from "vscode-languageclient/node";
78

89
export enum Command {
910
Start = "rubyLsp.start",
@@ -72,6 +73,22 @@ export interface PathConverterInterface {
7273
toRemoteUri: (localUri: vscode.Uri) => vscode.Uri;
7374
}
7475

76+
export class PathConverter implements PathConverterInterface {
77+
readonly pathMapping: [string, string][] = [];
78+
79+
toRemotePath(path: string) {
80+
return path;
81+
}
82+
83+
toLocalPath(path: string) {
84+
return path;
85+
}
86+
87+
toRemoteUri(localUri: vscode.Uri) {
88+
return localUri;
89+
}
90+
}
91+
7592
// Event emitter used to signal that the language status items need to be refreshed
7693
export const STATUS_EMITTER = new vscode.EventEmitter<
7794
WorkspaceInterface | undefined
@@ -155,3 +172,29 @@ export function featureEnabled(feature: keyof typeof FEATURE_FLAGS): boolean {
155172
// If that number is below the percentage, then the feature is enabled for this user
156173
return hashNum < percentage;
157174
}
175+
176+
export function parseCommand(commandString: string): Executable {
177+
// Regular expression to split arguments while respecting quotes
178+
const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;
179+
180+
const parts =
181+
commandString.match(regex)?.map((arg) => {
182+
// Remove surrounding quotes, if any
183+
return arg.replace(/^['"]|['"]$/g, "");
184+
}) ?? [];
185+
186+
// Extract environment variables
187+
const env: Record<string, string> = {};
188+
while (parts[0] && parts[0].includes("=")) {
189+
const [key, value] = parts.shift()?.split("=") ?? [];
190+
if (key) {
191+
env[key] = value || "";
192+
}
193+
}
194+
195+
// The first part is the command, the rest are arguments
196+
const command = parts.shift() || "";
197+
const args = parts;
198+
199+
return { command, args, options: { env } };
200+
}

vscode/src/ruby.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
/* eslint-disable no-process-env */
22
import path from "path";
33
import os from "os";
4-
import { ExecOptions } from "child_process";
54

65
import * as vscode from "vscode";
7-
import { Executable } from "vscode-languageclient/node";
8-
9-
import { asyncExec, PathConverterInterface, RubyInterface } from "./common";
6+
import { Executable, ExecutableOptions } from "vscode-languageclient/node";
7+
8+
import {
9+
asyncExec,
10+
parseCommand,
11+
PathConverter,
12+
PathConverterInterface,
13+
RubyInterface,
14+
} from "./common";
1015
import { WorkspaceChannel } from "./workspaceChannel";
1116
import { Shadowenv } from "./ruby/shadowenv";
1217
import { Chruby } from "./ruby/chruby";
13-
import { PathConverter, VersionManager } from "./ruby/versionManager";
18+
import { VersionManager } from "./ruby/versionManager";
1419
import { Mise } from "./ruby/mise";
1520
import { RubyInstaller } from "./ruby/rubyInstaller";
1621
import { Rbenv } from "./ruby/rbenv";
@@ -69,9 +74,9 @@ export class Ruby implements RubyInterface {
6974

7075
private readonly shell = process.env.SHELL?.replace(/(\s+)/g, "\\$1");
7176
private _env: NodeJS.ProcessEnv = {};
72-
private _manager?: VersionManager;
73-
private _pathConverter: PathConverterInterface = new PathConverter();
7477
private _error = false;
78+
private _pathConverter: PathConverterInterface;
79+
private _wrapCommand: (executable: Executable) => Executable;
7580
private readonly context: vscode.ExtensionContext;
7681
private readonly customBundleGemfile?: string;
7782
private readonly outputChannel: WorkspaceChannel;
@@ -87,6 +92,8 @@ export class Ruby implements RubyInterface {
8792
this.workspaceFolder = workspaceFolder;
8893
this.outputChannel = outputChannel;
8994
this.telemetry = telemetry;
95+
this._pathConverter = new PathConverter();
96+
this._wrapCommand = (executable: Executable) => executable;
9097

9198
const customBundleGemfile: string = vscode.workspace
9299
.getConfiguration("rubyLsp")
@@ -119,10 +126,6 @@ export class Ruby implements RubyInterface {
119126
return this._pathConverter;
120127
}
121128

122-
set pathConverter(pathConverter: PathConverterInterface) {
123-
this._pathConverter = pathConverter;
124-
}
125-
126129
get env() {
127130
return this._env;
128131
}
@@ -203,12 +206,22 @@ export class Ruby implements RubyInterface {
203206
}
204207
}
205208

206-
runActivatedScript(command: string, options: ExecOptions = {}) {
207-
return this._manager!.runActivatedScript(command, options);
209+
runActivatedScript(script: string, options: ExecutableOptions = {}) {
210+
const parsedExecutable = parseCommand(script);
211+
const executable = this.activateExecutable({
212+
...parsedExecutable,
213+
options,
214+
});
215+
const command = [executable.command, ...(executable.args || [])].join(" ");
216+
217+
return asyncExec(command, {
218+
cwd: this.workspaceFolder.uri.fsPath || executable.options?.cwd,
219+
env: { ...process.env, ...executable.options?.env },
220+
});
208221
}
209222

210223
activateExecutable(executable: Executable) {
211-
return this._manager!.activateExecutable(executable);
224+
return this._wrapCommand(executable);
212225
}
213226

214227
async manuallySelectRuby() {
@@ -263,20 +276,25 @@ export class Ruby implements RubyInterface {
263276
}
264277

265278
private async runActivation(manager: VersionManager) {
266-
const { env, version, yjit, gemPath } = await manager.activate();
279+
const { env, version, yjit, gemPath, pathConverter, wrapCommand } =
280+
await manager.activate();
267281
const [major, minor, _patch] = version.split(".").map(Number);
268282

269283
this.sanitizeEnvironment(env);
270284

271-
this.pathConverter = await manager.buildPathConverter(this.workspaceFolder);
272-
273285
// We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths
274286
process.env = env;
275287
this._env = env;
276-
this._manager = manager;
277288
this.rubyVersion = version;
278289
this.yjitEnabled = (yjit && major > 3) || (major === 3 && minor >= 2);
279290
this.gemPath.push(...gemPath);
291+
292+
if (pathConverter) {
293+
this._pathConverter = pathConverter;
294+
}
295+
if (wrapCommand) {
296+
this._wrapCommand = wrapCommand;
297+
}
280298
}
281299

282300
// Fetch information related to the Ruby version. This can only be invoked after activation, so that `rubyVersion` is

vscode/src/ruby/compose.ts

Lines changed: 127 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable no-process-env */
2-
import { ExecOptions } from "child_process";
32
import path from "path";
3+
import os from "os";
4+
import { StringDecoder } from "string_decoder";
5+
import { ExecOptions } from "child_process";
46

57
import * as vscode from "vscode";
68
import { Executable } from "vscode-languageclient/node";
@@ -10,8 +12,13 @@ import {
1012
ContainerPathConverter,
1113
fetchPathMapping,
1214
} from "../docker";
15+
import { parseCommand, spawn } from "../common";
1316

14-
import { VersionManager, ActivationResult } from "./versionManager";
17+
import {
18+
VersionManager,
19+
ActivationResult,
20+
ACTIVATION_SEPARATOR,
21+
} from "./versionManager";
1522

1623
// Compose
1724
//
@@ -24,52 +31,63 @@ export class Compose extends VersionManager {
2431
async activate(): Promise<ActivationResult> {
2532
await this.ensureConfigured();
2633

27-
const parsedResult = await this.runEnvActivationScript(
28-
`${this.composeRunCommand()} ${this.composeServiceName()} ruby`,
34+
const rubyCommand = `${this.composeRunCommand()} ${this.composeServiceName()} ruby -W0 -rjson`;
35+
const { stderr: output } = await this.runRubyCode(
36+
rubyCommand,
37+
this.activationScript,
2938
);
3039

40+
this.outputChannel.debug(`Activation output: ${output}`);
41+
42+
const activationContent = new RegExp(
43+
`${ACTIVATION_SEPARATOR}(.*)${ACTIVATION_SEPARATOR}`,
44+
).exec(output);
45+
46+
const parsedResult = this.parseWithErrorHandling(activationContent![1]);
47+
const pathConverter = await this.buildPathConverter();
48+
49+
const wrapCommand = (executable: Executable) => {
50+
const composeCommad = parseCommand(
51+
`${this.composeRunCommand()} ${this.composeServiceName()}`,
52+
);
53+
54+
const command = {
55+
command: composeCommad.command,
56+
args: [
57+
...(composeCommad.args ?? []),
58+
executable.command,
59+
...(executable.args ?? []),
60+
],
61+
options: {
62+
...executable.options,
63+
env: {
64+
...executable.options?.env,
65+
...composeCommad.options?.env,
66+
},
67+
},
68+
};
69+
70+
return command;
71+
};
72+
3173
return {
3274
env: { ...process.env },
3375
yjit: parsedResult.yjit,
3476
version: parsedResult.version,
3577
gemPath: parsedResult.gemPath,
78+
pathConverter,
79+
wrapCommand,
3680
};
3781
}
3882

39-
runActivatedScript(command: string, options: ExecOptions = {}) {
40-
return this.runScript(
41-
`${this.composeRunCommand()} ${this.composeServiceName()} ${command}`,
42-
options,
43-
);
44-
}
45-
46-
activateExecutable(executable: Executable) {
47-
const composeCommand = this.parseCommand(
48-
`${this.composeRunCommand()} ${this.composeServiceName()}`,
49-
);
50-
51-
return {
52-
command: composeCommand.command,
53-
args: [
54-
...composeCommand.args,
55-
executable.command,
56-
...(executable.args || []),
57-
],
58-
options: {
59-
...executable.options,
60-
env: { ...(executable.options?.env || {}), ...composeCommand.env },
61-
},
62-
};
63-
}
64-
65-
async buildPathConverter(workspaceFolder: vscode.WorkspaceFolder) {
83+
protected async buildPathConverter() {
6684
const pathMapping = fetchPathMapping(
6785
this.composeConfig,
6886
this.composeServiceName(),
6987
);
7088

7189
const stats = Object.entries(pathMapping).map(([local, remote]) => {
72-
const absolute = path.resolve(workspaceFolder.uri.fsPath, local);
90+
const absolute = path.resolve(this.workspaceFolder.uri.fsPath, local);
7391
return vscode.workspace.fs.stat(vscode.Uri.file(absolute)).then(
7492
(stat) => ({ stat, local, remote, absolute }),
7593
() => ({ stat: undefined, local, remote, absolute }),
@@ -162,6 +180,83 @@ export class Compose extends VersionManager {
162180
});
163181
}
164182

183+
protected runRubyCode(
184+
rubyCommand: string,
185+
code: string,
186+
): Promise<{ stdout: string; stderr: string }> {
187+
return new Promise((resolve, reject) => {
188+
this.outputChannel.info(
189+
`Ruby \`${rubyCommand}\` running Ruby code: \`${code}\``,
190+
);
191+
192+
const {
193+
command,
194+
args,
195+
options: { env } = { env: {} },
196+
} = parseCommand(rubyCommand);
197+
const ruby = spawn(command, args, this.execOptions({ env }));
198+
199+
let stdout = "";
200+
let stderr = "";
201+
202+
const stdoutDecoder = new StringDecoder("utf-8");
203+
const stderrDecoder = new StringDecoder("utf-8");
204+
205+
ruby.stdout.on("data", (data) => {
206+
stdout += stdoutDecoder.write(data);
207+
208+
if (stdout.includes("END_OF_RUBY_CODE_OUTPUT")) {
209+
stdout = stdout.replace(/END_OF_RUBY_CODE_OUTPUT.*/s, "");
210+
resolve({ stdout, stderr });
211+
}
212+
});
213+
ruby.stderr.on("data", (data) => {
214+
stderr += stderrDecoder.write(data);
215+
});
216+
ruby.on("error", (error) => {
217+
reject(error);
218+
});
219+
ruby.on("close", (status) => {
220+
if (status) {
221+
reject(new Error(`Process exited with status ${status}: ${stderr}`));
222+
} else {
223+
resolve({ stdout, stderr });
224+
}
225+
});
226+
227+
const script = [
228+
"begin",
229+
...code.split("\n").map((line) => ` ${line}`),
230+
"ensure",
231+
' puts "END_OF_RUBY_CODE_OUTPUT"',
232+
"end",
233+
].join("\n");
234+
235+
this.outputChannel.info(`Running Ruby code:\n${script}`);
236+
237+
ruby.stdin.write(script);
238+
ruby.stdin.end();
239+
});
240+
}
241+
242+
protected execOptions(options: ExecOptions = {}): ExecOptions {
243+
let shell: string | undefined;
244+
245+
// If the user has configured a default shell, we use that one since they are probably sourcing their version
246+
// manager scripts in that shell's configuration files. On Windows, we never set the shell no matter what to ensure
247+
// that activation runs on `cmd.exe` and not PowerShell, which avoids complex quoting and escaping issues.
248+
if (vscode.env.shell.length > 0 && os.platform() !== "win32") {
249+
shell = vscode.env.shell;
250+
}
251+
252+
return {
253+
cwd: this.bundleUri.fsPath,
254+
shell,
255+
...options,
256+
env: { ...process.env, ...options.env },
257+
};
258+
}
259+
165260
private async getComposeConfig(): Promise<ComposeConfig> {
166261
try {
167262
const { stdout, stderr: _stderr } = await this.runScript(

0 commit comments

Comments
 (0)