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 import java.io.IOException;
23
24 import java.nio.file.Files;
25 import java.nio.file.LinkOption;
26 import java.nio.file.attribute.FileTime;
27
28 import java.util.Map;
29 import java.util.TreeMap;
30 import java.util.concurrent.TimeUnit;
31
32 import org.codehaus.plexus.util.cli.CommandLineException;
33 import org.codehaus.plexus.util.cli.Commandline;// constructor
34 import static org.codehaus.plexus.util.cli.CommandLineUtils.executeCommandLine;
35 import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer;
36
37 /**
38 * Execution of an executable with given arguments
39 * in a given working directory logging on {@link #log}.
40 * Sole interface to <code>org.codehaus.plexus.util.cli</code>.
41 */
42 class CommandExecutor {
43
44 /**
45 * Represents the result of the invocation of a command
46 * consisting of the {@link #output} and its {@link #returnCode}.
47 * In addition the {@link CommandExecutor.ReturnCodeChecker}
48 * given by {@link #checker} plays a role to determine
49 * whether the return code signifies success,
50 * which is returned by {@link #getSuccess()}.
51 */
52 static class CmdResult {
53 final String output;
54 final private ReturnCodeChecker checker;
55 final int returnCode;
56
57 CmdResult(String output, ReturnCodeChecker checker, int returnCode) {
58 this.output = output;
59 this.checker = checker;
60 this.returnCode = returnCode;
61 }
62
63 boolean getSuccess() {
64 return !this.checker.hasFailed(this.returnCode);
65 }
66
67 public String toString() {
68 StringBuilder res = new StringBuilder();
69 res.append("<CmdResult>");
70 res.append("\nreturnCode" + this.returnCode);
71 res.append("\nsuccess" + this.getSuccess());
72 res.append("\n</CmdResult>");
73 return res.toString();
74 }
75 } // class CmdResult
76
77 /**
78 * The way return codes are checked: Not at all, if nonzero and special treatments.
79 * This is used in
80 * {@link CommandExecutor#executeEnvR0(File, File, String, String[], File...)}
81 * to decide whether the return code shall indicate that execution failed.
82 * TBD: shall be part of category
83 */
84 enum ReturnCodeChecker {
85 /**
86 * Never detect fail of execution.
87 * At time of this writing, this is not used.
88 */
89 Never {
90 boolean hasFailed(int returnCode) {
91 return false;
92 }
93 },
94 /**
95 * Detect fail of execution if return code is nonzero.
96 * This is the usual case.
97 * Deviation is only for check tools which must decide problems of the check tool itself
98 * from failed checks.
99 */
100 IsNonZero {
101 boolean hasFailed(int returnCode) {
102 return returnCode != 0;
103 }
104 },
105 /**
106 * Detect fail of execution only if return code is 1.
107 * <p>
108 * Currently used for chk only.
109 * Its return values are really strange:
110 * <ul>
111 * <li>1 if an error in execution occurs,
112 * e.g. option -neee although -n requires a number. </li>
113 * <li>3 if an error was found except if case 1 occurs.
114 * Note that all findings are warnings
115 * if not configured as errors with -exx, xx a number. </li>
116 * <li>2 if a warning was found, except if one of the above cases occur.
117 * one can deactivate always. </li>
118 * <li>0 if neither of the above occurred.
119 * Note that still warnings could be given but deactivated,
120 * e.g. excluded linewise. </li>
121 * </ul>
122 *
123 * @see LatexProcessor#runChktex(LatexMainDesc)
124 * @see Settings#getChkTexCommand()
125 */
126 IsOne {
127 boolean hasFailed(int returnCode) {
128 return returnCode == 1;
129 }
130 },
131 /**
132 * Detect fail of execution if return code is neither 0 nor 1.
133 * <p>
134 * Currently used for diff and for verapdf only.
135 * It is applicable to the diff tool:
136 * <ul>
137 * <li>0 checked that the files coincide,
138 * <li>1 checked that the files differ,
139 * <li>2 could not check files.
140 * </ul>
141 * Unfortunately diff-pdf-visually has encoding 0 same, 2 difference, 1 trouble.
142 * Thus it is not directly usable, only via a wrapper exchanging 1 and 3.
143 * <p>
144 * It is also usable for verapdf which is 0 for tests passed, 1 for tests failed
145 * and the many other codes for reasons for not carrying out the tests, so no
146 * decision.
147 *
148 * @see LatexProcessor#runDiffPdf(File, File)
149 * @see Settings#getDiffPdfCommand()
150 * @see LatexProcessor#runValidatePdf(LatexMainDesc)
151 * @see Settings#getVerifyStdCommand()
152 */
153 IsNotZeroOrOne {
154 boolean hasFailed(int returnCode) {
155 return !(returnCode == 0 || returnCode == 1);
156 }
157 };
158
159 /**
160 * Given a <code>returnCode</code> decides whether the tool failed.
161 */
162 abstract boolean hasFailed(int returnCode);
163 } // enum ReturnCodeChecker
164
165
166 /**
167 * Represents an environment used to reproduce a given PDF file
168 * consisting of
169 * <ul>
170 * <li><code>SOURCE_DATE_EPOCH</code>,
171 * which is set to the timestamp of the PDF file to be reproduced
172 * before the environment is used.
173 * That way, the resulting PDF file obtains the same timestamp.
174 * <li><code>FORCE_SOURCE_DATE</code> which is set to <code>1</code>
175 * forcing certain compilers to use <code>SOURCE_DATE_EPOCH</code>
176 * also for visual data, not only for metadata,
177 * <li><code>TZ</code>, the current timezone set to <code>UTC</code>,
178 * which is the timezone of the PDF file to be reproduced.
179 * </ul>
180 *
181 * This environment is relevant for compilation into PDF,
182 * whether via DVI/XDV or directly.
183 * In the first case, it is applied in both steps.
184 *
185 * @see #ENV_TIMEZONE
186 * @see #DATE_EPOCH
187 */
188 // before using this, key DATE_EPOCH with according value is put
189 private static final Map<String, String> ENV_TIMESTAMP_FORCE_TZ;
190
191 // this one is immutable
192 /**
193 * Represents an environment used to create a PDF file
194 * which is later to be reproduced
195 * but does nto yet reproduce another PDF file.
196 * Thus it defines the timezone <code>UTC</code>
197 * without specifying a timestamp or how to use it.
198 *
199 * @see #ENV_TIMESTAMP_FORCE_TZ
200 */
201 private static final Map<String, String> ENV_TIMEZONE;
202
203 // this one is immutable
204 /**
205 * Represents the empty environment.
206 */
207 private static final Map<String, String> ENV_EMPTY;
208
209 /**
210 * The key for {@link #ENV_TIMESTAMP_FORCE_TZ}
211 * to set <code>SOURCE_DATE_EPOCH</code>
212 */
213 private static final String DATE_EPOCH = "SOURCE_DATE_EPOCH";
214
215 static {
216 ENV_TIMESTAMP_FORCE_TZ = new TreeMap<String, String>();
217 ENV_TIMESTAMP_FORCE_TZ.put("FORCE_SOURCE_DATE","1");
218 ENV_TIMESTAMP_FORCE_TZ.put("TZ","utc");
219
220 ENV_TIMEZONE = new TreeMap<String, String>();
221 ENV_TIMEZONE.put("TZ","utc");
222
223 ENV_EMPTY = new TreeMap<String, String>();
224 }
225
226 /*
227 * The environment for the next command execution
228 * {@link # execute(File, File, String, String[], File...)}
229 */
230 private Map<String, String> env;
231
232 private final LogWrapper log;
233
234
235 /**
236 * Creates an executor with the given logger
237 * and empty environment {@link #ENV_EMPTY}.
238 *
239 * @param log
240 * the current logger.
241 */
242 CommandExecutor(LogWrapper log) {
243 envReset();
244 this.log = log;
245 }
246
247
248
249 void envReset() {
250 this.env = ENV_EMPTY;
251 }
252
253 void envUtc() {
254 this.env = ENV_TIMEZONE;
255 }
256
257 void envSetTimestampAndTZutc(long timestampSec) {
258 ENV_TIMESTAMP_FORCE_TZ.put(DATE_EPOCH, Long.toString(timestampSec));
259 this.env = ENV_TIMESTAMP_FORCE_TZ;
260 }
261
262 /**
263 * Executes <code>command</code> in <code>workingDir</code>
264 * in the environment given by {@link #env}
265 * with list of arguments given by <code>args</code>
266 * and logs if one of the expected target files
267 * given by <code>resFile</code> is not newly created,
268 * i.e. if it does not exist or is not updated.
269 * This is a convenience method of
270 * {@link #execute(File, File, Map<String,String>, String, ReturnCodeChecker, String[], File... )},
271 * where the boolean signifies whether the return code is checked.
272 * This is set to <code>true</code> in this method.
273 * <p>
274 * Logging:
275 * <ul>
276 * <li> EEX01: return code other than 0.
277 * <li> EEX02: no target file
278 * <li> EEX03: target file not updated
279 * <li> WEX04: cannot read target file
280 * <li> WEX05: may emit false warnings
281 * </ul>
282 *
283 * @param workingDir
284 * the working directory or <code>null</code>.
285 * The shell changes to that directory
286 * before invoking <code>command</code>
287 * with arguments <code>args</code> if this is not <code>null</code>.
288 * Argument <code>null</code> is allowed only
289 * if no result files are given by <code>resFile</code>.
290 * Essentially this is just needed to determine the version.
291 * @param pathToExecutable
292 * the path to the executable <code>command</code>.
293 * This may be <code>null</code> if <code>command</code>
294 * is on the execution path
295 * @param command
296 * the name of the program to be executed
297 * @param args
298 * the list of arguments,
299 * each containing a blank enclosed in double quotes.
300 * @param resFiles
301 * optional result files, i.e. target files which shall be updated
302 * by this command.
303 * @return
304 * the output of the command which comprises the output stream
305 * and whether the return code is nonzero, i.e. the command succeeded.
306 * The io stream is used in tests only whereas the return code is used for pdfdiffs.
307 * @throws BuildFailureException
308 * TEX01 if invocation of <code>command</code> fails very basically:
309 * <ul>
310 * <li><!-- see Commandline.execute() -->
311 * the file expected to be the working directory
312 * does not exist or is not a directory.
313 * <li><!-- see Commandline.execute() -->
314 * {@link Runtime#exec(String, String[], File)} fails
315 * throwing an {@link java.io.IOException}.
316 * <li> <!-- see CommandLineCallable.call() -->
317 * an error inside systemOut parser occurs
318 * <li> <!-- see CommandLineCallable.call() -->
319 * an error inside systemErr parser occurs
320 * <li> Wrapping an {@link InterruptedException}
321 * on the process to be executed thrown by {@link Process#waitFor()}.
322 * </ul>
323 */
324 CmdResult executeEnvR0(File workingDir,
325 File pathToExecutable,
326 String command,
327 String[] args,
328 File... resFiles) throws BuildFailureException {
329 return execute(workingDir, pathToExecutable, this.env,
330 command, ReturnCodeChecker.IsNonZero, args, resFiles);
331 }
332
333 /**
334 * Executes <code>command</code> in <code>workingDir</code>
335 * with list of arguments given by <code>args</code>
336 * and logs if after execution the result file <code>resFile</code> does not exist.
337 * CAUTION: In contrast to
338 * {@link #executeEnvR0(File,File,String,String[],File...)},
339 * It is not checked that the result files are updated
340 * and it is just one result file neglecting log files and that like.
341 * This method is suited to build tools updateing only by need
342 * and currently it is only used for <code>latexmk</code> like tools.
343 * <p>
344 * Logging:
345 * <ul>
346 * <li> EEX01: return code other than 0.
347 * <li> EEX02: no target file
348 * </ul>
349 *
350 * @param workingDir
351 * the working directory or <code>null</code>.
352 * The shell changes to that directory
353 * before invoking <code>command</code>
354 * with arguments <code>args</code> if this is not <code>null</code>.
355 * Argument <code>null</code> is allowed only
356 * if no result files are given by <code>resFile</code>.
357 * Essentially this is just needed to determine the version.
358 * @param pathToExecutable
359 * the path to the executable <code>command</code>.
360 * This may be <code>null</code> if <code>command</code>
361 * is on the execution path
362 * @param command
363 * the name of the program to be executed
364 * @param args
365 * the list of arguments,
366 * each containing a blank enclosed in double quotes.
367 * @param resFile
368 * a result file which must exist after this command has been processed.
369 * It need which shall be updated
370 * by this command.
371 * @return
372 * the output of the command which comprises the output stream
373 * and whether the return code is nonzero, i.e. the command succeeded.
374 * The io stream is used in tests only whereas the return code is used for pdfdiffs.
375 * @throws BuildFailureException
376 * TEX01 if invocation of <code>command</code> fails very basically:
377 * <ul>
378 * <li><!-- see Commandline.execute() -->
379 * the file expected to be the working directory
380 * does not exist or is not a directory.
381 * <li><!-- see Commandline.execute() -->
382 * {@link Runtime#exec(String, String[], File)} fails
383 * throwing an {@link java.io.IOException}.
384 * <li> <!-- see CommandLineCallable.call() -->
385 * an error inside systemOut parser occurs
386 * <li> <!-- see CommandLineCallable.call() -->
387 * an error inside systemErr parser occurs
388 * <li> Wrapping an {@link InterruptedException}
389 * on the process to be executed thrown by {@link Process#waitFor()}.
390 * </ul>
391 */
392 CmdResult executeBuild(File workingDir,
393 File pathToExecutable,
394 String command,
395 String[] args,
396 File resFile) throws BuildFailureException {
397 CmdResult res = executeEnvR0(workingDir, pathToExecutable, command, args);
398 existsOrErr(command, resFile);
399 return res;
400 }
401
402 /**
403 * Executes <code>command</code> in <code>workingDir</code>
404 * in the environment <code>env</code>
405 * with list of arguments given by <code>args</code>
406 * checking the return value via <code>checker</code>
407 * and logs a warning if one of the expected target files
408 * given by <code>resFiles</code> is not guaranteed to be newly created,
409 * or be updated.
410 * <p>
411 * Logging:
412 * <ul>
413 * <li> EEX01: proper execution failed:
414 * return code other than 0 and <code>checkReturnCode</code> is set.
415 * <li> EEX02: a target file after execution missing
416 * <li> EEX03: a target file is not updated,
417 * i.e. timestamp after not later than before.
418 * This implies the file existed
419 * before and after execution of <code>command</code>
420 * and that timestamps were readable before and after.
421 * <li> WEX04: cannot read timestamp of a target file,
422 * either before execution or after.
423 * Readability of the timestamp before execution is checked only
424 * if the file exists and also readability of the timestamp after
425 * is checked only if the file existed before and after execution.
426 * <li> WEX05: may emit false warnings: if modification times are too close
427 * and this cannot be corrected by sleeping.
428 * </ul>
429 *
430 * @param workingDir
431 * the working directory or <code>null</code>.
432 * The shell changes to that directory
433 * before invoking <code>command</code>
434 * with arguments <code>args</code> if this is not <code>null</code>.
435 * Argument <code>null</code> is allowed only
436 * if no result files are given by <code>resFile</code>.
437 * Essentially this is just needed to determine the version.
438 * @param pathToExecutable
439 * the path to the executable <code>command</code>.
440 * This may be <code>null</code> if <code>command</code>
441 * is on the execution path
442 * @param env
443 * the environment, i.e. the set of environment variables
444 * the command below is to be executed.
445 * @param command
446 * the name of the program to be executed
447 * @param args
448 * the list of arguments,
449 * each containing a blank enclosed in double quotes.
450 * @param checker
451 * the checker for the return code
452 * which decides whether an execution error EEX01 has to be logged.
453 * @param resFiles
454 * optional result files, i.e. target files which shall be updated
455 * by this command.
456 * @return
457 * the output of the command which comprises the output stream
458 * and whether the return code is nonzero, i.e. the command succeeded.
459 * The io stream is used in tests only whereas the return code is used for pdfdiffs.
460 * @throws BuildFailureException
461 * TEX01 if invocation of <code>command</code> fails very basically:
462 * <ul>
463 * <li><!-- see Commandline.execute() -->
464 * the file expected to be the working directory
465 * does not exist or is not a directory.
466 * <li><!-- see Commandline.execute() -->
467 * {@link Runtime#exec(String, String[], File)} fails
468 * throwing an {@link java.io.IOException}.
469 * <li> <!-- see CommandLineCallable.call() -->
470 * an error inside systemOut parser occurs
471 * <li> <!-- see CommandLineCallable.call() -->
472 * an error inside systemErr parser occurs
473 * <li> Wrapping an {@link InterruptedException}
474 * on the process to be executed thrown by {@link Process#waitFor()}.
475 * </ul>
476 */
477 private CmdResult execute(File workingDir,
478 File pathToExecutable,
479 Map<String,String> env,
480 String command,
481 ReturnCodeChecker checker,
482 String[] args,
483 File... resFiles) throws BuildFailureException {
484 // analyze old result files
485 //assert resFile.length > 0;
486
487 // determine target files and their timestamps before execution
488 boolean[] existsTarget = new boolean[resFiles.length];
489 Long[] lastModifiedTargetMs = new Long[resFiles.length];
490 long currentTimeMs = System.currentTimeMillis();
491 long minTimePastMs = Long.MAX_VALUE;
492 Long modTimeOrNull;
493 for (int idx = 0; idx < resFiles.length; idx++) {
494 existsTarget[idx] = resFiles[idx].exists();
495 if (existsTarget[idx]) {
496 // if modification time undetermined: null and emit warning WEX04
497 modTimeOrNull = modTimeOrNull(resFiles[idx]);
498 lastModifiedTargetMs[idx] = modTimeOrNull;
499 if (modTimeOrNull == null) {
500 // Here, already a warning WEX04 has been emitted
501 // also lastModifiedTargetMs[idx] == null
502 continue;
503 }
504 assert modTimeOrNull <= currentTimeMs;
505 // correct even if lastModifiedTarget[idx]==0
506 minTimePastMs = Math.min(minTimePastMs, currentTimeMs - modTimeOrNull);
507 }
508 }
509
510 // FIXME: this is based on a file system
511 // with modification time in steps of seconds, i.e. 1000ms
512 if (minTimePastMs < 1001) {
513 try {
514 // 1001 is the minimal span of time to change modification time
515 Thread.sleep(1001 - minTimePastMs);// for update control of target
516 } catch (InterruptedException ie) {
517 this.log.warn("WEX05: Update control may emit false warnings. ");
518 }
519 }
520
521 if (workingDir == null && resFiles.length != 0) {
522 throw new IllegalStateException(
523 "Working directory shall be determined but was null. ");
524 }
525
526 // Proper execution
527 // may throw BuildFailureException TEX01, log warning EEX01
528 CmdResult res =
529 execute(workingDir, pathToExecutable, env, command, checker, args);
530
531 // may log EEX02, EEX03, WEX04
532 for (int idx = 0; idx < resFiles.length; idx++) {
533 isUpdatedOrWarn(command, resFiles[idx], existsTarget[idx],
534 lastModifiedTargetMs[idx]);
535 }
536
537 return res;
538 }
539
540 // execution with environment given by {@link #ENV_EMPTY}
541 CmdResult executeEmptyEnv(File workingDir,
542 File pathToExecutable,
543 String command,
544 ReturnCodeChecker checker,
545 String[] args,
546 File... resFiles) throws BuildFailureException {
547 return execute(workingDir, pathToExecutable, ENV_EMPTY,
548 command, checker, args, resFiles);
549 }
550
551 // CmdResult executeEmptyEnvR0(File workingDir, File pathToExecutable,
552 // String command, String[] args, File... resFiles)
553 // throws BuildFailureException {
554 // return execute(workingDir, pathToExecutable, ENV_EMPTY, command,
555 // CommandExecutor.ReturnCodeChecker.IsNonZero, args, resFiles);
556 // }
557
558 /**
559 * Returns the time of modification of this file or <code>null</code> if not readable.
560 *
561 * Warnings: WEX04
562 *
563 * @param file
564 * The file to be checked.
565 * @return
566 * the time of modification of this file or <code>null</code> if not readable.
567 */
568 Long modTimeOrNull(File file) {
569 try {
570 // may throw IOException
571 FileTime fTime =
572 Files.getLastModifiedTime(file.toPath(), LinkOption.NOFOLLOW_LINKS);
573 return fTime.to(TimeUnit.MILLISECONDS);
574 } catch (IOException ioe) {
575 this.log.warn("WEX04: Cannot read target file '" + file.getName()
576 + "'; may be outdated. ");
577 return null;
578 }
579 }
580
581 /**
582 * If the given file <code>target</code> does not exist
583 * logs an error EEX02: no target file mentioning <code>command</code>.
584 * Note that no error means only that the file exists,
585 * not that it has been updated.
586 *
587 * @param command
588 * the command that should build <code>target</code> by need.
589 * @param target
590 * @return
591 * whether <code>target</code> exists.
592 * Equivalently, whether no error is logged.
593 */
594 private boolean existsOrErr(String command, File target) {
595 if (!target.exists()) {
596 this.log.error("EEX02: Running " + command + " failed: No target file '"
597 + target.getName() + "' written. ");
598 return false;
599 }
600 return true;
601 }
602
603 // FIXME: return value nowhere used
604 /**
605 * Returns whether the file <code>target</code> could be checked to be updated
606 * by the command named <code>command</code> and
607 * emits a warning <code>EEX03</code> if it has not been updated.
608 * It is invoked only by
609 * {@link #execute(File, File, Map<String,String>, String, ReturnCodeChecker, String[], File[])}
610 * after the command has been invoked.
611 * The file <code>target</code> is updated if it exists and
612 * either did not exist before according to <code>existedBefore</code>
613 * or has a readable modification time
614 * later than the former modification time <code>lastModifiedBefore</code>
615 * which implies that this has been readable also, i.e. is not <code>null</code>.
616 *
617 * Logging:
618 * <ul>
619 * <li> EEX02: no target file
620 * <li> EEX03: target file not updated
621 * <li> WEX04: cannot read target file
622 * </ul>
623 *
624 * @param command
625 * the name of the program to be executed
626 * @param target
627 * The file to be supervised.
628 * @param existedBefore
629 * Whether the file existed before invoking <code>command</code>.
630 * @param lastModifiedBefore
631 * The time of last modification before invoking <code>command</code> if known;
632 * else <code>null</code>.
633 * @return
634 * whether <code>target</code> has been updated.
635 */
636 private boolean isUpdatedOrWarn(String command,
637 File target,
638 boolean existedBefore,
639 Long lastModifiedBefore) {
640 // may emit EEX02
641 if (!existsOrErr(command, target)) {
642 return false;
643 }
644 assert target.exists();
645 if (!existedBefore) {
646 // Here target was updated
647 return true;
648 }
649 assert existedBefore && target.exists();
650
651 if (lastModifiedBefore == null) {
652 // Here, orignal modification time was not readable
653 // warning already emitted
654 return false;
655 }
656
657 // if modification time undetermined: null and emit warning WEX04
658 Long lastModifiedAfter = modTimeOrNull(target);//target.lastModified();
659 if (lastModifiedAfter == null) {
660 // modification time not readable; warning already emitted
661 return false;
662 }
663
664 if (lastModifiedAfter <= lastModifiedBefore) {
665 assert lastModifiedAfter == lastModifiedBefore;
666 this.log.error("EEX03: Running " + command + " failed: Target file '"
667 + target.getName() + "' is not updated. ");
668 return false;
669 }
670 return true;
671 }
672
673 /**
674 * Execute <code>command</code> with arguments <code>args</code>
675 * in the environment <code>env</code>
676 * in the working directory <code>workingDir</code>
677 * and return the output.
678 * Here, <code>pathToExecutable</code> is the path
679 * to the executable. It may be null.
680 * <p>
681 * Logging:
682 * EEX01 for return code other than 0.
683 *
684 * @param workingDir
685 * the working directory or <code>null</code>.
686 * The shell changes to that directory
687 * before invoking <code>command</code>
688 * with arguments <code>args</code> if this is not <code>null</code>.
689 * @param pathToExecutable
690 * the path to the executable <code>command</code>.
691 * This may be <code>null</code> if <code>command</code>
692 * is on the execution path.
693 * @param env
694 * the environment, i.e. the set of environment variables
695 * the command below is to be executed.
696 * @param command
697 * the name of the program to be executed.
698 * @param checker
699 * the checker for the return code
700 * which decides whether an execution error EEX01 has to be logged.
701 * @param args
702 * the list of arguments,
703 * each containing a blank enclosed in double quotes.
704 * @return
705 * the output of the command which comprises the output stream
706 * and whether the return code is nonzero, i.e. the command succeeded.
707 * @throws BuildFailureException
708 * TEX01 if invocation of <code>command</code> fails very basically:
709 * <ul>
710 * <li><!-- see Commandline.execute() -->
711 * the file expected to be the working directory
712 * does not exist or is not a directory.
713 * <li><!-- see Commandline.execute() -->
714 * {@link Runtime#exec(String, String[], File)} fails
715 * throwing an {@link java.io.IOException}.
716 * <li> <!-- see CommandLineCallable.call() -->
717 * an error inside systemOut parser occurs
718 * <li> <!-- see CommandLineCallable.call() -->
719 * an error inside systemErr parser occurs
720 * <li> Wrapping an {@link InterruptedException}
721 * on the process to be executed thrown by {@link Process#waitFor()}.
722 * </ul>
723 */
724 private CmdResult execute(File workingDir,
725 File pathToExecutable,
726 Map<String,String> env,
727 String command,
728 ReturnCodeChecker checker,
729 String[] args) throws BuildFailureException {
730 // prepare execution
731 String executable = new File(pathToExecutable, command).getPath();
732 Commandline cl = new Commandline(executable);
733 cl.getShell().setQuotedArgumentsEnabled(true);
734 for (Map.Entry<String, String> entry : env.entrySet()) {
735 cl.addEnvironment(entry.getKey(), entry.getValue());
736 }
737 cl.addArguments(args);
738 if (workingDir != null) {
739 cl.setWorkingDirectory(workingDir.getPath());
740 }
741 StringStreamConsumer output = new StringStreamConsumer();
742 log.debug("Executing: " + cl + " in: " + workingDir + ". ");
743
744 // perform execution and collect results
745 int returnCode = -1;
746 try {
747 // may throw CommandLineException
748 returnCode = executeCommandLine(cl, output, output);
749 if (checker.hasFailed(returnCode)) {
750 this.log.error("EEX01: Running " + command + " failed with return code "
751 + returnCode + ". ");
752 }
753 } catch (CommandLineException e) {
754 throw new BuildFailureException("TEX01: Error running " + command + ". ",
755 e);
756 }
757 // TBD: what if returnCode=-1 is not overwritten?
758 // how to distinguish from real return code -1?
759 // replace above by Integer returnCode = null;
760 // and insert here assert returnCode != null;
761
762 log.debug("Output:\n" + output.getOutput() + "\n");
763 // TBD: fix bug: return code based on checker.
764 // also not success but store return code itself
765 return new CmdResult(output.getOutput(), checker, returnCode);
766 }
767 }