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