// 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;
}
}
}