using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Windows.Forms; using System.Xml.Serialization; using TradeIdeas.MiscSupport; using TradeIdeas.ServerConnection; namespace TclShell { public partial class ScriptExecutor : Form, IConnectionNameListener { private string _historyFile = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "\\TradeIdeasPro\\TclShellHistory.txt"; private string _historyFileSerialized = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "\\TradeIdeasPro\\TclShellHistory.serialized.txt"; public ScriptExecutor() { InitializeComponent(); // Seems like this should be set in the form builder. // I don't know how to do that. coroutineComboBox.SelectedIndex = 0; LoadHistory(); FormClosing += ScriptExecutor_FormClosing; if (null != MainForm.ConnectionName) Text = "Script Executor – " + MainForm.ConnectionName; } void ScriptExecutor_FormClosing(object sender, FormClosingEventArgs e) { List history = new List(); foreach (string historyItem in recentItemsComboBox.Items.OfType()) { history.Add(historyItem); } //File.WriteAllLines(_historyFile, history); SerializeList(history, _historyFileSerialized); } private void SerializeList(List list, string fileName) { var serializer = new XmlSerializer(typeof(List)); using (var stream = File.OpenWrite(fileName)) { serializer.Serialize(stream, list); } } private List Deserialize(string fileName) { List toReturn = new List(); var serializer = new XmlSerializer(typeof(List)); using (var stream = File.OpenRead(fileName)) { var other = (List)(serializer.Deserialize(stream)); toReturn.Clear(); toReturn.AddRange(other); } return toReturn; } private void LoadHistory() { try { List history = new List(); if (File.Exists(_historyFileSerialized)) history = Deserialize(_historyFileSerialized); else history = File.ReadAllLines(_historyFile).ToList(); foreach (string historyItem in history) { string scrubbedHistoryItem = historyItem.Replace("\n", Environment.NewLine); recentItemsComboBox.Items.Add(scrubbedHistoryItem); } } catch (Exception e) { string debugView = e.StackTrace; } } private void sendButton_Click(object sender, EventArgs e) { Send(false); } private readonly List _cancelTokens = new List(); private void Send(bool streaming) { TalkWithServer.CancelToken cancelToken = streaming?(new TalkWithServer.CancelToken()):null; string tclNamespace; if (socketRadioButton.Checked) // Create a namespace that will automatically be deleted when the current // socket is disconnected. tclNamespace = "[ti::get_socket_temp_ns]::tcl_shell"; else if (customRadioButton.Checked) // Use the specified namespace. If library code executes in a specific // namespace, and you want to try that code in here, or call that code, // it might make sense to execute in the same namespace. tclNamespace = customTextBox.Text.TclQuote(); else // By default use the top level namespace. tclNamespace = "::"; string script = scriptTextBox.Text; // Use apply to create a new stack frame. Unless the script explicitly says // "global", "variable" or something similar, all variables will be local to // this stack frame, and they will disappear after this script finished. string toExecute = "apply [list {} " + script.Replace("\r\n", "\n").TclQuote() + " " + tclNamespace + ']'; if (coroutineComboBox.SelectedIndex > 0) { // If we're in a coroutine and we raise an exception, that exception might or // might not be returned from the initial call. If the coroutine calls yield // before the exception, then the exception will be lost. Otherwise the exception // will be displayed as the result of the initial call. // // Since we can't be sure, always send the error to the log. But also return // a copy of the error message in case someone is listening to the result. string ResponseChannel = MainForm.ResponseChannel; // Ideally we'd use a command to find the return channel on the server side. // That would avoid issues, like the client is out of date. But that would // make the code more complicated. We'd need to run the function in advance, // then save the value in a temporary variable, in case we needed it. if (null != ResponseChannel) toExecute = "try " + toExecute.TclQuote() + " on error {msg} {ti::reply_to_client " + ResponseChannel.TclQuote() + " \"Error: ([clock format [clock seconds]])\\n$msg\\n$errorInfo\";error $msg}"; if (coroutineComboBox.SelectedIndex == 1) toExecute = "coroutine [ti::make_coroutine_name] " + toExecute; else if (coroutineComboBox.SelectedIndex == 2) toExecute = "coroutine [ti::make_coroutine_name -daemon] " + toExecute; } if (socketRadioButton.Checked) // The socket namespace is unique to this program. We need to create it // before we try to use it, or the apply will fail. This is not necessary // for the top level namespace because that always exists. We could do this // for a custom namespace. However, a custom namespace probably refers to // something we sourced from a TCL file. If that namespace doesn't already // exist, the user probably had a typo when he entered the namespace. toExecute = "namespace eval " + tclNamespace + " {}; " + toExecute; if (streaming) logTextBox.AppendText("<<<< " + cancelToken.GetHashCode() + " <<<<\r\n" + script + "\r\n"); else logTextBox.AppendText("<<<<\r\n" + script + "\r\n"); if (!ServerConnectionHolder.Connected) logTextBox.AppendText("!!!! Not connected.\r\n"); else { var message = TalkWithServer.CreateMessage("command", "tcl_command", "script", toExecute); if (streaming) { ServerConnectionHolder.Connection.SendMessage(message, Response, streaming, cancelToken, cancelToken); _cancelTokens.Add(cancelToken); } else ServerConnectionHolder.Connection.SendMessage(message, Response); } object previousSelection = recentItemsComboBox.SelectedItem; int toSelect; if (null == previousSelection) // It was empty so now select the first row. toSelect = 0; else if (script.Equals(previousSelection)) // We've sent an existing item to the server. That item will be moved to the top of the // list and other items will be pushed down. Keep the same index. Something relevant // will be pushed into that slot. This helps us if we want to repeat several item in the // same order. toSelect = recentItemsComboBox.SelectedIndex; else // Do not change anything. The control will automatically keep the same item selected. // That might or might not be the same index. toSelect = -1; recentItemsComboBox.Items.Remove(script); recentItemsComboBox.Items.Insert(0, script); if (toSelect >= 0) recentItemsComboBox.SelectedIndex = toSelect; } private void Response(byte[] body, object clientId) { if (InvokeRequired) Invoke((MethodInvoker)delegate { Response(body, clientId); }); else { if (null == body) logTextBox.AppendText("!!!! Connection error.\r\n"); else { if (clientId is TalkWithServer.CancelToken) { logTextBox.AppendText(">>>> " + clientId.GetHashCode() + " >>>>\r\n"); } else logTextBox.AppendText(">>>>\r\n"); logTextBox.AppendText(TalkWithServer.BytesToStr(body).Replace("\n", "\r\n") + "\r\n"); } } } private void CopyFromComboBox() { if (recentItemsComboBox.SelectedIndex < 0) // Nothing selected. return; string previousScript = scriptTextBox.Text; scriptTextBox.Text = recentItemsComboBox.SelectedItem.ToString(); if ((previousScript != "") && (!recentItemsComboBox.Items.Contains(previousScript))) recentItemsComboBox.Items.Add(previousScript); } private void recentItemsComboBox_SelectionChangeCommitted(object sender, EventArgs e) { CopyFromComboBox(); } private void recentItemsComboBox_DropDownClosed(object sender, EventArgs e) { CopyFromComboBox(); } private void customTextBox_TextChanged(object sender, EventArgs e) { customRadioButton.Checked = true; } private void sendStreamingButton_Click(object sender, EventArgs e) { Send(true); } private void ScriptExecutor_FormClosed(object sender, FormClosedEventArgs e) { TalkWithServer connection = ServerConnectionHolder.Connection; if (null == connection) return; CancelAllStreaming(); } private void CancelAllStreaming() { foreach (var cancelToken in _cancelTokens) cancelToken.Cancel(); _cancelTokens.Clear(); } private void cancelAllStreamingButton_Click(object sender, EventArgs e) { // This exists mostly for debugging. Typically if you didn't want // any more results for a window, you'd close that window. CancelAllStreaming(); } /// /// The last connection name that we reported. /// /// It's not a big deal for the window title, but I don't want to spam /// the log if we reconnect a lot. /// /// This can be null. This is null before the first time we connect. /// private string _connectionName; void IConnectionNameListener.OnNewConnectionName(string connectionName) { if (_connectionName == connectionName) return; Text = "Script Executor – " + connectionName; // "connecting to" or "trying to connect to" but not necessarily "connected to". // These should be displaying unicode "black circle" characers. U+25CF logTextBox.AppendText("● ● ● Connecting to " + connectionName + " ● ● ● " + DateTime.Now.ToString("F") + " ● ● ●\r\n"); _connectionName = connectionName; } } }