2 * Copyright (C) 2007-2012 Argeo GmbH
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org
.argeo
.slc
.core
.execution
.tasks
;
19 import java
.io
.FileOutputStream
;
20 import java
.io
.FileWriter
;
21 import java
.io
.IOException
;
22 import java
.io
.InputStream
;
23 import java
.io
.OutputStream
;
24 import java
.io
.PipedInputStream
;
25 import java
.io
.PipedOutputStream
;
26 import java
.io
.Writer
;
27 import java
.util
.ArrayList
;
28 import java
.util
.Collections
;
29 import java
.util
.HashMap
;
30 import java
.util
.List
;
32 import java
.util
.UUID
;
34 import javax
.security
.auth
.callback
.CallbackHandler
;
36 import org
.apache
.commons
.exec
.CommandLine
;
37 import org
.apache
.commons
.exec
.DefaultExecutor
;
38 import org
.apache
.commons
.exec
.ExecuteException
;
39 import org
.apache
.commons
.exec
.ExecuteResultHandler
;
40 import org
.apache
.commons
.exec
.ExecuteStreamHandler
;
41 import org
.apache
.commons
.exec
.ExecuteWatchdog
;
42 import org
.apache
.commons
.exec
.Executor
;
43 import org
.apache
.commons
.exec
.LogOutputStream
;
44 import org
.apache
.commons
.exec
.PumpStreamHandler
;
45 import org
.apache
.commons
.exec
.ShutdownHookProcessDestroyer
;
46 import org
.apache
.commons
.io
.FileUtils
;
47 import org
.apache
.commons
.io
.IOUtils
;
48 import org
.apache
.commons
.logging
.Log
;
49 import org
.apache
.commons
.logging
.LogFactory
;
50 import org
.argeo
.slc
.SlcException
;
51 import org
.argeo
.slc
.UnsupportedException
;
52 import org
.argeo
.slc
.core
.execution
.ExecutionResources
;
53 import org
.argeo
.slc
.core
.test
.SimpleResultPart
;
54 import org
.argeo
.slc
.test
.TestResult
;
55 import org
.argeo
.slc
.test
.TestStatus
;
56 import org
.springframework
.core
.io
.Resource
;
57 import org
.springframework
.security
.core
.Authentication
;
58 import org
.springframework
.security
.core
.context
.SecurityContextHolder
;
60 /** Execute an OS specific system call. */
61 public class SystemCall
implements Runnable
{
62 public final static String LOG_STDOUT
= "System.out";
64 private final Log log
= LogFactory
.getLog(getClass());
66 private String execDir
;
68 private String cmd
= null;
69 private List
<Object
> command
= null;
71 private Executor executor
= new DefaultExecutor();
72 private Boolean synchronous
= true;
74 private String stdErrLogLevel
= "ERROR";
75 private String stdOutLogLevel
= "INFO";
77 private Resource stdOutFile
= null;
78 private Resource stdErrFile
= null;
80 private Resource stdInFile
= null;
82 * If no {@link #stdInFile} provided, writing to this stream will write to
83 * the stdin of the process.
85 private OutputStream stdInSink
= null;
87 private Boolean redirectStdOut
= false;
89 private List
<SystemCallOutputListener
> outputListeners
= Collections
90 .synchronizedList(new ArrayList
<SystemCallOutputListener
>());
92 private Map
<String
, List
<Object
>> osCommands
= new HashMap
<String
, List
<Object
>>();
93 private Map
<String
, String
> osCmds
= new HashMap
<String
, String
>();
94 private Map
<String
, String
> environmentVariables
= new HashMap
<String
, String
>();
96 private Boolean logCommand
= false;
97 private Boolean redirectStreams
= true;
98 private Boolean exceptionOnFailed
= true;
99 private Boolean mergeEnvironmentVariables
= true;
101 private Authentication authentication
;
103 private String osConsole
= null;
104 private String generateScript
= null;
107 private Long watchdogTimeout
= 24 * 60 * 60 * 1000l;
109 private TestResult testResult
;
111 private ExecutionResources executionResources
;
113 /** Sudo the command, as root if empty or as user if not. */
114 private String sudo
= null;
115 // TODO make it more secure and robust, test only once
116 private final String sudoPrompt
= UUID
.randomUUID().toString();
117 private String askPassProgram
= "/usr/libexec/openssh/ssh-askpass";
118 @SuppressWarnings("unused")
119 private boolean firstLine
= true;
120 @SuppressWarnings("unused")
121 private CallbackHandler callbackHandler
;
122 /** Chroot to the this path (must not be empty) */
123 private String chroot
= null;
126 /** Current watchdog, null if process is completed */
127 ExecuteWatchdog currentWatchdog
= null;
129 /** Empty constructor */
130 public SystemCall() {
135 * Constructor based on the provided command list.
140 public SystemCall(List
<Object
> command
) {
141 this.command
= command
;
145 * Constructor based on the provided command.
148 * the command. If the provided string contains no space a
149 * command list is initialized with the argument as first
150 * component (useful for chained construction)
152 public SystemCall(String cmd
) {
153 if (cmd
.indexOf(' ') < 0) {
154 command
= new ArrayList
<Object
>();
161 /** Executes the system call. */
163 authentication
= SecurityContextHolder
.getContext().getAuthentication();
166 Writer stdOutWriter
= null;
167 OutputStream stdOutputStream
= null;
168 Writer stdErrWriter
= null;
169 InputStream stdInStream
= null;
170 if (stdOutFile
!= null)
172 stdOutputStream
= createOutputStream(stdOutFile
);
174 stdOutWriter
= createWriter(stdOutFile
, true);
176 if (stdErrFile
!= null) {
177 stdErrWriter
= createWriter(stdErrFile
, true);
179 if (stdOutFile
!= null && !redirectStdOut
)
180 stdErrWriter
= createWriter(stdOutFile
, true);
184 if (stdInFile
!= null)
185 stdInStream
= stdInFile
.getInputStream();
187 stdInStream
= new PipedInputStream();
188 stdInSink
= new PipedOutputStream(
189 (PipedInputStream
) stdInStream
);
191 } catch (IOException e2
) {
192 throw new SlcException("Cannot open a stream for " + stdInFile
, e2
);
195 if (log
.isTraceEnabled()) {
196 log
.debug("os.name=" + System
.getProperty("os.name"));
197 log
.debug("os.arch=" + System
.getProperty("os.arch"));
198 log
.debug("os.version=" + System
.getProperty("os.version"));
201 // Execution directory
202 File dir
= new File(getExecDirToUse());
203 // if (!dir.exists())
206 // Watchdog to check for lost processes
207 Executor executorToUse
;
208 if (executor
!= null)
209 executorToUse
= executor
;
211 executorToUse
= new DefaultExecutor();
212 executorToUse
.setWatchdog(createWatchdog());
214 if (redirectStreams
) {
215 // Redirect standard streams
216 executorToUse
.setStreamHandler(createExecuteStreamHandler(
217 stdOutWriter
, stdOutputStream
, stdErrWriter
, stdInStream
));
219 // Dummy stream handler (otherwise pump is used)
220 executorToUse
.setStreamHandler(new DummyexecuteStreamHandler());
223 executorToUse
.setProcessDestroyer(new ShutdownHookProcessDestroyer());
224 executorToUse
.setWorkingDirectory(dir
);
226 // Command line to use
227 final CommandLine commandLine
= createCommandLine();
229 log
.info("Execute command:\n" + commandLine
230 + "\n in working directory: \n" + dir
+ "\n");
233 Map
<String
, String
> environmentVariablesToUse
= null;
234 environmentVariablesToUse
= new HashMap
<String
, String
>();
235 if (mergeEnvironmentVariables
)
236 environmentVariablesToUse
.putAll(System
.getenv());
237 if (environmentVariables
.size() > 0)
238 environmentVariablesToUse
.putAll(environmentVariables
);
241 ExecuteResultHandler executeResultHandler
= createExecuteResultHandler(commandLine
);
244 // THE EXECUTION PROPER
249 int exitValue
= executorToUse
.execute(commandLine
,
250 environmentVariablesToUse
);
251 executeResultHandler
.onProcessComplete(exitValue
);
252 } catch (ExecuteException e1
) {
253 if (e1
.getExitValue() == Executor
.INVALID_EXITVALUE
) {
254 Thread
.currentThread().interrupt();
257 // Sleep 1s in order to make sure error logs are flushed
259 executeResultHandler
.onProcessFailed(e1
);
262 executorToUse
.execute(commandLine
, environmentVariablesToUse
,
263 executeResultHandler
);
264 } catch (SlcException e
) {
266 } catch (Exception e
) {
267 throw new SlcException("Could not execute command " + commandLine
,
270 IOUtils
.closeQuietly(stdOutWriter
);
271 IOUtils
.closeQuietly(stdErrWriter
);
272 IOUtils
.closeQuietly(stdInStream
);
273 IOUtils
.closeQuietly(stdInSink
);
278 public synchronized String
function() {
279 final StringBuffer buf
= new StringBuffer("");
280 SystemCallOutputListener tempOutputListener
= new SystemCallOutputListener() {
281 private Long lineCount
= 0l;
283 public void newLine(SystemCall systemCall
, String line
,
293 addOutputListener(tempOutputListener
);
295 removeOutputListener(tempOutputListener
);
296 return buf
.toString();
299 public String
asCommand() {
300 return createCommandLine().toString();
304 public String
toString() {
309 * Build a command line based on the properties. Can be overridden by
310 * specific command wrappers.
312 protected CommandLine
createCommandLine() {
313 // Check if an OS specific command overrides
314 String osName
= System
.getProperty("os.name");
315 List
<Object
> commandToUse
= null;
316 if (osCommands
.containsKey(osName
))
317 commandToUse
= osCommands
.get(osName
);
319 commandToUse
= command
;
320 String cmdToUse
= null;
321 if (osCmds
.containsKey(osName
))
322 cmdToUse
= osCmds
.get(osName
);
326 CommandLine commandLine
= null;
328 // Which command definition to use
329 if (commandToUse
== null && cmdToUse
== null)
330 throw new SlcException("Please specify a command.");
331 else if (commandToUse
!= null && cmdToUse
!= null)
332 throw new SlcException(
333 "Specify the command either as a line or as a list.");
334 else if (cmdToUse
!= null) {
335 if (chroot
!= null && !chroot
.trim().equals(""))
336 cmdToUse
= "chroot \"" + chroot
+ "\" " + cmdToUse
;
338 environmentVariables
.put("SUDO_ASKPASS", askPassProgram
);
339 if (!sudo
.trim().equals(""))
340 cmdToUse
= "sudo -p " + sudoPrompt
+ " -u " + sudo
+ " "
343 cmdToUse
= "sudo -p " + sudoPrompt
+ " " + cmdToUse
;
346 // GENERATE COMMAND LINE
347 commandLine
= CommandLine
.parse(cmdToUse
);
348 } else if (commandToUse
!= null) {
349 if (commandToUse
.size() == 0)
350 throw new SlcException("Command line is empty.");
352 if (chroot
!= null && sudo
!= null) {
353 commandToUse
.add(0, "chroot");
354 commandToUse
.add(1, chroot
);
358 environmentVariables
.put("SUDO_ASKPASS", askPassProgram
);
359 commandToUse
.add(0, "sudo");
360 commandToUse
.add(1, "-p");
361 commandToUse
.add(2, sudoPrompt
);
362 if (!sudo
.trim().equals("")) {
363 commandToUse
.add(3, "-u");
364 commandToUse
.add(4, sudo
);
368 // GENERATE COMMAND LINE
369 commandLine
= new CommandLine(commandToUse
.get(0).toString());
371 for (int i
= 1; i
< commandToUse
.size(); i
++) {
372 if (log
.isTraceEnabled())
373 log
.debug(commandToUse
.get(i
));
374 commandLine
.addArgument(commandToUse
.get(i
).toString());
377 // all cases covered previously
378 throw new UnsupportedException();
381 if (generateScript
!= null) {
382 File scriptFile
= new File(getExecDirToUse() + File
.separator
385 FileUtils
.writeStringToFile(scriptFile
,
386 (osConsole
!= null ? osConsole
+ " " : "")
387 + commandLine
.toString());
388 } catch (IOException e
) {
389 throw new SlcException("Could not generate script "
392 commandLine
= new CommandLine(scriptFile
);
394 if (osConsole
!= null)
395 commandLine
= CommandLine
.parse(osConsole
+ " "
396 + commandLine
.toString());
403 * Creates a {@link PumpStreamHandler} which redirects streams to the custom
406 protected ExecuteStreamHandler
createExecuteStreamHandler(
407 final Writer stdOutWriter
, final OutputStream stdOutputStream
,
408 final Writer stdErrWriter
, final InputStream stdInStream
) {
411 OutputStream stdout
= stdOutputStream
!= null ? stdOutputStream
412 : new LogOutputStream() {
413 protected void processLine(String line
, int level
) {
415 // if (sudo != null && callbackHandler != null
416 // && line.startsWith(sudoPrompt)) {
418 // PasswordCallback pc = new PasswordCallback(
419 // "sudo password", false);
420 // Callback[] cbs = { pc };
421 // callbackHandler.handle(cbs);
422 // char[] pwd = pc.getPassword();
423 // char[] arr = Arrays.copyOf(pwd,
425 // arr[arr.length - 1] = '\n';
426 // IOUtils.write(arr, stdInSink);
427 // stdInSink.flush();
428 // } catch (Exception e) {
429 // throw new SlcException(
430 // "Cannot retrieve sudo password", e);
433 // firstLine = false;
436 if (line
!= null && !line
.trim().equals(""))
439 if (stdOutWriter
!= null)
440 appendLineToFile(stdOutWriter
, line
);
444 OutputStream stderr
= new LogOutputStream() {
445 protected void processLine(String line
, int level
) {
446 if (line
!= null && !line
.trim().equals(""))
448 if (stdErrWriter
!= null)
449 appendLineToFile(stdErrWriter
, line
);
453 PumpStreamHandler pumpStreamHandler
= new PumpStreamHandler(stdout
,
454 stderr
, stdInStream
) {
457 public void stop() throws IOException
{
458 // prevents the method to block when joining stdin
459 if (stdInSink
!= null)
460 IOUtils
.closeQuietly(stdInSink
);
465 return pumpStreamHandler
;
468 /** Creates the default {@link ExecuteResultHandler}. */
469 protected ExecuteResultHandler
createExecuteResultHandler(
470 final CommandLine commandLine
) {
471 return new ExecuteResultHandler() {
473 public void onProcessComplete(int exitValue
) {
474 String msg
= "System call '" + commandLine
475 + "' properly completed.";
476 if (log
.isTraceEnabled())
478 if (testResult
!= null) {
479 forwardPath(testResult
);
480 testResult
.addResultPart(new SimpleResultPart(
481 TestStatus
.PASSED
, msg
));
486 public void onProcessFailed(ExecuteException e
) {
488 String msg
= "System call '" + commandLine
+ "' failed.";
489 if (testResult
!= null) {
490 forwardPath(testResult
);
491 testResult
.addResultPart(new SimpleResultPart(
492 TestStatus
.ERROR
, msg
, e
));
494 if (exceptionOnFailed
)
495 throw new SlcException(msg
, e
);
505 protected void forwardPath(TestResult testResult
) {
506 // TODO: allocate a TreeSPath
510 * Shortcut method getting the execDir to use
512 protected String
getExecDirToUse() {
514 if (execDir
!= null) {
517 return System
.getProperty("user.dir");
518 } catch (Exception e
) {
519 throw new SlcException("Cannot find exec dir", e
);
523 protected void logStdOut(String line
) {
524 for (SystemCallOutputListener outputListener
: outputListeners
)
525 outputListener
.newLine(this, line
, false);
526 log(stdOutLogLevel
, line
);
529 protected void logStdErr(String line
) {
530 for (SystemCallOutputListener outputListener
: outputListeners
)
531 outputListener
.newLine(this, line
, true);
532 log(stdErrLogLevel
, line
);
535 /** Log from the underlying streams. */
536 protected void log(String logLevel
, String line
) {
538 if (SecurityContextHolder
.getContext().getAuthentication() == null) {
539 SecurityContextHolder
.getContext()
540 .setAuthentication(authentication
);
543 if ("ERROR".equals(logLevel
))
545 else if ("WARN".equals(logLevel
))
547 else if ("INFO".equals(logLevel
))
549 else if ("DEBUG".equals(logLevel
))
551 else if ("TRACE".equals(logLevel
))
553 else if (LOG_STDOUT
.equals(logLevel
))
554 System
.out
.println(line
);
555 else if ("System.err".equals(logLevel
))
556 System
.err
.println(line
);
558 throw new SlcException("Unknown log level " + logLevel
);
561 /** Append line to a log file. */
562 protected void appendLineToFile(Writer writer
, String line
) {
564 writer
.append(line
).append('\n');
565 } catch (IOException e
) {
566 log
.error("Cannot write to log file", e
);
570 /** Creates the writer for the output/err files. */
571 protected Writer
createWriter(Resource target
, Boolean append
) {
572 FileWriter writer
= null;
576 if (executionResources
!= null)
577 file
= new File(executionResources
.getAsOsPath(target
, true));
579 file
= target
.getFile();
580 writer
= new FileWriter(file
, append
);
581 } catch (IOException e
) {
582 log
.error("Cannot get file for " + target
, e
);
583 IOUtils
.closeQuietly(writer
);
588 /** Creates an outputstream for the output/err files. */
589 protected OutputStream
createOutputStream(Resource target
) {
590 FileOutputStream out
= null;
594 if (executionResources
!= null)
595 file
= new File(executionResources
.getAsOsPath(target
, true));
597 file
= target
.getFile();
598 out
= new FileOutputStream(file
, false);
599 } catch (IOException e
) {
600 log
.error("Cannot get file for " + target
, e
);
601 IOUtils
.closeQuietly(out
);
606 /** Append the argument (for chaining) */
607 public SystemCall
arg(String arg
) {
609 command
= new ArrayList
<Object
>();
614 /** Append the argument (for chaining) */
615 public SystemCall
arg(String arg
, String value
) {
617 command
= new ArrayList
<Object
>();
624 public synchronized Boolean
isRunning() {
625 return currentWatchdog
!= null;
628 private synchronized ExecuteWatchdog
createWatchdog() {
629 // if (currentWatchdog != null)
630 // throw new SlcException("A process is already being monitored");
631 currentWatchdog
= new ExecuteWatchdog(watchdogTimeout
);
632 return currentWatchdog
;
635 private synchronized void releaseWatchdog() {
636 currentWatchdog
= null;
639 public synchronized void kill() {
640 if (currentWatchdog
!= null)
641 currentWatchdog
.destroyProcess();
645 public void setCmd(String command
) {
649 public void setCommand(List
<Object
> command
) {
650 this.command
= command
;
653 public void setExecDir(String execdir
) {
654 this.execDir
= execdir
;
657 public void setStdErrLogLevel(String stdErrLogLevel
) {
658 this.stdErrLogLevel
= stdErrLogLevel
;
661 public void setStdOutLogLevel(String stdOutLogLevel
) {
662 this.stdOutLogLevel
= stdOutLogLevel
;
665 public void setSynchronous(Boolean synchronous
) {
666 this.synchronous
= synchronous
;
669 public void setOsCommands(Map
<String
, List
<Object
>> osCommands
) {
670 this.osCommands
= osCommands
;
673 public void setOsCmds(Map
<String
, String
> osCmds
) {
674 this.osCmds
= osCmds
;
677 public void setEnvironmentVariables(Map
<String
, String
> environmentVariables
) {
678 this.environmentVariables
= environmentVariables
;
681 public Map
<String
, String
> getEnvironmentVariables() {
682 return environmentVariables
;
685 public void setWatchdogTimeout(Long watchdogTimeout
) {
686 this.watchdogTimeout
= watchdogTimeout
;
689 public void setStdOutFile(Resource stdOutFile
) {
690 this.stdOutFile
= stdOutFile
;
693 public void setStdErrFile(Resource stdErrFile
) {
694 this.stdErrFile
= stdErrFile
;
697 public void setStdInFile(Resource stdInFile
) {
698 this.stdInFile
= stdInFile
;
701 public void setTestResult(TestResult testResult
) {
702 this.testResult
= testResult
;
705 public void setLogCommand(Boolean logCommand
) {
706 this.logCommand
= logCommand
;
709 public void setRedirectStreams(Boolean redirectStreams
) {
710 this.redirectStreams
= redirectStreams
;
713 public void setExceptionOnFailed(Boolean exceptionOnFailed
) {
714 this.exceptionOnFailed
= exceptionOnFailed
;
717 public void setMergeEnvironmentVariables(Boolean mergeEnvironmentVariables
) {
718 this.mergeEnvironmentVariables
= mergeEnvironmentVariables
;
721 public void setOsConsole(String osConsole
) {
722 this.osConsole
= osConsole
;
725 public void setGenerateScript(String generateScript
) {
726 this.generateScript
= generateScript
;
729 public void setExecutionResources(ExecutionResources executionResources
) {
730 this.executionResources
= executionResources
;
733 public void setRedirectStdOut(Boolean redirectStdOut
) {
734 this.redirectStdOut
= redirectStdOut
;
737 public void addOutputListener(SystemCallOutputListener outputListener
) {
738 outputListeners
.add(outputListener
);
741 public void removeOutputListener(SystemCallOutputListener outputListener
) {
742 outputListeners
.remove(outputListener
);
745 public void setOutputListeners(
746 List
<SystemCallOutputListener
> outputListeners
) {
747 this.outputListeners
= outputListeners
;
750 public void setExecutor(Executor executor
) {
751 this.executor
= executor
;
754 public void setSudo(String sudo
) {
758 public void setCallbackHandler(CallbackHandler callbackHandler
) {
759 this.callbackHandler
= callbackHandler
;
762 public void setChroot(String chroot
) {
763 this.chroot
= chroot
;
766 private class DummyexecuteStreamHandler
implements ExecuteStreamHandler
{
768 public void setProcessErrorStream(InputStream is
) throws IOException
{
771 public void setProcessInputStream(OutputStream os
) throws IOException
{
774 public void setProcessOutputStream(InputStream is
) throws IOException
{
777 public void start() throws IOException
{