/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