File: Replay.cs
Web Access
Project: src\src\Tools\Replay\Replay.csproj (Replay)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Basic.CompilerLog.Util;
using Microsoft.CodeAnalysis.CommandLine;
using Microsoft.CodeAnalysis.Options;
using Mono.Options;
 
var options = ParseOptions(args);
if (Directory.Exists(options.OutputDirectory))
{
    Directory.Delete(options.OutputDirectory, recursive: true);
}
Directory.CreateDirectory(options.OutputDirectory);
Directory.CreateDirectory(options.TempDirectory);
 
return await RunAsync(options).ConfigureAwait(false);
 
static ReplayOptions ParseOptions(string[] args)
{
    string? outputDirectory = null;
    string? binlogPath = null;
    int maxParallel = 6;
    bool wait = false;
 
    var options = new Mono.Options.OptionSet()
    {
        { "b|binlogPath=", "The binary log to relpay", v => binlogPath = v },
        { "o|outputDirectory=", "The directory to output the build results", v => outputDirectory = v },
        { "m|maxParallel=", "The maximum number of parallel builds", (int v) => maxParallel = v },
        { "w|wait", "Wait for a key press after starting the server", o => wait = o is object },
    };
 
    var rest = options.Parse(args);
    if (rest.Count == 1)
    {
        binlogPath = rest[0];
    }
    else if (rest.Count > 1)
    {
        throw new Exception($"Too many arguments: {string.Join(" ", rest)}");
    }
 
    if (string.IsNullOrEmpty(binlogPath))
    {
        throw new Exception("Missing binlogPath");
    }
 
    if (string.IsNullOrEmpty(outputDirectory))
    {
        outputDirectory = Path.Combine(Path.GetTempPath(), "replay");
    }
 
    return new ReplayOptions(
        pipeName: Guid.NewGuid().ToString(),
        clientDirectory: AppDomain.CurrentDomain.BaseDirectory!,
        outputDirectory: outputDirectory,
        binlogPath: binlogPath,
        maxParallel: maxParallel,
        wait: wait);
}
 
static async Task<int> RunAsync(ReplayOptions options)
{
    Console.WriteLine($"Binary Log: {options.BinlogPath}");
    Console.WriteLine($"Client Directory: {options.ClientDirectory}");
    Console.WriteLine($"Output Directory: {options.OutputDirectory}");
    Console.WriteLine($"Pipe Name: {options.PipeName}");
    Console.WriteLine($"Parallel: {options.MaxParallel}");
    Console.WriteLine();
    Console.WriteLine("Starting server");
 
    using var compilerServerLogger = new CompilerServerLogger("replay", Path.Combine(options.OutputDirectory, "server.log"));
    if (!BuildServerConnection.TryCreateServer(options.ClientDirectory, options.PipeName, compilerServerLogger, out int serverProcessId))
    {
        Console.WriteLine("Failed to start server");
        return 1;
    }
 
    Console.WriteLine($"Process Id: {serverProcessId}");
    if (options.Wait)
    {
        Console.WriteLine("Press any key to continue");
        Console.ReadKey(intercept: true);
        Console.WriteLine("Continuing");
    }
 
    try
    {
        var compilerCalls = ReadAllCompilerCalls(options.BinlogPath);
        var stopwatch = new Stopwatch();
        stopwatch.Start();
 
        await foreach (var buildData in BuildAllAsync(options, compilerCalls, compilerServerLogger, CancellationToken.None))
        {
            Console.WriteLine($"{buildData.CompilerCall.GetDiagnosticName()} ... {buildData.BuildResponse.Type}");
        }
 
        stopwatch.Stop();
        Console.WriteLine($"Pipe Name: {options.PipeName}");
        Console.WriteLine($"Compilation Events: {compilerCalls.Count}");
        Console.WriteLine($"Time: {stopwatch.Elapsed:mm\\:ss}");
 
        return 0;
    }
    finally
    {
        Console.WriteLine("Shutting down server");
 
        await BuildServerConnection.RunServerShutdownRequestAsync(
            options.PipeName,
            timeoutOverride: null,
            waitForProcess: true,
            compilerServerLogger,
            cancellationToken: CancellationToken.None).ConfigureAwait(false);
    }
}
 
static List<CompilerCall> ReadAllCompilerCalls(string binlogPath)
{
    using var stream = new FileStream(binlogPath, FileMode.Open, FileAccess.Read, FileShare.Read);
    return BinaryLogUtil.ReadAllCompilerCalls(stream, new List<string>());
}
 
