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