File: tests\Infrastructure.Tests\ExecutableFile.cs
Web Access
Project: src\tests\AnalyzeCiFailure.Tests\AnalyzeCiFailure.Tests.csproj (AnalyzeCiFailure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Text;
 
namespace Infrastructure.Tests;
 
/// <summary>
/// Writes fake executables (for example, a stub <c>gh</c> placed on <c>PATH</c>) that a test runs right away.
/// </summary>
/// <remarks>
/// xUnit runs test classes in parallel, so other tests in this host start processes at any time. A child
/// forked while the host has a file open for writing inherits that descriptor until it calls <c>exec</c>,
/// and Linux fails <c>exec</c> of a file that any process has open for writing with <c>ETXTBSY</c>
/// ("Text file busy"; bash reports exit code 126). On Unix this helper writes and marks the file executable
/// from a short-lived <c>/bin/sh</c> child, so the test host never holds a writable descriptor to the file
/// that another fork could inherit.
/// </remarks>
internal static class ExecutableFile
{
    private const UnixFileMode DefaultMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
 
    public static void Write(string path, string contents, UnixFileMode mode = DefaultMode)
        => WriteAsync(path, contents, mode).GetAwaiter().GetResult();
 
    public static async Task WriteAsync(string path, string contents, UnixFileMode mode = DefaultMode)
    {
        if (OperatingSystem.IsWindows())
        {
            await File.WriteAllTextAsync(path, contents);
            return;
        }
 
        var startInfo = new ProcessStartInfo("/bin/sh")
        {
            RedirectStandardInput = true,
            RedirectStandardError = true,
            StandardInputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
            UseShellExecute = false,
        };
        startInfo.ArgumentList.Add("-c");
        startInfo.ArgumentList.Add("cat > \"$1\" && chmod \"$2\" \"$1\"");
        startInfo.ArgumentList.Add("sh");
        startInfo.ArgumentList.Add(path);
        startInfo.ArgumentList.Add(Convert.ToString((int)mode, 8));
 
        using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start /bin/sh.");
        var stderr = process.StandardError.ReadToEndAsync();
        await process.StandardInput.WriteAsync(contents);
        process.StandardInput.Close();
        await process.WaitForExitAsync();
 
        if (process.ExitCode != 0)
        {
            throw new InvalidOperationException($"Writing executable '{path}' failed with exit code {process.ExitCode}: {await stderr}");
        }
    }
}