forked from admin/VolcanicAgenticCore
81 lines
4.5 KiB
C#
81 lines
4.5 KiB
C#
using System.Globalization;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Scheduler;
|
|
|
|
/// <summary>Rules shared by the <c>schedule</c> tool (new jobs) and the store loader (stored jobs).</summary>
|
|
internal static class JobValidation
|
|
{
|
|
public const int MaxNameLength = 80;
|
|
public const int MaxTextLength = 4000;
|
|
public const int MaxTimeoutSeconds = 3600;
|
|
public static readonly TimeSpan PastGrace = TimeSpan.FromSeconds(60);
|
|
|
|
/// <summary>Validates a new job and returns its schedule (<c>at</c> resolved in <paramref name="zone"/>). Throws <see cref="JobValidationException"/>.</summary>
|
|
public static JobSchedule ValidateNew(NewJob job, DateTimeOffset now, TimeZoneInfo zone)
|
|
{
|
|
var name = job.Name?.Trim() ?? "";
|
|
if (name.Length == 0) throw new JobValidationException("name is required (a short label such as \"Call the dentist\").");
|
|
if (name.Length > MaxNameLength) throw new JobValidationException($"name is {Count(name.Length)} characters; the limit is {Count(MaxNameLength)}.");
|
|
var hasAt = !string.IsNullOrWhiteSpace(job.At);
|
|
var hasCron = !string.IsNullOrWhiteSpace(job.Cron);
|
|
if (hasAt == hasCron) throw new JobValidationException("Set exactly one of at (a one-off time) or cron (a recurring schedule).");
|
|
CheckFields(job.Kind, job.Text, job.Command, job.TimeoutSeconds);
|
|
|
|
if (hasCron)
|
|
{
|
|
Schedules.ParseCron(job.Cron!);
|
|
return new JobSchedule(null, job.Cron!.Trim());
|
|
}
|
|
var at = Schedules.ParseAt(job.At!, zone);
|
|
if (at < now - PastGrace)
|
|
{
|
|
var local = TimeZoneInfo.ConvertTime(at, zone);
|
|
var localNow = TimeZoneInfo.ConvertTime(now, zone);
|
|
throw new JobValidationException(string.Create(CultureInfo.InvariantCulture,
|
|
$"at {local:yyyy-MM-dd HH:mm} ({ZoneNames.Name(zone)}) is in the past; now is {localNow:yyyy-MM-dd HH:mm}. Pick a future time."));
|
|
}
|
|
return new JobSchedule(at, null);
|
|
}
|
|
|
|
/// <summary>Null when a stored job is well formed, else why it is dropped. An overdue one-off is fine (it fires late).</summary>
|
|
public static string? CheckStored(ScheduledJob job)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(job.Id) || string.IsNullOrWhiteSpace(job.Target)) return "missing id or target";
|
|
if (string.IsNullOrWhiteSpace(job.Name) || job.Name.Length > MaxNameLength) return "missing or too long name";
|
|
if (job.Schedule is null || (job.Schedule.At is null) == (job.Schedule.Cron is null)) return "needs exactly one of at and cron";
|
|
if (job.Schedule.Cron is { } cron) Schedules.ParseCron(cron);
|
|
CheckFields(job.Kind, job.Text, job.Command, job.TimeoutSeconds);
|
|
return null;
|
|
}
|
|
catch (JobValidationException ex)
|
|
{
|
|
return ex.Message;
|
|
}
|
|
}
|
|
|
|
private static void CheckFields(JobKind kind, string? text, string? command, int? timeoutSeconds)
|
|
{
|
|
var hasText = !string.IsNullOrWhiteSpace(text);
|
|
var hasCommand = !string.IsNullOrWhiteSpace(command);
|
|
switch (kind)
|
|
{
|
|
case JobKind.Reminder when !hasText: throw new JobValidationException("A reminder needs text (the message to send).");
|
|
case JobKind.Prompt when !hasText: throw new JobValidationException("A prompt job needs text (what to ask the agent).");
|
|
case JobKind.Reminder or JobKind.Prompt when hasCommand: throw new JobValidationException("command is only for watchers.");
|
|
case JobKind.Watcher when !hasCommand: throw new JobValidationException("A watcher needs command (the shell command to run).");
|
|
case JobKind.Watcher when hasText: throw new JobValidationException("A watcher takes no text: its script's output becomes the prompt.");
|
|
case not (JobKind.Reminder or JobKind.Prompt or JobKind.Watcher): throw new JobValidationException("kind must be reminder, prompt or watcher.");
|
|
}
|
|
if (hasText && text!.Length > MaxTextLength) throw new JobValidationException($"text is {Count(text.Length)} characters; the limit is {Count(MaxTextLength)}.");
|
|
if (timeoutSeconds is { } seconds)
|
|
{
|
|
if (kind != JobKind.Watcher) throw new JobValidationException("timeoutSeconds is only for watchers.");
|
|
if (seconds is < 1 or > MaxTimeoutSeconds) throw new JobValidationException("timeoutSeconds must be between 1 and 3600.");
|
|
}
|
|
}
|
|
|
|
private static string Count(int n) => n.ToString(CultureInfo.InvariantCulture);
|
|
}
|