View Javadoc
1   /*
2    * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.diffmergetool;
12  
13  import java.io.File;
14  import java.io.FileOutputStream;
15  import java.io.IOException;
16  import java.io.OutputStream;
17  import java.nio.file.Files;
18  import java.nio.file.Path;
19  import java.nio.file.Paths;
20  import java.util.Arrays;
21  import java.util.Map;
22  
23  import org.eclipse.jgit.errors.NoWorkTreeException;
24  import org.eclipse.jgit.util.FS;
25  import org.eclipse.jgit.util.FS.ExecutionResult;
26  import org.eclipse.jgit.util.FS_POSIX;
27  import org.eclipse.jgit.util.FS_Win32;
28  import org.eclipse.jgit.util.FS_Win32_Cygwin;
29  import org.eclipse.jgit.util.StringUtils;
30  import org.eclipse.jgit.util.SystemReader;
31  
32  /**
33   * Runs a command with help of FS.
34   */
35  public class CommandExecutor {
36  
37  	private FS fs;
38  
39  	private boolean checkExitCode;
40  
41  	private File commandFile;
42  
43  	private boolean useMsys2;
44  
45  	/**
46  	 * @param fs
47  	 *            the file system
48  	 * @param checkExitCode
49  	 *            should the exit code be checked for errors ?
50  	 */
51  	public CommandExecutor(FS fs, boolean checkExitCode) {
52  		this.fs = fs;
53  		this.checkExitCode = checkExitCode;
54  	}
55  
56  	/**
57  	 * @param command
58  	 *            the command string
59  	 * @param workingDir
60  	 *            the working directory
61  	 * @param env
62  	 *            the environment
63  	 * @return the execution result
64  	 * @throws ToolException
65  	 * @throws InterruptedException
66  	 * @throws IOException
67  	 */
68  	public ExecutionResult run(String command, File workingDir,
69  			Map<String, String> env)
70  			throws ToolException, IOException, InterruptedException {
71  		String[] commandArray = createCommandArray(command);
72  		try {
73  			ProcessBuilder pb = fs.runInShell(commandArray[0],
74  					Arrays.copyOfRange(commandArray, 1, commandArray.length));
75  			pb.directory(workingDir);
76  			Map<String, String> envp = pb.environment();
77  			if (env != null) {
78  				envp.putAll(env);
79  			}
80  			ExecutionResult result = fs.execute(pb, null);
81  			int rc = result.getRc();
82  			if (rc != 0) {
83  				boolean execError = isCommandExecutionError(rc);
84  				if (checkExitCode || execError) {
85  					throw new ToolException(
86  							"JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$
87  									+ "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$
88  									+ "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$
89  									+ "stderr: \n" //$NON-NLS-1$
90  									+ new String(
91  											result.getStderr().toByteArray(),
92  											SystemReader.getInstance()
93  													.getDefaultCharset()),
94  							result, execError);
95  				}
96  			}
97  			return result;
98  		} finally {
99  			deleteCommandArray();
100 		}
101 	}
102 
103 	/**
104 	 * @param path
105 	 *            the executable path
106 	 * @param workingDir
107 	 *            the working directory
108 	 * @param env
109 	 *            the environment
110 	 * @return the execution result
111 	 * @throws ToolException
112 	 * @throws InterruptedException
113 	 * @throws IOException
114 	 */
115 	public boolean checkExecutable(String path, File workingDir,
116 			Map<String, String> env)
117 			throws ToolException, IOException, InterruptedException {
118 		checkUseMsys2(path);
119 		String command = null;
120 		if (fs instanceof FS_Win32 && !useMsys2) {
121 			Path p = Paths.get(path);
122 			// Win32 (and not cygwin or MSYS2) where accepts only command / exe
123 			// name as parameter
124 			// so check if exists and executable in this case
125 			if (p.isAbsolute() && Files.isExecutable(p)) {
126 				return true;
127 			}
128 			// try where command for all other cases
129 			command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
130 		} else {
131 			command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
132 		}
133 		boolean available = true;
134 		try {
135 			ExecutionResult rc = run(command, workingDir, env);
136 			if (rc.getRc() != 0) {
137 				available = false;
138 			}
139 		} catch (IOException | InterruptedException | NoWorkTreeException
140 				| ToolException e) {
141 			// no op: is true to not hide possible tools from user
142 		}
143 		return available;
144 	}
145 
146 	private void deleteCommandArray() {
147 		deleteCommandFile();
148 	}
149 
150 	private String[] createCommandArray(String command)
151 			throws ToolException, IOException {
152 		String[] commandArray = null;
153 		checkUseMsys2(command);
154 		createCommandFile(command);
155 		if (fs instanceof FS_POSIX) {
156 			commandArray = new String[1];
157 			commandArray[0] = commandFile.getCanonicalPath();
158 		} else if (fs instanceof FS_Win32) {
159 			if (useMsys2) {
160 				commandArray = new String[3];
161 				commandArray[0] = "bash.exe"; //$NON-NLS-1$
162 				commandArray[1] = "-c"; //$NON-NLS-1$
163 				commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$
164 						"/"); //$NON-NLS-1$
165 			} else {
166 				commandArray = new String[1];
167 				commandArray[0] = commandFile.getCanonicalPath();
168 			}
169 		} else if (fs instanceof FS_Win32_Cygwin) {
170 			commandArray = new String[1];
171 			commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
172 		} else {
173 			throw new ToolException(
174 					"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
175 		}
176 		return commandArray;
177 	}
178 
179 	private void checkUseMsys2(String command) {
180 		useMsys2 = false;
181 		String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
182 		if (!StringUtils.isEmptyOrNull(useMsys2Str)) {
183 			if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
184 				useMsys2 = command.contains(".sh"); //$NON-NLS-1$
185 			} else {
186 				useMsys2 = Boolean.parseBoolean(useMsys2Str);
187 			}
188 		}
189 	}
190 
191 	private void createCommandFile(String command)
192 			throws ToolException, IOException {
193 		String fileExtension = null;
194 		if (useMsys2 || fs instanceof FS_POSIX
195 				|| fs instanceof FS_Win32_Cygwin) {
196 			fileExtension = ".sh"; //$NON-NLS-1$
197 		} else if (fs instanceof FS_Win32) {
198 			fileExtension = ".cmd"; //$NON-NLS-1$
199 			command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$
200 					+ System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$
201 		} else {
202 			throw new ToolException(
203 					"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
204 		}
205 		commandFile = File.createTempFile(".__", //$NON-NLS-1$
206 				"__jgit_tool" + fileExtension); //$NON-NLS-1$
207 		try (OutputStream outStream = new FileOutputStream(commandFile)) {
208 			byte[] strToBytes = command
209 					.getBytes(SystemReader.getInstance().getDefaultCharset());
210 			outStream.write(strToBytes);
211 			outStream.close();
212 		}
213 		commandFile.setExecutable(true);
214 	}
215 
216 	private void deleteCommandFile() {
217 		if (commandFile != null && commandFile.exists()) {
218 			commandFile.delete();
219 		}
220 	}
221 
222 	private boolean isCommandExecutionError(int rc) {
223 		if (useMsys2 || fs instanceof FS_POSIX
224 				|| fs instanceof FS_Win32_Cygwin) {
225 			// 126: permission for executing command denied
226 			// 127: command not found
227 			if ((rc == 126) || (rc == 127)) {
228 				return true;
229 			}
230 		}
231 		else if (fs instanceof FS_Win32) {
232 			// 9009, 0x2331: Program is not recognized as an internal or
233 			// external command, operable program or batch file. Indicates that
234 			// command, application name or path has been misspelled when
235 			// configuring the Action.
236 			if (rc == 9009) {
237 				return true;
238 			}
239 		}
240 		return false;
241 	}
242 
243 }