View Javadoc
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 }