|
1 | 1 | package org.jfrog.build.extractor.executor;
|
2 | 2 |
|
3 |
| -import org.apache.commons.io.IOUtils; |
| 3 | +import org.apache.commons.lang.SystemUtils; |
4 | 4 | import org.jfrog.build.api.util.Log;
|
5 | 5 | import org.jfrog.build.extractor.clientConfiguration.util.UrlUtils;
|
6 | 6 |
|
7 | 7 | import java.io.File;
|
8 | 8 | import java.io.IOException;
|
| 9 | +import java.io.InputStream; |
9 | 10 | import java.io.Serializable;
|
10 |
| -import java.util.HashMap; |
11 |
| -import java.util.List; |
12 |
| -import java.util.Map; |
| 11 | +import java.util.*; |
13 | 12 | import java.util.concurrent.ExecutorService;
|
14 | 13 | import java.util.concurrent.Executors;
|
15 | 14 | import java.util.concurrent.TimeUnit;
|
16 | 15 |
|
| 16 | +import static java.lang.String.format; |
| 17 | +import static java.lang.String.join; |
| 18 | + |
17 | 19 | /**
|
18 | 20 | * @author Yahav Itzhak
|
19 | 21 | */
|
20 | 22 | public class CommandExecutor implements Serializable {
|
21 | 23 | private static final long serialVersionUID = 1L;
|
| 24 | + private static final int TIMEOUT_EXIT_VALUE = 124; |
| 25 | + private static final int TIMEOUT_SECONDS = 10; |
22 | 26 |
|
23 |
| - private String[] env; |
24 |
| - private String executablePath; |
| 27 | + private final String[] env; |
| 28 | + private final String executablePath; |
25 | 29 |
|
26 | 30 | /**
|
27 | 31 | * @param executablePath - Executable path.
|
@@ -49,7 +53,7 @@ private void fixPathEnv(Map<String, String> env) {
|
49 | 53 | if (path == null) {
|
50 | 54 | return;
|
51 | 55 | }
|
52 |
| - if (isWindows()) { |
| 56 | + if (SystemUtils.IS_OS_WINDOWS) { |
53 | 57 | path = getFixedWindowsPath(path);
|
54 | 58 | } else {
|
55 | 59 | path = path.replaceAll(";", File.pathSeparator) + ":/usr/local/bin";
|
@@ -82,67 +86,100 @@ static String getFixedWindowsPath(String path) {
|
82 | 86 | String newPart = startPart + endPart.replaceAll(":", ";");
|
83 | 87 | newPathParts[index] = newPart;
|
84 | 88 | }
|
85 |
| - return String.join(";", newPathParts); |
| 89 | + return join(";", newPathParts); |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Replace credentials in the string command by '***'. |
| 94 | + * |
| 95 | + * @param command - The command |
| 96 | + * @param credentials - The credentials list |
| 97 | + * @return masked command. |
| 98 | + */ |
| 99 | + static String maskCredentials(String command, List<String> credentials) { |
| 100 | + if (credentials == null || credentials.isEmpty()) { |
| 101 | + return command; |
| 102 | + } |
| 103 | + // The mask pattern is a regex, which is used to mask all credentials |
| 104 | + String maskPattern = join("|", credentials); |
| 105 | + return command.replaceAll(maskPattern, "***"); |
86 | 106 | }
|
87 | 107 |
|
88 | 108 | /**
|
89 | 109 | * Execute a command in external process.
|
90 | 110 | *
|
91 |
| - * @param execDir - The execution dir (Usually path to project). Null means current directory. |
92 |
| - * @param args - Command arguments. |
93 |
| - * @return CommandResults |
| 111 | + * @param execDir - The execution dir (Usually path to project). Null means current directory. |
| 112 | + * @param args - Command arguments. |
| 113 | + * @param credentials - If specified, the credentials will be concatenated to the other commands. |
| 114 | + * The credentials will be makes in the log output. |
| 115 | + * @param logger - The logger which will log the running command. |
| 116 | + * @return CommandResults object |
94 | 117 | */
|
95 |
| - public CommandResults exeCommand(File execDir, List<String> args, Log logger) throws InterruptedException, IOException { |
| 118 | + public CommandResults exeCommand(File execDir, List<String> args, List<String> credentials, Log logger) throws InterruptedException, IOException { |
96 | 119 | args.add(0, executablePath);
|
97 |
| - Process process = null; |
98 | 120 | ExecutorService service = Executors.newFixedThreadPool(2);
|
99 | 121 | try {
|
100 |
| - CommandResults commandRes = new CommandResults(); |
101 |
| - process = runProcess(execDir, args, env, logger); |
102 |
| - StreamReader inputStreamReader = new StreamReader(process.getInputStream()); |
103 |
| - StreamReader errorStreamReader = new StreamReader(process.getErrorStream()); |
104 |
| - service.submit(inputStreamReader); |
105 |
| - service.submit(errorStreamReader); |
106 |
| - process.waitFor(); |
107 |
| - service.shutdown(); |
108 |
| - service.awaitTermination(10, TimeUnit.SECONDS); |
109 |
| - commandRes.setRes(inputStreamReader.getOutput()); |
110 |
| - commandRes.setErr(errorStreamReader.getOutput()); |
111 |
| - commandRes.setExitValue(process.exitValue()); |
112 |
| - return commandRes; |
| 122 | + Process process = runProcess(execDir, args, credentials, env, logger); |
| 123 | + // The output stream is not necessary in non-interactive scenarios, therefore we can close it now. |
| 124 | + process.getOutputStream().close(); |
| 125 | + try (InputStream inputStream = process.getInputStream(); |
| 126 | + InputStream errorStream = process.getErrorStream()) { |
| 127 | + StreamReader inputStreamReader = new StreamReader(inputStream); |
| 128 | + StreamReader errorStreamReader = new StreamReader(errorStream); |
| 129 | + service.submit(inputStreamReader); |
| 130 | + service.submit(errorStreamReader); |
| 131 | + process.waitFor(); |
| 132 | + service.shutdown(); |
| 133 | + boolean terminatedProperly = service.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 134 | + return getCommandResults(terminatedProperly, args, inputStreamReader.getOutput(), errorStreamReader.getOutput(), process.exitValue()); |
| 135 | + } |
113 | 136 | } finally {
|
114 |
| - closeStreams(process); |
115 | 137 | service.shutdownNow();
|
116 | 138 | }
|
117 | 139 | }
|
118 | 140 |
|
119 |
| - private static void closeStreams(Process process) { |
120 |
| - if (process != null) { |
121 |
| - IOUtils.closeQuietly(process.getInputStream()); |
122 |
| - IOUtils.closeQuietly(process.getOutputStream()); |
123 |
| - IOUtils.closeQuietly(process.getErrorStream()); |
| 141 | + private CommandResults getCommandResults(boolean terminatedProperly, List<String> args, String output, String error, int exitValue) { |
| 142 | + CommandResults commandRes = new CommandResults(); |
| 143 | + if (!terminatedProperly) { |
| 144 | + error += System.lineSeparator() + format("Process '%s' had been terminated forcibly after timeout of %d seconds.", |
| 145 | + join(" ", args), TIMEOUT_SECONDS); |
| 146 | + exitValue = TIMEOUT_EXIT_VALUE; |
124 | 147 | }
|
| 148 | + commandRes.setRes(output); |
| 149 | + commandRes.setErr(error); |
| 150 | + commandRes.setExitValue(exitValue); |
| 151 | + return commandRes; |
125 | 152 | }
|
126 | 153 |
|
127 |
| - private static boolean isWindows() { |
128 |
| - return System.getProperty("os.name").toLowerCase().contains("win"); |
129 |
| - } |
130 |
| - |
131 |
| - private static boolean isMac() { |
132 |
| - return System.getProperty("os.name").toLowerCase().contains("mac"); |
133 |
| - } |
134 |
| - |
135 |
| - private static Process runProcess(File execDir, List<String> args, String[] env, Log logger) throws IOException { |
136 |
| - if (isWindows()) { |
137 |
| - args.add(0, "cmd"); |
138 |
| - args.add(1, "/c"); |
139 |
| - } else if (isMac()) { |
140 |
| - args.add(0, "/bin/sh"); |
141 |
| - args.add(1, "-c"); |
| 154 | + private static Process runProcess(File execDir, List<String> args, List<String> credentials, String[] env, Log logger) throws IOException { |
| 155 | + if (credentials != null) { |
| 156 | + args.addAll(credentials); |
142 | 157 | }
|
143 |
| - if (logger != null) { |
144 |
| - logger.info("Executing command: " + UrlUtils.maskCredentialsInUrl(String.join(" ", args))); |
| 158 | + if (SystemUtils.IS_OS_WINDOWS) { |
| 159 | + args.addAll(0, Arrays.asList("cmd", "/c")); |
| 160 | + } else if (SystemUtils.IS_OS_MAC) { |
| 161 | + // In MacOS, the arguments for '/bin/sh -c' must be a single string. For example "/bin/sh","-c", "npm i". |
| 162 | + String strArgs = join(" ", args); |
| 163 | + args = new ArrayList<String>() {{ |
| 164 | + add("/bin/sh"); |
| 165 | + add("-c"); |
| 166 | + add(strArgs); |
| 167 | + }}; |
145 | 168 | }
|
| 169 | + logCommand(logger, args, credentials); |
146 | 170 | return Runtime.getRuntime().exec(args.toArray(new String[0]), env, execDir);
|
147 | 171 | }
|
| 172 | + |
| 173 | + private static void logCommand(Log logger, List<String> args, List<String> credentials) { |
| 174 | + if (logger == null) { |
| 175 | + return; |
| 176 | + } |
| 177 | + // Mask credentials in URL |
| 178 | + String output = UrlUtils.maskCredentialsInUrl(join(" ", args)); |
| 179 | + |
| 180 | + // Mask credentials arguments |
| 181 | + output = maskCredentials(output, credentials); |
| 182 | + |
| 183 | + logger.info("Executing command: " + output); |
| 184 | + } |
148 | 185 | }
|
0 commit comments