2 // ToolTask.cs: Base class for command line tool tasks.
5 // Marek Sieradzki (marek.sieradzki@gmail.com)
6 // Ankit Jain (jankit@novell.com)
7 // Marek Safar (marek.safar@gmail.com)
9 // (C) 2005 Marek Sieradzki
10 // Copyright 2009 Novell, Inc (http://www.novell.com)
11 // Copyright 2014 Xamarin Inc
13 // Permission is hereby granted, free of charge, to any person obtaining
14 // a copy of this software and associated documentation files (the
15 // "Software"), to deal in the Software without restriction, including
16 // without limitation the rights to use, copy, modify, merge, publish,
17 // distribute, sublicense, and/or sell copies of the Software, and to
18 // permit persons to whom the Software is furnished to do so, subject to
19 // the following conditions:
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System.Diagnostics;
34 using System.Collections;
35 using System.Collections.Specialized;
37 using System.Resources;
39 using System.Text.RegularExpressions;
40 using Microsoft.Build.Framework;
41 using Mono.XBuild.Utilities;
42 using System.Threading;
43 using System.Collections.Generic;
45 using SCS = System.Collections.Specialized;
47 namespace Microsoft.Build.Utilities
49 public abstract class ToolTask : Task
54 string toolPath, toolExe;
55 Encoding responseFileEncoding;
56 MessageImportance standardErrorLoggingImportance;
57 MessageImportance standardOutputLoggingImportance;
58 StringBuilder toolOutput;
59 bool typeLoadException;
60 ManualResetEvent canceled;
65 this.standardErrorLoggingImportance = MessageImportance.High;
66 this.standardOutputLoggingImportance = MessageImportance.Normal;
69 protected ToolTask (ResourceManager taskResources)
70 : this (taskResources, null)
74 protected ToolTask (ResourceManager taskResources,
75 string helpKeywordPrefix)
77 this.TaskResources = taskResources;
78 this.HelpKeywordPrefix = helpKeywordPrefix;
79 this.responseFileEncoding = Encoding.UTF8;
80 this.timeout = Int32.MaxValue;
81 canceled = new ManualResetEvent (false);
85 protected virtual bool CallHostObjectToExecute ()
90 string CreateToolPath ()
93 if (string.IsNullOrEmpty (ToolPath)) {
94 tp = GenerateFullPathToTool ();
95 if (string.IsNullOrEmpty (tp))
99 // GenerateFullPathToTool can return path including tool name
101 if (string.IsNullOrEmpty (ToolExe))
104 tp = Path.GetDirectoryName (tp);
109 var path = Path.Combine (tp, ToolExe);
110 if (!File.Exists (path)) {
112 Log.LogError ("Tool executable '{0}' could not be found", path);
119 public override bool Execute ()
121 if (SkipTaskExecution ())
124 var tool_path = CreateToolPath ();
125 if (tool_path == null)
128 exitCode = ExecuteTool (tool_path, GenerateResponseFileCommands (),
129 GenerateCommandLineCommands ());
131 // HandleTaskExecutionErrors is called only if exitCode != 0
132 return exitCode == 0 || HandleTaskExecutionErrors ();
136 protected virtual string GetWorkingDirectory ()
141 protected virtual int ExecuteTool (string pathToTool,
142 string responseFileCommands,
143 string commandLineCommands)
146 if (pathToTool == null)
147 throw new ArgumentNullException ("pathToTool");
149 string responseFileName;
150 responseFileName = null;
151 toolOutput = new StringBuilder ();
154 string responseFileSwitch = String.Empty;
155 if (!String.IsNullOrEmpty (responseFileCommands)) {
156 responseFileName = Path.GetTempFileName ();
157 File.WriteAllText (responseFileName, responseFileCommands);
158 responseFileSwitch = GetResponseFileSwitch (responseFileName);
161 var pinfo = GetProcessStartInfo (pathToTool, commandLineCommands, responseFileSwitch);
162 LogToolCommand (String.Format ("Tool {0} execution started with arguments: {1} {2}",
163 pinfo.FileName, commandLineCommands, responseFileCommands));
165 var pendingLineFragmentOutput = new StringBuilder ();
166 var pendingLineFragmentError = new StringBuilder ();
167 var environmentOverride = GetAndLogEnvironmentVariables ();
169 // When StartProcess returns, the process has already .Start()'ed
170 // If we subscribe to the events after that, then for processes that
171 // finish executing before we can subscribe, we won't get the output/err
173 ProcessWrapper pw = ProcessService.StartProcess (pinfo,
174 (_, msg) => ProcessLine (pendingLineFragmentOutput, msg, StandardOutputLoggingImportance),
175 (_, msg) => ProcessLine (pendingLineFragmentError, msg, StandardErrorLoggingImportance),
177 environmentOverride);
179 pw.WaitForOutput (timeout == Int32.MaxValue ? Int32.MaxValue : timeout);
181 // Process any remaining line
182 ProcessLine (pendingLineFragmentOutput, StandardOutputLoggingImportance, true);
183 ProcessLine (pendingLineFragmentError, StandardErrorLoggingImportance, true);
185 exitCode = pw.ExitCode;
187 } catch (System.ComponentModel.Win32Exception e) {
188 Log.LogError ("Error executing tool '{0}': {1}", pathToTool, e.Message);
192 if (typeLoadException)
193 ProcessTypeLoadException ();
195 pendingLineFragmentOutput.Length = 0;
196 pendingLineFragmentError.Length = 0;
198 Log.LogMessage (MessageImportance.Low, "Tool {0} execution finished.", pathToTool);
201 DeleteTempFile (responseFileName);
205 void ProcessTypeLoadException ()
207 string output_str = toolOutput.ToString ();
208 Regex reg = new Regex (@".*WARNING.*used in (mscorlib|System),.*",
209 RegexOptions.Multiline);
211 if (reg.Match (output_str).Success)
213 "Error: A referenced assembly may be built with an incompatible " +
214 "CLR version. See the compilation output for more details.");
217 "Error: A dependency of a referenced assembly may be missing, or " +
218 "you may be referencing an assembly created with a newer CLR " +
219 "version. See the compilation output for more details.");
221 Log.LogError (output_str);
224 void ProcessLine (StringBuilder outputBuilder, MessageImportance importance, bool isLastLine)
226 if (outputBuilder.Length == 0)
229 if (isLastLine && !outputBuilder.ToString ().EndsWith (Environment.NewLine))
230 // last line, but w/o an trailing newline, so add that
231 outputBuilder.Append (Environment.NewLine);
233 ProcessLine (outputBuilder, null, importance);
236 void ProcessLine (StringBuilder outputBuilder, string line, MessageImportance importance)
238 // Add to any line fragment from previous call
240 outputBuilder.Append (line);
242 // Don't remove empty lines!
243 var lines = outputBuilder.ToString ().Split (new string [] {Environment.NewLine}, StringSplitOptions.None);
245 // Clear the builder. If any incomplete line is found,
246 // then it will get added back
247 outputBuilder.Length = 0;
248 for (int i = 0; i < lines.Length; i ++) {
249 string singleLine = lines [i];
250 if (i == lines.Length - 1 && !singleLine.EndsWith (Environment.NewLine)) {
251 // Last line doesn't end in newline, could be part of
252 // a bigger line. Save for later processing
253 outputBuilder.Append (singleLine);
257 toolOutput.AppendLine (singleLine);
259 // in case of typeLoadException, collect all the output
260 // and then handle in ProcessTypeLoadException
261 if (!typeLoadException)
262 LogEventsFromTextOutput (singleLine, importance);
266 protected virtual void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
268 if (singleLine.Length == 0) {
269 Log.LogMessage (singleLine, messageImportance);
273 if (singleLine.StartsWith ("Unhandled Exception: System.TypeLoadException") ||
274 singleLine.StartsWith ("Unhandled Exception: System.IO.FileNotFoundException")) {
275 typeLoadException = true;
278 // When IncludeDebugInformation is true, prevents the debug symbols stats from braeking this.
279 if (singleLine.StartsWith ("WROTE SYMFILE") ||
280 singleLine.StartsWith ("OffsetTable") ||
281 singleLine.StartsWith ("Compilation succeeded") ||
282 singleLine.StartsWith ("Compilation failed"))
285 var result = MSBuildErrorParser.TryParseLine (singleLine);
286 if (result == null) {
287 Log.LogMessage (messageImportance, singleLine);
291 string filename = result.Origin ?? GetType ().Name.ToUpper ();
293 if (result.IsError) {
320 protected virtual string GenerateCommandLineCommands ()
325 protected abstract string GenerateFullPathToTool ();
327 protected virtual string GenerateResponseFileCommands ()
332 protected virtual string GetResponseFileSwitch (string responseFilePath)
334 return String.Format ("@{0}", responseFilePath);
337 protected ProcessStartInfo GetProcessStartInfo (string pathToTool, string commandLineCommands, string responseFileSwitch)
339 var pinfo = new ProcessStartInfo (pathToTool, String.Format ("{0} {1}", commandLineCommands, responseFileSwitch));
341 pinfo.WorkingDirectory = GetWorkingDirectory () ?? Environment.CurrentDirectory;
342 pinfo.UseShellExecute = false;
343 pinfo.CreateNoWindow = true;
344 pinfo.RedirectStandardOutput = true;
345 pinfo.RedirectStandardError = true;
350 protected virtual bool HandleTaskExecutionErrors ()
352 if (!Log.HasLoggedErrors && exitCode != 0)
353 Log.LogError ("Tool exited with code: {0}. Output: {1}", exitCode,
354 toolOutput != null ? toolOutput.ToString () : String.Empty);
357 return ExitCode == 0 && !Log.HasLoggedErrors;
360 protected virtual HostObjectInitializationStatus InitializeHostObject ()
362 return HostObjectInitializationStatus.NoActionReturnSuccess;
365 protected virtual void LogToolCommand (string message)
367 Log.LogMessage (MessageImportance.Normal, message);
371 protected virtual void LogPathToTool (string toolName,
376 protected virtual bool SkipTaskExecution()
378 return !ValidateParameters ();
381 protected virtual bool ValidateParameters()
386 protected void DeleteTempFile (string fileName)
388 if (String.IsNullOrEmpty (fileName))
392 File.Delete (fileName);
393 } catch (IOException ioe) {
394 Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", ioe.Message);
395 } catch (UnauthorizedAccessException uae) {
396 Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", uae.Message);
400 // If EnvironmentVariables is defined, then merge EnvironmentOverride
401 // EnvironmentOverride is Obsolete'd in 4.0
403 // Returns the final set of environment variables and logs them
404 Dictionary<string, string> GetAndLogEnvironmentVariables ()
406 var env_vars = GetEnvironmentVariables ();
407 if (env_vars == null)
410 Log.LogMessage (MessageImportance.Low, "Environment variables being passed to the tool:");
411 foreach (var entry in env_vars)
412 Log.LogMessage (MessageImportance.Low, "\t{0}={1}", (string)entry.Key, (string)entry.Value);
417 Dictionary<string, string> GetEnvironmentVariables ()
419 var env_vars = new Dictionary<string, string> (StringComparer.InvariantCultureIgnoreCase);
421 if (EnvironmentVariables != null) {
422 foreach (string pair in EnvironmentVariables) {
423 string [] key_value = pair.Split ('=');
424 if (!String.IsNullOrEmpty (key_value [0]))
425 env_vars [key_value [0]] = key_value.Length > 1 ? key_value [1] : String.Empty;
429 if (EnvironmentOverride != null)
430 foreach (DictionaryEntry entry in EnvironmentOverride)
431 env_vars [(string)entry.Key] = (string)entry.Value;
436 protected virtual StringDictionary EnvironmentOverride
441 // Ignore EnvironmentOverride if this is set
442 public string[] EnvironmentVariables { get; set; }
445 public int ExitCode {
446 get { return exitCode; }
449 protected virtual Encoding ResponseFileEncoding
451 get { return responseFileEncoding; }
454 protected virtual Encoding StandardErrorEncoding
456 get { return Console.Error.Encoding; }
459 protected virtual MessageImportance StandardErrorLoggingImportance {
460 get { return standardErrorLoggingImportance; }
463 protected virtual Encoding StandardOutputEncoding
465 get { return Console.Out.Encoding; }
468 protected virtual MessageImportance StandardOutputLoggingImportance {
469 get { return standardOutputLoggingImportance; }
472 protected virtual bool HasLoggedErrors {
473 get { return Log.HasLoggedErrors; }
476 public virtual int Timeout
478 get { return timeout; }
479 set { timeout = value; }
482 public virtual string ToolExe
485 if (string.IsNullOrEmpty (toolExe))
490 set { toolExe = value; }
493 protected abstract string ToolName
498 public string ToolPath
500 get { return toolPath; }
501 set { toolPath = value; }
504 protected ManualResetEvent ToolCanceled {
510 public virtual void Cancel ()
515 protected MessageImportance StandardErrorImportanceToUse {
517 return MessageImportance.Normal;
521 protected MessageImportance StandardOutputImportanceToUse {
523 return MessageImportance.Low;
527 public bool LogStandardErrorAsError { get; set; }
528 public string StandardOutputImportance { get; set; }