// NAnt - A .NET build tool // Copyright (C) 2001-2003 Gerry Shaw // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Gerry Shaw (gerry_shaw@yahoo.com) // Scott Hernandez (ScottHernandez@hotmail.com) // Gert Driesen (gert.driesen@ardatis.com) using System; using System.Collections; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; using System.Xml; using NAnt.Core.Attributes; using NAnt.Core.Types; using NAnt.Core.Util; namespace NAnt.Core.Tasks { /// /// Provides the abstract base class for tasks that execute external applications. /// [Serializable()] public abstract class ExternalProgramBase : Task { #region Private Instance Fields private StreamReader _stdError; private StreamReader _stdOut; private ArgumentCollection _arguments = new ArgumentCollection(); private bool _useRuntimeEngine; private string _exeName; private int _timeout = Int32.MaxValue; private TextWriter _outputWriter; private TextWriter _errorWriter; private int _exitCode = UnknownExitCode; #endregion Private Instance Fields #region Public Static Fields /// /// Defines the exit code that will be returned by /// if the process could not be started, or did not exit (in time). /// public const int UnknownExitCode = -1000; #endregion Public Static Fields #region Private Static Fields private static readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// Will be used to ensure thread-safe operations. /// private static object _lockObject = new object(); #endregion Private Static Fields #region Public Instance Properties /// /// The name of the executable that should be used to launch the /// external program. /// /// /// The name of the executable that should be used to launch the external /// program, or if no name is specified. /// /// /// If available, the configured value in the NAnt configuration /// file will be used if no name is specified. /// [FrameworkConfigurable("exename")] public virtual string ExeName { get { return (_exeName != null) ? _exeName : Name; } set { _exeName = value; } } /// /// Gets the filename of the external program to start. /// /// /// The filename of the external program. /// /// /// Override in derived classes to explicitly set the location of the /// external tool. /// public virtual string ProgramFileName { get { return DetermineFilePath(); } } /// /// Gets the command-line arguments for the external program. /// /// /// The command-line arguments for the external program. /// public abstract string ProgramArguments { get; } /// /// Gets the file to which the standard output should be redirected. /// /// /// The file to which the standard output should be redirected, or /// if the standard output should not be /// redirected. /// /// /// The default implementation will never allow the standard output /// to be redirected to a file. Deriving classes should override this /// property to change this behaviour. /// public virtual FileInfo Output { get { return null; } set {} //so that it can be overriden. } /// /// Gets a value indicating whether output will be appended to the /// . /// /// /// if output should be appended to the ; /// otherwise, . /// public virtual bool OutputAppend { get { return false; } set {} //so that it can be overriden. } /// /// Gets the working directory for the application. /// /// /// The working directory for the application. /// public virtual DirectoryInfo BaseDirectory { get { return new DirectoryInfo(Project.BaseDirectory); } set {} // so that it can be overriden. } /// /// The maximum amount of time the application is allowed to execute, /// expressed in milliseconds. Defaults to no time-out. /// [TaskAttribute("timeout")] [Int32Validator()] public int TimeOut { get { return _timeout; } set { _timeout = value; } } /// /// The command-line arguments for the external program. /// [BuildElementArray("arg")] public virtual ArgumentCollection Arguments { get { return _arguments; } } /// /// Specifies whether the external program should be executed using a /// runtime engine, if configured. The default is . /// /// /// if the external program should be executed /// using a runtime engine; otherwise, . /// [FrameworkConfigurable("useruntimeengine")] public virtual bool UseRuntimeEngine { get { return _useRuntimeEngine; } set { _useRuntimeEngine = value; } } /// /// Gets or sets the to which standard output /// messages of the external program will be written. /// /// /// The to which standard output messages of /// the external program will be written. /// /// /// By default, standard output messages wil be written to the build log /// with level . /// public virtual TextWriter OutputWriter { get { if (_outputWriter == null) { _outputWriter = new LogWriter(this, Level.Info, CultureInfo.InvariantCulture); } return _outputWriter; } set { _outputWriter = value; } } /// /// Gets or sets the to which error output /// of the external program will be written. /// /// /// The to which error output of the external /// program will be written. /// /// /// By default, error output wil be written to the build log with level /// . /// public virtual TextWriter ErrorWriter { get { if (_errorWriter == null) { _errorWriter = new LogWriter(this, Level.Warning, CultureInfo.InvariantCulture); } return _errorWriter; } set { _errorWriter = value; } } /// /// Gets the value that the process specified when it terminated. /// /// /// The code that the associated process specified when it terminated, /// or -1000 if the process could not be started or did not /// exit (in time). /// public int ExitCode { get { return _exitCode; } } #endregion Public Instance Properties #region Override implementation of Task /// /// Starts the external process and captures its output. /// /// /// The external process did not finish within the configured timeout. /// -or- /// The exit code of the external process indicates a failure. /// protected override void ExecuteTask() { Thread outputThread = null; Thread errorThread = null; try { // Start the external process Process process = StartProcess(); outputThread = new Thread(new ThreadStart(StreamReaderThread_Output)); errorThread = new Thread(new ThreadStart(StreamReaderThread_Error)); _stdOut = process.StandardOutput; _stdError = process.StandardError; outputThread.Start(); errorThread.Start(); // Wait for the process to terminate process.WaitForExit(TimeOut); // Wait for the threads to terminate outputThread.Join(2000); errorThread.Join(2000); if (!process.HasExited) { try { process.Kill(); } catch { // ignore possible exceptions that are thrown when the // process is terminated } throw new BuildException( String.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1118"), ProgramFileName, TimeOut), Location); } _exitCode = process.ExitCode; if (process.ExitCode != 0) { throw new BuildException( String.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1119"), ProgramFileName, process.ExitCode), Location); } } catch (BuildException e) { if (FailOnError) { throw; } else { logger.Error("Execution Error", e); Log(Level.Error, e.Message); } } catch (Exception e) { logger.Error("Execution Error", e); throw new BuildException( string.Format(CultureInfo.InvariantCulture, "{0}: {1} had errors. Please see log4net log.", GetType().ToString(), ProgramFileName), Location, e); } finally { // ensure outputThread is always aborted if (outputThread != null && outputThread.IsAlive) { outputThread.Abort(); } // ensure errorThread is always aborted if (errorThread != null && errorThread.IsAlive) { errorThread.Abort(); } } } #endregion Override implementation of Task #region Public Instance Methods /// /// Gets the command-line arguments, separated by spaces. /// public string CommandLine { get { // append any nested arguments to the command line StringBuilder arguments = new StringBuilder(ProgramArguments); foreach (Argument arg in Arguments) { if (arg.IfDefined && !arg.UnlessDefined) { arguments.Append(' '); arguments.Append(arg.ToString()); } } return arguments.ToString(); } } #endregion Public Instance Methods #region Protected Instance Methods /// /// Updates the of the specified /// . /// /// The of which the should be updated. protected virtual void PrepareProcess(Process process){ // create process (redirect standard output to temp buffer) if (Project.TargetFramework != null && UseRuntimeEngine && Project.TargetFramework.RuntimeEngine != null) { process.StartInfo.FileName = Project.TargetFramework.RuntimeEngine.FullName; process.StartInfo.Arguments = string.Format(CultureInfo.InvariantCulture, "\"{0}\" {1}", ProgramFileName, CommandLine); } else { process.StartInfo.FileName = ProgramFileName; process.StartInfo.Arguments = CommandLine; } process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; //required to allow redirects process.StartInfo.UseShellExecute = false; // do not start process in new window process.StartInfo.CreateNoWindow = true; process.StartInfo.WorkingDirectory = BaseDirectory.FullName; // set framework-specific environment variables if executing the // external process using the runtime engine of the currently // active framework if (Project.TargetFramework != null && UseRuntimeEngine) { foreach (EnvironmentVariable environmentVariable in Project.TargetFramework.EnvironmentVariables) { if (environmentVariable.IfDefined && !environmentVariable.UnlessDefined) { if (environmentVariable.Value == null) { process.StartInfo.EnvironmentVariables[environmentVariable.VariableName] = ""; } else { process.StartInfo.EnvironmentVariables[environmentVariable.VariableName] = environmentVariable.Value; } } } } } /// /// Starts the process and handles errors. /// /// The that was started. protected virtual Process StartProcess() { Process p = new Process(); PrepareProcess(p); try { string msg = string.Format( CultureInfo.InvariantCulture, ResourceUtils.GetString("String_Starting_Program"), p.StartInfo.WorkingDirectory, p.StartInfo.FileName, p.StartInfo.Arguments); logger.Info(msg); Log(Level.Verbose, msg); p.Start(); return p; } catch (Exception ex) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1121"), p.StartInfo.FileName), Location, ex); } } #endregion Protected Instance Methods #region Private Instance Methods /// /// Reads from the stream until the external program is ended. /// private void StreamReaderThread_Output() { StreamReader reader = _stdOut; bool doAppend = OutputAppend; while (true) { string logContents = reader.ReadLine(); if (logContents == null) { break; } // ensure only one thread writes to the log at any time lock (_lockObject) { OutputWriter.WriteLine(logContents); if (Output != null) { StreamWriter writer = new StreamWriter(Output.FullName, doAppend); writer.WriteLine(logContents); doAppend = true; writer.Close(); } } } OutputWriter.Flush(); } /// /// Reads from the stream until the external program is ended. /// private void StreamReaderThread_Error() { StreamReader reader = _stdError; bool doAppend = OutputAppend; while (true) { string logContents = reader.ReadLine(); if (logContents == null) { break; } // ensure only one thread writes to the log at any time lock (_lockObject) { ErrorWriter.WriteLine(logContents); if (Output != null) { StreamWriter writer = new StreamWriter(Output.FullName, doAppend); writer.WriteLine(logContents); doAppend = true; writer.Close(); } } } ErrorWriter.Flush(); } /// /// Determines the path of the external program that should be executed. /// /// /// A fully qualifies pathname including the program name. /// /// The task is not available or not configured for the current framework. private string DetermineFilePath() { string fullPath = ""; // if the Exename is already specified as a full path then just use that. if (ExeName != null && Path.IsPathRooted(ExeName)) { return ExeName; } // get the ProgramLocation attribute ProgramLocationAttribute programLocationAttribute = (ProgramLocationAttribute) Attribute.GetCustomAttribute(this.GetType(), typeof(ProgramLocationAttribute)); if (programLocationAttribute != null) { // ensure we have a valid framework set. if ((programLocationAttribute.LocationType == LocationType.FrameworkDir || programLocationAttribute.LocationType == LocationType.FrameworkSdkDir) && (Project.TargetFramework == null)) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1120") + Environment.NewLine, Name)); } switch (programLocationAttribute.LocationType) { case LocationType.FrameworkDir: if (Project.TargetFramework.FrameworkDirectory != null) { string frameworkDir = Project.TargetFramework.FrameworkDirectory.FullName; fullPath = Path.Combine(frameworkDir, ExeName + ".exe"); } else { throw new BuildException( string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1124"), Project.TargetFramework.Name)); } break; case LocationType.FrameworkSdkDir: if (Project.TargetFramework.SdkDirectory != null) { string sdkDirectory = Project.TargetFramework.SdkDirectory.FullName; fullPath = Path.Combine(sdkDirectory, ExeName + ".exe"); } else { throw new BuildException( string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1122"), Project.TargetFramework.Name)); } break; } } else { // rely on it being on the path. fullPath = ExeName; } return fullPath; } #endregion Private Instance Methods } }