static async IAsyncEnumerable<BuildData> BuildAllAsync(
    ReplayOptions options,
    List<CompilerCall> compilerCalls,
    CompilerServerLogger compilerServerLogger,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    var index = 0;
    var maxParallel = options.MaxParallel;
    var tasks = new List<Task<BuildData>>(capacity: maxParallel);
    var outputSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
    do
    {
        while (tasks.Count < maxParallel && index < compilerCalls.Count)
        {
            var compilerCall = compilerCalls[index];
            tasks.Add(BuildAsync(options, compilerCall, GetOutputName(compilerCall), compilerServerLogger, cancellationToken));
            index++;
        }
 
        var completedTask = await Task.WhenAny(tasks).ConfigureAwait(false);
        if (!tasks.Remove(completedTask))
        {
            throw new Exception("Task was not in the list");
        }
 
        var buildData = await completedTask.ConfigureAwait(false);
        yield return buildData;
    } while (index < compilerCalls.Count);
 
    string GetOutputName(CompilerCall compilerCall)
    {
        string name;
        if (compilerCall.Kind is CompilerCallKind.Regular)
        {
            name = $"{compilerCall.ProjectFileName}-{compilerCall.TargetFramework}";
        }
        else
        {
            name = $"{compilerCall.ProjectFileName}-{compilerCall.TargetFramework}-{compilerCall.Kind}";
        }
 
        if (!outputSet.Add(name))
        {
            name = $"{name}-{outputSet.Count}";
        }
 
        return name;
    }
}
 
static async Task<BuildData> BuildAsync(
    ReplayOptions options,
    CompilerCall compilerCall,
    string outputName,
    CompilerServerLogger compilerServerLogger,
    CancellationToken cancellationToken)
{
    var args = compilerCall.GetArguments();
    var outputDirectory = Path.Combine(options.OutputDirectory, outputName);
    Directory.CreateDirectory(outputDirectory);
    RewriteOutputPaths(outputDirectory, args);
 
    var request = BuildServerConnection.CreateBuildRequest(
        outputName,
        compilerCall.IsCSharp ? RequestLanguage.CSharpCompile : RequestLanguage.VisualBasicCompile,
        args.ToList(),
        workingDirectory: compilerCall.ProjectDirectory,
        tempDirectory: options.TempDirectory,
        keepAlive: null,
        libDirectory: null);
    var response = await BuildServerConnection.RunServerBuildRequestAsync(
        request,
        options.PipeName,
        options.ClientDirectory,
        compilerServerLogger,
        cancellationToken).ConfigureAwait(false);
    return new BuildData(compilerCall, response);
}
 
static void RewriteOutputPaths(string outputDirectory, string[] args)
{
    var comparison = StringComparison.OrdinalIgnoreCase;
    var hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    for (var i = 0; i < args.Length; i++)
    {
        var line = args[i];
        var isOption = line.Length > 0 && line[0] is '-' or '/';
        if (!isOption)
        {
            continue;
        }
 
        line = line.Substring(1);
        // Map all of the output items to the build output directory
        if (line.StartsWith("out", comparison) ||
            line.StartsWith("refout", comparison) ||
            line.StartsWith("doc", comparison) ||
            line.StartsWith("errorlog", comparison))
        {
            var index = line.IndexOf(':');
            var argValue = line.AsSpan().Slice(index + 1).ToString();
 
            string fileName;
            if (Path.IsPathRooted(argValue))
            {
                fileName = Path.GetFileName(argValue);
            }
            else
            {
                fileName = argValue;
            }
 
            if (!hashSet.Add(fileName))
            {
                fileName = Path.Combine(hashSet.Count.ToString(), fileName);
            }
 
            var filePath = Path.Combine(outputDirectory, fileName);
            Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
            var argName = line.AsSpan().Slice(0, index).ToString();
            args[i] = $"/{argName}:{filePath}";
        }
 
        if (line.StartsWith("generatedfilesout", comparison))
        {
            var generatedDir = Path.Combine(outputDirectory, "generated");
            Directory.CreateDirectory(generatedDir);
            args[i] = $"/generatedfilesout:{generatedDir}";
        }
    }
}
 
internal sealed class ReplayOptions(
    string pipeName,
    string clientDirectory,
    string outputDirectory,
    string binlogPath,
    int maxParallel,
    bool wait)
{
    internal string PipeName { get; } = pipeName;
    internal string ClientDirectory { get; } = clientDirectory;
    internal string OutputDirectory { get; } = outputDirectory;
    internal string BinlogPath { get; } = binlogPath;
    internal int MaxParallel { get; } = maxParallel;
    internal string TempDirectory { get; } = Path.Combine(outputDirectory, "temp");
    internal bool Wait { get; } = wait;
}
 
internal readonly struct BuildData(CompilerCall compilerCall, BuildResponse response)
{
    internal CompilerCall CompilerCall { get; } = compilerCall;
    internal BuildResponse BuildResponse { get; } = response;
}