auto import from //branches/cupcake/...@127101

This commit is contained in:
The Android Open Source Project
2009-01-20 14:03:55 -08:00
parent b8d704a517
commit 7b53e39377
57 changed files with 1009 additions and 1269 deletions

View File

@@ -31,6 +31,11 @@ import java.io.File;
*/
public final class SdkConstants {
/** An SDK Project's AndroidManifest.xml file */
public static final String FN_ANDROID_MANIFEST_XML= "AndroidManifest.xml";
/** An SDK Project's build.xml file */
public final static String FN_BUILD_XML = "build.xml";
/** Name of the framework library, i.e. "android.jar" */
public static final String FN_FRAMEWORK_LIBRARY = "android.jar";
/** Name of the layout attributes, i.e. "attrs.xml" */
@@ -129,7 +134,9 @@ public final class SdkConstants {
/** Name of the addon libs folder. */
public final static String FD_ADDON_LIBS = "libs";
/** Namespace for the resource XML, i.e. "http://schemas.android.com/apk/res/android" */
public final static String NS_RESOURCES = "http://schemas.android.com/apk/res/android";
/* Folder path relative to the SDK root */
/** Path of the documentation directory relative to the sdk folder.
* This is an OS path, ending with a separator. */
@@ -206,7 +213,7 @@ public final class SdkConstants {
/** Returns the appropriate name for the 'android' command, which is 'android.bat' for
* Windows and 'android' for all other platforms. */
public static String AndroidCmdName() {
public static String androidCmdName() {
String os = System.getProperty("os.name");
String cmd = "android";
if (os.startsWith("Windows")) {
@@ -215,4 +222,15 @@ public final class SdkConstants {
return cmd;
}
/** Returns the appropriate name for the 'mksdcard' command, which is 'mksdcard.exe' for
* Windows and 'mkdsdcard' for all other platforms. */
public static String mkSdCardCmdName() {
String os = System.getProperty("os.name");
String cmd = "mksdcard";
if (os.startsWith("Windows")) {
cmd += ".exe";
}
return cmd;
}
}

View File

@@ -21,14 +21,27 @@ import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.project.ProjectProperties.PropertyType;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/**
* Creates the basic files needed to get an Android project up and running. Also
@@ -38,16 +51,31 @@ import java.util.Map;
*/
public class ProjectCreator {
/** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */
private final static String PH_JAVA_FOLDER = "PACKAGE_PATH";
/** Package name substitution string used in template files, i.e. "PACKAGE" */
private final static String PH_PACKAGE = "PACKAGE";
/** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". */
private final static String PH_ACTIVITY_NAME = "ACTIVITY_NAME";
/** Project name substitution string used in template files, i.e. "PROJECT_NAME". */
private final static String PH_PROJECT_NAME = "PROJECT_NAME";
private final static String FOLDER_TESTS = "tests";
public enum OutputLevel {
SILENT, NORMAL, VERBOSE;
/** Silent mode. Project creation will only display errors. */
SILENT,
/** Normal mode. Project creation will display what's being done, display
* error but not warnings. */
NORMAL,
/** Verbose mode. Project creation will display what's being done, errors and warnings. */
VERBOSE;
}
/**
* Exception thrown when a project creation fails, typically because a template
* file cannot be written.
*/
private static class ProjectCreateException extends Exception {
/** default UID. This will not be serialized anyway. */
private static final long serialVersionUID = 1L;
@@ -78,7 +106,8 @@ public class ProjectCreator {
/**
* Creates a new project.
* @param folderPath the folder of the project to create. This folder must exist.
*
* @param folderPath the folder of the project to create.
* @param projectName the name of the project.
* @param packageName the package of the project.
* @param activityName the activity of the project as it will appear in the manifest.
@@ -113,16 +142,16 @@ public class ProjectCreator {
try {
String[] content = projectFolder.list();
if (content == null) {
error = "Project directory %1$s is not a directory.";
error = "Project folder '%1$s' is not a directory.";
} else if (content.length != 0) {
error = "Project directory %1$s is not empty. Please consider using '%2$s update' instead.";
error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead.";
}
} catch (Exception e1) {
e = e1;
}
if (e != null || error != null) {
mLog.error(e, error, projectFolder, SdkConstants.AndroidCmdName());
mLog.error(e, error, projectFolder, SdkConstants.androidCmdName());
}
}
@@ -164,6 +193,21 @@ public class ProjectCreator {
keywords.put(PH_ACTIVITY_NAME, activityName);
}
// Take the project name from the command line if there's one
if (projectName != null) {
keywords.put(PH_PROJECT_NAME, projectName);
} else {
if (activityName != null) {
// Use the activity as project name
keywords.put(PH_PROJECT_NAME, activityName);
} else {
// We need a project name. Just pick up the basename of the project
// directory.
projectName = projectFolder.getName();
keywords.put(PH_PROJECT_NAME, projectName);
}
}
// create the source folder and the java package folders.
final String srcFolderPath = SdkConstants.FD_SOURCES + File.separator + packagePath;
File sourceFolder = createDirs(projectFolder, srcFolderPath);
@@ -198,10 +242,13 @@ public class ProjectCreator {
manifestTemplate = "AndroidManifest.tests.template";
}
installTemplate(manifestTemplate, new File(projectFolder, "AndroidManifest.xml"),
installTemplate(manifestTemplate,
new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML),
keywords, target);
installTemplate("build.template", new File(projectFolder, "build.xml"), keywords);
installTemplate("build.template",
new File(projectFolder, SdkConstants.FN_BUILD_XML),
keywords);
// if this is not a test project, then we create one.
if (isTestProject == false) {
@@ -219,6 +266,293 @@ public class ProjectCreator {
}
}
/**
* Updates an existing project.
* <p/>
* Workflow:
* <ul>
* <li> Check AndroidManifest.xml is present (required)
* <li> Check there's a default.properties with a target *or* --target was specified
* <li> Update default.prop if --target was specified
* <li> Refresh/create "sdk" in local.properties
* <li> Build.xml: create if not present or no <androidinit(\w|/>) in it
* </ul>
*
* @param folderPath the folder of the project to update. This folder must exist.
* @param target the project target. Can be null.
* @param projectName The project name from --name. Can be null.
*/
public void updateProject(String folderPath, IAndroidTarget target, String projectName ) {
// project folder must exist and be a directory, since this is an update
File projectFolder = new File(folderPath);
if (!projectFolder.isDirectory()) {
mLog.error(null, "Project folder '%1$s' is not a valid directory, this is not an Android project you can update.",
projectFolder);
return;
}
// Check AndroidManifest.xml is present
File androidManifest = new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML);
if (!androidManifest.isFile()) {
mLog.error(null,
"%1$s not found in '%2$s', this is not an Android project you can update.",
SdkConstants.FN_ANDROID_MANIFEST_XML,
folderPath);
return;
}
// Check there's a default.properties with a target *or* --target was specified
ProjectProperties props = ProjectProperties.load(folderPath, PropertyType.DEFAULT);
if (props == null || props.getProperty(ProjectProperties.PROPERTY_TARGET) == null) {
if (target == null) {
mLog.error(null,
"There is no %1$s file in '%2$s'. Please provide a --target to the '%3$s update' command.",
PropertyType.DEFAULT.getFilename(),
folderPath,
SdkConstants.androidCmdName());
return;
}
}
// Update default.prop iif --target was specified
if (target != null) {
props = ProjectProperties.create(folderPath, PropertyType.DEFAULT);
props.setAndroidTarget(target);
try {
props.save();
println("Updated %1$s", PropertyType.DEFAULT.getFilename());
} catch (IOException e) {
mLog.error(e, "Failed to write %1$s file in '%2$s'",
PropertyType.DEFAULT.getFilename(),
folderPath);
return;
}
}
// Refresh/create "sdk" in local.properties
props = ProjectProperties.create(folderPath, PropertyType.LOCAL);
props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder);
try {
props.save();
println("Updated %1$s", PropertyType.LOCAL.getFilename());
} catch (IOException e) {
mLog.error(e, "Failed to write %1$s file in '%2$s'",
PropertyType.LOCAL.getFilename(),
folderPath);
return;
}
// Build.xml: create if not present or no <androidinit/> in it
File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML);
boolean needsBuildXml = projectName != null || !buildXml.exists();
if (!needsBuildXml) {
// Note that "<androidinit" must be followed by either a whitespace, a "/" (for the
// XML /> closing tag) or an end-of-line. This way we know the XML tag is really this
// one and later we will be able to use an "androidinit2" tag or such as necessary.
needsBuildXml = !checkFileContainsRegexp(buildXml, "<androidinit(?:\\s|/|$)");
if (needsBuildXml) {
println("File %1$s is too old and needs to be updated.", SdkConstants.FN_BUILD_XML);
}
}
if (needsBuildXml) {
// create the map for place-holders of values to replace in the templates
final HashMap<String, String> keywords = new HashMap<String, String>();
// Take the project name from the command line if there's one
if (projectName != null) {
keywords.put(PH_PROJECT_NAME, projectName);
} else {
extractPackageFromManifest(androidManifest, keywords);
if (keywords.containsKey(PH_ACTIVITY_NAME)) {
// Use the activity as project name
keywords.put(PH_PROJECT_NAME, keywords.get(PH_ACTIVITY_NAME));
} else {
// We need a project name. Just pick up the basename of the project
// directory.
projectName = projectFolder.getName();
keywords.put(PH_PROJECT_NAME, projectName);
}
}
if (mLevel == OutputLevel.VERBOSE) {
println("Regenerating %1$s with project name %2$s",
SdkConstants.FN_BUILD_XML,
keywords.get(PH_PROJECT_NAME));
}
try {
installTemplate("build.template",
new File(projectFolder, SdkConstants.FN_BUILD_XML),
keywords);
} catch (ProjectCreateException e) {
mLog.error(e, null);
}
}
}
/**
* Returns true if any line of the input file contains the requested regexp.
*/
private boolean checkFileContainsRegexp(File file, String regexp) {
Pattern p = Pattern.compile(regexp);
try {
BufferedReader in = new BufferedReader(new FileReader(file));
String line;
while ((line = in.readLine()) != null) {
if (p.matcher(line).find()) {
return true;
}
}
in.close();
} catch (Exception e) {
// ignore
}
return false;
}
/**
* Extracts a "full" package & activity name from an AndroidManifest.xml.
* <p/>
* The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}.
* If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_NAME}.
* When no activity is found, this key is not created.
*
* @param manifestFile The AndroidManifest.xml file
* @param outKeywords Place where to put the out parameters: package and activity names.
* @return True if the package/activity was parsed and updated in the keyword dictionary.
*/
private boolean extractPackageFromManifest(File manifestFile,
Map<String, String> outKeywords) {
try {
final String nsPrefix = "android";
final String nsURI = SdkConstants.NS_RESOURCES;
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
public String getNamespaceURI(String prefix) {
if (nsPrefix.equals(prefix)) {
return nsURI;
}
return XMLConstants.NULL_NS_URI;
}
public String getPrefix(String namespaceURI) {
if (nsURI.equals(namespaceURI)) {
return nsPrefix;
}
return null;
}
@SuppressWarnings("unchecked")
public Iterator getPrefixes(String namespaceURI) {
if (nsURI.equals(namespaceURI)) {
ArrayList<String> list = new ArrayList<String>();
list.add(nsPrefix);
return list.iterator();
}
return null;
}
});
InputSource source = new InputSource(new FileReader(manifestFile));
String packageName = xpath.evaluate("/manifest/@package", source);
source = new InputSource(new FileReader(manifestFile));
// Select the "android:name" attribute of all <activity> nodes but only if they
// contain a sub-node <intent-filter><action> with an "android:name" attribute which
// is 'android.intent.action.MAIN' and an <intent-filter><category> with an
// "android:name" attribute which is 'android.intent.category.LAUNCHER'
String expression = String.format("/manifest/application/activity" +
"[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " +
"intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" +
"/@%1$s:name", nsPrefix);
NodeList activityNames = (NodeList) xpath.evaluate(expression, source,
XPathConstants.NODESET);
// If we get here, both XPath expressions were valid so we're most likely dealing
// with an actual AndroidManifest.xml file. The nodes may not have the requested
// attributes though, if which case we should warn.
if (packageName == null || packageName.length() == 0) {
mLog.error(null,
"Missing <manifest package=\"...\"> in '%1$s'",
manifestFile.getName());
return false;
}
// Get the first activity that matched earlier. If there is no activity,
// activityName is set to an empty string and the generated "combined" name
// will be in the form "package." (with a dot at the end).
String activityName = "";
if (activityNames.getLength() > 0) {
activityName = activityNames.item(0).getNodeValue();
}
if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) {
println("WARNING: There is more than one activity defined in '%1$s'.\n" +
"Only the first one will be used. If this is not appropriate, you need\n" +
"to specify one of these values manually instead:",
manifestFile.getName());
for (int i = 0; i < activityNames.getLength(); i++) {
String name = activityNames.item(i).getNodeValue();
name = combinePackageActivityNames(packageName, name);
println("- %1$s", name);
}
}
if (activityName.length() == 0) {
mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" +
"No activity will be generated.",
nsPrefix, manifestFile.getName());
} else {
outKeywords.put(PH_ACTIVITY_NAME, activityName);
}
outKeywords.put(PH_PACKAGE, packageName);
return true;
} catch (IOException e) {
mLog.error(e, "Failed to read %1$s", manifestFile.getName());
} catch (XPathExpressionException e) {
Throwable t = e.getCause();
mLog.error(t == null ? e : t,
"Failed to parse %1$s",
manifestFile.getName());
}
return false;
}
private String combinePackageActivityNames(String packageName, String activityName) {
// Activity Name can have 3 forms:
// - ".Name" means this is a class name in the given package name.
// The full FQCN is thus packageName + ".Name"
// - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name"
// - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is.
// To be valid, the package name should have at least two components. This is checked
// later during the creation of the build.xml file, so we just need to detect there's
// a dot but not at pos==0.
int pos = activityName.indexOf('.');
if (pos == 0) {
return packageName + activityName;
} else if (pos > 0) {
return activityName;
} else {
return packageName + "." + activityName;
}
}
/**
* Installs a new file that is based on a template file provided by a given target.
* Each match of each key from the place-holder map in the template will be replaced with its
@@ -272,6 +606,9 @@ public class ProjectCreator {
*/
private void installFullPathTemplate(String sourcePath, File destFile,
Map<String, String> placeholderMap) throws ProjectCreateException {
boolean existed = destFile.exists();
try {
BufferedWriter out = new BufferedWriter(new FileWriter(destFile));
BufferedReader in = new BufferedReader(new FileReader(sourcePath));
@@ -293,17 +630,26 @@ public class ProjectCreator {
destFile, e.getMessage());
}
println("Added file %1$s", destFile);
println("%1$s file %2$s",
existed ? "Updated" : "Added",
destFile);
}
/**
* Prints a message unless silence is enabled.
* <p/>
* This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from
* {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}.
*
* @param format Format for String.format
* @param args Arguments for String.format
*/
private void println(String format, Object... args) {
if (mLevel == OutputLevel.VERBOSE) {
System.out.println(String.format(format, args));
if (mLevel != OutputLevel.SILENT) {
if (!format.endsWith("\n")) {
format += "\n";
}
mLog.printf(format, args);
}
}

View File

@@ -47,6 +47,10 @@ public final class ProjectProperties {
mFilename = filename;
mHeader = header;
}
public String getFilename() {
return mFilename;
}
}
private final static String LOCAL_HEADER =

View File

@@ -20,14 +20,17 @@ import com.android.prefs.AndroidLocation;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -42,13 +45,15 @@ public final class VmManager {
private final static String VM_INFO_PATH = "path";
private final static String VM_INFO_TARGET = "target";
private final static String IMAGE_USERDATA = "userdata.img";
private final static String CONFIG_INI = "config.ini";
private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\.ini$",
Pattern.CASE_INSENSITIVE);
private final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("\\d+[MK]?");
public static final class VmInfo {
String name;
String path;
@@ -69,10 +74,12 @@ public final class VmManager {
private final ArrayList<VmInfo> mVmList = new ArrayList<VmInfo>();
private ISdkLog mSdkLog;
private final SdkManager mSdk;
public VmManager(SdkManager sdk, ISdkLog sdkLog) throws AndroidLocationException {
mSdk = sdk;
mSdkLog = sdkLog;
buildVmList(sdk);
buildVmList();
}
/**
@@ -104,12 +111,12 @@ public final class VmManager {
* @param name the name of the VM
* @param target the target of the VM
* @param skinName the name of the skin. Can be null.
* @param sdcardPath the path to the sdCard. Can be null.
* @param sdcardSize the size of a local sdcard to create. Can be 0 for no local sdcard.
* @param sdcard the parameter value for the sdCard. Can be null. This is either a path to
* an existing sdcard image or a sdcard size (\d+, \d+K, \dM).
* @param hardwareConfig the hardware setup for the VM
*/
public VmInfo createVm(String parentFolder, String name, IAndroidTarget target,
String skinName, String sdcardPath, int sdcardSize, Map<String,String> hardwareConfig,
String skinName, String sdcard, Map<String,String> hardwareConfig,
ISdkLog log) {
try {
@@ -128,7 +135,7 @@ public final class VmManager {
}
return null;
}
// create the vm folder.
vmFolder.mkdir();
@@ -177,15 +184,40 @@ public final class VmManager {
}
}
if (sdcardPath != null) {
File sdcard = new File(sdcardPath);
if (sdcard.isFile()) {
values.put("sdcard", sdcardPath);
} else if (log != null) {
log.warning("sdcarad image '%1$s' does not exists.", sdcardPath);
if (sdcard != null) {
File sdcardFile = new File(sdcard);
if (sdcardFile.isFile()) {
values.put("sdcard", sdcard);
} else {
// check that it matches the pattern for sdcard size
Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard);
if (m.matches()) {
// create the sdcard.
sdcardFile = new File(vmFolder, "sdcard.img");
String path = sdcardFile.getAbsolutePath();
// execute mksdcard with the proper parameters.
File toolsFolder = new File(mSdk.getLocation(), SdkConstants.FD_TOOLS);
File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName());
if (mkSdCard.isFile() == false) {
log.error(null, "'%1$s' is missing from the SDK tools folder.",
mkSdCard.getName());
return null;
}
if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) {
return null; // mksdcard output has already been displayed, no need to
// output anything else.
}
// add its path to the values.
values.put("sdcard", path);
} else {
log.error(null, "'%1$s' is not recognized as a valid sdcard value", sdcard);
return null;
}
}
} else if (sdcardSize != 0) {
// TODO: create sdcard image.
}
if (hardwareConfig != null) {
@@ -227,7 +259,7 @@ public final class VmManager {
return null;
}
private void buildVmList(SdkManager sdk) throws AndroidLocationException {
private void buildVmList() throws AndroidLocationException {
// get the Android prefs location.
String vmRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_VMS;
@@ -253,14 +285,14 @@ public final class VmManager {
});
for (File vm : vms) {
VmInfo info = parseVmInfo(vm, sdk);
VmInfo info = parseVmInfo(vm);
if (info != null) {
mVmList.add(info);
}
}
}
private VmInfo parseVmInfo(File path, SdkManager sdk) {
private VmInfo parseVmInfo(File path) {
Map<String, String> map = SdkManager.parsePropertyFile(path, mSdkLog);
String vmPath = map.get(VM_INFO_PATH);
@@ -273,7 +305,7 @@ public final class VmManager {
return null;
}
IAndroidTarget target = sdk.getTargetFromHashString(targetHash);
IAndroidTarget target = mSdk.getTargetFromHashString(targetHash);
if (target == null) {
return null;
}
@@ -301,4 +333,118 @@ public final class VmManager {
writer.close();
}
private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) {
try {
String[] command = new String[3];
command[0] = toolLocation;
command[1] = size;
command[2] = location;
Process process = Runtime.getRuntime().exec(command);
ArrayList<String> errorOutput = new ArrayList<String>();
ArrayList<String> stdOutput = new ArrayList<String>();
int status = grabProcessOutput(process, errorOutput, stdOutput,
true /* waitForReaders */);
if (status != 0) {
log.error(null, "Failed to create the SD card.");
for (String error : errorOutput) {
log.error(null, error);
}
return false;
}
return true;
} catch (InterruptedException e) {
log.error(null, "Failed to create the SD card.");
} catch (IOException e) {
log.error(null, "Failed to create the SD card.");
}
return false;
}
/**
* Gets the stderr/stdout outputs of a process and returns when the process is done.
* Both <b>must</b> be read or the process will block on windows.
* @param process The process to get the ouput from
* @param errorOutput The array to store the stderr output. cannot be null.
* @param stdOutput The array to store the stdout output. cannot be null.
* @param waitforReaders if true, this will wait for the reader threads.
* @return the process return code.
* @throws InterruptedException
*/
private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput,
final ArrayList<String> stdOutput, boolean waitforReaders)
throws InterruptedException {
assert errorOutput != null;
assert stdOutput != null;
// read the lines as they come. if null is returned, it's
// because the process finished
Thread t1 = new Thread("") { //$NON-NLS-1$
@Override
public void run() {
// create a buffer to read the stderr output
InputStreamReader is = new InputStreamReader(process.getErrorStream());
BufferedReader errReader = new BufferedReader(is);
try {
while (true) {
String line = errReader.readLine();
if (line != null) {
errorOutput.add(line);
} else {
break;
}
}
} catch (IOException e) {
// do nothing.
}
}
};
Thread t2 = new Thread("") { //$NON-NLS-1$
@Override
public void run() {
InputStreamReader is = new InputStreamReader(process.getInputStream());
BufferedReader outReader = new BufferedReader(is);
try {
while (true) {
String line = outReader.readLine();
if (line != null) {
stdOutput.add(line);
} else {
break;
}
}
} catch (IOException e) {
// do nothing.
}
}
};
t1.start();
t2.start();
// it looks like on windows process#waitFor() can return
// before the thread have filled the arrays, so we wait for both threads and the
// process itself.
if (waitforReaders) {
try {
t1.join();
} catch (InterruptedException e) {
}
try {
t2.join();
} catch (InterruptedException e) {
}
}
// get the return code from the process
return process.waitFor();
}
}