/*******************************************************************************
* Copyright (c) 2011 Arapiki Solutions Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* "Peter Smith <psmith@arapiki.com>" - initial API and
* implementation and/or initial documentation
*******************************************************************************/
package com.buildml.scanner.legacy;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import com.buildml.model.IBuildStore;
import com.buildml.scanner.FatalBuildScannerError;
import com.buildml.utils.os.ShellResult;
import com.buildml.utils.os.SystemUtils;
/**
* This class is the main entry point for scanning a legacy shell-command-based
* build process, and creating a corresponding BuildStore. This is a wrapper for
* the "cfs" command-line tool, and we also parse the "cfs.trace file" that cfs generates.
* The output from this class is a fully populated BuildStore.
*
* @author "Peter Smith <psmith@arapiki.com>"
*/
public class LegacyBuildScanner {
/*=====================================================================================*
* TYPES/FIELDS
*=====================================================================================*/
/** The default trace file name, which is used if no other name is provided. */
private static final String DEFAULT_TRACE_FILE_NAME = "cfs.trace";
/** The trace file name the user has chosen. */
private String traceFilePathName;
/**
* The BuildStore we should generate, or null if we should parse the trace file without
* creating a BuildStore.
*/
private IBuildStore buildStore = null;
/**
* The debug verbosity level for showing the progress of a trace. 0 = none, 1 = some,
* 2 = extended debug. Note that unless debugStream is also set, there will be no
* debug output at all.
*/
private int debugLevel = 0;
/** The default log file name, used if no name is chosen by the user. */
private static final String DEFAULT_LOG_FILE_NAME = "cfs.log";
/** The debug log file name the user has chosen. */
private String logFileName;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new LegacyBuildScanner object. By default, there is no BuildStore attached
* to this scanner (use setBuildStore() to set one), and the default trace and log file
* names will be used (use setTraceFile() and setLogFile() to change them).
*/
public LegacyBuildScanner() {
/* set the default trace and log file names */
setTraceFile(null);
setLogFile(null);
setBuildStore(null);
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Set this scanner's trace file name. An output file with this name will be generated
* when traceShellCommand() is called, and will be read when parseTraceFile() is called.
* Given the potentially enormous size of a trace file (> 1GB), it doesn't make sense
* to keep this information in memory, so an intermediate file is necessary.
*
* @param traceFilePathName The name of the file to scan to/from. If null, set
* the path name back to the default.
*/
public void setTraceFile(String traceFilePathName) {
/* if a file name is provided... */
if (traceFilePathName != null) {
this.traceFilePathName = traceFilePathName;
}
/* else, null means revert to default name */
else {
this.traceFilePathName = DEFAULT_TRACE_FILE_NAME;
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the current trace file path name.
* @return The current trace file path name.
*/
public String getTraceFile() {
return this.traceFilePathName;
}
/*-------------------------------------------------------------------------------------*/
/**
* Set this scanner's log file name. This file will be freshly created, and used as
* the debug log file for all CFS operations (system calls, etc), as well as all output
* from the TraceFileScanner() class.
*
* @param logFileName The name of the log file to write debug information to.
*/
public void setLogFile(String logFileName) {
/* if a file name is provided... */
if (logFileName != null) {
this.logFileName = logFileName;
}
/* else, null means revert to default name */
else {
this.logFileName = DEFAULT_LOG_FILE_NAME;
}
/*
* Make sure it's an absolute path, otherwise when the program being traced does
* a chdir, it'll start writing the log file into that directory instead.
*/
this.logFileName = new File(this.logFileName).getAbsolutePath();
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the current debug log file name.
* @return The current debug log file name.
*/
public String getLogFile() {
return this.logFileName;
}
/*-------------------------------------------------------------------------------------*/
/**
* Set the BuildStore object that the scanner should add the build process to.
*
* @param buildStore The BuildStore to collect information in, or null to not collect
* information.
*/
public void setBuildStore(IBuildStore buildStore) {
this.buildStore = buildStore;
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the BuildStore object that this scanner has been asked to add the build process to.
*
* @return The BuildStore we'll write the trace file's data into.
*/
public IBuildStore getBuildStore() {
return buildStore;
}
/*-------------------------------------------------------------------------------------*/
/**
* Set the debug level of the scanner to control how much debug output is displayed.
*
* @param level 0 (none), 1 (basic debug), 2 (extended debug). Any value > 2
* is consider to be the same as 2.
*/
public void setDebugLevel(int level) {
/* validate the range, and restrict to meaningful values (without giving an error) */
if (level < 0) {
level = 0;
} else if (level > 2) {
level = 2;
}
debugLevel = level;
}
/*-------------------------------------------------------------------------------------*/
/**
* Return the current debug level (0, 1 or 2).
*
* @return The current debug level (0, 1 or 2).
*/
public int getDebugLevel() {
return debugLevel;
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoke a shell command, trace the behavior of the command to see which sub-processes
* are created, and which files are accessed by those processes, then generate a trace file
* as output. Note: this method does not create a BuildStore, but generates the
* trace file that parseTraceFile() can use to populate a BuildStore.
*
* @param args The shell command line arguments (as would normally be passed into a main()
* function).
* @param workingDir If not null, the directory in which to execute the command (if null,
* use the current directory).
* @param outStream The PrintStream on which the traced command's output should be displayed.
* @param useShell If true, pass the single (quoted) command line argument through a shell.
* @throws InterruptedException The scan operation was interrupted before it completed fully.
* @throws IOException The build command was not found, or failed to execute for some reason.
*/
public void traceShellCommand(String args[], File workingDir, PrintStream outStream, boolean useShell)
throws IOException, InterruptedException {
/* locate the "cfs" executable program (in $BUILDML_HOME/bin) */
String buildMlHome = System.getenv("BUILDML_HOME");
if (buildMlHome == null) {
buildMlHome = System.getProperty("BUILDML_HOME");
if (buildMlHome == null) {
throw new IOException(
"Unable to locate cfs tool. BUILDML_HOME environment variable not set.");
}
}
/*
* Create an array of all the command line arguments. If the user
* specified --trace-file, we also pass that to the cfs command.
*/
ArrayList<String> allArgs = new ArrayList<String>(args.length + 10);
allArgs.add(buildMlHome + "/bin/cfs");
/* pass the trace file name (which will default to "cfs.trace" otherwise) */
allArgs.add("-o");
allArgs.add(traceFilePathName);
/* pass the log file name (which will default to "cfs.log" otherwise) */
allArgs.add("-l");
allArgs.add(logFileName);
/* pass debug flags */
allArgs.add("-d");
allArgs.add(String.valueOf(getDebugLevel()));
/* should the command argument be passed through a shell? */
if (useShell) {
allArgs.add("-c");
}
/* now the command's arguments */
for (int i = 0; i < args.length; i++) {
allArgs.add(args[i]);
}
/*
* Execute the command, echoing the output/error to our console (but don't capture it
* in a buffer since we won't be looking at it.
*/
String allArgsArray[] = allArgs.toArray(new String[0]);
ShellResult result =
SystemUtils.executeShellCmd(allArgsArray, "", outStream, false, workingDir);
if (result.getReturnCode() != 0) {
String errString = "";
for (int i = 0; i < allArgsArray.length; i++) {
errString += (allArgsArray[i] + " ");
}
throw new IOException("Failed to execute shell command: " + errString);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Parse the content of an existing trace file, as is generated by traceShellCommand().
* If the caller had previously invoked setBuildStore() with a BuildStore object, this
* method will add the parsed information to that BuildStore.
*/
public void parseTraceFile() {
/*
* We now have a cfs.trace file in the current directory. We should parse this file
* and read the content into our BuildStore.
*/
TraceFileScanner scanner = null;
/*
* Open the log file for writing (we append, since we don't want to overwrite
* data that CFS stored in the file).
*/
PrintStream debugOut;
try {
debugOut = new PrintStream(new FileOutputStream(logFileName, true));
} catch (FileNotFoundException e1) {
throw new FatalBuildScannerError("Log file not found: " + logFileName);
}
try {
scanner = new TraceFileScanner(traceFilePathName,
getBuildStore(), debugOut, getDebugLevel());
scanner.parse();
scanner.close();
} catch (FileNotFoundException e) {
throw new FatalBuildScannerError("Trace file not found: " + traceFilePathName);
} catch (IOException e) {
throw new FatalBuildScannerError("Can't parse trace file: " + traceFilePathName);
}
/* close the debug log file */
debugOut.close();
}
/*-------------------------------------------------------------------------------------*/
}