using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Data; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using IronPython.Hosting; using IronPython.Runtime; using Microsoft.Scripting; using Microsoft.Scripting.Hosting; using Microsoft.Win32; namespace FileWatcher { public partial class FileGroupTab : UserControl { /// /// Used for catching output from the Python runtime /// private EventRaisingStreamWriter m_LogStream = null; private EventRaisingStreamWriter m_ErrorStream = null; private MemoryStream m_MemoryStream = new MemoryStream(); private delegate void SetTextCallback(object sender, MyEvtArgs e); private delegate void SetStringCallback(string e); // TODO: Push the updated files to a multiprocessing queue that I create inside of the scope of the script private ScriptEngine pyEngine = null; private ScriptRuntime pyRuntime = null; private ScriptScope pyScope = null; BindingList m_FolderList = new BindingList(); List m_FileWatchers = new List(); List m_UpdatedFiles = new List(); TimerPlus m_Timer = new TimerPlus(); System.Timers.Timer m_TickCounter = new System.Timers.Timer(); private FileGroupSettings m_Settings = null; private static Regex regexSpaces = new Regex(@"^([a-zA-Z0-9_]*\*?[a-zA-Z0-9_]*(\.(\*|[a-zA-Z0-9]+))\s?)+$"); private static Regex regexMS = new Regex(@"^\d+ms$"); private static Regex regexS = new Regex(@"^\d+s$"); private static Regex regexM = new Regex(@"^\d+m$"); private static Regex regexH = new Regex(@"^\d+h$"); private static Regex regexTime = new Regex(@"^(\d+(ms|s|m|h)\s?)+$"); private static Regex regexAbsolutePath = new Regex(@"^[a-zA-Z]\:\\$"); private static Regex regexNetworkPath = new Regex(@"^\\\\$"); public FileGroupTab() { InitializeComponent(); m_Timer.Enabled = false; m_Timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Tick); m_TickCounter.Enabled = false; m_TickCounter.Elapsed += new System.Timers.ElapsedEventHandler(timer_TickCountdown); if (pyEngine == null) { Dictionary options = new Dictionary(); options["Debug"] = true; pyEngine = Python.CreateEngine(options); pyScope = pyEngine.CreateScope(); m_LogStream = new EventRaisingStreamWriter(m_MemoryStream); m_LogStream.StringWritten += new EventHandler>(script_StringWritten); pyEngine.Runtime.IO.SetOutput(m_MemoryStream, m_LogStream); m_ErrorStream = new EventRaisingStreamWriter(m_MemoryStream); m_ErrorStream.StringWritten += new EventHandler>(script_ErrorStringWritten); pyEngine.Runtime.IO.SetErrorOutput(m_MemoryStream, m_ErrorStream); } foldersListBox.DataSource = m_FolderList; m_FolderList.ListChanged += new ListChangedEventHandler(foldersListBox_ListChanged); toolStripTextBox1_TextChanged(this, null); } public List GetPaths() { List paths = new List(); foreach (string str in foldersListBox.Items) { paths.Add(str); } return paths; } public bool AreExtenionsValid() { return regexSpaces.Matches(FileExtensionsToolBox.Text).Count != 0; } public string ExtensionString { get { return FileExtensionsToolBox.Text; } set { FileExtensionsToolBox.Text = value; } } public string[] Extensions { get { return FileExtensionsToolBox.Text.Split(' '); } set { FileExtensionsToolBox.Text = string.Join(" ", value); } } public string ScriptPath { get { return scriptPathTextBox.Text; } set { scriptPathTextBox.Text = value; } } public string GetScriptAbsolutePath() { bool match1 = regexAbsolutePath.Matches(ScriptPath).Count > 0; bool match2 = regexNetworkPath.Matches(ScriptPath).Count > 0; if (match1 == false && match2 == false) { // Path is not an absolute path, make it one return Path.GetFullPath(Path.Combine(new FileInfo(Application.ExecutablePath).DirectoryName, ScriptPath)); } return ScriptPath; } public string Delay { get { return toolStripTextBox1.Text; } set { toolStripTextBox1.Text = value; } } public FileGroupSettings Settings { get { if (null == m_Settings || m_Settings.IsDisposed) { m_Settings = new FileGroupSettings(); //m_Settings.Text = ""; } return m_Settings; } } public List Directories { get { return m_FolderList.Cast().ToList(); } set { m_FolderList.Clear(); foldersListBox.DataSource = null; foldersListBox.SelectionMode = SelectionMode.None; foreach (string i in value) m_FolderList.Add(i); foldersListBox.DataSource = m_FolderList; foldersListBox.SelectionMode = SelectionMode.MultiExtended; } } static public string GetPythonDir() { // if you want better debugging information, return the iron python path String ironPythonDir = "C:\\Program Files (x86)\\IronPython 2.7"; if (Directory.Exists(ironPythonDir)) { return ironPythonDir; } // Use the Libs folder in the local path (and let there be an error if it's not there) return new FileInfo(Application.ExecutablePath).DirectoryName; // Iron Python doesn't seem properly compatible with Python, don't use the Python libs. /*string[] paths = Environment.GetEnvironmentVariable("PATH").Split(';'); foreach (string s in paths) { if (s.ToLower().IndexOf("python") != -1 && File.Exists(Path.Combine(s, "python.exe"))) { return s; } } return null;*/ } private bool CompileSourceAndExecute() { string code = ""; string pythonDir = GetPythonDir(); if (pythonDir == null) { scriptWarning.Visible = true; scriptWarning.ToolTipText = "Could not find your Python path, try adding it to your system PATH and try again."; Logger.LogError("Could not find your Python path, try adding it to your system PATH and try again."); return false; } string pythonLibs = Path.Combine(pythonDir, "Lib"); if (Directory.Exists(pythonLibs)) { code += "import sys" + Environment.NewLine + "if '" + pythonLibs + "' not in sys.path: sys.path.append('" + pythonLibs + "')" + Environment.NewLine; } else { scriptWarning.Visible = true; scriptWarning.ToolTipText = "Could not find your Python Lib path in '" + pythonLibs + "'. Good luck."; Logger.LogError("Could not find your Python Lib path in '" + pythonLibs + "'. Good luck."); return false; } try { // TODO: backup list of unprocessed files and move it to the new scope // WARNING: for some reason the dependencies that are loaded from the // previous script do not get reloaded, even though they should be in // the scope that we're disposing and not in the new scope. Will have // to consider destroying the Python Engine and recreating that as well. pyScope = pyEngine.CreateScope(); pyEngine.Execute(code.Replace("\\", "\\\\"), pyScope); pyEngine.ExecuteFile(GetScriptAbsolutePath(),pyScope); //ScriptSource source = pyEngine.CreateScriptSourceFromString(code); // object result = source.Execute(pyScope); } catch (Exception e) { if (e is Microsoft.Scripting.SyntaxErrorException) { scriptWarning.Visible = true; scriptWarning.ToolTipText = "There is a syntax error: '" + e.Message + "' on line " + ((Microsoft.Scripting.SyntaxErrorException)e).Line; Logger.LogError("There is a syntax error in '" + Path.GetFileName(ScriptPath) + "': " + e.Message + "' on line " + ((Microsoft.Scripting.SyntaxErrorException)e).Line); return false; } else if (e is System.Reflection.TargetInvocationException) { scriptWarning.Visible = true; scriptWarning.ToolTipText = "There is an invokation error: '" + e.Message + "'" + Environment.NewLine + e.StackTrace; Logger.LogError("There is an invokation error in '" + Path.GetFileName(ScriptPath) + "': " + e.Message + "'" + Environment.NewLine + e.StackTrace); return false; } else if (e is System.ArgumentException) { scriptWarning.Visible = true; scriptWarning.ToolTipText = "There is an argument error: '" + e.Message + "'" + Environment.NewLine + e.StackTrace; Logger.LogError("There is an argument error in '" + Path.GetFileName(ScriptPath) + "': " + e.Message + "'" + Environment.NewLine + e.StackTrace); return false; } else if (e is IronPython.Runtime.Exceptions.ImportException) { scriptWarning.Visible = true; scriptWarning.ToolTipText = "There is an import error: '" + e.Message + "'" + Environment.NewLine + e.StackTrace; Logger.LogError("There is an import error in '" + Path.GetFileName(ScriptPath) + "': " + e.Message + "'" + Environment.NewLine + e.StackTrace); return false; } else { scriptWarning.Visible = true; scriptWarning.ToolTipText = "There is an unknown error: '" + e.Message + "'" + Environment.NewLine + e.StackTrace; Logger.LogError("There is an unknown error in '" + Path.GetFileName(ScriptPath) + "': " + e.Message + "'" + Environment.NewLine + e.StackTrace); return false; } } try { Func, bool> updated = pyScope.GetVariable, bool>>("process_updated_files"); } catch (System.MissingMemberException) { scriptWarning.Visible = true; scriptWarning.ToolTipText = "The function 'process_updated_files' is missing, it accepts a list of files."; Logger.LogError("'" + Path.GetFileName(ScriptPath) + "' is missing the function 'process_updated_files', which accepts a list of files. "); return false; } Logger.Log("Loaded and compiled '" + Path.GetFileName(ScriptPath) + "'"); SetInterval(); return true; } private void toolStripButton5_Click(object sender, EventArgs e) { } private void toolStripButton7_Click(object sender, EventArgs e) { } public void AddWatchPath(string path ) { foldersListBox.DataSource = null; foldersListBox.SelectionMode = SelectionMode.None; m_FolderList.Add(path); foldersListBox.DataSource = m_FolderList; foldersListBox.SelectionMode = SelectionMode.MultiExtended; } public void RemoveWatchPath(string path) { foldersListBox.DataSource = null; foldersListBox.SelectionMode = SelectionMode.None; m_FolderList.Remove(path); foldersListBox.DataSource = m_FolderList; foldersListBox.SelectionMode = SelectionMode.MultiExtended; } private void toolStripButton5_Click_1(object sender, EventArgs e) { //Form1.s_FolderDialogue.InitialDirectory = Environment.CurrentDirectory; if (Form1.s_FolderDialogue.ShowDialog()) { AddWatchPath(Form1.s_FolderDialogue.FileName); Form1.Instance.AutoSave(); } } private void toolStripButton6_Click(object sender, EventArgs e) { while (foldersListBox.SelectedItems.Count > 0) { RemoveWatchPath((string)foldersListBox.SelectedItems[0]); } Form1.Instance.AutoSave(); } private void toolStripButton7_Click_1(object sender, EventArgs e) { Process myProcess = new Process(); myProcess.StartInfo.FileName = Preferences.Instance.GetDefaultEditor(); //not the full application path if (String.IsNullOrEmpty(myProcess.StartInfo.FileName)) { MessageBox.Show("Your selected default and backup text editors cannot be found, please open up preferences ( Ctrl + P ) and select an editor that can be found on the local computer.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // TODO: check if directory is relative or not myProcess.StartInfo.Arguments = GetScriptAbsolutePath(); myProcess.Start(); } private void FileExtensionsToolBox_TextChanged(object sender, EventArgs e) { bool isMatch = regexSpaces.IsMatch(FileExtensionsToolBox.Text); extensionWarning.Visible = isMatch == false; UpdateFileWatchers(); } private void openFileDialog1_FileOk(object sender, CancelEventArgs e) { } private void toolStripButton1_Click(object sender, EventArgs e) { // TODO: check if directory is relative or not openFileDialog1.InitialDirectory = Path.GetDirectoryName(GetScriptAbsolutePath()); openFileDialog1.FileName = Path.GetFileName(ScriptPath); if (openFileDialog1.ShowDialog() == DialogResult.OK) { scriptPathTextBox.Text = Utils.MakeRelativePath(System.IO.Path.GetDirectoryName(System.Windows.Forms.Application.ExecutablePath), openFileDialog1.FileName); LoadScript(); } } private void toolStripButton2_Click(object sender, EventArgs e) { Settings.ShowDialog(this); } public void SetDelayCountDownText(string str) { if (toolStrip.InvokeRequired) { SetStringCallback d = new SetStringCallback(SetDelayCountDownText); this.Invoke(d, new object[] { str }); } else { delayCountDownLabel.Text = str; } } private void script_StringWritten(object sender, MyEvtArgs e) { if (richTextBox1.InvokeRequired) { SetTextCallback d = new SetTextCallback(script_StringWritten); this.Invoke(d, new object[] { sender, e }); } else { richTextBox1.AppendText(e.Value, Color.Black); richTextBox1.SelectionStart = richTextBox1.Text.Length; richTextBox1.ScrollToCaret(); } } private void script_ErrorStringWritten(object sender, MyEvtArgs e) { if (richTextBox1.InvokeRequired) { SetTextCallback d = new SetTextCallback(script_ErrorStringWritten); this.Invoke(d, new object[] { sender, e }); } else { richTextBox1.AppendText(e.Value, Color.Maroon); richTextBox1.SelectionStart = richTextBox1.Text.Length; richTextBox1.ScrollToCaret(); } } private void fileSystemWatcher1_Changed(object sender, FileSystemEventArgs e) { try { // DO NOT check to see if equal to directory if ((File.GetAttributes(e.FullPath) & FileAttributes.Directory) == FileAttributes.Directory) { return; } } catch (FileNotFoundException) { return; } if (m_Timer.Enabled == false || Settings.restartDelayCB.Checked) { m_Timer.Stop(); m_TickCounter.Stop(); m_Timer.Enabled = false; m_TickCounter.Enabled = false; SetInterval(); m_Timer.Enabled = true; m_TickCounter.Enabled = true; m_Timer.Start(); m_TickCounter.Start(); } if (Settings.stopScriptCB.Checked) { scriptWorker.CancelAsync(); } // TODO: check to see if the path is relative or absolute string fullPath = Path.Combine(new FileInfo(Application.ExecutablePath).DirectoryName, e.FullPath); if (false == m_UpdatedFiles.Contains(fullPath)) { m_UpdatedFiles.Add(fullPath); } } private void scriptPathTextBox_TextChanged(object sender, EventArgs e) { LoadScript(); } void UpdateFileWatchers() { if (false == AreExtenionsValid()) { return; } foreach (FileSystemWatcher fsw in m_FileWatchers) { fsw.Dispose(); } m_FileWatchers.Clear(); foreach (string s in Directories) { string path = Path.Combine(new FileInfo(Application.ExecutablePath).DirectoryName, s); foreach (string ext in Extensions) { if (false == Directory.Exists(path)) continue; FileSystemWatcher fileWatcher = new FileSystemWatcher(); // TODO: check to see if the path is relative fileWatcher.Path = path; fileWatcher.Filter = ext; fileWatcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; fileWatcher.Created += fileSystemWatcher1_Changed; fileWatcher.Changed += fileSystemWatcher1_Changed; fileWatcher.Renamed += fileSystemWatcher1_Changed; fileWatcher.Deleted += fileSystemWatcher1_Changed; // TODO: handle deleted files differently fileWatcher.EnableRaisingEvents = true; fileWatcher.IncludeSubdirectories = true; m_FileWatchers.Add(fileWatcher); } } } void foldersListBox_ListChanged(object sender, ListChangedEventArgs e) { switch (e.ListChangedType) { case ListChangedType.ItemMoved: case ListChangedType.ItemAdded: case ListChangedType.ItemChanged: case ListChangedType.ItemDeleted: UpdateFileWatchers(); break; } } private void timer_Tick(object sender, EventArgs e) { SetDelayCountDownText("--ms"); m_Timer.Stop(); m_Timer.Enabled = false; m_TickCounter.Stop(); m_TickCounter.Enabled = false; Form1.Instance.notifyIcon1.ShowBalloonTip(1000, "Script Running", "Running '" + ScriptPath + "'", ToolTipIcon.Info); //scriptWorker.RunWorkerAsync(m_UpdatedFiles); // run script in separate thread Func, bool> updated = pyScope.GetVariable, bool>>("process_updated_files"); try { var updateFiles = m_UpdatedFiles; m_UpdatedFiles.Clear(); updated(updateFiles); Logger.Log("Just finished running '" + ScriptPath + "'"); Form1.Instance.notifyIcon1.ShowBalloonTip(500, "Script Ran", "Finished running '" + ScriptPath + "'", ToolTipIcon.Info); } catch (System.Exception ex) { Logger.LogError("'" + ScriptPath + "' finished running with errors"); Form1.Instance.notifyIcon1.ShowBalloonTip(500, "Script Error", "'" + ScriptPath + "' finished running with errors", ToolTipIcon.Error); if (ex is IronPython.Runtime.Exceptions.ImportException) { Logger.Log("Import exception in '" + ScriptPath + "':" + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + ex.TargetSite); } else if (ex is System.MissingMemberException) { Logger.Log("Missing member exception in '" + ScriptPath + "':" + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + ex.TargetSite); } else if (ex is System.IO.DirectoryNotFoundException) { Logger.Log("Directory not found exception in '" + ScriptPath + "':" + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + ex.TargetSite); } else if (ex is System.ArgumentException) { Logger.Log("Directory not found exception in '" + ScriptPath + "':" + ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + ex.TargetSite); } else { Logger.Log("Error running '" + ScriptPath + "':" + ex.Message + Environment.NewLine + ex.StackTrace); } } SetInterval(); } private void timer_TickCountdown(object sender, EventArgs e) { string time_elapsed = ""; const double one_hour_to_ms = 3600000; const double one_minute_to_ms = 60000; const double one_second_to_ms = 1000.0; double ms = m_Timer.TimeRemaining.TotalMilliseconds; int hours = 0; while (ms > one_hour_to_ms) { ms -= one_hour_to_ms; ++hours; } int minutes = 0; while (ms > one_minute_to_ms) { ms -= one_minute_to_ms; ++minutes; } int seconds = 0; while (ms > one_second_to_ms) { ms -= one_second_to_ms; ++seconds; } // round up if (ms > 0) { ++seconds; } if (hours > 0) { time_elapsed += hours + "h"; } if (minutes > 0) { time_elapsed += (time_elapsed.Length > 0 ? " " : "") + minutes + "m"; } if (seconds > 0) { time_elapsed += (time_elapsed.Length > 0 ? " " : "") + seconds + "s"; } SetDelayCountDownText(time_elapsed); } bool SetInterval() { // disable timer before setting interval if (m_Timer.Enabled) return false; bool matches = regexTime.IsMatch(toolStripTextBox1.Text); toolStripLabel2.Visible = matches == false; toolStripLabel2.ToolTipText = "Invalid delay"; if (matches) { int interval = 0; string[] values = toolStripTextBox1.Text.Split(' '); foreach (string val in values) { if (val.Length == 0) continue; if (regexMS.IsMatch(val)) { interval += Convert.ToInt32(Regex.Match(val, @"\d+").Value); } else if (regexS.IsMatch(val)) { interval += Convert.ToInt32(Regex.Match(val, @"\d+").Value) * 1000; } else if (regexM.IsMatch(val)) { interval += Convert.ToInt32(Regex.Match(val, @"\d+").Value) * 1000 * 60; } else if (regexH.IsMatch(val)) { interval += Convert.ToInt32(Regex.Match(val, @"\d+").Value) * 1000 * 60 * 24; } else { throw new Exception("Unexpected format"); } } if (interval != 0) { m_Timer.Interval = interval; m_TickCounter.Interval = 1000; return true; } toolStripLabel2.Visible = true; toolStripLabel2.ToolTipText = "Cannot have a value of 0"; } return false; } private void toolStripTextBox1_TextChanged(object sender, EventArgs e) { SetInterval(); } public void ToggleScriptUpdate(bool enabled) { if (enabled) { scriptMonitorButton.Image = Properties.Resources.drop_box; scriptMonitorButton.Enabled = true; scriptMonitorButton.ToolTipText = "Script updated, press to reload it"; } else { scriptMonitorButton.Image = Properties.Resources.block; scriptMonitorButton.Enabled = false; scriptMonitorButton.ToolTipText = "Script Monitor (when script is updated check here to reload script)"; } } public bool LoadScript() { ToggleScriptUpdate(false); string absolutePath = GetScriptAbsolutePath(); if ( false == File.Exists(absolutePath)) { scriptWatcher1.EnableRaisingEvents = false; scriptWarning.Visible = true; scriptWarning.ToolTipText = "The source file '" + absolutePath + "' could not be found."; Logger.Log("The source file '" + absolutePath + "' could not be found."); return false; } scriptWatcher1.Path = new FileInfo(absolutePath).DirectoryName; scriptWatcher1.Filter = Path.GetFileName(absolutePath); scriptWatcher1.Changed += scriptWatcher1_Changed; scriptWatcher1.EnableRaisingEvents = true; if (false == CompileSourceAndExecute()) { return false; } scriptWarning.Visible = false; scriptWarning.ToolTipText = ""; return true; } private void scriptWatcher1_Changed(object sender, FileSystemEventArgs e) { // script updated ToggleScriptUpdate(true); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { } private void scriptMonitorButton_Click(object sender, EventArgs e) { LoadScript(); } } }