1919 */
2020
2121import * as vscode from 'vscode' ;
22- import { execSync } from 'child_process' ;
22+ import * as cp from 'child_process' ;
23+ import * as fs from 'fs' ;
24+ import * as path from 'path' ;
25+
26+ /**
27+ * System calls wrapper for testability
28+ */
29+ export const sys = {
30+ spawnSync : cp . spawnSync ,
31+ existsSync : fs . existsSync
32+ } ;
2333
2434/**
2535 * DDEV project validation result
@@ -46,11 +56,8 @@ export class DdevUtils {
4656 */
4757 public static hasDdevProject ( workspacePath : string ) : boolean {
4858 try {
49- const ddevConfig = execSync ( 'test -f .ddev/config.yaml && echo "exists"' , {
50- cwd : workspacePath ,
51- encoding : 'utf-8'
52- } ) ;
53- return ddevConfig . includes ( 'exists' ) ;
59+ const configPath = path . join ( workspacePath , '.ddev' , 'config.yaml' ) ;
60+ return sys . existsSync ( configPath ) ;
5461 } catch ( error ) {
5562 return false ;
5663 }
@@ -64,11 +71,23 @@ export class DdevUtils {
6471 */
6572 public static isDdevRunning ( workspacePath : string ) : boolean {
6673 try {
67- execSync ( 'ddev exec echo " test"' , {
74+ const result = sys . spawnSync ( 'ddev' , [ ' exec' , ' echo' , ' test' ] , {
6875 cwd : workspacePath ,
69- stdio : 'ignore '
76+ encoding : 'utf-8 '
7077 } ) ;
71- return true ;
78+ if ( result . error ) {
79+ vscode . window . showErrorMessage (
80+ 'Failed to execute "ddev". Please ensure DDEV is installed and available in your PATH.' ,
81+ ) ;
82+ return false ;
83+ }
84+ if ( result . status === null ) {
85+ vscode . window . showErrorMessage (
86+ 'The "ddev" command did not exit normally. Please check your DDEV installation.' ,
87+ ) ;
88+ return false ;
89+ }
90+ return result . status === 0 ;
7291 } catch ( error ) {
7392 return false ;
7493 }
@@ -83,7 +102,7 @@ export class DdevUtils {
83102 */
84103 public static isToolInstalled ( toolName : string , workspacePath : string ) : boolean {
85104 try {
86- this . execDdev ( ` ${ toolName } --version` , workspacePath ) ;
105+ this . execDdev ( [ toolName , ' --version' ] , workspacePath ) ;
87106 return true ;
88107 } catch ( error ) {
89108 return false ;
@@ -109,7 +128,7 @@ export class DdevUtils {
109128
110129 // Try to run the tool
111130 try {
112- this . execDdev ( ` ${ toolName } --version` , workspacePath ) ;
131+ this . execDdev ( [ toolName , ' --version' ] , workspacePath ) ;
113132
114133 return {
115134 isValid : true
@@ -174,37 +193,60 @@ export class DdevUtils {
174193 /**
175194 * Execute a command in the DDEV container
176195 *
177- * @param command Command to execute
196+ * @param command Command to execute (as array of strings)
178197 * @param workspacePath Path to the workspace
179198 * @param allowedExitCodes Array of exit codes that should not throw (default: [0])
180199 * @returns Output of the command
181200 * @throws Error if the command fails with disallowed exit code
182201 */
183- public static execDdev ( command : string , workspacePath : string , allowedExitCodes : number [ ] = [ 0 ] ) : string {
202+ public static execDdev ( command : string [ ] , workspacePath : string , allowedExitCodes : number [ ] = [ 0 ] ) : string {
184203 try {
185- // Wrap command in bash -c to allow setting environment variables (specifically disabling Xdebug)
186- // This fixes issues where Xdebug causes the command to hang or run slowly
187- // We use single quotes for the bash command and escape any single quotes in the original command
188- const escapedCommand = command . replace ( / ' / g , "'\\''" ) ;
189- return execSync ( `ddev exec bash -c 'XDEBUG_MODE=off ${ escapedCommand } '` , {
204+ // Use spawnSync to avoid shell injection and safely pass arguments
205+ // We use 'env' to set environment variables inside the container
206+ const args = [ 'exec' , 'env' , 'XDEBUG_MODE=off' , ... command ] ;
207+
208+ const result = sys . spawnSync ( 'ddev' , args , {
190209 cwd : workspacePath ,
191210 encoding : 'utf-8'
192211 } ) ;
193- } catch ( error : any ) {
194- // Check if this is an acceptable exit code (e.g., PHPStan returns 1 when errors are found)
195- if ( error . status && allowedExitCodes . includes ( error . status ) ) {
212+
213+ if ( result . error ) {
214+ throw result . error ;
215+ }
216+
217+ // Check if this is an acceptable exit code
218+ if ( result . status !== null && allowedExitCodes . includes ( result . status ) ) {
196219 // Return stdout even if exit code is non-zero but allowed
197- console . log ( `Command exited with allowed code ${ error . status } : ${ command } ` ) ;
198- return error . stdout || '' ;
220+ if ( result . status !== 0 ) {
221+ console . log ( `Command exited with allowed code ${ result . status } : ${ command . join ( ' ' ) } ` ) ;
222+ }
223+ return result . stdout || '' ;
224+ }
225+
226+ if ( result . status !== 0 ) {
227+ // Enhance error message with more details
228+ const enhancedError = new Error ( result . stderr || 'Command execution failed' ) ;
229+ enhancedError . name = 'CommandError' ;
230+ ( enhancedError as any ) . status = result . status ;
231+ ( enhancedError as any ) . stderr = result . stderr ;
232+ ( enhancedError as any ) . stdout = result . stdout ;
233+ ( enhancedError as any ) . command = `ddev exec ${ command . join ( ' ' ) } ` ;
234+ ( enhancedError as any ) . workspacePath = workspacePath ;
235+ throw enhancedError ;
236+ }
237+ } catch ( error : any ) {
238+ // If error was already thrown above, rethrow it
239+ if ( error . name === 'CommandError' ) {
240+ throw error ;
199241 }
200242
201- // Enhance error message with more details
243+ // Handle unexpected errors
202244 const enhancedError = new Error ( error . message || 'Command execution failed' ) ;
203245 enhancedError . name = error . name || 'CommandError' ;
204246 ( enhancedError as any ) . status = error . status ;
205247 ( enhancedError as any ) . stderr = error . stderr ;
206248 ( enhancedError as any ) . stdout = error . stdout ;
207- ( enhancedError as any ) . command = `ddev exec ${ command } ` ;
249+ ( enhancedError as any ) . command = `ddev exec ${ command . join ( ' ' ) } ` ;
208250 ( enhancedError as any ) . workspacePath = workspacePath ;
209251
210252 throw enhancedError ;
0 commit comments