// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Agents.Pools; using EpicGames.Horde.Commits; using EpicGames.Horde.Issues; using EpicGames.Horde.Jobs; using EpicGames.Horde.Jobs.Schedules; using EpicGames.Horde.Jobs.Templates; using EpicGames.Horde.Projects; using EpicGames.Horde.Users; namespace EpicGames.Horde.Streams { /// /// Exception thrown when stream validation fails /// public class InvalidStreamException : Exception { /// public InvalidStreamException() { } /// public InvalidStreamException(string message) : base(message) { } /// public InvalidStreamException(string message, Exception innerEx) : base(message, innerEx) { } } /// /// Information about a stream /// public interface IStream { /// /// Name of the stream. /// StreamId Id { get; } /// /// Project that this stream belongs to /// ProjectId ProjectId { get; } /// /// Name of the stream /// string Name { get; } /// /// Path to the config file for this stream /// string? ConfigPath { get; } /// /// Current revision of the config file /// string ConfigRevision { get; } /// /// Order for this stream on the dashboard /// int Order { get; } /// /// Notification channel for all jobs in this stream /// string? NotificationChannel { get; } /// /// Notification channel filter for this template. Can be Success, Failure, or Warnings. /// string? NotificationChannelFilter { get; } /// /// Channel to post issue triage notifications /// string? TriageChannel { get; } /// /// Tabs for this stream on the dashboard /// IReadOnlyList Tabs { get; } /// /// Agent types configured for this stream /// IReadOnlyDictionary AgentTypes { get; } /// /// Workspace types configured for this stream /// IReadOnlyDictionary WorkspaceTypes { get; } /// /// List of templates available for this stream /// IReadOnlyDictionary Templates { get; } /// /// Workflows configured for this stream /// IReadOnlyList Workflows { get; } /// /// Default settings for preflights against this stream /// IDefaultPreflight? DefaultPreflight { get; } /// /// Stream is paused for builds until specified time /// DateTime? PausedUntil { get; } /// /// Comment/reason for why the stream was paused /// string? PauseComment { get; } /// /// Commits for this stream /// ICommitCollection Commits { get; } /// /// Get the latest stream state /// /// Cancellation toke for this operation /// Updated stream, or null if it no longer exists Task RefreshAsync(CancellationToken cancellationToken = default); /// /// Updates user-facing properties for an existing stream /// /// The new datetime for pausing builds /// The reason for pausing /// Cancellation token for the operation /// The updated stream if successful, null otherwise Task TryUpdatePauseStateAsync(DateTime? newPausedUntil, string? newPauseComment, CancellationToken cancellationToken = default); /// /// Attempts to update the last trigger time for a schedule /// /// The template ref id /// New last trigger time for the schedule /// New last trigger commit for the schedule /// New list of active jobs /// Cancellation token for the operation /// The updated stream if successful, null otherwise Task TryUpdateScheduleTriggerAsync(TemplateId templateRefId, DateTime? lastTriggerTimeUtc, CommitIdWithOrder? lastTriggerCommitId, List newActiveJobs, CancellationToken cancellationToken = default); /// /// Attempts to update a stream template ref /// /// The template ref to update /// The stream states to update, pass an empty list to clear all step states, otherwise will be a partial update based on included step updates /// Cancellation token for the operation /// Task TryUpdateTemplateRefAsync(TemplateId templateRefId, List? stepStates = null, CancellationToken cancellationToken = default); } /// /// Style for rendering a tab /// public enum TabStyle { /// /// Regular job list /// Normal, /// /// Omit job names, show condensed view /// Compact, } /// /// Information about a page to display in the dashboard for a stream /// public interface IStreamTab { /// /// Title of this page /// string Title { get; } /// /// Type of this tab /// string Type { get; } /// /// Presentation style for this page /// TabStyle Style { get; } /// /// Whether to show job names on this page /// bool ShowNames { get; } /// /// Whether to show all user preflights /// bool? ShowPreflights { get; } /// /// Names of jobs to include on this page. If there is only one name specified, the name column does not need to be displayed. /// IReadOnlyList? JobNames { get; } /// /// List of job template names to show on this page. /// IReadOnlyList? Templates { get; } /// /// Columns to display for different types of aggregates /// IReadOnlyList? Columns { get; } } /// /// Type of a column in a jobs tab /// public enum TabColumnType { /// /// Contains labels /// Labels, /// /// Contains parameters /// Parameter } /// /// Describes a column to display on the jobs page /// public interface IStreamTabColumn { /// /// The type of column /// TabColumnType Type { get; } /// /// Heading for this column /// string Heading { get; } /// /// Category of aggregates to display in this column. If null, includes any aggregate not matched by another column. /// string? Category { get; } /// /// Parameter to show in this column /// string? Parameter { get; } /// /// Relative width of this column. /// int? RelativeWidth { get; } } /// /// Mapping from a BuildGraph agent type to a set of machines on the farm /// public interface IAgentType { /// /// Pool of agents to use for this agent type /// PoolId Pool { get; } /// /// Name of the workspace to sync /// string? Workspace { get; } /// /// Path to the temporary storage dir /// string? TempStorageDir { get; } /// /// Environment variables to be set when executing the job /// IReadOnlyDictionary? Environment { get; } } /// /// Information about a workspace type /// public interface IWorkspaceType { /// /// Name of the Perforce server cluster to use /// string? Cluster { get; } /// /// The Perforce server and port (eg. perforce:1666) /// string? ServerAndPort { get; } /// /// User to log into Perforce with (defaults to buildmachine) /// string? UserName { get; } /// /// Identifier to distinguish this workspace from other workspaces. Defaults to the workspace type name. /// string? Identifier { get; } /// /// Override for the stream to sync /// string? Stream { get; } /// /// Custom view for the workspace /// IReadOnlyList? View { get; } /// /// Whether to use an incrementally synced workspace /// bool? Incremental { get; } /// /// Whether to use the AutoSDK /// bool? UseAutoSdk { get; } /// /// View for the AutoSDK paths to sync. If null, the whole thing will be synced. /// IReadOnlyList? AutoSdkView { get; } /// /// Method to use when syncing/materializing data from Perforce /// string? Method { get; } /// /// Minimum disk space that must be available *after* syncing this workspace (in megabytes) /// If not available, the job will be aborted. /// long? MinScratchSpace { get; } /// /// Threshold for when to trigger an automatic conform of agent. Measured in megabytes free on disk. /// Set to null or 0 to disable. /// long? ConformDiskFreeSpace { get; } } /// /// Specifies defaults for running a preflight /// public interface IDefaultPreflight { /// /// The template id to query /// TemplateId? TemplateId { get; } /// /// Query for the change to use /// IChangeQuery? Change { get; } } /// /// Job template in a stream /// public interface ITemplateRef { /// /// The template id /// TemplateId Id { get; } /// /// Whether to show badges in UGS for these jobs /// bool ShowUgsBadges { get; } /// /// Whether to show alerts in UGS for these jobs /// bool ShowUgsAlerts { get; } /// /// Notification channel for this template. Overrides the stream channel if set. /// string? NotificationChannel { get; } /// /// Notification channel filter for this template. Can be a combination of "Success", "Failure" and "Warnings" separated by pipe characters. /// string? NotificationChannelFilter { get; } /// /// Triage channel for this template. Overrides the stream channel if set. /// string? TriageChannel { get; } /// /// List of schedules for this template /// ISchedule? Schedule { get; } /// /// List of chained job triggers /// IReadOnlyList? ChainedJobs { get; } /// /// List of template step states /// IReadOnlyList StepStates { get; } /// /// Default change to use for this job /// IReadOnlyList? DefaultChange { get; } } /// /// Trigger for another template /// public interface IChainedJobTemplate { /// /// Name of the target that needs to complete before starting the other template /// string Trigger { get; } /// /// Id of the template to trigger /// TemplateId TemplateId { get; } /// /// Whether to use the default change for the template rather than the change for the parent job. /// bool UseDefaultChangeForTemplate { get; } } /// /// Information about a paused template step /// public interface ITemplateStep { /// /// Name of the step /// string Name { get; } /// /// User who paused the step /// UserId PausedByUserId { get; } /// /// The UTC time when the step was paused /// DateTime PauseTimeUtc { get; } } /// /// Extension methods for streams /// public static class StreamExtensions { /// /// Updates an existing stream /// /// The stream to update /// The new datetime for pausing builds /// The reason for pausing /// Cancellation token for the operation /// Async task object public static async Task TryUpdatePauseStateAsync(this IStream? stream, DateTime? newPausedUntil = null, string? newPauseComment = null, CancellationToken cancellationToken = default) { for (; stream != null; stream = await stream.RefreshAsync(cancellationToken)) { IStream? newStream = await stream.TryUpdatePauseStateAsync(newPausedUntil, newPauseComment, cancellationToken); if (newStream != null) { return newStream; } } return null; } /// /// Attempts to update the last trigger time for a schedule /// /// The stream to update /// The template ref id /// /// /// Jobs to add /// Jobs to remove /// Cancellation token for the operation /// True if the stream was updated public static async Task TryUpdateScheduleTriggerAsync(this IStream stream, TemplateId templateRefId, DateTime? lastTriggerTimeUtc = null, CommitIdWithOrder? lastTriggerCommitId = null, List? addJobs = null, List? removeJobs = null, CancellationToken cancellationToken = default) { IStream? newStream = stream; while (newStream != null) { ITemplateRef? templateRef; if (!newStream.Templates.TryGetValue(templateRefId, out templateRef)) { break; } if (templateRef.Schedule == null) { break; } IEnumerable newActiveJobs = templateRef.Schedule.ActiveJobs; if (removeJobs != null) { newActiveJobs = newActiveJobs.Except(removeJobs); } if (addJobs != null) { newActiveJobs = newActiveJobs.Union(addJobs); } newStream = await newStream.TryUpdateScheduleTriggerAsync(templateRefId, lastTriggerTimeUtc, lastTriggerCommitId, newActiveJobs.ToList(), cancellationToken); if (newStream != null) { return newStream; } newStream = await stream.RefreshAsync(cancellationToken); } return null; } /// /// Check if stream is paused for new builds /// /// The stream object /// Current time (allow tests to pass in a fake clock) /// If stream is paused public static bool IsPaused(this IStream stream, DateTime currentTime) { return stream.PausedUntil != null && stream.PausedUntil > currentTime; } } }