1
1
/* eslint-disable no-process-env */
2
- import { ExecOptions } from "child_process" ;
3
2
import path from "path" ;
3
+ import os from "os" ;
4
+ import { StringDecoder } from "string_decoder" ;
5
+ import { ExecOptions } from "child_process" ;
4
6
5
7
import * as vscode from "vscode" ;
6
8
import { Executable } from "vscode-languageclient/node" ;
@@ -10,8 +12,13 @@ import {
10
12
ContainerPathConverter ,
11
13
fetchPathMapping ,
12
14
} from "../docker" ;
15
+ import { parseCommand , spawn } from "../common" ;
13
16
14
- import { VersionManager , ActivationResult } from "./versionManager" ;
17
+ import {
18
+ VersionManager ,
19
+ ActivationResult ,
20
+ ACTIVATION_SEPARATOR ,
21
+ } from "./versionManager" ;
15
22
16
23
// Compose
17
24
//
@@ -24,52 +31,63 @@ export class Compose extends VersionManager {
24
31
async activate ( ) : Promise < ActivationResult > {
25
32
await this . ensureConfigured ( ) ;
26
33
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 ,
29
38
) ;
30
39
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
+
31
73
return {
32
74
env : { ...process . env } ,
33
75
yjit : parsedResult . yjit ,
34
76
version : parsedResult . version ,
35
77
gemPath : parsedResult . gemPath ,
78
+ pathConverter,
79
+ wrapCommand,
36
80
} ;
37
81
}
38
82
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 ( ) {
66
84
const pathMapping = fetchPathMapping (
67
85
this . composeConfig ,
68
86
this . composeServiceName ( ) ,
69
87
) ;
70
88
71
89
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 ) ;
73
91
return vscode . workspace . fs . stat ( vscode . Uri . file ( absolute ) ) . then (
74
92
( stat ) => ( { stat, local, remote, absolute } ) ,
75
93
( ) => ( { stat : undefined , local, remote, absolute } ) ,
@@ -162,6 +180,83 @@ export class Compose extends VersionManager {
162
180
} ) ;
163
181
}
164
182
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 ( / E N D _ O F _ R U B Y _ C O D E _ O U T P U T .* / 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
+
165
260
private async getComposeConfig ( ) : Promise < ComposeConfig > {
166
261
try {
167
262
const { stdout, stderr : _stderr } = await this . runScript (
0 commit comments