/* ****************************************************************************
*
* 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
|