File: Scripts\Common\FileHelper.cs
Web Access
Project: src\tests\Aspire.Acquisition.Tests\Aspire.Acquisition.Tests.csproj (Aspire.Acquisition.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 Aspire.Acquisition.Tests.Scripts;
 
/// <summary>
/// Small file-system helpers shared across test classes.
/// </summary>
internal static class FileHelper
{
    /// <summary>
    /// Writes a script that the test will execute directly. On Unix the file is written by a short-lived
    /// child process so that the test host never holds a writable descriptor for it.
    /// </summary>
    /// <remarks>
    /// xUnit runs test classes in parallel, so other tests in the host fork child processes at any time.
    /// A child forked while the host has the script open for writing inherits that descriptor until it
    /// calls exec, and exec of the script fails with ETXTBSY ("Text file busy") while any process holds
    /// it open for writing. Closing, chmod-ing or renaming the file in the host does not help because the
    /// inherited descriptor still refers to the same inode.
    /// </remarks>
    internal static async Task WriteExecutableFileAsync(string path, string contents)
    {
        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 700 \"$1\"");
        startInfo.ArgumentList.Add("sh");
        startInfo.ArgumentList.Add(path);
 
        using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start /bin/sh.");
        var stderrTask = process.StandardError.ReadToEndAsync();
        await process.StandardInput.WriteAsync(contents);
        process.StandardInput.Close();
        await process.WaitForExitAsync();
        var stderr = await stderrTask;
 
        if (process.ExitCode != 0)
        {
            throw new IOException($"Failed to write executable file '{path}' (exit code {process.ExitCode}): {stderr}");
        }
    }
}