| File: DockerCommand.cs | Web Access |
| Project: src\playground\NativeIntegrations\CSharp\DockerCompose\NativeIntegrations.DockerCompose.csproj (NativeIntegrations.DockerCompose) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using System.Diagnostics; using System.Text; namespace NativeIntegrations.DockerCompose; public sealed record DockerCommandOptions( string WorkingDirectory, IReadOnlyDictionary<string, string> Environment, TimeSpan Timeout, CancellationToken CancellationToken); public sealed record DockerCommandResult(int ExitCode, string Stdout, string Stderr); public delegate Task<DockerCommandResult> DockerCommandRunner(string executable, IReadOnlyList<string> arguments, DockerCommandOptions options); public static class DockerCommand { public static async Task<DockerCommandResult> RunAsync(string executable, IReadOnlyList<string> arguments, DockerCommandOptions options) { options.CancellationToken.ThrowIfCancellationRequested(); using var process = new Process { StartInfo = new(executable) { WorkingDirectory = options.WorkingDirectory, UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true } }; foreach (var argument in arguments) { process.StartInfo.ArgumentList.Add(argument); } process.StartInfo.Environment.Clear(); foreach (var (key, value) in options.Environment) { process.StartInfo.Environment.Add(key, value); } try { process.Start(); } catch (Win32Exception) { throw new InvalidOperationException("Could not start Docker. Install Docker CLI and ensure it is on PATH."); } process.StandardInput.Close(); using var deadline = CancellationTokenSource.CreateLinkedTokenSource(options.CancellationToken); deadline.CancelAfter(options.Timeout); var length = 0; // Drain both pipes concurrently: waiting for either alone can deadlock a child // that fills the other pipe. The combined byte budget also bounds memory usage. var stdout = ReadAsync(process.StandardOutput.BaseStream); var stderr = ReadAsync(process.StandardError.BaseStream); Task output = Task.WhenAll(stdout, stderr); try { await process.WaitForExitAsync(deadline.Token).ConfigureAwait(false); // Reads observe the deadline too: an exited child can leave inherited // pipes open in a descendant, so process exit alone cannot bound draining. await output.ConfigureAwait(false); if (length > 1024 * 1024) { throw new InvalidOperationException("Docker command exceeded the bounded output limit."); } return new(process.ExitCode, await stdout.ConfigureAwait(false), await stderr.ConfigureAwait(false)); } catch (OperationCanceledException) when (deadline.IsCancellationRequested) { if (!process.HasExited) { process.Kill(entireProcessTree: true); } await process.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); await output.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); options.CancellationToken.ThrowIfCancellationRequested(); throw new InvalidOperationException("Docker command was cancelled, timed out, or exceeded the bounded output limit."); } async Task<string> ReadAsync(Stream stream) { using var captured = new MemoryStream(); var buffer = new byte[8192]; int count; while ((count = await stream.ReadAsync(buffer, deadline.Token).ConfigureAwait(false)) != 0) { if (Interlocked.Add(ref length, count) > 1024 * 1024) { await deadline.CancelAsync().ConfigureAwait(false); } else { captured.Write(buffer, 0, count); } } return Encoding.UTF8.GetString(captured.GetBuffer(), 0, checked((int)captured.Length)); } } }