File: RemoteConsoleHost.cs

package info (click to toggle)
dlr-languages 20090805%2Bgit.e6b28d27%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 51,484 kB
  • ctags: 59,257
  • sloc: cs: 298,829; ruby: 159,643; xml: 19,872; python: 2,820; yacc: 1,960; makefile: 96; sh: 65
file content (290 lines) | stat: -rw-r--r-- 12,354 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

#if !SILVERLIGHT // Remoting

using System;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Serialization;
using System.Threading;

namespace Microsoft.Scripting.Hosting.Shell.Remote {
    /// <summary>
    /// ConsoleHost where the ScriptRuntime is hosted in a separate process (referred to as the remote runtime server)
    /// 
    /// The RemoteConsoleHost spawns the remote runtime server and specifies an IPC channel name to use to communicate
    /// with each other. The remote runtime server creates and initializes a ScriptRuntime and a ScriptEngine, and publishes
    /// it over the specified IPC channel at a well-known URI. Note that the RemoteConsoleHost cannot easily participate
    /// in the initialization of the ScriptEngine as classes like LanguageContext are not remotable.
    /// 
    /// The RemoteConsoleHost then starts the interactive loop and executes commands on the ScriptEngine over the remoting channel.
    /// The RemoteConsoleHost listens to stdout of the remote runtime server and echos it locally to the user.
    /// </summary>
    public abstract class RemoteConsoleHost : ConsoleHost, IDisposable {
        Process _remoteRuntimeProcess;
        internal RemoteCommandDispatcher _remoteCommandDispatcher;
        private string _channelName = RemoteConsoleHost.GetChannelName();
        private IpcChannel _clientChannel;
        private AutoResetEvent _remoteOutputReceived = new AutoResetEvent(false);
        private ScriptScope _scriptScope;

        #region Private methods

        private static string GetChannelName() {
            return "RemoteRuntime-" + Guid.NewGuid().ToString();
        }

        private ProcessStartInfo GetProcessStartInfo() {
            ProcessStartInfo processInfo = new ProcessStartInfo();
            processInfo.Arguments = RemoteRuntimeServer.RemoteRuntimeArg + " " + _channelName;
            processInfo.CreateNoWindow = true;

            // Set UseShellExecute to false to enable redirection.
            processInfo.UseShellExecute = false;

            // Redirect the standard streams. The output streams will be read asynchronously using an event handler.
            processInfo.RedirectStandardError = true;
            processInfo.RedirectStandardOutput = true;
            // The input stream can be ignored as the remote server process does not need to read any input
            processInfo.RedirectStandardInput = true;

            CustomizeRemoteRuntimeStartInfo(processInfo);
            Debug.Assert(processInfo.FileName != null);
            return processInfo;
        }

        private void StartRemoteRuntimeProcess() {
            Process process = new Process();

            process.StartInfo = GetProcessStartInfo();

            process.OutputDataReceived += new DataReceivedEventHandler(OnOutputDataReceived);
            process.ErrorDataReceived += new DataReceivedEventHandler(OnErrorDataReceived);

            process.Exited += new EventHandler(OnRemoteRuntimeExited);
            _remoteRuntimeProcess = process;

            process.Start();

            // Start the asynchronous read of the output streams.
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            // wire up exited 
            process.EnableRaisingEvents = true;

            // Wait for the output marker to know when the startup output is complete
            _remoteOutputReceived.WaitOne();

            if (process.HasExited) {
                throw new RemoteRuntimeStartupException("Remote runtime terminated during startup with exitcode " + process.ExitCode);
            }
        }

        private T GetRemoteObject<T>(string uri) {
            T result = (T)Activator.GetObject(typeof(T), "ipc://" + _channelName + "/" + uri);

            // Ensure that the remote object is responsive by calling a virtual method (which will be executed remotely)
            Debug.Assert(result.ToString() != null);

            return result;
        }

