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
|