FlashPatcher/FlashPatcher/TaskService/TaskServiceCronExt.cs

443 lines
15 KiB
C#
Raw Normal View History

2021-05-11 06:45:56 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace Microsoft.Win32.TaskScheduler
{
public abstract partial class Trigger
{
/// <summary>Creates a trigger using a cron string.</summary>
/// <param name="cronString">String using cron defined syntax for specifying a time interval. See remarks for syntax.</param>
/// <returns>Array of <see cref="Trigger"/> representing the specified cron string.</returns>
/// <exception cref="System.NotImplementedException">Unsupported cron string.</exception>
/// <remarks>
/// <note type="note"> This method does not support all combinations of cron strings. Please test extensively before use. Please post an issue with any
/// syntax that should work, but doesn't.</note>
/// <para>The following combinations are known <c>not</c> to work:</para>
/// <list type="bullet">
/// <item><description>Intervals on months (e.g. "* * * */5 *")</description></item>
/// <item><description>Intervals on DOW (e.g. "* * * * MON/3")</description></item>
/// </list>
/// <para>
/// This section borrows liberally from the site http://www.nncron.ru/help/EN/working/cron-format.htm. The cron format consists of five fields separated
/// by white spaces:
/// </para>
/// <code>
/// &lt;Minute&gt; &lt;Hour&gt; &lt;Day_of_the_Month&gt; &lt;Month_of_the_Year&gt; &lt;Day_of_the_Week&gt;
/// </code>
/// <para>Each item has bounds as defined by the following:</para>
/// <code>
/// * * * * *
/// | | | | |
/// | | | | +---- Day of the Week (range: 1-7, 1 standing for Monday)
/// | | | +------ Month of the Year (range: 1-12)
/// | | +-------- Day of the Month (range: 1-31)
/// | +---------- Hour (range: 0-23)
/// +------------ Minute (range: 0-59)
/// </code>
/// <para>Any of these 5 fields may be an asterisk (*). This would mean the entire range of possible values, i.e. each minute, each hour, etc.</para>
/// <para>
/// Any of the first 4 fields can be a question mark ("?"). It stands for the current time, i.e. when a field is processed, the current time will be
/// substituted for the question mark: minutes for Minute field, hour for Hour field, day of the month for Day of month field and month for Month field.
/// </para>
/// <para>Any field may contain a list of values separated by commas, (e.g. 1,3,7) or a range of values (two integers separated by a hyphen, e.g. 1-5).</para>
/// <para>
/// After an asterisk (*) or a range of values, you can use character / to specify that values are repeated over and over with a certain interval between
/// them. For example, you can write "0-23/2" in Hour field to specify that some action should be performed every two hours (it will have the same effect
/// as "0,2,4,6,8,10,12,14,16,18,20,22"); value "*/4" in Minute field means that the action should be performed every 4 minutes, "1-30/3" means the same
/// as "1,4,7,10,13,16,19,22,25,28".
/// </para>
/// </remarks>
public static Trigger[] FromCronFormat([NotNull] string cronString)
{
var cron = CronExpression.Parse(cronString);
System.Diagnostics.Debug.WriteLine($"{cronString}=M:{cron.Minutes}; H:{cron.Hours}; D:{cron.Days}; M:{cron.Months}; W:{cron.DOW}");
var ret = new List<Trigger>();
// There isn't a clean mechanism to handle intervals on DOW or months, so punt
//if (cron.DOW.IsIncr) throw new NotSupportedException();
//if (cron.Months.IsIncr) throw new NotSupportedException();
// WeeklyTrigger
if (cron.Days.FullRange && cron.Months.FullRange && !cron.DOW.IsEvery)
{
var tr = new WeeklyTrigger(cron.DOW.ToDOW());
ret.AddRange(ProcessCronTimes(cron, tr));
}
// MonthlyDOWTrigger
if (!cron.DOW.FullRange && (!cron.Days.FullRange || !cron.Months.FullRange))
{
var tr = new MonthlyDOWTrigger(cron.DOW.ToDOW(), cron.Months.ToMOY(), WhichWeek.AllWeeks);
ret.AddRange(ProcessCronTimes(cron, tr));
}
// MonthlyTrigger
if (!cron.Days.FullRange || !cron.Months.FullRange && cron.DOW.FullRange)
{
var tr = new MonthlyTrigger(1, cron.Months.ToMOY()) { DaysOfMonth = cron.Days.Values.ToArray() };
ret.AddRange(ProcessCronTimes(cron, tr));
}
// DailyTrigger
if (cron.Days.FullRange && cron.Months.FullRange && cron.DOW.IsEvery)
{
var tr = new DailyTrigger((short)cron.Days.Increment);
ret.AddRange(ProcessCronTimes(cron, tr));
}
// Fail out
if (ret.Count == 0)
throw new NotSupportedException();
return ret.ToArray();
}
private static IEnumerable<Trigger> ProcessCronTimes(CronExpression cron, Trigger baseTrigger)
{
// Sequential hours, every minute
// "* * * * *"
// "* 2-6 * * *"
if (cron.Minutes.FullRange && (cron.Hours.IsEvery || cron.Hours.IsRange))
{
System.Diagnostics.Debug.WriteLine("Minutes.FullRange && (Hours.IsEvery || Hours.IsRange)");
yield return MakeTrigger(
new TimeSpan(cron.Hours.FirstValue, 0, 0),
TimeSpan.FromMinutes(cron.Minutes.Increment),
TimeSpan.FromHours(cron.Hours.Duration));
}
// Non-sequential hours, every minute
// "* 3,5,6 * * *"
// "* 3-15/3 * * *"
else if (cron.Minutes.FullRange && (cron.Hours.IsList || cron.Hours.IsIncr))
{
System.Diagnostics.Debug.WriteLine("Minutes.FullRange && (Hours.IsList || Hours.IsIncr)");
foreach (var h in cron.Hours.Values)
{
yield return MakeTrigger(
new TimeSpan(h, 0, 0),
TimeSpan.FromMinutes(cron.Minutes.Increment),
TimeSpan.FromHours(1));
}
}
// Non-repeating minutes, every hour
// "3,6 * * * *" Every hour starting at 12:03 and 12:06
// "3-33 * * * *"
// "3-33/6 * * * *"
// "3,6 * 3-5 * *"
// "3-33 3-5 * * *"
// "3-33/6 3-5 * * *"
else if (!cron.Minutes.FullRange && (cron.Hours.IsEvery || cron.Hours.IsRange))
{
System.Diagnostics.Debug.WriteLine("!Minutes.FullRange && (Hours.IsEvery || Hours.IsRange)");
foreach (var m in cron.Minutes.Values)
{
yield return MakeTrigger(
new TimeSpan(cron.Hours.FirstValue, m, 0),
TimeSpan.FromHours(1),
TimeSpan.FromHours(cron.Hours.Duration));
}
}
// Sequential or repeating minutes, and non-sequential hours
else if ((cron.Minutes.IsRange || cron.Minutes.IsIncr) && (cron.Hours.IsList || cron.Hours.IsIncr))
{
System.Diagnostics.Debug.WriteLine("(Minutes.IsRange || Minutes.IsIncr) && (Hours.IsList || Hours.IsIncr)");
foreach (var h in cron.Hours.Values)
{
yield return MakeTrigger(
new TimeSpan(h, cron.Minutes.FirstValue, 0),
TimeSpan.FromMinutes(cron.Minutes.Increment),
TimeSpan.FromMinutes(cron.Minutes.Duration));
}
}
// Non-sequential, hours and minutes
// "3,6 3,6 * * *" Every day at 3:03, 3:06, 6:03 and 6:06
// "3/6 3/6 * * *" Every day at 3:03, 3:06, 6:03 and 6:06
else
{
System.Diagnostics.Debug.WriteLine("Minutes.IsList && (Hours.IsIncr || Hours.IsList)");
foreach (var h in cron.Hours.Values)
foreach (var m in cron.Minutes.Values)
yield return MakeTrigger(new TimeSpan(h, m, 0));
}
Trigger MakeTrigger(TimeSpan start, TimeSpan interval = default, TimeSpan duration = default)
{
var newTr = (Trigger)baseTrigger.Clone();
newTr.StartBoundary = newTr.StartBoundary.Date + start;
if (interval != default)
{
newTr.Repetition.Interval = interval;
newTr.Repetition.Duration = duration;
}
return newTr;
}
}
internal class CronExpression
{
private FieldVal[] Fields = new FieldVal[5];
private CronExpression() { }
public enum CronFieldType { Minutes, Hours, Days, Months, DaysOfWeek };
public FieldVal Days => Fields[2];
public FieldVal DOW => Fields[4];
public FieldVal Hours => Fields[1];
public FieldVal Minutes => Fields[0];
public FieldVal Months => Fields[3];
public static CronExpression Parse(string cronString)
{
var ret = new CronExpression();
if (cronString == null)
throw new ArgumentNullException(nameof(cronString));
var tokens = cronString.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length != 5)
{
throw new ArgumentException($"'{cronString}' is not a valid crontab expression. It must contain at least 5 components of a schedule "
+ "(in the sequence of minutes, hours, days, months, days of week).");
}
// min, hr, days, months, daysOfWeek
for (var i = 0; i < ret.Fields.Length; i++)
ret.Fields[i] = FieldVal.Parse(tokens[i], (CronFieldType)i);
return ret;
}
public struct FieldVal
{
private const string rangeRegEx = @"^(?:(?<A>\*)|(?<D1>\d+)(?:-(?<D2>\d+))?)(?:\/(?<I>\d+))?$";
private readonly static Dictionary<string, string> dow = new Dictionary<string, string>(7)
{
{ "SUN", "0" },
{ "MON", "1" },
{ "TUE", "2" },
{ "WED", "3" },
{ "THU", "4" },
{ "FRI", "5" },
{ "SAT", "6" },
};
private readonly static Dictionary<string, string> mon = new Dictionary<string, string>(12)
{
{ "JAN", "1" },
{ "FEB", "2" },
{ "MAR", "3" },
{ "APR", "4" },
{ "MAY", "5" },
{ "JUN", "6" },
{ "JUL", "7" },
{ "AUG", "8" },
{ "SEP", "9" },
{ "OCT", "10" },
{ "NOV", "11" },
{ "DEC", "12" },
};
private readonly static Dictionary<CronFieldType, MinMax> validRange = new Dictionary<CronFieldType, MinMax>(5)
{
{ CronFieldType.Days, new MinMax(1, 31) },
{ CronFieldType.DaysOfWeek, new MinMax(0, 6) },
{ CronFieldType.Hours, new MinMax(0, 23) },
{ CronFieldType.Minutes, new MinMax(0, 59) },
{ CronFieldType.Months, new MinMax(1, 12) },
};
private CronFieldType cft;
private FieldFlags flags;
private int incr;
private int[] vals;
public FieldVal(CronFieldType cft) { this.cft = cft; flags = 0; vals = new int[0]; incr = 1; FullRange = false; }
enum FieldFlags { List, Every, Range, Increment };
public int Duration => vals.Length == 1 ? 1 : vals[1] - vals[0] + 1;
public int Increment => incr;
public bool IsEvery { get => flags == FieldFlags.Every; private set => flags = FieldFlags.Every; }
public bool IsIncr { get => flags == FieldFlags.Increment; private set => flags = FieldFlags.Increment; }
public bool IsList { get => flags == 0; private set => flags = FieldFlags.List; }
public bool IsRange { get => flags == FieldFlags.Range; private set => flags = FieldFlags.Range; }
public bool FullRange { get; private set; }
public int FirstValue => vals[0];
public IEnumerable<int> Values
{
get
{
if (flags == 0)
{
foreach (var i in vals)
yield return i;
}
else
{
for (int i = vals[0]; i <= vals[1]; i += incr)
yield return i;
}
}
}
public DaysOfTheWeek ToDOW()
{
if (IsEvery) return DaysOfTheWeek.AllDays;
DaysOfTheWeek ret = 0;
foreach (var i in Values)
ret |= (DaysOfTheWeek)(1 << i);
return ret;
}
public MonthsOfTheYear ToMOY()
{
if (IsEvery) return MonthsOfTheYear.AllMonths;
MonthsOfTheYear ret = 0;
foreach (var i in Values)
ret |= (MonthsOfTheYear)(1 << (i - 1));
return ret;
}
public static FieldVal Parse(string str, CronFieldType cft)
{
var res = new FieldVal(cft);
if (string.IsNullOrEmpty(str))
throw new ArgumentNullException(nameof(str), "A crontab field value cannot be empty.");
// Do substitutions
str = DoSubs(str, cft);
// Look first for a list of values (e.g. 1,2,3).
if (System.Text.RegularExpressions.Regex.IsMatch(str, @"^\d+(,\d+)*$"))
{
if (str.Contains("/")) throw new NotSupportedException();
res.vals = str.Split(',').Select(ParseInt).OrderBy(i => i).Distinct().ToArray();
res.Validate();
return res;
}
// Look for *|nn[-nn][/n] pattern
var match = System.Text.RegularExpressions.Regex.Match(str, rangeRegEx);
if (match.Success)
{
bool hasAst = res.FullRange = match.Groups["A"].Success;
if (match.Groups["I"].Success)
{
res.incr = ParseInt(match.Groups["I"].Value);
res.IsIncr = true;
}
else
{
if (hasAst)
res.IsEvery = true;
else
res.IsRange = true;
}
var mm = validRange[cft];
var start = hasAst ? mm.Min : ParseInt(match.Groups["D1"].Value);
var end = hasAst ? mm.Max : (match.Groups["D2"].Success ? ParseInt(match.Groups["D2"].Value) : (res.IsIncr ? mm.Max : start));
if (end < start) throw new ArgumentOutOfRangeException();
if (start == end && res.IsRange)
{
res.IsList = true;
res.vals = new[] { start };
}
else
res.vals = new[] { start, end };
res.Validate();
return res;
}
throw new FormatException();
}
public override string ToString() => $"Type:{flags}; Vals:{string.Join(",", vals.Select(i => i.ToString()).ToArray())}; Incr:{incr}";
private void Validate()
{
var l = validRange[cft];
if (vals.Any(i => i < l.Min || i > l.Max)) throw new ArgumentOutOfRangeException();
if (IsIncr && (incr < l.Min || incr > l.Max)) throw new ArgumentOutOfRangeException();
}
private static string DoSubs(string str, CronFieldType cft)
{
var sb = new System.Text.StringBuilder(str);
// Handle SUN-SAT strings
if (cft == CronFieldType.DaysOfWeek)
{
foreach (var kv in dow)
sb.Replace(kv.Key, kv.Value);
}
// Handle JANDEC strings
if (cft == CronFieldType.Months)
{
foreach (var kv in mon)
sb.Replace(kv.Key, kv.Value);
}
// Check for "?" and substitute current time
if (sb.Length == 1 && sb.ToString() == "?")
{
var now = DateTime.Now;
var nval = 0;
switch (cft)
{
case CronFieldType.Minutes:
nval = now.Minute;
break;
case CronFieldType.Hours:
nval = now.Hour;
break;
case CronFieldType.Days:
nval = now.Day;
break;
case CronFieldType.Months:
nval = now.Month;
break;
case CronFieldType.DaysOfWeek:
nval = (int)now.DayOfWeek;
break;
default:
break;
}
sb.Remove(0, 1);
sb.Append(nval);
}
// Expand or collapse ranges
var minMax = validRange[cft];
foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(sb.ToString(), @"(\d+)-(\d+)"))
{
var low = ParseInt(m.Groups[1].Value);
var high = ParseInt(m.Groups[2].Value);
if (low == minMax.Min && high == minMax.Max)
sb.Replace(m.Value, "*");
else if (sb.ToString().Contains(','))
{
var rsb = new System.Text.StringBuilder(low.ToString());
for (int i = low; i < high; i++)
rsb.Append($",{i + 1}");
sb.Replace(m.Value, rsb.ToString());
}
}
return sb.ToString();
}
private static int ParseInt(string str) => int.Parse(str.Trim());
private struct MinMax
{
public int Min, Max;
public MinMax(int min, int max) { Min = min; Max = max; }
}
}
}
}
}