001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2006 Continuent, Inc.
004: * Contact: sequoia@continuent.org
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * Initial developer(s): Emmanuel Cecchet.
019: * Contributor(s): ______________________.
020: */package org.continuent.sequoia.controller.backup.backupers;
021:
022: import java.io.BufferedWriter;
023: import java.io.File;
024: import java.io.IOException;
025: import java.io.OutputStream;
026: import java.io.StringWriter;
027: import java.util.ArrayList;
028: import java.util.Timer;
029: import java.util.TimerTask;
030:
031: import org.continuent.sequoia.common.log.Trace;
032:
033: /**
034: * This class defines a NativeCommandExec, which abstracts out native command
035: * execution logic into a small and easily verified class that does not have
036: * dependencies on backuper implementations. Once instantiated this class can
037: * execute a series of commands in sequence.
038: *
039: * @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a>
040: * @version 1.0
041: */
042: public class NativeCommandExec {
043: /** Maximum milliseconds to wait for output threads to complete. */
044: private static final int THREAD_WAIT_MAX = 30 * 1000;
045:
046: // Development logger
047: private static Trace logger = Trace
048: .getLogger(NativeCommandExec.class.getName());
049:
050: // Output from most recent command execution.
051: ArrayList stdout;
052: ArrayList stderr;
053:
054: // Defines a class to hold native commands, be they array or a simple string.
055: class NativeCommand {
056: String command;
057: String[] commands;
058:
059: // Creates a native command based on a string.
060: NativeCommand(String cmd) {
061: this .command = cmd;
062: }
063:
064: // Creates a native command based on an array.
065: NativeCommand(String[] cmds) {
066: this .commands = cmds;
067: }
068:
069: // Return true if this command is a string rather than an array.
070: boolean isString() {
071: return (command != null);
072: }
073:
074: // Return the command text in form suitable for error messages.
075: String commandText() {
076: if (isString())
077: return command;
078: else {
079: // Concatenate commands for logging purposes.
080: StringBuffer buf = new StringBuffer();
081: for (int i = 0; i < commands.length; i++) {
082: buf.append(commands[i] + " ");
083: }
084: return buf.toString();
085: }
086: }
087:
088: // Return the command string or null if this is an array command.
089: String getCommand() {
090: return command;
091: }
092:
093: // Return the command array or null if this is a string command.
094: String[] getCommands() {
095: return commands;
096: }
097: }
098:
099: /**
100: * Creates a new <code>NativeCommandExec</code> object
101: */
102: public NativeCommandExec() {
103: }
104:
105: /**
106: * Returns all stdout from the most recent command.
107: *
108: * @return stdout contents (may be truncated)
109: */
110: public ArrayList getStdout() {
111: return stdout;
112: }
113:
114: /**
115: * Returns all stderr from the most recent command.
116: *
117: * @return stderr contents (may be truncated)
118: */
119: public ArrayList getStderr() {
120: return stderr;
121: }
122:
123: /**
124: * Clears output arrays.
125: */
126: public void initOutput() {
127: stdout = new ArrayList();
128: stderr = new ArrayList();
129: }
130:
131: /**
132: * Utility method to execute a native command with careful logging and
133: * coverage of all command outcomes.
134: *
135: * @param cmd Command string
136: * @param input Array containing lines of input
137: * @param output Optional output stream to catch data written to standard
138: * output
139: * @param timeout Timeout in seconds (0 = infinite)
140: * @param workingDirectory working directory for the command (null to inherit
141: * from Sequoia working dir)
142: * @param ignoreStdErrOutput true if output on std error should be ignored,
143: * else any output on stderr will be considered as a failure
144: * @return true if command is successful
145: */
146: public boolean safelyExecNativeCommand(String cmd,
147: NativeCommandInputSource input, OutputStream output,
148: int timeout, File workingDirectory,
149: boolean ignoreStdErrOutput) {
150: NativeCommand nc = new NativeCommand(cmd);
151: return safelyExecNativeCommand0(nc, input, output, timeout,
152: workingDirectory, ignoreStdErrOutput);
153: }
154:
155: /**
156: * @see #safelyExecNativeCommand(String, NativeCommandInputSource,
157: * OutputStream, int, File)
158: */
159: public boolean safelyExecNativeCommand(String cmd,
160: NativeCommandInputSource input, OutputStream output,
161: int timeout, boolean ignoreStdErrOutput) {
162: return safelyExecNativeCommand(cmd, input, output, timeout,
163: null, ignoreStdErrOutput);
164: }
165:
166: /**
167: * Utility method to execute a native command with careful logging and
168: * coverage of all command outcomes.
169: *
170: * @param cmds Command array
171: * @param input Input source for native command
172: * @param output Optional output stream to catch data written to standard
173: * output
174: * @param timeout Timeout in seconds (0 = infinite)
175: * @param workingDirectory working directory for the command (null to inherit
176: * from Sequoia working dir)
177: * @param ignoreStdErrOutput true if output on std error should be ignored,
178: * else any output on stderr will be considered as a failure
179: * @return true if command is successful
180: */
181: public boolean safelyExecNativeCommand(String[] cmds,
182: NativeCommandInputSource input, OutputStream output,
183: int timeout, File workingDirectory,
184: boolean ignoreStdErrOutput) {
185: NativeCommand nc = new NativeCommand(cmds);
186: return safelyExecNativeCommand0(nc, input, output, timeout,
187: workingDirectory, ignoreStdErrOutput);
188: }
189:
190: /**
191: * @see #safelyExecNativeCommand(String[], NativeCommandInputSource,
192: * OutputStream, int, File)
193: */
194: public boolean safelyExecNativeCommand(String[] cmds,
195: NativeCommandInputSource input, OutputStream output,
196: int timeout, boolean ignoreStdErrOutput) {
197: return safelyExecNativeCommand(cmds, input, output, timeout,
198: null, ignoreStdErrOutput);
199: }
200:
201: /**
202: * Internal method to execute a native command with careful logging and
203: * coverage of all command outcomes. This method among other things logs
204: * output fully in the event of a failure.
205: *
206: * @param cmd Command holder object
207: * @param input Command input source
208: * @param output Optional output stream to catch data written to standard
209: * output
210: * @param timeout Timeout in seconds (0 = infinite)
211: * @param workingDirectory working directory for the command (null to inherit
212: * from Sequoia working dir)
213: * @param ignoreStdErrOutput true if output on std error should be ignored,
214: * else any output on stderr will be considered as a failure
215: * @return true if command is successful
216: */
217: protected boolean safelyExecNativeCommand0(NativeCommand nc,
218: NativeCommandInputSource input, OutputStream output,
219: int timeout, File workingDirectory,
220: boolean ignoreStdErrOutput) {
221: boolean status;
222: try {
223: int errorCount;
224: if (nc.isString())
225: errorCount = executeNativeCommand(nc.getCommand(),
226: input, output, timeout, workingDirectory,
227: ignoreStdErrOutput);
228: else
229: errorCount = executeNativeCommand(nc.getCommands(),
230: input, output, timeout, workingDirectory,
231: ignoreStdErrOutput);
232: status = (errorCount == 0);
233: if (!status)
234: logger.warn("Native command failed with error count="
235: + errorCount);
236: } catch (InterruptedException e) {
237: logger.warn("Native command timed out: command="
238: + nc.commandText() + " timeout=" + timeout);
239: status = false;
240: } catch (IOException e) {
241: logger.warn("Native command I/O failed: command="
242: + nc.commandText(), e);
243: status = false;
244: }
245:
246: // If status is false, dump all output to the development log.
247: if (status == false) {
248: logOutput();
249: logErrors();
250: }
251:
252: return status;
253: }
254:
255: /**
256: * Manages execution of a command once it has been started as a process.
257: *
258: * @param commandText The command text for logging messages
259: * @param process The process object used to manage the command
260: * @param inputSource Input source object or null if no input
261: * @param output Optional output stream to catch standard out
262: * @param timeout Time in seconds to await command (0 = forever)
263: * @param ignoreStdErrOutput true if output on std error should be ignored,
264: * else any output on stderr will be considered as a failure
265: * @return 0 if successful, any number otherwise
266: * @throws InterruptedException If there is a timeout failure
267: */
268: protected int manageCommandExecution(String commandText,
269: Process process, NativeCommandInputSource inputSource,
270: OutputStream output, int timeout, boolean ignoreStdErrOutput)
271: throws InterruptedException {
272: if (logger.isInfoEnabled()) {
273: logger
274: .info("Starting execution of \"" + commandText
275: + "\"");
276: }
277:
278: // Spawn 2 threads to capture the process output, prevents blocking
279: NativeCommandOutputThread inStreamThread = new NativeCommandOutputThread(
280: process.getInputStream(), output);
281: NativeCommandOutputThread errStreamThread = new NativeCommandOutputThread(
282: process.getErrorStream(), null);
283: inStreamThread.start();
284: errStreamThread.start();
285:
286: // Manage command execution.
287: TimerTask task = null;
288: try {
289: // Schedule the timer if we need a timeout.
290: if (timeout > 0) {
291: final Thread t = Thread.currentThread();
292: task = new TimerTask() {
293: public void run() {
294: t.interrupt();
295: }
296: };
297: Timer timer = new Timer();
298: timer.schedule(task, timeout * 1000);
299: }
300:
301: // Provide standard input if present.
302: if (inputSource != null) {
303: OutputStream processOutput = process.getOutputStream();
304: try {
305: inputSource.write(processOutput);
306: } catch (IOException e) {
307: logger
308: .warn(
309: "Writing of data to stdin halted by exception",
310: e);
311: } finally {
312: try {
313: inputSource.close();
314: } catch (IOException e) {
315: logger
316: .warn(
317: "Input source close operation generated exception",
318: e);
319: }
320: try {
321: processOutput.close();
322: } catch (IOException e) {
323: logger
324: .warn(
325: "Process stdin close operation generated exception",
326: e);
327: }
328: }
329: }
330:
331: // Wait for process to complete.
332: process.waitFor();
333:
334: // Wait for threads to complete. Not strictly necessary
335: // but makes it more likely we will read output properly.
336: inStreamThread.join(THREAD_WAIT_MAX);
337: errStreamThread.join(THREAD_WAIT_MAX);
338: } catch (InterruptedException e) {
339: logger.warn("Command exceeded timeout: " + commandText);
340: process.destroy();
341: throw e;
342: } finally {
343: // Cancel the timer and cleanup process stdin.
344: if (task != null)
345: task.cancel();
346:
347: // Collect output--this needs to happen no matter what.
348: stderr = errStreamThread.getOutput();
349: stdout = inStreamThread.getOutput();
350: }
351:
352: if (logger.isInfoEnabled()) {
353: logger.info("Command \"" + commandText + "\" logged "
354: + stderr.size()
355: + " errors and terminated with exitcode "
356: + process.exitValue());
357: }
358:
359: if (ignoreStdErrOutput)
360: return process.exitValue();
361: else {
362: if (stderr.size() > 0) // Return non-zero code if errors on stderr
363: return -1;
364: else
365: return process.exitValue();
366: }
367: }
368:
369: /**
370: * Executes a native operating system command expressed as a String.
371: *
372: * @param command String of command to execute
373: * @param input Native command input
374: * @param output Optional output to catch standard out
375: * @param timeout Time in seconds to await command (0 = forever)
376: * @param workingDirectory working directory for the command (null to inherit
377: * from Sequoia working dir)
378: * @param ignoreStdErrOutput true if output on std error should be ignored,
379: * else any output on stderr will be considered as a failure
380: * @return 0 if successful, any number otherwise
381: * @exception IOException if an I/O error occurs
382: * @throws InterruptedException If there is a timeout failure
383: */
384: public int executeNativeCommand(String command,
385: NativeCommandInputSource input, OutputStream output,
386: int timeout, File workingDirectory,
387: boolean ignoreStdErrOutput) throws IOException,
388: InterruptedException {
389: initOutput();
390: Process process = Runtime.getRuntime().exec(command, null,
391: workingDirectory);
392: return manageCommandExecution(command, process, input, output,
393: timeout, ignoreStdErrOutput);
394: }
395:
396: /**
397: * @see #executeNativeCommand(String, NativeCommandInputSource, OutputStream,
398: * int, File)
399: */
400: public int executeNativeCommand(String command,
401: NativeCommandInputSource input, OutputStream output,
402: int timeout, boolean ignoreStdErrOutput)
403: throws IOException, InterruptedException {
404: return executeNativeCommand(command, input, output, timeout,
405: null, ignoreStdErrOutput);
406: }
407:
408: /**
409: * Executes a native operating system command expressed as an array.
410: *
411: * @param commands Array of strings (command + args) to execute
412: * @param input Native command input
413: * @param output Command output stream
414: * @param timeout Time in seconds to await command (0 = forever)
415: * @param workingDirectory working directory for the command (null to inherit
416: * from Sequoia working dir)
417: * @param ignoreStdErrOutput true if output on std error should be ignored,
418: * else any output on stderr will be considered as a failure
419: * @return 0 if successful, any number otherwise
420: * @exception IOException if an I/O error occurs
421: * @throws InterruptedException If there is a timeout failure
422: */
423: public int executeNativeCommand(String[] commands,
424: NativeCommandInputSource input, OutputStream output,
425: int timeout, File workingDirectory,
426: boolean ignoreStdErrOutput) throws IOException,
427: InterruptedException {
428: initOutput();
429: Process process = Runtime.getRuntime().exec(commands, null,
430: workingDirectory);
431: return manageCommandExecution(new NativeCommand(commands)
432: .commandText(), process, input, output, timeout,
433: ignoreStdErrOutput);
434: }
435:
436: /**
437: * @see #executeNativeCommand(String[], NativeCommandInputSource,
438: * OutputStream, int, File)
439: */
440: public int executeNativeCommand(String[] commands,
441: NativeCommandInputSource input, OutputStream output,
442: int timeout, boolean ignoreStdErrOutput)
443: throws IOException, InterruptedException {
444: return executeNativeCommand(commands, input, output, timeout,
445: null, ignoreStdErrOutput);
446: }
447:
448: /**
449: * Write process standard output to development log.
450: */
451: public void logOutput() {
452: log("stdout", stdout);
453: }
454:
455: /**
456: * Write process error output to development log.
457: */
458: public void logErrors() {
459: log("stderr", stderr);
460: }
461:
462: /**
463: * Utility routine to log output.
464: *
465: * @param name Output type e.g., stdout or stderr
466: * @param outArray List of lines of output
467: */
468: protected void log(String name, ArrayList outArray) {
469: StringWriter sw = new StringWriter();
470: BufferedWriter writer = new BufferedWriter(sw);
471: if (outArray != null) {
472: int arraySize = outArray.size();
473: for (int i = 0; i < arraySize; i++) {
474: String line = (String) outArray.get(i);
475: try {
476: writer.newLine();
477: writer.write(line);
478: } catch (IOException e) {
479: // This would be very unexpected when writing to a string.
480: logger
481: .error(
482: "Unexpected exception while trying to log process output",
483: e);
484: }
485: }
486: }
487:
488: // Flush out collected output.
489: try {
490: writer.flush();
491: writer.close();
492: } catch (IOException e) {
493: }
494:
495: String outText = sw.toString();
496: logger.info("Process output (" + name + "):" + outText);
497: }
498: }
|