File: ConsoleRestartManager.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 (223 lines) | stat: -rw-r--r-- 9,323 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
/* ****************************************************************************
 *
 * 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