        private void InitializeRemoteScriptEngine() {
            StartRemoteRuntimeProcess();

            _remoteCommandDispatcher = GetRemoteObject<RemoteCommandDispatcher>(RemoteRuntimeServer.CommandDispatcherUri);

            _scriptScope = _remoteCommandDispatcher.ScriptScope;
            Engine = _scriptScope.Engine;

            // Register a channel for the reverse direction, when the remote runtime process wants to fire events
            // or throw an exception
            string clientChannelName = _channelName.Replace("RemoteRuntime", "RemoteConsole");
            _clientChannel = RemoteRuntimeServer.CreateChannel(clientChannelName, clientChannelName);
            ChannelServices.RegisterChannel(_clientChannel, false);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void OnRemoteRuntimeExited(object sender, EventArgs args) {
            Debug.Assert(((Process)sender).HasExited);
            Debug.Assert(sender == _remoteRuntimeProcess || _remoteRuntimeProcess == null);

            EventHandler remoteRuntimeExited = RemoteRuntimeExited;
            if (remoteRuntimeExited != null) {
                remoteRuntimeExited(sender, args);
            }

            // StartRemoteRuntimeProcess also blocks on this event. Signal it in case the 
            // remote runtime terminates during startup itself.
            _remoteOutputReceived.Set();

            // Nudge the ConsoleHost to exit the REPL loop
            Terminate(_remoteRuntimeProcess.ExitCode);
        }

        [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
        protected virtual void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs) {
            if (String.IsNullOrEmpty(eventArgs.Data)) {
                return;
            }

            string output = eventArgs.Data as string;

            if (output.Contains(RemoteCommandDispatcher.OutputCompleteMarker)) {
                Debug.Assert(output == RemoteCommandDispatcher.OutputCompleteMarker);
                _remoteOutputReceived.Set();
            } else {
                ConsoleIO.WriteLine(output, Style.Out);
            }
        }

        private void OnErrorDataReceived(object sender, DataReceivedEventArgs eventArgs) {
            if (!String.IsNullOrEmpty(eventArgs.Data)) {
                ConsoleIO.WriteLine((string)eventArgs.Data, Style.Error);
            }
        }

        #endregion

        public override void Terminate(int exitCode) {
            if (CommandLine == null) {
                // Terminate may be called during startup when CommandLine has not been initialized.
                // We could fix this by initializing CommandLine before starting the remote runtime process
                return;
            }

            base.Terminate(exitCode);
        }

        protected override CommandLine CreateCommandLine() {
            return new RemoteConsoleCommandLine(_scriptScope, _remoteCommandDispatcher, _remoteOutputReceived);
        }

        public ScriptScope ScriptScope { get { return CommandLine.ScriptScope; } }
        public Process RemoteRuntimeProcess { get { return _remoteRuntimeProcess; } }

        // TODO: We have to catch all exceptions as we are executing user code in the remote runtime, and we cannot control what 
        // exception it may throw. This could be fixed if we built our own remoting channel which returned an error code
        // instead of propagating exceptions back from the remote runtime.
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected override void UnhandledException(ScriptEngine engine, Exception e) {
            ((RemoteConsoleCommandLine)CommandLine).UnhandledExceptionWorker(e);
        }
        /// <summary>
        /// Called if the remote runtime process exits by itself. ie. without the remote console killing it.
        /// </summary>
        internal event EventHandler RemoteRuntimeExited;

        /// <summary>
        /// Allows the console to customize the environment variables, working directory, etc.
        /// </summary>
        /// <param name="processInfo">At the least, processInfo.FileName should be initialized</param>
        public abstract void CustomizeRemoteRuntimeStartInfo(ProcessStartInfo processInfo);

        /// <summary>
        /// Aborts the current active call to Execute by doing Thread.Abort
        /// </summary>
        /// <returns>true if a Thread.Abort was actually called. false if there is no active call to Execute</returns>
        public bool AbortCommand() {
            return _remoteCommandDispatcher.AbortCommand();
        }

        public override int Run(string[] args) {
            var runtimeSetup = CreateRuntimeSetup();
            var options = new ConsoleHostOptions();
            ConsoleHostOptionsParser = new ConsoleHostOptionsParser(options, runtimeSetup);

            try {
                ParseHostOptions(args);
            } catch (InvalidOptionException e) {
                Console.Error.WriteLine("Invalid argument: " + e.Message);
                return ExitCode = 1;
            }

            _languageOptionsParser = CreateOptionsParser();

            // Create IConsole early (with default settings) in order to be able to display startup output
            ConsoleIO = CreateConsole(null, null, new ConsoleOptions());

            InitializeRemoteScriptEngine();
            Runtime = Engine.Runtime;

            ExecuteInternal();

            return ExitCode;
        }

        #region IDisposable Members

        public virtual void Dispose(bool disposing) {
            if (!disposing) {
                // Managed fields cannot be reliably accessed during finalization since they may already have been finalized
                return;
            }

            _remoteOutputReceived.Close();

            if (_clientChannel != null) {
                ChannelServices.UnregisterChannel(_clientChannel);
                _clientChannel = null;
            }

            if (_remoteRuntimeProcess != null) {
                _remoteRuntimeProcess.Exited -= OnRemoteRuntimeExited;

                // Closing stdin is a signal to the remote runtime to exit the process.
                _remoteRuntimeProcess.StandardInput.Close();
                _remoteRuntimeProcess.WaitForExit(5000);

                if (!_remoteRuntimeProcess.HasExited) {
                    _remoteRuntimeProcess.Kill();
                    _remoteRuntimeProcess.WaitForExit();
                }

                _remoteRuntimeProcess = null;
            }
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }

    [Serializable]
    public class RemoteRuntimeStartupException : Exception {
        public RemoteRuntimeStartupException() { }

        public RemoteRuntimeStartupException(string message)
            : base(message) {
        }

        public RemoteRuntimeStartupException(string message, Exception innerException)
            : base(message, innerException) {
        }

        protected RemoteRuntimeStartupException(SerializationInfo info, StreamingContext context)
            : base(info, context) {
        }
    }
}

#endif