// Spludlow Software // Copyright © Samuel P. Ludlow 2020 All Rights Reserved // Distributed under the terms of the GNU General Public License version 3 // Distributed WITHOUT ANY WARRANTY; without implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE // https://www.spludlow.co.uk/LICENCE.TXT // The Spludlow logo is a registered trademark of Samuel P. Ludlow and may not be used without permission // v1.14 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Data; using System.Timers; namespace Spludlow { public class Scheduler { private const int CheckIntervalSeconds = 15; private Timer Timer; private DateTime PreviousElapsed; private DataTable ScheduleTable = new DataTable(); private DataTable CallsTable = null; private System.Threading.EventWaitHandle WaitFinished = null; public void Run() { this.CallsTable = ReadConfig().Tables[0]; this.PreviousElapsed = DateTime.Now; using (this.Timer = new System.Timers.Timer()) { this.Timer.Interval = CheckIntervalSeconds * 1000; this.Timer.AutoReset = true; this.Timer.Enabled = false; this.Timer.Elapsed += Timer_Elapsed; this.Timer.Start(); using (this.WaitFinished = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.ManualReset)) { Spludlow.Log.Report("Scheduler; Started Waiting on main thread."); this.WaitFinished.WaitOne(); this.Timer.Stop(); Spludlow.Log.Finish("Scheduler; Stopped after being asked."); } } } private void Timer_Elapsed(object sender, ElapsedEventArgs e) { DateTime currentTime = e.SignalTime; DateTime previousTime = this.PreviousElapsed; this.PreviousElapsed = currentTime; StringBuilder text = new StringBuilder("((CallTime > '@StartTime') AND (CallTime <= '@EndTime'))"); text.Replace("@StartTime", previousTime.ToString()); text.Replace("@EndTime", currentTime.ToString()); string selectText = text.ToString(); List callIds = new List(); lock (this.ScheduleTable) { if (this.ScheduleTable.Columns.Count == 0 || (previousTime.Date != currentTime.Date)) { this.ScheduleTable = DailySchedule(currentTime, this.CallsTable); Spludlow.Log.Report("Scheduler, Daily Schedule", this.CallsTable, this.ScheduleTable); } foreach (DataRow row in this.ScheduleTable.Select(selectText)) { int callId = (int)row["CallId"]; if (callIds.Contains(callId) == false) callIds.Add(callId); else Spludlow.Log.Warning("Scheduler, Timer_Elapsed; Duplicate Calls: " + callId, this.CallsTable, this.ScheduleTable); } } foreach (int callId in callIds) { DataRow callRow = this.CallsTable.Rows.Find(callId); if (callRow == null) throw new ApplicationException("Scheduler, Timer_Elapsed; Can find call row: " + callId); CallSet callSet = (CallSet)callRow["CallSet"]; string schedule = (string)callRow["Schedule"]; try { Spludlow.Call.Run(callSet); Spludlow.Log.Info("Scheduler; Despatched " + callId + ":" + schedule, callSet); } catch (Exception ee) { Spludlow.Log.Error("Scheduler; Despatching " + callId + ":" + schedule, callSet, ee); } callSet.Reset(); } } public void Stop() { if (this.WaitFinished == null) return; this.WaitFinished.Set(); } public static Spludlow.Variations DayVariations = new Variations(new string[] { "monday mon mo", "tuesday tue tu", "wednesday wed we", "thrusday thu th", "friday fri fr", "saturday sat sa", "sunday sun su", "weekday week", "weekend end", }); public static DataSet ReadConfig(string host) { return (DataSet)Spludlow.Call.Now(host, "Spludlow", "Spludlow.Scheduler", "ReadConfig"); //, CallFlags.InProcess); } public static DataSet ReadConfig() { string filename = Spludlow.Config.ProgramData + @"\Config\Scheduler.txt"; DataTable table = Spludlow.Data.TextTable.ReadText(new string[] { "CallId CallSet Schedule TimeStart TimeEnd Interval Date Info", "Int32* Spludlow.CallSet String String String String String String", }); if (File.Exists(filename) == true) { using (FileStream fileStream = Spludlow.Io.Files.FileStreamOpenRead(filename)) { using (StreamReader reader = new StreamReader(fileStream)) { int callId = 1; string rawLine; while ((rawLine = reader.ReadLine()) != null) { string line = rawLine.Trim(); if (line.Length == 0 || line[0] == '#') continue; string[] words = Spludlow.Text.Split(line, '\t', true); if (words.Length < 5) throw new ApplicationException("Scheduler, ReadConfig; Lines require at least 5 paramters (TimeSpec Address ATM)"); string schedule = words[0]; string address = words[1]; CallMethod method = new CallMethod(); method.SetAddress(address); Spludlow.CallText.Read(method, words, 2, reader); method.Flags |= CallFlags.DontDuplicate; CallSet callSet = new CallSet(method); callSet.Schedule = schedule; string timeStart = ""; string timeEnd = ""; string interval = ""; string date = ""; // 10:00-15:00@1:30/25/11/2020 int index; index = schedule.IndexOf("/"); if (index != -1) { date = schedule.Substring(index + 1); schedule = schedule.Substring(0, index); } index = schedule.IndexOf("@"); if (index != -1) { interval = schedule.Substring(index + 1); schedule = schedule.Substring(0, index); } if (schedule.Length > 0) { index = schedule.IndexOf("-"); if (index != -1) { timeStart = schedule.Substring(0, index); timeEnd = schedule.Substring(index + 1); } else { timeStart = schedule; } } CallMethod callMethod = callSet.CurrentMethod; StringBuilder info = new StringBuilder(); //info.Append(callMethod.Assembly); //info.Append(", "); info.Append(callMethod.Type); info.Append("."); info.Append(callMethod.Method); info.Append("("); info.Append(Spludlow.Parameters.ShortText(callMethod.Parameters)); info.Append(")"); string conArgs = Spludlow.Parameters.ShortText(callMethod.ConstructorArguments); if (conArgs.Length > 0) { info.Append("-ctor("); info.Append(Spludlow.Parameters.ShortText(callMethod.ConstructorArguments)); info.Append(")"); } table.Rows.Add(callId, callSet, words[0], timeStart, timeEnd, interval, date, info.ToString()); ++callId; } } } } else { CreateConfigFile(filename); Spludlow.Log.Warning("Scheduler, ReadConfig; No config file, created empty: " + filename); } return Spludlow.Data.ADO.WireDataSet(table); } private static void CreateConfigFile(string filename) { StringBuilder text = new StringBuilder(); text.AppendLine("# Spludlow Scheduler provides a simple way to run methods at the times you want."); text.AppendLine("# [TimeSpec] [Address] [Assembly] [Type] [Method] [Parameters]"); text.AppendLine(); text.AppendLine(); text.AppendLine(); File.WriteAllText(filename, text.ToString()); } public static DataTable DailySchedule(DateTime dateNow, DataTable table) { dateNow = dateNow.Date; DataTable dayTable = Spludlow.Data.TextTable.ReadText(new string[] { "CallId CallTime Info OnToday", "Int32 DateTime String Boolean", }); foreach (DataRow sourceRow in table.Rows) { int callId = (int)sourceRow["CallId"]; string dateSpec = (string)sourceRow["Date"]; StringBuilder text = new StringBuilder(); text.Append(callId); text.Append(":"); text.Append((string)sourceRow["Info"]); text.Append(":"); text.Append((string)sourceRow["Schedule"]); string info = text.ToString(); bool onToday = OnToday(dateNow, dateSpec); if (onToday == false) { dayTable.Rows.Add(callId, DBNull.Value, info, onToday); continue; } string timeStart = (string)sourceRow["TimeStart"]; string timeEnd = (string)sourceRow["TimeEnd"]; string interval = (string)sourceRow["Interval"]; if (timeEnd != "" && interval == "") throw new ApplicationException("Must specify an interval if using a time range."); if (interval == "") { dayTable.Rows.Add(callId, ParseTime(dateNow, timeStart), info, onToday); } else { DateTime startDate = dateNow.Date; if (timeStart != "") startDate = ParseTime(dateNow, timeStart); DateTime endDate = startDate.AddDays(1); if (timeEnd != "") endDate = ParseTime(dateNow, timeEnd); TimeSpan span = ParseSpan(interval); for (DateTime callDate = startDate; callDate <= endDate; callDate = callDate.Add(span)) dayTable.Rows.Add(callId, callDate, info, onToday); } } return dayTable; } public static DateTime ParseTime(DateTime dateNow, string timeText) { string[] words = Spludlow.Text.Split(timeText, ':', true); if (words.Length != 2) throw new ApplicationException("Bad time:\t" + timeText); return new DateTime(dateNow.Year, dateNow.Month, dateNow.Day, Int32.Parse(words[0]), Int32.Parse(words[1]), 0); } public static TimeSpan ParseSpan(string spanText) { if (spanText.Contains(":") == false) { int totalMins = Int32.Parse(spanText); int hours = totalMins / 60; int minutes = totalMins % 60; spanText = hours + ":" + minutes; } string[] words = Spludlow.Text.Split(spanText, ':', true); if (words.Length != 2) throw new ApplicationException("Bad time span:\t" + spanText); return new TimeSpan(Int32.Parse(words[0]), Int32.Parse(words[1]), 0); } public static bool OnToday(DateTime dateNow, string dateSpec) // expect round date { if (dateSpec == "") return true; DateTime date; int slashCount = 0; foreach (char ch in dateSpec) { if (ch == '/') ++slashCount; } if (slashCount > 0) { if (slashCount == 1) dateSpec += "/" + dateNow.Year; date = DateTime.Parse(dateSpec); return (date == dateNow); } int digitCount = 0; int letterCount = 0; foreach (char ch in dateSpec) { if (Char.IsDigit(ch) == true) ++digitCount; if (Char.IsLetter(ch) == true) ++letterCount; } int number = -1; string dayName = ""; if (digitCount > 0) number = Int32.Parse(dateSpec.Substring(0, digitCount)); if (letterCount > 0) dayName = dateSpec.Substring(digitCount); if (number != -1) { if (dayName != "") { // Number & Name - Postition of day in month 1mon=1st monday in month, 0fri=last friday in month return (dateNow == DayOfWeekInMonth(dateNow, DaysList(dayName), number)); } else { // Number - Day of the month, 0=last day in month return (dateNow == DateOfMonth(dateNow, number)); } } else { // Name - Week day on month, mon, tue, week (weekday), end (weekend) return OnDayOfWeek(dateNow, dateSpec); } } private static List DaysList(string dayName) { dayName = DayVariations.Match(dayName); List list = new List(); if (dayName == "weekday") { list.Add(DayOfWeek.Monday); list.Add(DayOfWeek.Tuesday); list.Add(DayOfWeek.Wednesday); list.Add(DayOfWeek.Thursday); list.Add(DayOfWeek.Friday); return list; } if (dayName == "weekend") { list.Add(DayOfWeek.Saturday); list.Add(DayOfWeek.Sunday); return list; } list.Add((DayOfWeek)Enum.Parse(typeof(DayOfWeek), dayName, true)); return list; } public static bool OnDayOfWeek(DateTime dateNow, string day) { day = DayVariations.Match(day); if (day == "weekday") { if (dateNow.DayOfWeek == DayOfWeek.Saturday || dateNow.DayOfWeek == DayOfWeek.Sunday) return false; else return true; } if (day == "weekend") { if (dateNow.DayOfWeek == DayOfWeek.Saturday || dateNow.DayOfWeek == DayOfWeek.Sunday) return true; else return false; } if (Enum.GetName(typeof(DayOfWeek), dateNow.DayOfWeek).ToLower() == day) return true; else return false; } public static DateTime DayOfWeekInMonth(DateTime monthDate, List dayOfWeeks, int position) // test !!! { monthDate = monthDate.Date; DateTime date = new DateTime(monthDate.Year, monthDate.Month, 1); if (position == 0) { date = date.AddMonths(1); date = date.AddDays(-1); while (dayOfWeeks.Contains(date.DayOfWeek) == false) date = date.AddDays(-1); return date; } while (position > 0) { if (dayOfWeeks.Contains(date.DayOfWeek) == true) { --position; if (position == 0) continue; } date = date.AddDays(1); } return date; } public static DateTime DateOfMonth(DateTime monthDate, int position) { monthDate = monthDate.Date; if (position != 0) return new DateTime(monthDate.Year, monthDate.Month, position); DateTime date = new DateTime(monthDate.Year, monthDate.Month, 1); date = date.AddMonths(1); date = date.AddDays(-1); return date; } private static void ReportWorker(Dictionary hostSchedules, string host, DateTime dateNow) { try { DataTable callsTable = Spludlow.Scheduler.ReadConfig(host).Tables[0]; DataTable scheduleTable = Spludlow.Scheduler.DailySchedule(dateNow, callsTable); lock (hostSchedules) { hostSchedules.Add(host, scheduleTable); } } catch (Exception ee) { Spludlow.Log.Error("Scheduler, ReportWorker; host:" + host, ee); } } public static DataTable Report(string[] hosts, DateTime dateNow) { Dictionary hostSchedules = new Dictionary(); List tasks = new List(); foreach (string host in hosts) { tasks.Add(new Task(() => ReportWorker(hostSchedules, host, dateNow))); } foreach (Task task in tasks) task.Start(); Task.WaitAll(tasks.ToArray()); List times = new List(); foreach (string host in hostSchedules.Keys) { foreach (DataRow row in hostSchedules[host].Rows) { if (row.IsNull("CallTime") == true) continue; DateTime callTime = (DateTime)row["CallTime"]; if (times.Contains(callTime) == false) times.Add(callTime); } } times.Sort(); DataTable resultTable = new DataTable(); resultTable.Columns.Add("CallTime", typeof(DateTime)); foreach (string host in hostSchedules.Keys) resultTable.Columns.Add(host, typeof(string)); resultTable.PrimaryKey = new DataColumn[] { resultTable.Columns["CallTime"] }; foreach (DateTime callTime in times) { DataRow row = resultTable.NewRow(); row["CallTime"] = callTime; foreach (string host in hostSchedules.Keys) row[host] = ""; resultTable.Rows.Add(row); } foreach (string host in hostSchedules.Keys) { DataTable scheduleTable = hostSchedules[host]; foreach (DataRow scheduleRow in scheduleTable.Rows) { if (scheduleRow.IsNull("CallTime") == true) continue; DateTime callTime = (DateTime)scheduleRow["CallTime"]; string info = (string)scheduleRow["Info"]; DataRow resultRow = resultTable.Rows.Find(callTime); if (resultRow == null) throw new ApplicationException("Schedule, Report; Cant find result row"); string existing = (string)resultRow[host]; StringBuilder text = new StringBuilder(); text.Append(existing); if (text.Length > 0) text.Append(", "); text.Append(info); resultRow[host] = text.ToString(); } } return resultTable; } } }