// // repl.cs: Support for using the compiler in interactive mode (read-eval-print loop) // // Authors: // Miguel de Icaza (miguel@gnome.org) // // Dual licensed under the terms of the MIT X11 or GNU GPL // // Copyright 2001, 2002, 2003 Ximian, Inc (http://www.ximian.com) // Copyright 2004, 2005, 2006, 2007, 2008 Novell, Inc // Copyright 2011-2013 Xamarin Inc // // // TODO: // Do not print results in Evaluate, do that elsewhere in preparation for Eval refactoring. // Driver.PartialReset should not reset the coretypes, nor the optional types, to avoid // computing that on every call. // using System; using System.IO; using System.Text; using System.Globalization; using System.Collections; using System.Reflection; using System.Reflection.Emit; using System.Threading; using System.Net; using System.Net.Sockets; using System.Collections.Generic; using Mono.CSharp; namespace Mono { public class Driver { public static string StartupEvalExpression; static int? attach; static string target_host; static int target_port; static string agent; static int Main (string [] args) { var cmd = new CommandLineParser (Console.Out); cmd.UnknownOptionHandler += HandleExtraArguments; // Enable unsafe code by default var settings = new CompilerSettings () { Unsafe = true }; if (!cmd.ParseArguments (settings, args)) return 1; var startup_files = new string [settings.SourceFiles.Count]; int i = 0; foreach (var source in settings.SourceFiles) startup_files [i++] = source.FullPathName; settings.SourceFiles.Clear (); TextWriter agent_stderr = null; ReportPrinter printer; if (agent != null) { agent_stderr = new StringWriter (); printer = new StreamReportPrinter (agent_stderr); } else { printer = new ConsoleReportPrinter (); } var eval = new Evaluator (new CompilerContext (settings, printer)); eval.InteractiveBaseClass = typeof (InteractiveBaseShell); eval.DescribeTypeExpressions = true; eval.WaitOnTask = true; CSharpShell shell; #if !ON_DOTNET if (attach.HasValue) { shell = new ClientCSharpShell_v1 (eval, attach.Value); } else if (agent != null) { new CSharpAgent (eval, agent, agent_stderr).Run (startup_files); return 0; } else #endif if (target_host != null) shell = new ClientCSharpShell (eval, target_host, target_port); else shell = new CSharpShell (eval); return shell.Run (startup_files); } static int HandleExtraArguments (string [] args, int pos) { switch (args [pos]) { case "-e": if (pos + 1 < args.Length) { StartupEvalExpression = args[pos + 1]; return pos + 1; } break; case "--attach": if (pos + 1 < args.Length) { attach = Int32.Parse (args[1]); return pos + 1; } break; default: if (args [pos].StartsWith ("--server=")){ var hostport = args [pos].Substring (9); int p = hostport.IndexOf (':'); if (p == -1){ target_host = hostport; target_port = 10000; } else { target_host = hostport.Substring (0,p); if (!int.TryParse (hostport.Substring (p), out target_port)){ Console.Error.WriteLine ("Usage is: --server[=host[:port]"); Environment.Exit (1); } } return pos + 1; } if (args [pos].StartsWith ("--client")){ target_host = "localhost"; target_port = 10000; return pos + 1; } if (args [pos].StartsWith ("--agent:")) { agent = args[pos]; return pos + 1; } else { return -1; } } return -1; } } public class InteractiveBaseShell : InteractiveBase { static bool tab_at_start_completes; static InteractiveBaseShell () { tab_at_start_completes = false; } internal static Mono.Terminal.LineEditor Editor; public static bool TabAtStartCompletes { get { return tab_at_start_completes; } set { tab_at_start_completes = value; if (Editor != null) Editor.TabAtStartCompletes = value; } } public static new string help { get { return InteractiveBase.help + " TabAtStartCompletes - Whether tab will complete even on empty lines\n"; } } } public class CSharpShell { static bool isatty = true, is_unix = false; protected string [] startup_files; Mono.Terminal.LineEditor editor; bool dumb; readonly Evaluator evaluator; public CSharpShell (Evaluator evaluator) { this.evaluator = evaluator; } protected virtual void ConsoleInterrupt (object sender, ConsoleCancelEventArgs a) { // Do not about our program a.Cancel = true; evaluator.Interrupt (); } void SetupConsole () { if (is_unix){ string term = Environment.GetEnvironmentVariable ("TERM"); dumb = term == "dumb" || term == null || isatty == false; } else dumb = false; editor = new Mono.Terminal.LineEditor ("csharp", 300); InteractiveBaseShell.Editor = editor; editor.AutoCompleteEvent += delegate (string s, int pos){ string prefix = null; string complete = s.Substring (0, pos); string [] completions = evaluator.GetCompletions (complete, out prefix); return new Mono.Terminal.LineEditor.Completion (prefix, completions); }; #if false // // This is a sample of how completions sould be implemented. // editor.AutoCompleteEvent += delegate (string s, int pos){ // Single match: "Substring": Sub-string if (s.EndsWith ("Sub")){ return new string [] { "string" }; } // Multiple matches: "ToString" and "ToLower" if (s.EndsWith ("T")){ return new string [] { "ToString", "ToLower" }; } return null; }; #endif Console.CancelKeyPress += ConsoleInterrupt; } string GetLine (bool primary) { string prompt = primary ? InteractiveBase.Prompt : InteractiveBase.ContinuationPrompt; if (dumb){ if (isatty) Console.Write (prompt); return Console.ReadLine (); } else { return editor.Edit (prompt, ""); } } delegate string ReadLiner (bool primary); void InitializeUsing () { Evaluate ("using System; using System.Linq; using System.Collections.Generic; using System.Collections;"); } void InitTerminal (bool show_banner) { int p = (int) Environment.OSVersion.Platform; is_unix = (p == 4) || (p == 128); #if NET_4_5 isatty = !Console.IsInputRedirected && !Console.IsOutputRedirected; #else isatty = true; #endif // Work around, since Console is not accounting for // cursor position when writing to Stderr. It also // has the undesirable side effect of making // errors plain, with no coloring. // Report.Stderr = Console.Out; SetupConsole (); if (isatty && show_banner) Console.WriteLine ("Mono C# Shell, type \"help;\" for help\n\nEnter statements below."); } void ExecuteSources (IEnumerable sources, bool ignore_errors) { foreach (string file in sources){ try { try { bool first = true; using (System.IO.StreamReader r = System.IO.File.OpenText (file)){ ReadEvalPrintLoopWith (p => { var line = r.ReadLine (); if (first){ if (line.StartsWith ("#!")) line = r.ReadLine (); first = false; } return line; }); } } catch (FileNotFoundException){ Console.Error.WriteLine ("cs2001: Source file `{0}' not found", file); return; } } catch { if (!ignore_errors) throw; } } } protected virtual void LoadStartupFiles () { string dir = Path.Combine ( Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "csharp"); if (!Directory.Exists (dir)) return; List sources = new List (); List libraries = new List (); foreach (string file in System.IO.Directory.GetFiles (dir)){ string l = file.ToLower (); if (l.EndsWith (".cs")) sources.Add (file); else if (l.EndsWith (".dll")) libraries.Add (file); } foreach (string file in libraries) evaluator.LoadAssembly (file); ExecuteSources (sources, true); } void ReadEvalPrintLoopWith (ReadLiner readline) { string expr = null; while (!InteractiveBase.QuitRequested){ string input = readline (expr == null); if (input == null) return; if (input == "") continue; expr = expr == null ? input : expr + "\n" + input; expr = Evaluate (expr); } } public int ReadEvalPrintLoop () { if (startup_files != null && startup_files.Length == 0) InitTerminal (startup_files.Length == 0 && Driver.StartupEvalExpression == null); InitializeUsing (); LoadStartupFiles (); if (startup_files != null && startup_files.Length != 0) { ExecuteSources (startup_files, false); } else { if (Driver.StartupEvalExpression != null){ ReadEvalPrintLoopWith (p => { var ret = Driver.StartupEvalExpression; Driver.StartupEvalExpression = null; return ret; }); } else { ReadEvalPrintLoopWith (GetLine); } editor.SaveHistory (); } Console.CancelKeyPress -= ConsoleInterrupt; return 0; } protected virtual string Evaluate (string input) { bool result_set; object result; try { input = evaluator.Evaluate (input, out result, out result_set); if (result_set){ PrettyPrint (Console.Out, result); Console.WriteLine (); } } catch (Exception e){ Console.WriteLine (e); return null; } return input; } static void p (TextWriter output, string s) { output.Write (s); } static string EscapeString (string s) { return s.Replace ("\"", "\\\""); } static void EscapeChar (TextWriter output, char c) { if (c == '\''){ output.Write ("'\\''"); return; } if (c > 32){ output.Write ("'{0}'", c); return; } switch (c){ case '\a': output.Write ("'\\a'"); break; case '\b': output.Write ("'\\b'"); break; case '\n': output.Write ("'\\n'"); break; case '\v': output.Write ("'\\v'"); break; case '\r': output.Write ("'\\r'"); break; case '\f': output.Write ("'\\f'"); break; case '\t': output.Write ("'\\t"); break; default: output.Write ("'\\x{0:x}", (int) c); break; } } // Some types (System.Json.JsonPrimitive) implement // IEnumerator and yet, throw an exception when we // try to use them, helper function to check for that // condition static internal bool WorksAsEnumerable (object obj) { IEnumerable enumerable = obj as IEnumerable; if (enumerable != null){ try { enumerable.GetEnumerator (); return true; } catch { // nothing, we return false below } } return false; } internal static void PrettyPrint (TextWriter output, object result) { if (result == null){ p (output, "null"); return; } if (result is Array){ Array a = (Array) result; p (output, "{ "); int top = a.GetUpperBound (0); for (int i = a.GetLowerBound (0); i <= top; i++){ PrettyPrint (output, a.GetValue (i)); if (i != top) p (output, ", "); } p (output, " }"); } else if (result is bool){ if ((bool) result) p (output, "true"); else p (output, "false"); } else if (result is string){ p (output, String.Format ("\"{0}\"", EscapeString ((string)result))); } else if (result is IDictionary){ IDictionary dict = (IDictionary) result; int top = dict.Count, count = 0; p (output, "{"); foreach (DictionaryEntry entry in dict){ count++; p (output, "{ "); PrettyPrint (output, entry.Key); p (output, ", "); PrettyPrint (output, entry.Value); if (count != top) p (output, " }, "); else p (output, " }"); } p (output, "}"); } else if (WorksAsEnumerable (result)) { int i = 0; p (output, "{ "); foreach (object item in (IEnumerable) result) { if (i++ != 0) p (output, ", "); PrettyPrint (output, item); } p (output, " }"); } else if (result is char) { EscapeChar (output, (char) result); } else { p (output, result.ToString ()); } } public virtual int Run (string [] startup_files) { this.startup_files = startup_files; return ReadEvalPrintLoop (); } } // // Stream helper extension methods // public static class StreamHelper { static DataConverter converter = DataConverter.LittleEndian; static void GetBuffer (this Stream stream, byte [] b) { int n, offset = 0; int len = b.Length; do { n = stream.Read (b, offset, len); if (n == 0) throw new IOException ("End reached"); offset += n; len -= n; } while (len > 0); } public static int GetInt (this Stream stream) { byte [] b = new byte [4]; stream.GetBuffer (b); return converter.GetInt32 (b, 0); } public static string GetString (this Stream stream) { int len = stream.GetInt (); if (len == 0) return ""; byte [] b = new byte [len]; stream.GetBuffer (b); return Encoding.UTF8.GetString (b); } public static void WriteInt (this Stream stream, int n) { byte [] bytes = converter.GetBytes (n); stream.Write (bytes, 0, bytes.Length); } public static void WriteString (this Stream stream, string s) { stream.WriteInt (s.Length); byte [] bytes = Encoding.UTF8.GetBytes (s); stream.Write (bytes, 0, bytes.Length); } } public enum AgentStatus : byte { // Received partial input, complete PARTIAL_INPUT = 1, // The result was set, expect the string with the result RESULT_SET = 2, // No result was set, complete RESULT_NOT_SET = 3, // Errors and warnings string follows ERROR = 4, // Stdout STDOUT = 5, } class ClientCSharpShell : CSharpShell { string target_host; int target_port; public ClientCSharpShell (Evaluator evaluator, string target_host, int target_port) : base (evaluator) { this.target_port = target_port; this.target_host = target_host; } T ConnectServer (Func callback, Action error) { try { var client = new TcpClient (target_host, target_port); var ns = client.GetStream (); T ret = callback (ns); ns.Flush (); ns.Close (); client.Close (); return ret; } catch (Exception e){ error (e); return default(T); } } protected override string Evaluate (string input) { return ConnectServer ((ns)=> { try { ns.WriteString ("EVALTXT"); ns.WriteString (input); while (true) { AgentStatus s = (AgentStatus) ns.ReadByte (); switch (s){ case AgentStatus.PARTIAL_INPUT: return input; case AgentStatus.ERROR: string err = ns.GetString (); Console.Error.WriteLine (err); break; case AgentStatus.STDOUT: string stdout = ns.GetString (); Console.WriteLine (stdout); break; case AgentStatus.RESULT_NOT_SET: return null; case AgentStatus.RESULT_SET: string res = ns.GetString (); Console.WriteLine (res); return null; } } } catch (Exception e){ Console.Error.WriteLine ("Error evaluating expression, exception: {0}", e); } return null; }, (e) => { Console.Error.WriteLine ("Error communicating with server {0}", e); }); } public override int Run (string [] startup_files) { // The difference is that we do not call Evaluator.Init, that is done on the target this.startup_files = startup_files; return ReadEvalPrintLoop (); } protected override void ConsoleInterrupt (object sender, ConsoleCancelEventArgs a) { ConnectServer ((ns)=> { ns.WriteString ("INTERRUPT"); return 0; }, (e) => { }); } } #if !ON_DOTNET // // A shell connected to a CSharpAgent running in a remote process. // - maybe add 'class_name' and 'method_name' arguments to LoadAgent. // - Support Gtk and Winforms main loops if detected, this should // probably be done as a separate agent in a separate place. // class ClientCSharpShell_v1 : CSharpShell { NetworkStream ns, interrupt_stream; public ClientCSharpShell_v1 (Evaluator evaluator, int pid) : base (evaluator) { // Create a server socket we listen on whose address is passed to the agent TcpListener listener = new TcpListener (new IPEndPoint (IPAddress.Loopback, 0)); listener.Start (); TcpListener interrupt_listener = new TcpListener (new IPEndPoint (IPAddress.Loopback, 0)); interrupt_listener.Start (); string agent_assembly = typeof (ClientCSharpShell).Assembly.Location; string agent_arg = String.Format ("--agent:{0}:{1}" , ((IPEndPoint)listener.Server.LocalEndPoint).Port, ((IPEndPoint)interrupt_listener.Server.LocalEndPoint).Port); var vm = new Attach.VirtualMachine (pid); vm.Attach (agent_assembly, agent_arg); /* Wait for the client to connect */ TcpClient client = listener.AcceptTcpClient (); ns = client.GetStream (); TcpClient interrupt_client = interrupt_listener.AcceptTcpClient (); interrupt_stream = interrupt_client.GetStream (); Console.WriteLine ("Connected."); } // // A remote version of Evaluate // protected override string Evaluate (string input) { ns.WriteString (input); while (true) { AgentStatus s = (AgentStatus) ns.ReadByte (); switch (s){ case AgentStatus.PARTIAL_INPUT: return input; case AgentStatus.ERROR: string err = ns.GetString (); Console.Error.WriteLine (err); break; case AgentStatus.RESULT_NOT_SET: return null; case AgentStatus.RESULT_SET: string res = ns.GetString (); Console.WriteLine (res); return null; } } } public override int Run (string [] startup_files) { // The difference is that we do not call Evaluator.Init, that is done on the target this.startup_files = startup_files; return ReadEvalPrintLoop (); } protected override void ConsoleInterrupt (object sender, ConsoleCancelEventArgs a) { // Do not about our program a.Cancel = true; interrupt_stream.WriteByte (0); int c = interrupt_stream.ReadByte (); if (c != -1) Console.WriteLine ("Execution interrupted"); } } // // This is the agent loaded into the target process when using --attach. // class CSharpAgent { NetworkStream interrupt_stream; readonly Evaluator evaluator; TextWriter stderr; public CSharpAgent (Evaluator evaluator, String arg, TextWriter stderr) { this.evaluator = evaluator; this.stderr = stderr; new Thread (new ParameterizedThreadStart (Run)).Start (arg); } public void InterruptListener () { while (true){ int b = interrupt_stream.ReadByte(); if (b == -1) return; evaluator.Interrupt (); interrupt_stream.WriteByte (0); } } public void Run (object o) { string arg = (string)o; string ports = arg.Substring (8); int sp = ports.IndexOf (':'); int port = Int32.Parse (ports.Substring (0, sp)); int interrupt_port = Int32.Parse (ports.Substring (sp+1)); Console.WriteLine ("csharp-agent: started, connecting to localhost:" + port); TcpClient client = new TcpClient ("127.0.0.1", port); TcpClient interrupt_client = new TcpClient ("127.0.0.1", interrupt_port); Console.WriteLine ("csharp-agent: connected."); NetworkStream s = client.GetStream (); interrupt_stream = interrupt_client.GetStream (); new Thread (InterruptListener).Start (); try { // Add all assemblies loaded later AppDomain.CurrentDomain.AssemblyLoad += AssemblyLoaded; // Add all currently loaded assemblies foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies ()) { // Some assemblies seem to be already loaded, and loading them again causes 'defined multiple times' errors if (a.GetName ().Name != "mscorlib" && a.GetName ().Name != "System.Core" && a.GetName ().Name != "System") evaluator.ReferenceAssembly (a); } RunRepl (s); } finally { AppDomain.CurrentDomain.AssemblyLoad -= AssemblyLoaded; client.Close (); interrupt_client.Close (); Console.WriteLine ("csharp-agent: disconnected."); } } void AssemblyLoaded (object sender, AssemblyLoadEventArgs e) { evaluator.ReferenceAssembly (e.LoadedAssembly); } public void RunRepl (NetworkStream s) { string input = null; while (!InteractiveBase.QuitRequested) { try { string error_string; StringWriter error_output = (StringWriter)stderr; string line = s.GetString (); bool result_set; object result; if (input == null) input = line; else input = input + "\n" + line; try { input = evaluator.Evaluate (input, out result, out result_set); } catch (Exception e) { s.WriteByte ((byte) AgentStatus.ERROR); s.WriteString (e.ToString ()); s.WriteByte ((byte) AgentStatus.RESULT_NOT_SET); continue; } if (input != null){ s.WriteByte ((byte) AgentStatus.PARTIAL_INPUT); continue; } // Send warnings and errors back error_string = error_output.ToString (); if (error_string.Length != 0){ s.WriteByte ((byte) AgentStatus.ERROR); s.WriteString (error_output.ToString ()); error_output.GetStringBuilder ().Clear (); } if (result_set){ s.WriteByte ((byte) AgentStatus.RESULT_SET); StringWriter sr = new StringWriter (); CSharpShell.PrettyPrint (sr, result); s.WriteString (sr.ToString ()); } else { s.WriteByte ((byte) AgentStatus.RESULT_NOT_SET); } } catch (IOException) { break; } catch (Exception e){ Console.WriteLine (e); } } } } public class UnixUtils { [System.Runtime.InteropServices.DllImport ("libc", EntryPoint="isatty")] extern static int _isatty (int fd); public static bool isatty (int fd) { try { return _isatty (fd) == 1; } catch { return false; } } } #endif } namespace Mono.Management { interface IVirtualMachine { void LoadAgent (string filename, string args); } }