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
|
/* ****************************************************************************
*
* 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.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace Microsoft.Scripting.Hosting.Shell.Remote {
/// <summary>
/// Supports detecting the remote runtime being killed, and starting up a new one.
///
/// Threading model:
///
/// ConsoleRestartManager creates a separate thread on which to create and execute the consoles.
/// There are usually atleast three threads involved:
///
/// 1. Main app thread: Instantiates ConsoleRestartManager and accesses its APIs. This thread has to stay
/// responsive to user input and so the ConsoleRestartManager APIs cannot be long-running or blocking.
/// Since the remote runtime process can terminate asynchronously, the current RemoteConsoleHost can
/// change at any time (if auto-restart is enabled). The app should typically not care which instance of
/// RemoteConsoleHost is currently being used. The flowchart of this thread is:
/// Create ConsoleRestartManager
/// ConsoleRestartManager.Start
/// Loop:
/// Respond to user input | Send user input to console for execution | BreakExecution | RestartConsole | GetMemberNames
/// ConsoleRestartManager.Terminate
/// TODO: Currently, BreakExecution and GetMemberNames are called by the main thread synchronously.
/// Since they execute code in the remote runtime, they could take arbitrarily long. We should change
/// this so that the main app thread can never be blocked indefinitely.
///
/// 2. Console thread: Dedicated thread for creating RemoteConsoleHosts and executing code (which could
/// take a long time or block indefinitely).
/// Wait for ConsoleRestartManager.Start to be called
/// Loop:
/// Create RemoteConsoleHost
/// Wait for signal for:
/// Execute code | RestartConsole | Process.Exited
///
/// 3. CompletionPort async callbacks:
/// Process.Exited | Process.OutputDataReceived | Process.ErrorDataReceived
///
/// 4. Finalizer thred
/// Some objects may have a Finalize method (which possibly calls Dispose). Not many (if any) types
/// should have a Finalize method.
///
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1012:AbstractTypesShouldNotHaveConstructors")] // TODO: This is public only because the test (RemoteConsole.py) needs it to be so. The test should be rewritten
public abstract class ConsoleRestartManager {
private RemoteConsoleHost _remoteConsoleHost;
private Thread _consoleThread;
private bool _exitOnNormalExit;
private bool _terminating;
/// <summary>
/// Accessing _remoteConsoleHost from a thread other than console thread can result in race.
/// If _remoteConsoleHost is accessed while holding _accessLock, it is guaranteed to be
/// null or non-disposed.
/// </summary>
private object _accessLock = new object();
/// <summary>
/// This is created on the "creating thread", and goes on standby. Start needs to be called for activation.
/// </summary>
/// <param name="exitOnNormalExit">A host might want one of two behaviors:
/// 1. Keep the REPL loop alive indefinitely, even when a specific instance of the RemoteConsoleHost terminates normally
/// 2. Close the REPL loop when an instance of the RemoteConsoleHost terminates normally, and restart the loop
/// only if the instance terminates abnormally.</param>
public ConsoleRestartManager(bool exitOnNormalExit) {
_exitOnNormalExit = exitOnNormalExit;
_consoleThread = new Thread(Run);
_consoleThread.Name = "Console thread";
}
protected object AccessLock { get { return _accessLock; } }
public Thread ConsoleThread { get { return _consoleThread; } }
protected RemoteConsoleHost CurrentConsoleHost { get { return _remoteConsoleHost; } }
public abstract RemoteConsoleHost CreateRemoteConsoleHost();
/// <summary>
/// Needs to be called for activation.
/// </summary>
public void Start() {
Debug.Assert(Thread.CurrentThread != _consoleThread);
if (_consoleThread.IsAlive) {
throw new InvalidOperationException("Console thread is already running.");
}
_consoleThread.Start();
}
private void Run() {
#if DEBUG
try {
RunWorker();
} catch (Exception e) {
Debug.Assert(false, "Unhandled exception on console thread:\n\n" + e.ToString());
}
#else
RunWorker();
#endif
}
private void RunWorker() {
Debug.Assert(Thread.CurrentThread == _consoleThread);
while (true) {
RemoteConsoleHost remoteConsoleHost = CreateRemoteConsoleHost();
// Reading _terminating and setting of _remoteConsoleHost should be done atomically.
// Terminate() does the reverse operation (setting _terminating reading _remoteConsoleHost) atomically
lock (_accessLock) {
if (_terminating) {
return;
}
_remoteConsoleHost = remoteConsoleHost;
}
try {
try {
int exitCode = remoteConsoleHost.Run(new string[0]);
if (_exitOnNormalExit && exitCode == 0) {
return;
}
} catch (RemoteRuntimeStartupException) {
}
} finally {
lock (_accessLock) {
remoteConsoleHost.Dispose();
_remoteConsoleHost = null;
}
}
}
}
// 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")]
public IList<string> GetMemberNames(string expression) {
Debug.Assert(Thread.CurrentThread != _consoleThread);
lock (_accessLock) {
if (_remoteConsoleHost == null) {
return null;
}
ScriptEngine engine = _remoteConsoleHost.Engine;
try {
ScriptScope scope = _remoteConsoleHost.ScriptScope;
ObjectOperations operations = engine.CreateOperations(scope);
ScriptSource src = engine.CreateScriptSourceFromString(expression, SourceCodeKind.Expression);
return operations.GetMemberNames(src.ExecuteAndWrap(scope));
} catch {
return null;
}
}
}
public void BreakExecution() {
Debug.Assert(Thread.CurrentThread != _consoleThread);
lock (_accessLock) {
if (_remoteConsoleHost == null) {
return;
}
try {
_remoteConsoleHost.AbortCommand();
} catch (System.Runtime.Remoting.RemotingException) {
// The remote runtime may be terminated or non-responsive
}
}
}
public void RestartConsole() {
Debug.Assert(Thread.CurrentThread != _consoleThread);
lock (_accessLock) {
if (_remoteConsoleHost == null) {
return;
}
_remoteConsoleHost.Terminate(0);
}
}
/// <summary>
/// Request (from another thread) the console REPL loop to terminate
/// </summary>
public void Terminate() {
Debug.Assert(Thread.CurrentThread != _consoleThread);
lock (_accessLock) {
_terminating = true;
_remoteConsoleHost.Terminate(0);
}
_consoleThread.Join();
}
}
}
#endif
|