// Copyright Epic Games, Inc. All Rights Reserved.
//#define ENABLE_PUBLIC_DEBUG_CONTROLLER
//#define ENABLE_SECURE_DEBUG_CONTROLLER
#if ENABLE_PUBLIC_DEBUG_CONTROLLER
using Horde.Build.Collections;
using Horde.Build.Models;
using Horde.Build.Utilities;
namespace Horde.Build.Controllers
{
///
/// Public endpoints for the debug controller
///
[ApiController]
public class PublicDebugController : ControllerBase
{
///
/// The connection tracker service singleton
///
RequestTrackerService RequestTrackerService;
IHostApplicationLifetime ApplicationLifetime;
IDogStatsd DogStatsd;
///
/// Constructor
///
///
///
///
public PublicDebugController(RequestTrackerService RequestTrackerService, IHostApplicationLifetime ApplicationLifetime, IDogStatsd DogStatsd)
{
RequestTrackerService = RequestTrackerService;
ApplicationLifetime = ApplicationLifetime;
DogStatsd = DogStatsd;
}
///
/// Prints all the headers for the incoming request
///
/// Http result
[HttpGet]
[Route("/api/v1/debug/headers")]
public IActionResult GetRequestHeaders()
{
StringBuilder Content = new StringBuilder();
Content.AppendLine("
");
foreach (KeyValuePair Pair in HttpContext.Request.Headers)
{
foreach (string Value in Pair.Value)
{
Content.AppendLine(HttpUtility.HtmlEncode($"{Pair.Key}: {Value}"));
}
}
Content.Append("
");
return new ContentResult { ContentType = "text/html", StatusCode = (int)HttpStatusCode.OK, Content = Content.ToString() };
}
///
/// Waits specified number of milliseconds and then returns a response
/// Used for testing timeouts proxy settings.
///
/// Http result
[HttpGet]
[Route("/api/v1/debug/wait")]
public async Task GetAndWait([FromQuery] int WaitTimeMs = 1000)
{
await Task.Delay(WaitTimeMs);
string Content = $"Waited {WaitTimeMs} ms. " + new Random().Next(0, 10000000);
return new ContentResult { ContentType = "text/plain", StatusCode = (int)HttpStatusCode.OK, Content = Content };
}
///
/// Waits specified number of milliseconds and then throws an exception
/// Used for testing graceful shutdown and interruption of outstanding requests.
///
/// Http result
[HttpGet]
[Route("/api/v1/debug/exception")]
public async Task ThrowException([FromQuery] int WaitTimeMs = 0)
{
await Task.Delay(WaitTimeMs);
throw new Exception("Test exception triggered by debug controller!");
}
///
/// Trigger an increment of a DogStatsd metric
///
/// Http result
[HttpGet]
[Route("/api/v1/debug/metric")]
public ActionResult TriggerMetric([FromQuery] int Value = 10)
{
DogStatsd.Increment("hordeMetricTest", Value);
return Ok("Incremented metric 'hordeMetricTest' Type: " + DogStatsd.GetType());
}
///
/// Display metrics related to the .NET runtime
///
/// Http result
[HttpGet]
[Route("/api/v1/debug/dotnet-metrics")]
public ActionResult DotNetMetrics()
{
ThreadPool.GetMaxThreads(out int MaxWorkerThreads, out int MaxIoThreads);
ThreadPool.GetAvailableThreads(out int FreeWorkerThreads, out int FreeIoThreads);
ThreadPool.GetMinThreads(out int MinWorkerThreads, out int MinIoThreads);
int BusyIoThreads = MaxIoThreads - FreeIoThreads;
int BusyWorkerThreads = MaxWorkerThreads - FreeWorkerThreads;
StringBuilder Content = new StringBuilder();
Content.AppendLine("Threads:");
Content.AppendLine("-------------------------------------------------------------");
Content.AppendLine("Worker busy={0,-5} free={1,-5} min={2,-5} max={3,-5}", BusyWorkerThreads, FreeWorkerThreads, MinWorkerThreads, MaxWorkerThreads);
Content.AppendLine(" IOCP busy={0,-5} free={1,-5} min={2,-5} max={3,-5}", BusyIoThreads, FreeIoThreads, MinIoThreads, MaxWorkerThreads);
NumberFormatInfo Nfi = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
Nfi.NumberGroupSeparator = " ";
string FormatBytes(long Number)
{
return (Number / 1024 / 1024).ToString("#,0", Nfi) + " MB";
}
GCMemoryInfo GcMemoryInfo = GC.GetGCMemoryInfo();
Content.AppendLine("");
Content.AppendLine("");
Content.AppendLine("Garbage collection (GC):");
Content.AppendLine("-------------------------------------------------------------");
Content.AppendLine(" Latency mode: " + GCSettings.LatencyMode);
Content.AppendLine(" Is server GC: " + GCSettings.IsServerGC);
Content.AppendLine(" Total memory: " + FormatBytes(GC.GetTotalMemory(false)));
Content.AppendLine(" Total allocated: " + FormatBytes(GC.GetTotalAllocatedBytes(false)));
Content.AppendLine(" Heap size: " + FormatBytes(GcMemoryInfo.HeapSizeBytes));
Content.AppendLine(" Fragmented: " + FormatBytes(GcMemoryInfo.FragmentedBytes));
Content.AppendLine(" Memory Load: " + FormatBytes(GcMemoryInfo.MemoryLoadBytes));
Content.AppendLine(" Total available memory: " + FormatBytes(GcMemoryInfo.TotalAvailableMemoryBytes));
Content.AppendLine("High memory load threshold: " + FormatBytes(GcMemoryInfo.HighMemoryLoadThresholdBytes));
return Ok(Content.ToString());
}
///
/// Force a full GC of all generations
///
/// Prints time taken in ms
[HttpGet]
[Route("/api/v1/debug/force-gc")]
public ActionResult ForceTriggerGc()
{
Stopwatch Timer = new Stopwatch();
Timer.Start();
GC.Collect();
Timer.Stop();
return Ok($"Time taken: {Timer.Elapsed.TotalMilliseconds} ms");
}
///
/// Lists requests in progress
///
/// HTML result
[HttpGet]
[Route("/api/v1/debug/requests-in-progress")]
public ActionResult GetRequestsInProgress()
{
StringBuilder Content = new StringBuilder();
Content.AppendLine("");
Content.AppendLine("Requests in progress
");
Content.AppendLine("");
Content.AppendLine("");
Content.AppendLine("| Request Trace ID | ");
Content.AppendLine("Path | ");
Content.AppendLine("Started At | ");
Content.AppendLine("Age | ");
Content.AppendLine("
");
List> Requests = RequestTrackerService.GetRequestsInProgress().ToList();
Requests.Sort((A, B) => A.Value.StartedAt.CompareTo(B.Value.StartedAt));
foreach (KeyValuePair Entry in Requests)
{
Content.Append("");
Content.AppendLine($"| {Entry.Key} | ");
Content.AppendLine($"{Entry.Value.Request.Path} | ");
Content.AppendLine($"{Entry.Value.StartedAt} | ");
Content.AppendLine($"{Entry.Value.GetTimeSinceStartInMs()} ms | ");
Content.Append("
");
}
Content.Append("
\n\n");
return new ContentResult { ContentType = "text/html", StatusCode = (int)HttpStatusCode.OK, Content = Content.ToString() };
}
/*
// Used during development only
[HttpGet]
[Route("/api/v1/debug/stop")]
public ActionResult StopApp()
{
Task.Run(async () =>
{
await Task.Delay(100);
ApplicationLifetime.StopApplication();
});
return new ContentResult { ContentType = "text/plain", StatusCode = (int)HttpStatusCode.OK, Content = "App stopping..." };
}
/**/
}
#endif
#if ENABLE_SECURE_DEBUG_CONTROLLER
///
/// Controller managing account status
///
[ApiController]
[Authorize]
public class SecureDebugController : ControllerBase
{
///
/// The ACL service singleton
///
AclService AclService;
///
/// The database service instance
///
DatabaseService DatabaseService;
///
/// The job task source singelton
///
JobTaskSource JobTaskSource;
///
/// The globals singleton
///
ISingletonDocument GlobalsSingleton;
///
/// Collection of pool documents
///
IPoolCollection PoolCollection;
///
/// Collection of project documents
///
IProjectCollection ProjectCollection;
///
/// Collection of agent documents
///
IAgentCollection AgentCollection;
///
/// Collection of session documents
///
ISessionCollection SessionCollection;
///
/// Collection of lease documents
///
ILeaseCollection LeaseCollection;
///
/// Collection of template documents
///
ITemplateCollection TemplateCollection;
///
/// Collection of stream documents
///
IStreamCollection StreamCollection;
///
/// The graph collection singleton
///
IGraphCollection GraphCollection;
///
/// The log file collection singleton
///
ILogFileCollection LogFileCollection;
///
/// Perforce client
///
IPerforceService Perforce;
///
/// Fleet manager
///
IFleetManager FleetManager;
///
/// Settings
///
private IOptionsMonitor Settings;
///
/// Constructor
///
/// The ACL service singleton
/// The database service instance
/// The dispatch service singleton
/// The globals singleton
/// Collection of pool documents
/// Collection of project documents
/// Collection of agent documents
/// Collection of session documents
/// Collection of lease documents
/// Collection of template documents
/// Collection of stream documents
/// The graph collection
/// The log file collection
/// Perforce client
/// The default fleet manager
/// Settings
public SecureDebugController(AclService AclService, DatabaseService DatabaseService, JobTaskSource JobTaskSource, ISingletonDocument GlobalsSingleton, IPoolCollection PoolCollection, IProjectCollection ProjectCollection, IAgentCollection AgentCollection, ISessionCollection SessionCollection, ILeaseCollection LeaseCollection, ITemplateCollection TemplateCollection, IStreamCollection StreamCollection, IGraphCollection GraphCollection, ILogFileCollection LogFileCollection, IPerforceService Perforce, IFleetManager FleetManager, IOptionsMonitor Settings)
{
AclService = AclService;
DatabaseService = DatabaseService;
JobTaskSource = JobTaskSource;
GlobalsSingleton = GlobalsSingleton;
PoolCollection = PoolCollection;
ProjectCollection = ProjectCollection;
AgentCollection = AgentCollection;
SessionCollection = SessionCollection;
LeaseCollection = LeaseCollection;
TemplateCollection = TemplateCollection;
StreamCollection = StreamCollection;
GraphCollection = GraphCollection;
LogFileCollection = LogFileCollection;
Perforce = Perforce;
FleetManager = FleetManager;
Settings = Settings;
}
///
/// Prints all the environment variables
///
/// Http result
[HttpGet]
[Route("/api/v1/debug/environment")]
public async Task GetServerEnvVars()
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
StringBuilder Content = new StringBuilder();
Content.AppendLine("");
foreach (System.Collections.DictionaryEntry? Pair in System.Environment.GetEnvironmentVariables())
{
if (Pair != null)
{
Content.AppendLine(HttpUtility.HtmlEncode($"{Pair.Value.Key}={Pair.Value.Value}"));
}
}
Content.Append("");
return new ContentResult { ContentType = "text/html", StatusCode = (int)HttpStatusCode.OK, Content = Content.ToString() };
}
///
/// Returns diagnostic information about the current state of the queue
///
/// Information about the queue
[HttpGet]
[Route("/api/v1/debug/queue")]
public async Task> GetQueueStatusAsync()
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
return JobTaskSource.GetStatus();
}
///
/// Returns the complete config Horde uses
///
/// Information about the config
[HttpGet]
[Route("/api/v1/debug/config")]
public async Task GetConfig()
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
JsonSerializerOptions Options = new JsonSerializerOptions
{
WriteIndented = true
};
return Ok(JsonSerializer.Serialize(Settings.CurrentValue, Options));
}
///
/// Queries for all graphs
///
/// The graph definitions
[HttpGet]
[Route("/api/v1/debug/graphs")]
[ProducesResponseType(200, Type = typeof(GetGraphResponse))]
public async Task>> GetGraphsAsync([FromQuery] int? Index = null, [FromQuery] int? Count = null, [FromQuery] PropertyFilter? Filter = null)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
List Graphs = await GraphCollection.FindAllAsync(null, Index, Count);
return Graphs.ConvertAll(x => new GetGraphResponse(x).ApplyFilter(Filter));
}
///
/// Queries for a particular graph by hash
///
/// The graph definition
[HttpGet]
[Route("/api/v1/debug/graphs/{GraphId}")]
[ProducesResponseType(200, Type = typeof(GetGraphResponse))]
public async Task> GetGraphAsync(string GraphId, [FromQuery] PropertyFilter? Filter = null)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
IGraph Graph = await GraphCollection.GetAsync(ContentHash.Parse(GraphId));
return new GetGraphResponse(Graph).ApplyFilter(Filter);
}
///
/// Query all the job templates.
///
/// Filter for properties to return
/// Information about all the job templates
[HttpGet]
[Route("/api/v1/debug/templates")]
[ProducesResponseType(typeof(List), 200)]
public async Task> GetTemplatesAsync([FromQuery] PropertyFilter? Filter = null)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
List Templates = await TemplateCollection.FindAllAsync();
return Templates.ConvertAll(x => new GetTemplateResponse(x).ApplyFilter(Filter));
}
///
/// Retrieve information about a specific job template.
///
/// Id of the template to get information about
/// List of properties to return
/// Information about the requested template
[HttpGet]
[Route("/api/v1/debug/templates/{TemplateHash}")]
[ProducesResponseType(typeof(GetTemplateResponse), 200)]
public async Task> GetTemplateAsync(string TemplateHash, [FromQuery] PropertyFilter? Filter = null)
{
ContentHash TemplateHashValue = ContentHash.Parse(TemplateHash);
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
ITemplate? Template = await TemplateCollection.GetAsync(TemplateHashValue);
if (Template == null)
{
return NotFound();
}
return Template.ApplyFilter(Filter);
}
///
/// Retrieve metadata about a specific log file
///
/// Id of the log file to get information about
/// Filter for the properties to return
/// Information about the requested project
[HttpGet]
[Route("/api/v1/debug/logs/{LogFileId}")]
public async Task> GetLogAsync(LogId LogFileId, [FromQuery] PropertyFilter? Filter = null)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
ILogFile? LogFile = await LogFileCollection.GetLogFileAsync(LogFileId);
if (LogFile == null)
{
return NotFound();
}
return LogFile.ApplyFilter(Filter);
}
///
/// Populate the database with test data
///
/// Async task
[HttpGet]
[Route("/api/v1/debug/collections/{Name}")]
public async Task> GetDocumentsAsync(string Name, [FromQuery] string? Filter = null, [FromQuery] int Index = 0, [FromQuery] int Count = 10)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
IMongoCollection> Collection = DatabaseService.GetCollection>(Name);
List> Documents = await Collection.Find(Filter ?? "{}").Skip(Index).Limit(Count).ToListAsync();
return Documents;
}
///
/// Create P4 login ticket for the specified username
///
/// Perforce ticket
[HttpGet]
[Route("/api/v1/debug/p4ticket")]
public async Task> CreateTicket([FromQuery] string? Username = null)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
if (Username == null)
{
return BadRequest();
}
return await Perforce.CreateTicket(PerforceCluster.DefaultName, Username);
}
///
/// Debugs perforce service commands
///
/// Perforce ticket
[HttpGet]
[Route("/api/v1/debug/perforce/stress")]
public async Task> GetPerforceStress()
{
if (!await AclService.AuthorizeAsync(AclAction.AdminWrite, User))
{
return Forbid();
}
Dictionary Results = new Dictionary();
const string DevBuild = "//UE4/Dev-Build";
string? PerforceUser = User.GetPerforceUser();
object UpdateLock = new object();
// ParallelTest.ForAsync doesn't seem to handle async lambdas
IEnumerable Tasks = Enumerable.Range(0, 16).Select(async (Idx) => {
int LatestChange = await Perforce.GetLatestChangeAsync(PerforceCluster.DefaultName, DevBuild, PerforceUser);
int CodeChange = await Perforce.GetCodeChangeAsync(PerforceCluster.DefaultName, DevBuild, LatestChange);
lock (UpdateLock)
{
Results.Add($"Parallel Perforce Result {Idx}", $"LatestChange: {LatestChange} - CodeChange: {CodeChange}");
}
});
await Task.WhenAll(Tasks);
Tasks = Enumerable.Range(0, 16).Select(async (Idx) => {
int LatestChange = await Perforce.GetLatestChangeAsync(PerforceCluster.DefaultName, DevBuild, PerforceUser);
int CodeChange = await Perforce.GetCodeChangeAsync(PerforceCluster.DefaultName, DevBuild, LatestChange);
lock (UpdateLock)
{
Results.Add($"Parallel Perforce Result {Idx + 16}", $"LatestChange: {LatestChange} - CodeChange: {CodeChange}");
}
});
await Task.WhenAll(Tasks);
StringBuilder Content = new StringBuilder();
Content.AppendLine("");
foreach (KeyValuePair Pair in Results)
{
Content.AppendLine(HttpUtility.HtmlEncode($"{Pair.Key}={Pair.Value}"));
}
Content.Append("
");
return new ContentResult { ContentType = "text/html", StatusCode = (int)HttpStatusCode.OK, Content = Content.ToString() };
}
///
/// Debugs perforce service commands
///
/// Perforce ticket
[HttpGet]
[Route("/api/v1/debug/perforce/commands")]
public async Task> GetPerforceCommands()
{
if (!await AclService.AuthorizeAsync(AclAction.AdminWrite, User))
{
return Forbid();
}
StringBuilder Content = new StringBuilder();
const string DevBuild = "//UE4/Dev-Build";
// a test CL I have setup in Dev-Build
const int TestCL = 16687685;
string? PerforceUser = User.GetPerforceUser();
if (string.IsNullOrEmpty(PerforceUser))
{
throw new Exception("Perforce user required");
}
Dictionary Results = new Dictionary();
// NOTE: This is in Private-Build not DevBuild
int NewChangeId = await Perforce.CreateNewChangeAsync(PerforceCluster.DefaultName, "//UE4/Private-Build", "Counter.txt");
List ChangeDetails = await Perforce.GetChangeDetailsAsync(PerforceCluster.DefaultName, "//UE4/Private-Build", new int[] { NewChangeId }, PerforceUser);
Results.Add("CreateNewChangeAsync", $"{NewChangeId} : {ChangeDetails[0].Description}");
await Perforce.UpdateChangelistDescription(PerforceCluster.DefaultName, TestCL, $"Updated Description: New Change CL was {NewChangeId}");
ChangeDetails = await Perforce.GetChangeDetailsAsync(PerforceCluster.DefaultName, DevBuild, new int[] { TestCL }, PerforceUser);
Results.Add("UpdateChangelistDescription", $"{TestCL} : {ChangeDetails[0].Description}");
int LatestChange = await Perforce.GetLatestChangeAsync(PerforceCluster.DefaultName, DevBuild, PerforceUser);
Results.Add("GetLatestChangeAsync", LatestChange.ToString(CultureInfo.InvariantCulture));
int CodeChange = await Perforce.GetCodeChangeAsync(PerforceCluster.DefaultName, DevBuild, LatestChange);
Results.Add("GetCodeChangeAsync", CodeChange.ToString(CultureInfo.InvariantCulture));
PerforceUserInfo? UserInfo = await Perforce.GetUserInfoAsync(PerforceCluster.DefaultName, PerforceUser);
if (UserInfo == null)
{
Results.Add("GetUserInfoAsync", "null");
}
else
{
Results.Add("GetUserInfoAsync", $"{UserInfo.Login} : {UserInfo.Email}");
}
// changes
List Changes = await Perforce.GetChangesAsync(PerforceCluster.DefaultName, DevBuild, null, LatestChange, 10, PerforceUser);
int Count = 0;
foreach (ChangeSummary Summary in Changes)
{
Results.Add($"GetChangesAsync Result {Count++}", $"{Summary.Number} : {Summary.Author} - {Summary.Description}");
}
ChangeDetails = await Perforce.GetChangeDetailsAsync(PerforceCluster.DefaultName, DevBuild, Changes.Select(Change => Change.Number).ToArray(), PerforceUser);
Count = 0;
foreach (ChangeDetails Details in ChangeDetails)
{
Results.Add($"GetChangeDetailsAsync Result {Count++}", $"{Details.Number} : {Details.Author} - {Details.Files.ToJson()}");
}
String Ticket = "Failed (Expected if not running service account)";
try
{
await Perforce.CreateTicket(PerforceCluster.DefaultName, PerforceUser);
Ticket = "Success";
}
catch
{
}
Results.Add($"CreateTicket", $"{Ticket}");
List FileSummaries = await Perforce.FindFilesAsync(PerforceCluster.DefaultName, ChangeDetails.SelectMany(Details => Details.Files).Select(File => $"{DevBuild}{File}"));
Count = 0;
foreach (FileSummary File in FileSummaries)
{
Results.Add($"FindFilesAsync Result {Count++}", $"{File.Change} : {File.DepotPath} : {File.Exists}");
}
// print async
byte[] Bytes = await Perforce.PrintAsync(PerforceCluster.DefaultName, $"{DevBuild}/RunUAT.bat");
Results.Add($"PrintAsync: {DevBuild}/RunUAT.bat ", $"{System.Text.Encoding.Default.GetString(Bytes)}");
Bytes = await Perforce.PrintAsync(PerforceCluster.DefaultName, $"{DevBuild}/RunUAT.bat@13916380");
Results.Add($"PrintAsync: {DevBuild}/RunUAT.bat@13916380", $"{System.Text.Encoding.Default.GetString(Bytes)}");
// need to be running as service account for these
int? DuplicateCL = null;
int ShelvedChangeOnDevBuild = 16687685;
try
{
DuplicateCL = await Perforce.DuplicateShelvedChangeAsync(PerforceCluster.DefaultName, ShelvedChangeOnDevBuild);
Results.Add("DuplicateShelvedChangeAsync", $"Duplicated CL {DuplicateCL} from Shelved CL {ShelvedChangeOnDevBuild}");
}
catch
{
Results.Add($"DuplicateShelvedChangeAsync", "Failed - expected unless using service account");
}
if (DuplicateCL.HasValue)
{
// Note change client much exist for the update
ChangeDetails = await Perforce.GetChangeDetailsAsync(PerforceCluster.DefaultName, DevBuild, new int[]{ DuplicateCL.Value}, PerforceUser);
Results.Add("UpdateChangelistDescription - PreUpdate", $"{DuplicateCL} : {ChangeDetails[0].Description}");
try
{
await Perforce.UpdateChangelistDescription(PerforceCluster.DefaultName, DuplicateCL.Value, "Updated Description from Horde");
ChangeDetails = await Perforce.GetChangeDetailsAsync(PerforceCluster.DefaultName, DevBuild, new int[]{ DuplicateCL.Value}, PerforceUser);
Results.Add("UpdateChangelistDescription - PostUpdate", $"{DuplicateCL} : {ChangeDetails[0].Description}");
}
catch
{
Results.Add("UpdateChangelistDescription - Faile", $"Client from change {DuplicateCL} must exist");
}
await Perforce.DeleteShelvedChangeAsync(PerforceCluster.DefaultName, DuplicateCL.Value);
ChangeDetails = await Perforce.GetChangeDetailsAsync(PerforceCluster.DefaultName, DevBuild, new int[] {DuplicateCL.Value} , PerforceUser);
Results.Add("GetChangeDetailsAsync - After Delete", $"ChangeDetails.Count: {ChangeDetails.Count}");
}
byte[] ImageBytes = await Perforce.PrintAsync(PerforceCluster.DefaultName, $"{DevBuild}/Samples/Games/ShooterGame/ShooterGame.png");
Content.AppendLine("");
foreach (KeyValuePair Pair in Results)
{
Content.AppendLine(HttpUtility.HtmlEncode($"{Pair.Key}={Pair.Value}"));
}
Content.Append($"PrintAsync Binary: {DevBuild}/Samples/Games/ShooterGame/ShooterGame.png\n
");
Content.Append($"
");
Content.Append("");
return new ContentResult { ContentType = "text/html", StatusCode = (int)HttpStatusCode.OK, Content = Content.ToString() };
}
///
/// Simulates a crash of the native p4 bridge
///
[HttpGet]
[Route("/api/v1/debug/perforce/debugcrash")]
public async Task GetPerforceDebugCrash()
{
if (!await AclService.AuthorizeAsync(AclAction.AdminWrite, User))
{
return Forbid();
}
P4Debugging.DebugCrash();
return Ok();
}
///
/// Debugging fleet managers
///
/// Nothing
[HttpGet]
[Route("/api/v1/debug/fleetmanager")]
public async Task> FleetManage([FromQuery] string? PoolId = null, [FromQuery] string? ChangeType = null, [FromQuery] int? Count = null)
{
if (!await AclService.AuthorizeAsync(AclAction.AdminRead, User))
{
return Forbid();
}
if (PoolId == null)
{
return BadRequest("Missing 'PoolId' query parameter");
}
IPool? Pool = await PoolCollection.GetAsync(new PoolId(PoolId));
if (Pool == null)
{
return BadRequest($"Pool with ID '{PoolId}' not found");
}
if (!(ChangeType == "expand" || ChangeType == "shrink"))
{
return BadRequest("Missing 'ChangeType' query parameter and/or must be set to 'expand' or 'shrink'");
}
if (Count == null || Count.Value <= 0)
{
return BadRequest("Missing 'Count' query parameter and/or must be greater than 0");
}
List Agents = new List();
string Message = "No operation";
if (ChangeType == "expand")
{
await FleetManager.ExpandPool(Pool, Agents, Count.Value);
Message = $"Expanded pool {Pool.Name} by {Count}";
}
else if (ChangeType == "shrink")
{
Agents = await AgentCollection.FindAsync();
Agents = Agents.FindAll(a => a.IsInPool(Pool.Id)).ToList();
await FleetManager.ShrinkPool(Pool, Agents, Count.Value);
Message = $"Shrank pool {Pool.Name} by {Count}";
}
return new ContentResult { ContentType = "text/plain", StatusCode = (int)HttpStatusCode.OK, Content = Message };
}
///
/// Starts the profiler session
///
/// Text message
[HttpGet]
[Route("/api/v1/debug/profiler/start")]
public async Task StartProfiler()
{
await DotTrace.EnsurePrerequisiteAsync();
string SnapshotDir = Path.Join(Path.GetTempPath(), "horde-profiler-snapshots");
if (!Directory.Exists(SnapshotDir))
{
Directory.CreateDirectory(SnapshotDir);
}
var Config = new DotTrace.Config();
Config.SaveToDir(SnapshotDir);
DotTrace.Attach(Config);
DotTrace.StartCollectingData();
return new ContentResult { ContentType = "text/plain", StatusCode = (int)HttpStatusCode.OK, Content = "Profiling session started. Using dir " + SnapshotDir };
}
///
/// Stops the profiler session
///
/// Text message
[HttpGet]
[Route("/api/v1/debug/profiler/stop")]
public ActionResult StopProfiler()
{
DotTrace.SaveData();
DotTrace.Detach();
return new ContentResult { ContentType = "text/plain", StatusCode = (int)HttpStatusCode.OK, Content = "Profiling session stopped" };
}
///
/// Downloads the captured profiling snapshots
///
/// A .zip file containing the profiling snapshots
[HttpGet]
[Route("/api/v1/debug/profiler/download")]
public ActionResult DownloadProfilingData()
{
string SnapshotZipFile = DotTrace.GetCollectedSnapshotFilesArchive(false);
if (!System.IO.File.Exists(SnapshotZipFile))
{
return NotFound("The generated snapshot .zip file was not found");
}
return PhysicalFile(SnapshotZipFile, "application/zip", Path.GetFileName(SnapshotZipFile));
}
}
}
#endif