Skip to content

Commit 81bfd3a

Browse files
authored
Fix running executables with spaces in their paths in Windows (#777)
1 parent 2c97694 commit 81bfd3a

File tree

4 files changed

+96
-89
lines changed

4 files changed

+96
-89
lines changed

build-info-extractor-go/src/main/java/org/jfrog/build/extractor/go/GoDriver.java

+1-45
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package org.jfrog.build.extractor.go;
22

3-
import com.google.common.collect.Maps;
43
import org.apache.commons.lang3.StringUtils;
5-
import org.apache.commons.lang3.SystemUtils;
64
import org.jfrog.build.api.util.Log;
75
import org.jfrog.build.extractor.executor.CommandExecutor;
86
import org.jfrog.build.extractor.executor.CommandResults;
@@ -26,60 +24,18 @@ public class GoDriver implements Serializable {
2624
private static final String GO_GET_CMD = "get";
2725
private static final String GO_LIST_MODULE_CMD = "list -m";
2826
private static final String GO_VERSION_CMD = "version";
29-
private static final String DEFAULT_EXECUTABLE_NAME = "go";
3027

3128
private static final long serialVersionUID = 1L;
3229
private final CommandExecutor commandExecutor;
3330
private final File workingDirectory;
3431
private final Log logger;
3532

3633
public GoDriver(String executablePath, Map<String, String> env, File workingDirectory, Log logger) {
37-
this.commandExecutor = generateCommandExecutor(executablePath, env);
34+
this.commandExecutor = new CommandExecutor(StringUtils.defaultIfEmpty(executablePath, "go"), env);
3835
this.workingDirectory = workingDirectory;
3936
this.logger = logger;
4037
}
4138

42-
/**
43-
* Generate a new mutable copy of environment variables map with the Go executable directory path inserted to the beginning of the Path.
44-
*
45-
* @param executablePath Go executable path
46-
* @param env Environment variables map
47-
* @return a new Environment variables map
48-
*/
49-
static Map<String, String> generateWindowsEnv(String executablePath, Map<String, String> env) {
50-
// If executablePath ends with "go" or "go.exe" - remove it from the directory path
51-
executablePath = StringUtils.removeEnd(executablePath, ".exe");
52-
executablePath = StringUtils.removeEnd(executablePath, DEFAULT_EXECUTABLE_NAME);
53-
54-
// Insert the Go executable directory path to the beginning of the Path environment variable
55-
// Make sure to copy the environment variables map to avoid changing the original map or in case it is immutable
56-
Map<String, String> newEnv = Maps.newHashMap(env);
57-
String windowsPathEnvKey = "Path";
58-
if (newEnv.containsKey(windowsPathEnvKey)) {
59-
newEnv.put(windowsPathEnvKey, executablePath + File.pathSeparator + newEnv.get(windowsPathEnvKey));
60-
} else {
61-
newEnv.put(windowsPathEnvKey, executablePath);
62-
}
63-
return newEnv;
64-
}
65-
66-
/**
67-
* Create a CommandExecutor with the given executable path and environment variables.
68-
*
69-
* @param executablePath Go executable path
70-
* @param env Environment variables map
71-
* @return CommandExecutor
72-
*/
73-
private static CommandExecutor generateCommandExecutor(String executablePath, Map<String, String> env) {
74-
if (!SystemUtils.IS_OS_WINDOWS || StringUtils.isBlank(executablePath) || StringUtils.equals(executablePath, DEFAULT_EXECUTABLE_NAME) || env == null) {
75-
return new CommandExecutor(StringUtils.defaultIfEmpty(executablePath, DEFAULT_EXECUTABLE_NAME), env);
76-
}
77-
// Handling Windows case with a given executable path:
78-
// A bug was identified for the Go executable in Windows where the executable path may be incorrectly parsed
79-
// as two command arguments when the path contains spaces (e.g., "C:\Program Files\Go\bin\go.exe").
80-
return new CommandExecutor(DEFAULT_EXECUTABLE_NAME, generateWindowsEnv(executablePath, env));
81-
}
82-
8339
public CommandResults runCmd(String args, boolean verbose) throws IOException {
8440
List<String> argsList = new ArrayList<>(Arrays.asList(args.split(" ")));
8541
return runCmd(argsList, verbose);

build-info-extractor-go/src/test/java/org/jfrog/build/extractor/go/GoDriverTest.java

-19
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22

33
import com.google.common.collect.Sets;
44
import org.apache.commons.io.FileUtils;
5-
import org.apache.commons.lang3.SystemUtils;
65
import org.jfrog.build.api.util.NullLog;
76
import org.jfrog.build.extractor.executor.CommandResults;
87
import org.testng.Assert;
9-
import org.testng.SkipException;
108
import org.testng.annotations.Test;
119

1210
import java.io.File;
@@ -15,7 +13,6 @@
1513
import java.nio.file.Path;
1614
import java.nio.file.Paths;
1715
import java.util.Arrays;
18-
import java.util.Map;
1916
import java.util.Set;
2017
import java.util.stream.Collectors;
2118

@@ -117,20 +114,4 @@ public void testGoGet() throws IOException {
117114
FileUtils.deleteDirectory(projectDir);
118115
}
119116
}
120-
121-
@Test
122-
public void testGoDriverWindowsInit() throws IOException {
123-
if (!SystemUtils.IS_OS_WINDOWS) {
124-
throw new SkipException("Skipping non-windows test");
125-
}
126-
File projectDir = Files.createTempDirectory("").toFile();
127-
try {
128-
Map<String, String> systemEnv = System.getenv();
129-
String executablePath = "C:\\Program Files\\Go\\bin\\go";
130-
Map<String, String> executorEnv = GoDriver.generateWindowsEnv(executablePath, systemEnv);
131-
assertTrue(executorEnv.get("Path").startsWith("C:\\Program Files\\Go\\bin"));
132-
} finally {
133-
FileUtils.deleteDirectory(projectDir);
134-
}
135-
}
136117
}

build-info-extractor/src/main/java/org/jfrog/build/extractor/executor/CommandExecutor.java

+74-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jfrog.build.extractor.executor;
22

3+
import com.google.common.collect.Maps;
34
import org.apache.commons.lang3.SystemUtils;
45
import org.jfrog.build.api.util.Log;
56
import org.jfrog.build.extractor.UrlUtils;
@@ -8,11 +9,12 @@
89
import java.io.IOException;
910
import java.io.InputStream;
1011
import java.io.Serializable;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
1114
import java.util.*;
1215
import java.util.concurrent.ExecutorService;
1316
import java.util.concurrent.Executors;
1417
import java.util.concurrent.TimeUnit;
15-
import java.util.stream.Collectors;
1618

1719
import static java.lang.String.format;
1820
import static java.lang.String.join;
@@ -26,15 +28,15 @@ public class CommandExecutor implements Serializable {
2628
private static final int READER_SHUTDOWN_TIMEOUT_SECONDS = 30;
2729
private static final int PROCESS_TERMINATION_TIMEOUT_SECONDS = 30;
2830
private static final int EXECUTION_TIMEOUT_MINUTES = 120;
29-
private Map<String, String> env;
31+
private final Map<String, String> env;
3032
private final String executablePath;
3133

3234
/**
3335
* @param executablePath - Executable path.
3436
* @param env - Environment variables to use during execution.
3537
*/
3638
public CommandExecutor(String executablePath, Map<String, String> env) {
37-
this.executablePath = escapeSpacesInPath(executablePath);
39+
this.executablePath = executablePath.trim();
3840
Map<String, String> finalEnvMap = new HashMap<>(System.getenv());
3941
if (env != null) {
4042
Map<String, String> fixedEnvMap = new HashMap<>(env);
@@ -133,10 +135,9 @@ public CommandResults exeCommand(File execDir, List<String> args, List<String> c
133135
*/
134136
public CommandResults exeCommand(File execDir, List<String> args, List<String> credentials, Log logger, long timeout, TimeUnit unit) throws InterruptedException, IOException {
135137
List<String> command = new ArrayList<>(args);
136-
command.add(0, executablePath);
137138
ExecutorService service = Executors.newFixedThreadPool(2);
138139
try {
139-
Process process = runProcess(execDir, command, credentials, env, logger);
140+
Process process = runProcess(execDir, executablePath, command, credentials, env, logger);
140141
// The output stream is not necessary in non-interactive scenarios, therefore we can close it now.
141142
process.getOutputStream().close();
142143
try (InputStream inputStream = process.getInputStream(); InputStream errorStream = process.getErrorStream()) {
@@ -179,38 +180,86 @@ private CommandResults getCommandResults(boolean terminatedProperly, List<String
179180
return commandRes;
180181
}
181182

182-
private static Process runProcess(File execDir, List<String> args, List<String> credentials, Map<String, String> env, Log logger) throws IOException {
183+
private static Process runProcess(File execDir, String executablePath, List<String> args, List<String> credentials, Map<String, String> env, Log logger) throws IOException {
184+
// Make sure to copy the environment variables map to avoid changing the original map or in case it is immutable.
185+
Map<String, String> newEnv = Maps.newHashMap(env);
186+
187+
args = formatCommand(args, credentials, executablePath, newEnv);
188+
logCommand(logger, args, credentials);
189+
ProcessBuilder processBuilder = new ProcessBuilder(args)
190+
.directory(execDir);
191+
processBuilder.environment().putAll(newEnv);
192+
return processBuilder.start();
193+
}
194+
195+
/**
196+
* Formats a command for execution, incorporating credentials and environment variables.
197+
*
198+
* @param args the list of arguments to be included in the command
199+
* @param credentials if specified, the credentials will be concatenated to the command
200+
* @param executablePath the path to the executable to be executed
201+
* @param env environment variables map. It might be modified as part of the formatting process
202+
* @return the formatted command as a list of strings, ready for execution
203+
*/
204+
private static List<String> formatCommand(List<String> args, List<String> credentials, String executablePath, Map<String, String> env) {
183205
if (credentials != null) {
184206
args.addAll(credentials);
185207
}
208+
186209
if (SystemUtils.IS_OS_WINDOWS) {
187-
args.addAll(0, Arrays.asList("cmd", "/c"));
210+
formatWindowsCommand(args, executablePath, env);
211+
return args;
212+
}
213+
return formatUnixCommand(args, executablePath);
214+
}
215+
216+
/**
217+
* Formats a Windows command for execution.
218+
*
219+
* @param args the list of arguments to be included in the command
220+
* @param executablePath the path to the executable to be executed
221+
* @param env environment variables map. It might be modified as part of the formatting process
222+
*/
223+
private static void formatWindowsCommand(List<String> args, String executablePath, Map<String, String> env) {
224+
Path execPath = Paths.get(executablePath);
225+
if (execPath.isAbsolute()) {
226+
addToWindowsPath(env, execPath);
227+
args.add(0, execPath.getFileName().toString());
188228
} else {
189-
String strArgs = join(" ", args);
190-
args = new ArrayList<String>() {{
191-
add("/bin/sh");
192-
add("-c");
193-
add(strArgs);
194-
}};
229+
args.add(0, executablePath.replaceAll(" ", "^ "));
195230
}
196-
logCommand(logger, args, credentials);
197-
ProcessBuilder processBuilder = new ProcessBuilder(args)
198-
.directory(execDir);
199-
processBuilder.environment().putAll(env);
200-
return processBuilder.start();
231+
args.addAll(0, Arrays.asList("cmd", "/c"));
232+
}
233+
234+
private static List<String> formatUnixCommand(List<String> args, String executablePath) {
235+
args.add(0, executablePath.replaceAll(" ", "\\\\ "));
236+
String strArgs = join(" ", args);
237+
return new ArrayList<String>() {{
238+
add("/bin/sh");
239+
add("-c");
240+
add(strArgs);
241+
}};
201242
}
202243

203244
/**
204-
* Escape spaces in the input executable path and trim leading and trailing whitespaces.
245+
* Inserts the executable directory path at the beginning of the Path environment variable.
246+
* This is done to handle cases where the executable path contains spaces. In such scenarios, the "cmd" command used
247+
* to execute this command in Windows may incorrectly parse the path, treating the section after the space as an
248+
* argument for the command.
205249
*
206-
* @param executablePath - the executable path to process
207-
* @return escaped and trimmed executable path.
250+
* @param env environment variables map
251+
* @param execPath the executable path
208252
*/
209-
private static String escapeSpacesInPath(String executablePath) {
210-
if (executablePath == null) {
211-
return null;
253+
static void addToWindowsPath(Map<String, String> env, Path execPath) {
254+
String execDirPath = execPath.getParent().toString();
255+
256+
// Insert the executable directory path to the beginning of the Path environment variable.
257+
String windowsPathEnvKey = "Path";
258+
if (env.containsKey(windowsPathEnvKey)) {
259+
env.put(windowsPathEnvKey, execDirPath + File.pathSeparator + env.get(windowsPathEnvKey));
260+
} else {
261+
env.put(windowsPathEnvKey, execDirPath);
212262
}
213-
return executablePath.trim().replaceAll(" ", SystemUtils.IS_OS_WINDOWS ? "^ " : "\\\\ ");
214263
}
215264

216265
private static void logCommand(Log logger, List<String> args, List<String> credentials) {

build-info-extractor/src/test/java/org/jfrog/build/extractor/executor/CommandExecutorTest.java

+21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.jfrog.build.extractor.executor;
22

3+
import com.google.common.collect.Maps;
34
import org.apache.commons.io.FileUtils;
5+
import org.apache.commons.lang3.SystemUtils;
46
import org.apache.commons.lang3.exception.ExceptionUtils;
57
import org.jfrog.build.api.util.NullLog;
8+
import org.testng.SkipException;
69
import org.testng.annotations.DataProvider;
710
import org.testng.annotations.Test;
811
import org.testng.collections.Lists;
@@ -12,8 +15,10 @@
1215
import java.nio.charset.StandardCharsets;
1316
import java.nio.file.Files;
1417
import java.nio.file.Path;
18+
import java.nio.file.Paths;
1519
import java.util.ArrayList;
1620
import java.util.List;
21+
import java.util.Map;
1722

1823
import static org.testng.Assert.*;
1924

@@ -85,4 +90,20 @@ public void testExeCommandWithSpaces() throws IOException, InterruptedException
8590
FileUtils.forceDelete(tmpDir.toFile());
8691
}
8792
}
93+
94+
@Test
95+
public void testGenerateWindowsEnv() throws IOException {
96+
if (!SystemUtils.IS_OS_WINDOWS) {
97+
throw new SkipException("Skipping test on non-Windows OS");
98+
}
99+
File projectDir = Files.createTempDirectory("").toFile();
100+
try {
101+
Map<String, String> env = Maps.newHashMap(System.getenv());
102+
Path execPath = Paths.get("C:\\Program Files\\Go\\bin\\go");
103+
CommandExecutor.addToWindowsPath(env, execPath);
104+
assertTrue(env.get("Path").startsWith("C:\\Program Files\\Go\\bin"));
105+
} finally {
106+
FileUtils.deleteDirectory(projectDir);
107+
}
108+
}
88109
}

0 commit comments

Comments
 (0)