Cron: Bereich mit Schritt (7-22/3) lesen, ungueltige Ausdruecke im Scanner melden (#597)
Der Nachrichten-Digest im Dauerbetrieb der Test-VM lief nie: CronExpression verstand 'von-bis/schritt' nicht, TaskSchedule verschluckte den Fehler. Version 0.1.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ namespace ClawdDotNet.Core.Scheduling;
|
||||
|
||||
/// <summary>
|
||||
/// Einfaches Cron-Parsing für 5-Felder-Ausdrücke: Minute Stunde Tag Monat Wochentag
|
||||
/// Unterstützt: Zahlen, Wildcards (*), Bereiche (1-5), Listen (1,3,5), Schritte (*/5)
|
||||
/// Unterstützt: Zahlen, Wildcards (*), Bereiche (1-5), Listen (1,3,5), Schritte (*/5, 7-22/3, 10/15)
|
||||
/// </summary>
|
||||
public sealed class CronExpression
|
||||
{
|
||||
@@ -103,21 +103,19 @@ public sealed class CronExpression
|
||||
if (split.Length != 2)
|
||||
throw Bad(field, "Schrittangabe erwartet die Form 'basis/schritt'");
|
||||
|
||||
var start = split[0] == "*" ? min : ParseNumber(field, split[0], min, max);
|
||||
// Basis: "*" (ganzer Bereich), "a-b" (Bereich, #597) oder "a" (ab a bis Feldende).
|
||||
// "0 7-22/3 * * *" fiel vorher durch, und der Scanner verschwieg es.
|
||||
var (start, end) = split[0] == "*"
|
||||
? (min, max)
|
||||
: split[0].Contains('-')
|
||||
? ParseRange(field, split[0], min, max)
|
||||
: (ParseNumber(field, split[0], min, max), max);
|
||||
var step = ParseNumber(field, split[1], 1, max); // Schritt 0 wäre eine Endlosschleife
|
||||
for (var i = start; i <= max; i += step) result.Add(i);
|
||||
for (var i = start; i <= end; i += step) result.Add(i);
|
||||
}
|
||||
else if (part.Contains('-'))
|
||||
{
|
||||
var split = part.Split('-');
|
||||
if (split.Length != 2)
|
||||
throw Bad(field, "Bereich erwartet die Form 'von-bis'");
|
||||
|
||||
var from = ParseNumber(field, split[0], min, max);
|
||||
var to = ParseNumber(field, split[1], min, max);
|
||||
if (from > to)
|
||||
throw Bad(field, $"Bereich {from}-{to} ist rückwärts");
|
||||
|
||||
var (from, to) = ParseRange(field, part, min, max);
|
||||
for (var i = from; i <= to; i++) result.Add(i);
|
||||
}
|
||||
else
|
||||
@@ -129,6 +127,20 @@ public sealed class CronExpression
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (int From, int To) ParseRange(string field, string value, int min, int max)
|
||||
{
|
||||
var split = value.Split('-');
|
||||
if (split.Length != 2)
|
||||
throw Bad(field, "Bereich erwartet die Form 'von-bis'");
|
||||
|
||||
var from = ParseNumber(field, split[0], min, max);
|
||||
var to = ParseNumber(field, split[1], min, max);
|
||||
if (from > to)
|
||||
throw Bad(field, $"Bereich {from}-{to} ist rückwärts");
|
||||
|
||||
return (from, to);
|
||||
}
|
||||
|
||||
private static int ParseNumber(string field, string value, int min, int max)
|
||||
{
|
||||
if (!int.TryParse(value, out var n))
|
||||
|
||||
@@ -52,6 +52,9 @@ public sealed class TaskScanner : IAsyncDisposable
|
||||
/// läuft jede Minute, die Meldung soll nicht mitlaufen.</summary>
|
||||
private readonly HashSet<string> _warnedTimeZones = [];
|
||||
|
||||
/// <summary>Aufgaben, deren ungültiger Cron-Ausdruck bereits gemeldet wurde.</summary>
|
||||
private readonly HashSet<string> _warnedCron = [];
|
||||
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private Task? _loop;
|
||||
|
||||
@@ -173,6 +176,16 @@ public sealed class TaskScanner : IAsyncDisposable
|
||||
task.Id, task.Title, badZone);
|
||||
}
|
||||
|
||||
// Dasselbe für einen Cron-Ausdruck, den der Parser nicht versteht (#597).
|
||||
if (TaskSchedule.InvalidCron(task) is { } cronError
|
||||
&& _warnedCron.Add(task.Id))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Aufgabe {TaskId} ({Title}) hat den Termin cron \"{Cron}\", der nicht lesbar ist "
|
||||
+ "— sie wird nicht ausgeführt. {Fehler}",
|
||||
task.Id, task.Title, task.When!.Value, cronError);
|
||||
}
|
||||
|
||||
var occ = TaskSchedule.DueOccurrence(task, now);
|
||||
if (occ is null)
|
||||
continue;
|
||||
|
||||
@@ -60,6 +60,29 @@ public static class TaskSchedule
|
||||
? when.TimeZone
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Der Parserfehler eines Cron-Termins, wenn der Ausdruck ungültig ist — sonst
|
||||
/// <c>null</c>.
|
||||
///
|
||||
/// Ein solcher Termin feuert nie (<c>DueCron</c> fängt den Fehler ab, damit ein
|
||||
/// kaputter Ausdruck nicht den ganzen Takt kippt). Wie bei der Zeitzone meldet der
|
||||
/// Scanner das einmal je Aufgabe — vorher stand die Aufgabe als <c>todo</c> mit Termin
|
||||
/// im Taskboard und lief schlicht nie (#597).
|
||||
/// </summary>
|
||||
public static string? InvalidCron(TaskItem task)
|
||||
{
|
||||
if (task.When is not { Kind: TaskWhenKind.Cron } when) return null;
|
||||
try
|
||||
{
|
||||
CronExpression.Parse(when.Value);
|
||||
return null;
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? DueAt(TaskWhen when, string? marker, DateTime nowUtc)
|
||||
{
|
||||
if (marker is not null) return null; // ein einmaliger Termin feuert genau einmal
|
||||
|
||||
Reference in New Issue
Block a user