#region Copyright 2010-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.IO; using System.Reflection; using System.Runtime.Remoting; using System.Threading; using CSharpTest.Net.Utils; namespace CSharpTest.Net.Processes { /// /// Create an AppDomain configured to run the .Net Assembly provided and marshalls Console input/output to and /// from the app domain when run. This allow a more performant execution of .Net command-line tools while /// keeping with *most* of the behavior of running out-of-process. Some serious side effects can occur when /// using Environment.* settings like CurrentDirectory and ExitCode since these are shared with the appdomain. /// public class AssemblyRunner : IRunner { private static readonly string[] EmptyArgList = new string[0]; private readonly string _executable; private readonly AppDomain _workerDomain; private event ProcessOutputEventHandler _outputReceived; private event ProcessExitedEventHandler _processExited; private bool _disposed; private bool _isRunning; private string _workingDir; private volatile int _exitCode; private Thread _running; private Exception _exception; /// /// Constructs the AppDomain for the given executable by using it's path for the base directory and configuraiton file. /// public AssemblyRunner(string executable) { _executable = FileUtils.FindFullPath(Check.NotEmpty(executable)); AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = Path.GetDirectoryName(_executable); setup.ApplicationName = Path.GetFileNameWithoutExtension(_executable); setup.ConfigurationFile = _executable + ".config"; _workerDomain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup); } /// /// Ensures clean-up of the app domain... This has to be pushed off of the GC Cleanup thread as AppDoamin.Unload will /// fail on GC thread. /// ~AssemblyRunner() { if(!_disposed) { try { new Action(UnloadDomain).BeginInvoke(_workerDomain, null, null); } catch { } } } /// Ignores errors from the AppDomain.Unload since exceptions would be unhandled. static void UnloadDomain(AppDomain domain) { try { AppDomain.Unload(domain); } catch(Exception e) { System.Diagnostics.Trace.WriteLine(e.ToString(), typeof(AssemblyRunner).FullName); } } /// /// Returns true if this object's worker domain has been unloaded. /// public bool IsDisposed { get { return _disposed; } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public virtual void Dispose() { if (!_disposed) { _disposed = true; Kill(); AppDomain.Unload(_workerDomain); GC.SuppressFinalize(this); } } /// Returns a debug-view string of process/arguments to execute public override string ToString() { return _executable; } /// Notifies caller of writes to the std::err or std::out public event ProcessOutputEventHandler OutputReceived { add { lock (this) _outputReceived += value; } remove { lock (this) _outputReceived -= value; } } /// Notifies caller when the process exits public event ProcessExitedEventHandler ProcessExited { add { lock (this) _processExited += value; } remove { lock (this) _processExited -= value; } } [Obsolete("This is not implemented for domains", true)] TextWriter IRunner.StandardInput { get { throw new NotSupportedException(); } } /// Gets or sets the initial working directory for the process. public string WorkingDirectory { get { return _workingDir ?? Environment.CurrentDirectory; } set { _workingDir = value; } } /// Waits for the process to exit and returns the exit code public int ExitCode { get { WaitForExit(); return _exitCode; } } /// Returns true if this instance is running a process public bool IsRunning { get { return _isRunning && !WaitForExit(TimeSpan.Zero, false); } } /// Kills the process if it is still running public void Kill() { Thread t = _running; if (t != null && t.IsAlive) { t.Abort(); if (!WaitForExit(TimeSpan.FromMinutes(1))) throw new TimeoutException(); } } /// Closes std::in and waits for the process to exit public void WaitForExit() { WaitForExit(TimeSpan.MaxValue, true); } /// Closes std::in and waits for the process to exit, returns false if the process did not exit in the time given public bool WaitForExit(TimeSpan timeout) { return WaitForExit(timeout, true); } /// Waits for the process to exit, returns false if the process did not exit in the time given public bool WaitForExit(TimeSpan timeout, bool closeStdInput) { int waitTime = timeout.TotalMilliseconds >= int.MaxValue ? -1 : (int)timeout.TotalMilliseconds; Thread runner = _running; if (runner == null || !runner.IsAlive || runner.Join(waitTime)) { if (_exception != null) throw _exception; return true; } return false; } /// Runs the process and returns the exit code. public int Run() { return Run(EmptyArgList); } /// Runs the process with additional arguments and returns the exit code. public int Run(params string[] arguments) { return Run(null, arguments); } /// Runs the process with additional arguments and returns the exit code. public int Run(TextReader input, params string[] arguments) { Check.Assert(_isRunning == false); Execute(new StartArguments(input, arguments)); if (_exception != null) throw _exception; return ExitCode; } /// Starts the process and returns. public void Start() { Start(EmptyArgList); } /// Starts the process with additional arguments and returns. public void Start(params string[] arguments) { Start(null, arguments); } /// Starts the process with additional arguments and returns. public void Start(TextReader input, params string[] arguments) { Check.Assert(_isRunning == false); Thread t = new Thread(Execute); t.SetApartmentState(ApartmentState.MTA); t.Name = Path.GetFileName(_executable); try { _isRunning = true; t.Start(new StartArguments(input, arguments)); _running = t; } catch { _isRunning = false; throw; } } [Serializable] class DomainHook : MarshalByRefObject, IDisposable { public override object InitializeLifetimeService() { return null; } TextWriter _out, _err; TextReader _in; StringWriter _captureout, _captureerr; public void Dispose() { } public void Capture(string stdInput) { _out = Console.Out; _err = Console.Error; _in = Console.In; Console.SetOut(_captureout = new StringWriter()); Console.SetError(_captureerr = new StringWriter()); Console.SetIn(new StringReader(stdInput ?? String.Empty)); } public void GetOutput(out string stdout, out string stderr) { Console.SetIn(_in); _in = null; Console.SetOut(_out); Console.SetError(_err); _out = _err = null; stdout = _captureout.ToString(); _captureout.Dispose(); _captureout = null; stderr = _captureerr.ToString(); _captureerr.Dispose(); _captureerr = null; } } private class StartArguments { public StartArguments(TextReader input, string[] args) { StdInput = input; Arguments = args; } public readonly TextReader StdInput; public readonly string[] Arguments; } private void Execute(object objArgs) { string cwd = null; try { _exception = null; cwd = Environment.CurrentDirectory; StartArguments args = (StartArguments)objArgs; ObjectHandle obj = _workerDomain.CreateInstanceFrom(GetType().Assembly.Location, typeof(DomainHook).FullName); if (obj == null) throw new ApplicationException("Unable to hook child application domain."); using (DomainHook hook = (DomainHook)obj.Unwrap()) { hook.Capture(args.StdInput == null ? String.Empty : args.StdInput.ReadToEnd()); try { string[] arguments = args.Arguments; Environment.CurrentDirectory = WorkingDirectory; #if NET20 || NET35 _exitCode = _workerDomain.ExecuteAssembly(_executable, AppDomain.CurrentDomain.Evidence, arguments); #else _exitCode = _workerDomain.ExecuteAssembly(_executable, arguments); #endif } finally { string line, stdout, stderr; hook.GetOutput(out stdout, out stderr); if (_outputReceived != null) { using (StringReader r = new StringReader(stdout)) while (null != (line = r.ReadLine())) _outputReceived(this, new ProcessOutputEventArgs(line, false)); using (StringReader r = new StringReader(stderr)) while (null != (line = r.ReadLine())) _outputReceived(this, new ProcessOutputEventArgs(line, true)); } } } } catch (ThreadAbortException) { return; } catch (Exception e) { _exitCode = -1; try { ThreadStart savestack = Delegate.CreateDelegate(typeof(ThreadStart), e, "InternalPreserveStackTrace", false, false) as ThreadStart; if (savestack != null) savestack(); _exception = e; } catch { _exception = new TargetInvocationException(e); } } finally { _isRunning = false; _running = null; if(cwd != null) Environment.CurrentDirectory = cwd; if (_processExited != null) _processExited(this, new ProcessExitedEventArgs(_exitCode)); } } } }