/* **************************************************************************** * * 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. * * * ***************************************************************************/ using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using Microsoft.Scripting.Hosting.Providers; using Microsoft.Scripting.Runtime; using Microsoft.Scripting.Utils; namespace Microsoft.Scripting.Hosting.Shell { /// /// Command line hosting service. /// public class CommandLine { private LanguageContext _language; private IConsole _console; private ConsoleOptions _options; private ScriptScope _scope; private ScriptEngine _engine; private ICommandDispatcher _commandDispatcher; private int? _terminatingExitCode; protected IConsole Console { get { return _console; } } protected ConsoleOptions Options { get { return _options; } } protected ScriptEngine Engine { get { return _engine; } } public ScriptScope ScriptScope { get { return _scope; } protected set { _scope = value; } } /// /// Scope is not remotable, and this only works in the same AppDomain. /// protected Scope Scope { get { if (_scope == null) { return null; } return _scope.Scope; } set { _scope = new ScriptScope(_engine, value); } } protected LanguageContext Language { get { // LanguageContext is not remotable, and this only works in the same AppDomain. if (_language == null) { _language = HostingHelpers.GetLanguageContext(_engine); } return _language; } } protected virtual string Prompt { get { return ">>> "; } } public virtual string PromptContinuation { get { return "... "; } } protected virtual string Logo { get { return null; } } public CommandLine() { } protected virtual void Initialize() { if (_commandDispatcher == null) { _commandDispatcher = CreateCommandDispatcher(); } } protected virtual Scope CreateScope() { return new Scope(); } protected virtual ICommandDispatcher CreateCommandDispatcher() { return new SimpleCommandDispatcher(); } public virtual void Terminate(int exitCode) { // The default implementation just sets a flag. Derived types can support better termination _terminatingExitCode = exitCode; } /// /// Executes the comand line - depending upon the options provided we will /// either run a single file, a single command, or enter the interactive loop. /// public int Run(ScriptEngine engine, IConsole console, ConsoleOptions options) { ContractUtils.RequiresNotNull(engine, "engine"); ContractUtils.RequiresNotNull(console, "console"); ContractUtils.RequiresNotNull(options, "options"); _engine = engine; _options = options; _console = console; Initialize(); try { return Run(); #if !SILVERLIGHT // ThreadAbortException.ExceptionState } catch (System.Threading.ThreadAbortException tae) { if (tae.ExceptionState is KeyboardInterruptException) { Thread.ResetAbort(); return -1; } else { throw; } #endif } finally { Shutdown(); _engine = null; _options = null; _console = null; } } /// /// Runs the command line. Languages can override this to provide custom behavior other than: /// 1. Running a single command /// 2. Running a file /// 3. Entering the interactive console loop. /// /// protected virtual int Run() { int result; if (_options.Command != null) { result = RunCommand(_options.Command); } else if (_options.FileName != null) { result = RunFile(_options.FileName); } else { return RunInteractive(); } if (_options.Introspection) { return RunInteractiveLoop(); } return result; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] protected virtual void Shutdown() { try { _engine.Runtime.Shutdown(); } catch (Exception e) { UnhandledException(e); } } protected virtual int RunFile(string fileName) { return RunFile(_engine.CreateScriptSourceFromFile(fileName)); } protected virtual int RunCommand(string command) { return RunFile(_engine.CreateScriptSourceFromString(command, SourceCodeKind.Statements)); } /// /// Runs the specified filename /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] protected virtual int RunFile(ScriptSource source) { int result = 1; if (Options.HandleExceptions) { try { result = source.ExecuteProgram(); } catch (Exception e) { UnhandledException(e); } } else { result = source.ExecuteProgram(); } return result; } protected void PrintLogo() { if (Logo != null) { _console.Write(Logo, Style.Out); } } #region Interactivity /// /// Starts the interactive loop. Performs any initialization necessary before /// starting the loop and then calls RunInteractiveLoop to start the loop. /// /// Returns the exit code when the interactive loop is completed. /// protected virtual int RunInteractive() { PrintLogo(); return RunInteractiveLoop(); } /// /// Runs the interactive loop. Repeatedly parse and run interactive actions /// until an exit code is received. If any exceptions are unhandled displays /// them to the console /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] protected int RunInteractiveLoop() { if (_scope == null) { _scope = _engine.CreateScope(); } #if !SILVERLIGHT // Remote console string remoteRuntimeChannel = _options.RemoteRuntimeChannel; if (remoteRuntimeChannel != null) { // Publish the ScriptScope so that the host can use it Remote.RemoteRuntimeServer.StartServer(remoteRuntimeChannel, _scope); return 0; } #endif int? res = null; do { if (Options.HandleExceptions) { try { res = TryInteractiveAction(); #if SILVERLIGHT } catch (ExitProcessException e) { res = e.ExitCode; #endif } catch (Exception e) { if (CommandLine.IsFatalException(e)) { // Some exceptions are too dangerous to try to catch throw; } // There should be no unhandled exceptions in the interactive session // We catch all (most) exceptions here, and just display it, // and keep on going UnhandledException(e); } } else { res = TryInteractiveAction(); } } while (res == null); return res.Value; } internal static bool IsFatalException(Exception e) { ThreadAbortException tae = e as ThreadAbortException; if (tae != null) { #if SILVERLIGHT // ThreadAbortException.ExceptionState return true; #else if ((tae.ExceptionState as KeyboardInterruptException) == null) { return true; } #endif } return false; } protected virtual void UnhandledException(Exception e) { ExceptionOperations exceptionOperations = _engine.GetService(); _console.WriteLine(exceptionOperations.FormatException(e), Style.Error); } /// /// Attempts to run a single interaction and handle any language-specific /// exceptions. Base classes can override this and call the base implementation /// surrounded with their own exception handling. /// /// Returns null if successful and execution should continue, or an exit code. /// protected virtual int? TryInteractiveAction() { int? result = null; try { result = RunOneInteraction(); #if SILVERLIGHT // ThreadAbortException.ExceptionState } catch (ThreadAbortException) { #else } catch (ThreadAbortException tae) { KeyboardInterruptException pki = tae.ExceptionState as KeyboardInterruptException; if (pki != null) { UnhandledException(tae); Thread.ResetAbort(); } else { throw; } #endif } return result; } /// /// Parses a single interactive command or a set of statements and executes it. /// /// Returns null if successful and execution should continue, or the appropiate exit code. /// /// We check if the code read is an interactive command or statements is by checking for NewLine /// If the code contains NewLine, it's a set of statements (most probably from SendToConsole) /// If the code does not contain a NewLine, it's an interactive command typed by the user at the prompt /// private int? RunOneInteraction() { bool continueInteraction; string s = ReadStatement(out continueInteraction); if (continueInteraction == false) { return (_terminatingExitCode == null) ? 0 : _terminatingExitCode; } if (String.IsNullOrEmpty(s)) { // Is it an empty line? _console.Write(String.Empty, Style.Out); return null; } ExecuteCommand(_engine.CreateScriptSourceFromString(s, (s.Contains(Environment.NewLine))? SourceCodeKind.Statements : SourceCodeKind.InteractiveCode)); return null; } protected object ExecuteCommand(ScriptSource source) { ErrorListener errorListener = new ErrorSinkProxyListener(ErrorSink); CompiledCode compiledCode = source.Compile(_engine.GetCompilerOptions(_scope), errorListener); return _commandDispatcher.Execute(compiledCode, _scope); } protected virtual ErrorSink ErrorSink { get { return ErrorSink.Default; } } /// /// Private helper function to see if we should treat the current input as a blank link. /// /// We do this if we only have auto-indent text. /// private static bool TreatAsBlankLine(string line, int autoIndentSize) { if (line.Length == 0) return true; if (autoIndentSize != 0 && line.Trim().Length == 0 && line.Length == autoIndentSize) { return true; } return false; } /// /// Read a statement, which can potentially be a multiple-line statement suite (like a class declaration). /// /// Should the console session continue, or did the user indicate /// that it should be terminated? /// Expression to evaluate. null for empty input protected string ReadStatement(out bool continueInteraction) { StringBuilder b = new StringBuilder(); int autoIndentSize = 0; _console.Write(Prompt, Style.Prompt); while (true) { string line = ReadLine(autoIndentSize); continueInteraction = true; if (line == null || (_terminatingExitCode != null)) { continueInteraction = false; return null; } bool allowIncompleteStatement = TreatAsBlankLine(line, autoIndentSize); b.Append(line); b.Append("\n"); string code = b.ToString(); ScriptSource command = _engine.CreateScriptSourceFromString(code, (code.Contains(Environment.NewLine))? SourceCodeKind.Statements : SourceCodeKind.InteractiveCode); ScriptCodeParseResult props = command.GetCodeProperties(_engine.GetCompilerOptions(_scope)); if (SourceCodePropertiesUtils.IsCompleteOrInvalid(props, allowIncompleteStatement)) { return props != ScriptCodeParseResult.Empty ? code : null; } if (_options.AutoIndent && _options.AutoIndentSize != 0) { autoIndentSize = GetNextAutoIndentSize(code); } // Keep on reading input _console.Write(PromptContinuation, Style.Prompt); } } /// /// Gets the next level for auto-indentation /// protected virtual int GetNextAutoIndentSize(string text) { return 0; } protected virtual string ReadLine(int autoIndentSize) { return _console.ReadLine(autoIndentSize); } internal protected virtual TextWriter GetOutputWriter(bool isErrorOutput) { return isErrorOutput ? System.Console.Error : System.Console.Out; } //private static DynamicSite> _memberCompletionSite = // new DynamicSite>(OldDoOperationAction.Make(Operators.GetMemberNames)); public IList GetMemberNames(string code) { object value = _engine.CreateScriptSourceFromString(code, SourceCodeKind.Expression).Execute(_scope); return _engine.Operations.GetMemberNames(value); // TODO: why doesn't this work ??? //return _memberCompletionSite.Invoke(new CodeContext(_scope, _engine), value); } public virtual IList GetGlobals(string name) { List res = new List(); foreach (SymbolId scopeName in _scope.Scope.Keys) { string strName = SymbolTable.IdToString(scopeName); if (strName.StartsWith(name)) { res.Add(strName); } } return res; } #endregion class SimpleCommandDispatcher : ICommandDispatcher { public object Execute(CompiledCode compiledCode, ScriptScope scope) { return compiledCode.Execute(scope); } } } }