1 /*
2 * The akquinet maven-latex-plugin project
3 *
4 * Copyright (c) 2011 by akquinet tech@spree GmbH
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 package eu.simuline.m2latex.core;
20
21 import java.io.File;
22
23 import org.codehaus.plexus.util.cli.CommandLineException;
24 import org.codehaus.plexus.util.cli.Commandline;// constructor
25 import static org.codehaus.plexus.util.cli.CommandLineUtils.executeCommandLine;
26 import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer;
27
28 /**
29 * Execution of an executable with given arguments
30 * in a given working directory logging on {@link #log}.
31 * Sole interface to <code>org.codehaus.plexus.util.cli</code>.
32 */
33 class CommandExecutor {
34
35 private final LogWrapper log;
36
37 CommandExecutor(LogWrapper log) {
38 this.log = log;
39 }
40
41
42 /**
43 * Executes <code>command</code> in <code>workingDir</code>
44 * with list of arguments given by <code>args</code>
45 * and logs if one of the expected target files
46 * given by <code>resFile</code> is not newly created,
47 * i.e. if it does not exist or is not updated.
48 * Logging:
49 * <ul>
50 * <li> EEX01: return code other than 0.
51 * <li> EEX02: no target file
52 * <li> EEX03: target file not updated
53 * <li> WEX04: cannot read target file
54 * <li> WEX05: may emit false warnings
55 * </ul>
56 *
57 * @param workingDir
58 * the working directory or <code>null</code>.
59 * The shell changes to that directory
60 * before invoking <code>command</code>
61 * with arguments <code>args</code> if this is not <code>null</code>.
62 * Argument <code>null</code> is allowed only
63 * if no result files are given by <code>resFile</code>.
64 * Essentially this is just needed to determine the version.
65 * @param pathToExecutable
66 * the path to the executable <code>command</code>.
67 * This may be <code>null</code> if <code>command</code>
68 * is on the execution path
69 * @param command
70 * the name of the program to be executed
71 * @param args
72 * the list of arguments,
73 * each containing a blank enclosed in double quotes.
74 * @param resFile
75 * optional result files, i.e. target files which shall be updated
76 * by this command.
77 * @return
78 * The output of execution on stdio/errio (TBC).
79 * This is used in tests only.
80 * @throws BuildFailureException
81 * TEX01 if invocation of <code>command</code> fails very basically:
82 * <ul>
83 * <li><!-- see Commandline.execute() -->
84 * the file expected to be the working directory
85 * does not exist or is not a directory.
86 * <li><!-- see Commandline.execute() -->
87 * {@link Runtime#exec(String, String[], File)} fails
88 * throwing an {@link java.io.IOException}.
89 * <li> <!-- see CommandLineCallable.call() -->
90 * an error inside systemOut parser occurs
91 * <li> <!-- see CommandLineCallable.call() -->
92 * an error inside systemErr parser occurs
93 * <li> Wrapping an {@link InterruptedException}
94 * on the process to be executed thrown by {@link Process#waitFor()}.
95 * </ul>
96 */
97 String execute(File workingDir,
98 File pathToExecutable,
99 String command,
100 String[] args,
101 File... resFile) throws BuildFailureException {
102 // analyze old result files
103 assert resFile.length > 0;
104 boolean[] existsTarget = new boolean[resFile.length];
105 long[] lastModifiedTarget = new long[resFile.length];
106 long currentTime = System.currentTimeMillis();
107 long minTimePast = Long.MAX_VALUE;
108 for (int idx = 0; idx < resFile.length; idx++) {
109 existsTarget [idx] = resFile[idx].exists();
110 lastModifiedTarget[idx] = resFile[idx].lastModified();
111 assert lastModifiedTarget[idx] <= currentTime;
112 // correct even if lastModifiedTarget[idx]==0
113 minTimePast = Math.min(minTimePast,
114 currentTime-lastModifiedTarget[idx]);
115 }
116
117 // FIXME: this is based on a file system
118 // with modification time in steps of seconds, i.e. 1000ms
119 if (minTimePast < 1001) {
120 try {
121 // 1001 is the minimal span of time to change modification time
122 Thread.sleep(1001-minTimePast);// for update control of target
123 } catch (InterruptedException ie) {
124 this.log.warn
125 ("WEX05: Update control may emit false warnings. ");
126 }
127 }
128
129 if (workingDir == null && resFile.length != 0) {
130 throw new IllegalStateException
131 ("Working directory shall be determined but was null. ");
132 }
133
134 // Proper execution
135 // may throw BuildFailureException TEX01, log warning EEX01
136 String res = execute(workingDir, pathToExecutable, command, args);
137
138 // may log EEX02, EEX03, WEX04
139 for (int idx = 0; idx < resFile.length; idx++) {
140 isUpdatedOrWarn(command, resFile[idx],
141 existsTarget[idx], lastModifiedTarget[idx]);
142 }
143
144 return res;
145 }
146
147 // returns whether this method logged a warning
148 // FIXME: return value nowhere used
149 /**
150 * @param command
151 * the name of the program to be executed
152 *
153 * Logging:
154 * <ul>
155 * <li> EEX02: no target file
156 * <li> EEX03: target file not updated
157 * <li> WEX04: cannot read target file
158 * </ul>
159 */
160 private boolean isUpdatedOrWarn(String command,
161 File target,
162 boolean existedBefore,
163 long lastModifiedBefore) {
164 if (!target.exists()) {
165 this.log.error("EEX02: Running " + command +
166 " failed: No target file '" +
167 target.getName() + "' written. ");
168 return false;
169 }
170 assert target.exists();
171 if (!existedBefore) {
172 return true;
173 }
174 assert existedBefore && target.exists();
175
176 long lastModifiedAfter = target.lastModified();
177 if (lastModifiedBefore == 0 || lastModifiedAfter == 0) {
178 this.log.warn("WEX04: Cannot read target file '" +
179 target.getName() + "'; may be outdated. ");
180 return false;
181 }
182 assert lastModifiedBefore > 0 && lastModifiedAfter > 0;
183
184 if (lastModifiedAfter <= lastModifiedBefore) {
185 assert lastModifiedAfter == lastModifiedBefore;
186 this.log.error("EEX03: Running " + command +
187 " failed: Target file '" +
188 target.getName() + "' is not updated. ");
189 return false;
190 }
191 return true;
192 }
193
194 /**
195 * Execute <code>command</code> with arguments <code>args</code>
196 * in the working directory <code>workingDir</code>
197 * and return the output.
198 * Here, <code>pathToExecutable</code> is the path
199 * to the executable. It may be null.
200 * <p>
201 * Logging:
202 * EEX01 for return code other than 0.
203 *
204 * @param workingDir
205 * the working directory or <code>null</code>.
206 * The shell changes to that directory
207 * before invoking <code>command</code>
208 * with arguments <code>args</code> if this is not <code>null</code>.
209 * @param pathToExecutable
210 * the path to the executable <code>command</code>.
211 * This may be <code>null</code> if <code>command</code>
212 * is on the execution path
213 * @param command
214 * the name of the program to be executed
215 * @param args
216 * the list of arguments,
217 * each containing a blank enclosed in double quotes.
218 * @return
219 * the output of the command.
220 * @throws BuildFailureException
221 * TEX01 if invocation of <code>command</code> fails very basically:
222 * <ul>
223 * <li><!-- see Commandline.execute() -->
224 * the file expected to be the working directory
225 * does not exist or is not a directory.
226 * <li><!-- see Commandline.execute() -->
227 * {@link Runtime#exec(String, String[], File)} fails
228 * throwing an {@link java.io.IOException}.
229 * <li> <!-- see CommandLineCallable.call() -->
230 * an error inside systemOut parser occurs
231 * <li> <!-- see CommandLineCallable.call() -->
232 * an error inside systemErr parser occurs
233 * <li> Wrapping an {@link InterruptedException}
234 * on the process to be executed thrown by {@link Process#waitFor()}.
235 * </ul>
236 */
237 private String execute(File workingDir,
238 File pathToExecutable,
239 String command,
240 String[] args) throws BuildFailureException {
241
242 // prepare execution
243 String executable = new File(pathToExecutable, command).getPath();
244 Commandline cl = new Commandline(executable);
245 cl.getShell().setQuotedArgumentsEnabled(true);
246 cl.addArguments(args);
247 if (workingDir != null) {
248 cl.setWorkingDirectory(workingDir.getPath());
249 }
250 StringStreamConsumer output = new StringStreamConsumer();
251 log.debug("Executing: " + cl + " in: " + workingDir + ". ");
252
253 // perform execution and collect results
254 try {
255 // may throw CommandLineException
256 int returnCode = executeCommandLine(cl, output, output);
257 if (returnCode != 0) {
258 this.log.error("EEX01: Running " + command +
259 " failed with return code " + returnCode + ". ");
260 }
261 } catch (CommandLineException e) {
262 throw new BuildFailureException
263 ("TEX01: Error running " + command + ". ", e);
264 }
265
266 log.debug("Output:\n" + output.getOutput() + "\n");
267 return output.getOutput();
268 }
269 }