PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/DLR/Microsoft.Dynamic/Hosting/Shell/Remote/RemoteConsoleHost.cs

#
C# | 285 lines | 168 code | 61 blank | 56 comment | 21 complexity | 6f30092951ba0b7c5373bf6df3efc83c MD5 | raw file
Possible License(s): BSD-3-Clause
  1. /* ****************************************************************************
  2. *
  3. * Copyright (c) Microsoft Corporation.
  4. *
  5. * This source code is subject to terms and conditions of the Apache License, Version 2.0. A
  6. * copy of the license can be found in the License.html file at the root of this distribution. If
  7. * you cannot locate the Apache License, Version 2.0, please send an email to
  8. * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
  9. * by the terms of the Apache License, Version 2.0.
  10. *
  11. * You must not remove this notice, or any other, from this software.
  12. *
  13. *
  14. * ***************************************************************************/
  15. using System;
  16. using System.Diagnostics;
  17. using System.Runtime.Remoting;
  18. using System.Runtime.Remoting.Channels;
  19. using System.Runtime.Remoting.Channels.Ipc;
  20. using System.Runtime.Serialization;
  21. using System.Threading;
  22. namespace Microsoft.Scripting.Hosting.Shell.Remote {
  23. /// <summary>
  24. /// ConsoleHost where the ScriptRuntime is hosted in a separate process (referred to as the remote runtime server)
  25. ///
  26. /// The RemoteConsoleHost spawns the remote runtime server and specifies an IPC channel name to use to communicate
  27. /// with each other. The remote runtime server creates and initializes a ScriptRuntime and a ScriptEngine, and publishes
  28. /// it over the specified IPC channel at a well-known URI. Note that the RemoteConsoleHost cannot easily participate
  29. /// in the initialization of the ScriptEngine as classes like LanguageContext are not remotable.
  30. ///
  31. /// The RemoteConsoleHost then starts the interactive loop and executes commands on the ScriptEngine over the remoting channel.
  32. /// The RemoteConsoleHost listens to stdout of the remote runtime server and echos it locally to the user.
  33. /// </summary>
  34. public abstract class RemoteConsoleHost : ConsoleHost, IDisposable {
  35. Process _remoteRuntimeProcess;
  36. internal RemoteCommandDispatcher _remoteCommandDispatcher;
  37. private string _channelName = RemoteConsoleHost.GetChannelName();
  38. private IpcChannel _clientChannel;
  39. private AutoResetEvent _remoteOutputReceived = new AutoResetEvent(false);
  40. private ScriptScope _scriptScope;
  41. #region Private methods
  42. private static string GetChannelName() {
  43. return "RemoteRuntime-" + Guid.NewGuid().ToString();
  44. }
  45. private ProcessStartInfo GetProcessStartInfo() {
  46. ProcessStartInfo processInfo = new ProcessStartInfo();
  47. processInfo.Arguments = RemoteRuntimeServer.RemoteRuntimeArg + " " + _channelName;
  48. processInfo.CreateNoWindow = true;
  49. // Set UseShellExecute to false to enable redirection.
  50. processInfo.UseShellExecute = false;
  51. // Redirect the standard streams. The output streams will be read asynchronously using an event handler.
  52. processInfo.RedirectStandardError = true;
  53. processInfo.RedirectStandardOutput = true;
  54. // The input stream can be ignored as the remote server process does not need to read any input
  55. processInfo.RedirectStandardInput = true;
  56. CustomizeRemoteRuntimeStartInfo(processInfo);
  57. Debug.Assert(processInfo.FileName != null);
  58. return processInfo;
  59. }
  60. private void StartRemoteRuntimeProcess() {
  61. Process process = new Process();
  62. process.StartInfo = GetProcessStartInfo();
  63. process.OutputDataReceived += new DataReceivedEventHandler(OnOutputDataReceived);
  64. process.ErrorDataReceived += new DataReceivedEventHandler(OnErrorDataReceived);
  65. process.Exited += new EventHandler(OnRemoteRuntimeExited);
  66. _remoteRuntimeProcess = process;
  67. process.Start();
  68. // Start the asynchronous read of the output streams.
  69. process.BeginOutputReadLine();
  70. process.BeginErrorReadLine();
  71. // wire up exited
  72. process.EnableRaisingEvents = true;
  73. // Wait for the output marker to know when the startup output is complete
  74. _remoteOutputReceived.WaitOne();
  75. if (process.HasExited) {
  76. throw new RemoteRuntimeStartupException("Remote runtime terminated during startup with exitcode " + process.ExitCode);
  77. }
  78. }
  79. private T GetRemoteObject<T>(string uri) {
  80. T result = (T)Activator.GetObject(typeof(T), "ipc://" + _channelName + "/" + uri);
  81. // Ensure that the remote object is responsive by calling a virtual method (which will be executed remotely)
  82. Debug.Assert(result.ToString() != null);
  83. return result;
  84. }
  85. private void InitializeRemoteScriptEngine() {
  86. StartRemoteRuntimeProcess();
  87. _remoteCommandDispatcher = GetRemoteObject<RemoteCommandDispatcher>(RemoteRuntimeServer.CommandDispatcherUri);
  88. _scriptScope = _remoteCommandDispatcher.ScriptScope;
  89. Engine = _scriptScope.Engine;
  90. // Register a channel for the reverse direction, when the remote runtime process wants to fire events
  91. // or throw an exception
  92. string clientChannelName = _channelName.Replace("RemoteRuntime", "RemoteConsole");
  93. _clientChannel = RemoteRuntimeServer.CreateChannel(clientChannelName, clientChannelName);
  94. ChannelServices.RegisterChannel(_clientChannel, false);
  95. }
  96. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
  97. protected virtual void OnRemoteRuntimeExited(object sender, EventArgs args) {
  98. Debug.Assert(((Process)sender).HasExited);
  99. Debug.Assert(sender == _remoteRuntimeProcess || _remoteRuntimeProcess == null);
  100. EventHandler remoteRuntimeExited = RemoteRuntimeExited;
  101. if (remoteRuntimeExited != null) {
  102. remoteRuntimeExited(sender, args);
  103. }
  104. // StartRemoteRuntimeProcess also blocks on this event. Signal it in case the
  105. // remote runtime terminates during startup itself.
  106. _remoteOutputReceived.Set();
  107. // Nudge the ConsoleHost to exit the REPL loop
  108. Terminate(_remoteRuntimeProcess.ExitCode);
  109. }
  110. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")] // TODO: This is protected only for test code, which could be rewritten to not require this to be protected
  111. protected virtual void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs) {
  112. if (String.IsNullOrEmpty(eventArgs.Data)) {
  113. return;
  114. }
  115. string output = eventArgs.Data as string;
  116. if (output.Contains(RemoteCommandDispatcher.OutputCompleteMarker)) {
  117. Debug.Assert(output == RemoteCommandDispatcher.OutputCompleteMarker);
  118. _remoteOutputReceived.Set();
  119. } else {
  120. ConsoleIO.WriteLine(output, Style.Out);
  121. }
  122. }
  123. private void OnErrorDataReceived(object sender, DataReceivedEventArgs eventArgs) {
  124. if (!String.IsNullOrEmpty(eventArgs.Data)) {
  125. ConsoleIO.WriteLine((string)eventArgs.Data, Style.Error);
  126. }
  127. }
  128. #endregion
  129. public override void Terminate(int exitCode) {
  130. if (CommandLine == null) {
  131. // Terminate may be called during startup when CommandLine has not been initialized.
  132. // We could fix this by initializing CommandLine before starting the remote runtime process
  133. return;
  134. }
  135. base.Terminate(exitCode);
  136. }
  137. protected override CommandLine CreateCommandLine() {
  138. return new RemoteConsoleCommandLine(_scriptScope, _remoteCommandDispatcher, _remoteOutputReceived);
  139. }
  140. public ScriptScope ScriptScope { get { return CommandLine.ScriptScope; } }
  141. public Process RemoteRuntimeProcess { get { return _remoteRuntimeProcess; } }
  142. // TODO: We have to catch all exceptions as we are executing user code in the remote runtime, and we cannot control what
  143. // exception it may throw. This could be fixed if we built our own remoting channel which returned an error code
  144. // instead of propagating exceptions back from the remote runtime.
  145. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  146. protected override void UnhandledException(ScriptEngine engine, Exception e) {
  147. ((RemoteConsoleCommandLine)CommandLine).UnhandledExceptionWorker(e);
  148. }
  149. /// <summary>
  150. /// Called if the remote runtime process exits by itself. ie. without the remote console killing it.
  151. /// </summary>
  152. internal event EventHandler RemoteRuntimeExited;
  153. /// <summary>
  154. /// Allows the console to customize the environment variables, working directory, etc.
  155. /// </summary>
  156. /// <param name="processInfo">At the least, processInfo.FileName should be initialized</param>
  157. public abstract void CustomizeRemoteRuntimeStartInfo(ProcessStartInfo processInfo);
  158. /// <summary>
  159. /// Aborts the current active call to Execute by doing Thread.Abort
  160. /// </summary>
  161. /// <returns>true if a Thread.Abort was actually called. false if there is no active call to Execute</returns>
  162. public bool AbortCommand() {
  163. return _remoteCommandDispatcher.AbortCommand();
  164. }
  165. public override int Run(string[] args) {
  166. var runtimeSetup = CreateRuntimeSetup();
  167. var options = new ConsoleHostOptions();
  168. ConsoleHostOptionsParser = new ConsoleHostOptionsParser(options, runtimeSetup);
  169. try {
  170. ParseHostOptions(args);
  171. } catch (InvalidOptionException e) {
  172. Console.Error.WriteLine("Invalid argument: " + e.Message);
  173. return ExitCode = 1;
  174. }
  175. // Create IConsole early (with default settings) in order to be able to display startup output
  176. ConsoleIO = CreateConsole(null, null, new ConsoleOptions());
  177. InitializeRemoteScriptEngine();
  178. Runtime = Engine.Runtime;
  179. ExecuteInternal();
  180. return ExitCode;
  181. }
  182. #region IDisposable Members
  183. public virtual void Dispose(bool disposing) {
  184. if (!disposing) {
  185. // Managed fields cannot be reliably accessed during finalization since they may already have been finalized
  186. return;
  187. }
  188. _remoteOutputReceived.Close();
  189. if (_clientChannel != null) {
  190. ChannelServices.UnregisterChannel(_clientChannel);
  191. _clientChannel = null;
  192. }
  193. if (_remoteRuntimeProcess != null) {
  194. _remoteRuntimeProcess.Exited -= OnRemoteRuntimeExited;
  195. // Closing stdin is a signal to the remote runtime to exit the process.
  196. _remoteRuntimeProcess.StandardInput.Close();
  197. _remoteRuntimeProcess.WaitForExit(5000);
  198. if (!_remoteRuntimeProcess.HasExited) {
  199. _remoteRuntimeProcess.Kill();
  200. _remoteRuntimeProcess.WaitForExit();
  201. }
  202. _remoteRuntimeProcess = null;
  203. }
  204. }
  205. public void Dispose() {
  206. Dispose(true);
  207. GC.SuppressFinalize(this);
  208. }
  209. #endregion
  210. }
  211. [Serializable]
  212. public class RemoteRuntimeStartupException : Exception {
  213. public RemoteRuntimeStartupException() { }
  214. public RemoteRuntimeStartupException(string message)
  215. : base(message) {
  216. }
  217. public RemoteRuntimeStartupException(string message, Exception innerException)
  218. : base(message, innerException) {
  219. }
  220. protected RemoteRuntimeStartupException(SerializationInfo info, StreamingContext context)
  221. : base(info, context) {
  222. }
  223. }
  224. }