diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index e58096c1..97a59fa4 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index f0e67b6c..82e67343 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -380,6 +380,20 @@ internal async Task MakeRequestForHijackedStreamAsync( TimeSpan timeout, CancellationToken cancellationToken) { + // The Docker Engine API docs sounds like these headers are optional, but if they + // aren't include in the request, the daemon doesn't set up the raw stream + // correctly. Either the headers are always required, or they're necessary + // specifically in Docker Desktop environments because of some internal communication + // (using a proxy). + + if (headers == null) + { + headers = new Dictionary(); + } + + headers.Add("Connection", "tcp"); + headers.Add("Upgrade", "Upgrade"); + var response = await PrivateMakeRequestAsync(timeout, HttpCompletionOption.ResponseHeadersRead, method, path, queryString, headers, body, cancellationToken) .ConfigureAwait(false); @@ -455,7 +469,7 @@ private HttpRequestMessage PrepareRequest(HttpMethod method, string path, IQuery private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpResponseMessage response, IEnumerable handlers) { - var isErrorResponse = statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest; + var isErrorResponse = (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest) && statusCode != HttpStatusCode.SwitchingProtocols; string responseBody = null; diff --git a/src/Docker.DotNet/DockerPipeStream.cs b/src/Docker.DotNet/DockerPipeStream.cs index 83398598..0221849d 100644 --- a/src/Docker.DotNet/DockerPipeStream.cs +++ b/src/Docker.DotNet/DockerPipeStream.cs @@ -2,8 +2,8 @@ namespace Docker.DotNet; internal class DockerPipeStream : WriteClosableStream, IPeekableStream { - private readonly PipeStream _stream; private readonly EventWaitHandle _event = new EventWaitHandle(false, EventResetMode.AutoReset); + private readonly PipeStream _stream; public DockerPipeStream(PipeStream stream) { @@ -26,7 +26,6 @@ public override long Length public override long Position { get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } } @@ -37,7 +36,7 @@ public override long Position private static extern int GetOverlappedResult(SafeHandle handle, ref NativeOverlapped overlapped, out int numBytesWritten, int wait); [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool PeekNamedPipe(SafeHandle handle, byte[] buffer, uint nBufferSize, ref uint bytesRead, ref uint bytesAvail, ref uint BytesLeftThisMessage); + private static extern bool PeekNamedPipe(SafeHandle handle, byte[] buffer, uint nBufferSize, ref uint bytesRead, ref uint bytesAvail, ref uint bytesLeftThisMessage); public override void CloseWrite() { @@ -45,11 +44,7 @@ public override void CloseWrite() // calls to achieve this since CoreCLR ignores a zero-byte write. var overlapped = new NativeOverlapped(); -#if NET45 - var handle = _event.SafeWaitHandle; -#else var handle = _event.GetSafeWaitHandle(); -#endif // Set the low bit to tell Windows not to send the result of this IO to the // completion port. diff --git a/src/Docker.DotNet/Endpoints/ContainerOperations.cs b/src/Docker.DotNet/Endpoints/ContainerOperations.cs index 3239a5c3..735ab0e1 100644 --- a/src/Docker.DotNet/Endpoints/ContainerOperations.cs +++ b/src/Docker.DotNet/Endpoints/ContainerOperations.cs @@ -25,7 +25,7 @@ internal ContainerOperations(DockerClient client) _client = client; } - public async Task> ListContainersAsync(ContainersListParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task> ListContainersAsync(ContainersListParameters parameters, CancellationToken cancellationToken = default) { if (parameters == null) { @@ -36,7 +36,7 @@ internal ContainerOperations(DockerClient client) return await _client.MakeRequestAsync(_client.NoErrorHandlers, HttpMethod.Get, "containers/json", queryParameters, cancellationToken).ConfigureAwait(false); } - public async Task CreateContainerAsync(CreateContainerParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task CreateContainerAsync(CreateContainerParameters parameters, CancellationToken cancellationToken = default) { IQueryString qs = null; @@ -54,7 +54,7 @@ internal ContainerOperations(DockerClient client) return await _client.MakeRequestAsync(new[] { NoSuchImageHandler }, HttpMethod.Post, "containers/create", qs, data, cancellationToken).ConfigureAwait(false); } - public async Task InspectContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + public async Task InspectContainerAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -64,7 +64,7 @@ internal ContainerOperations(DockerClient client) return await _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/json", cancellationToken).ConfigureAwait(false); } - public async Task InspectContainerAsync(string id, ContainerInspectParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task InspectContainerAsync(string id, ContainerInspectParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -80,7 +80,7 @@ internal ContainerOperations(DockerClient client) return await _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/json", queryString, cancellationToken).ConfigureAwait(false); } - public async Task ListProcessesAsync(string id, ContainerListProcessesParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task ListProcessesAsync(string id, ContainerListProcessesParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -96,32 +96,16 @@ internal ContainerOperations(DockerClient client) return await _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/top", queryParameters, cancellationToken).ConfigureAwait(false); } - public Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, IProgress progress, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException(nameof(id)); - } - - if (parameters == null) - { - throw new ArgumentNullException(nameof(parameters)); - } + using var multiplexedStream = await GetContainerLogsAsync(id, parameters, cancellationToken) + .ConfigureAwait(false); - IQueryString queryParameters = new QueryString(parameters); - return _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/logs", queryParameters, cancellationToken); + await StreamUtil.MonitorStreamAsync(multiplexedStream, progress, cancellationToken) + .ConfigureAwait(false); } - public Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken, IProgress progress) - { - return StreamUtil.MonitorStreamAsync( - GetContainerLogsAsync(id, parameters, cancellationToken), - _client, - cancellationToken, - progress); - } - - public async Task GetContainerLogsAsync(string id, bool tty, ContainerLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -133,14 +117,18 @@ public Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, throw new ArgumentNullException(nameof(parameters)); } - IQueryString queryParameters = new QueryString(parameters); + var queryParameters = new QueryString(parameters); - Stream result = await _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/logs", queryParameters, cancellationToken).ConfigureAwait(false); + var containerInspectResponse = await InspectContainerAsync(id, cancellationToken) + .ConfigureAwait(false); - return new MultiplexedStream(result, !tty); + var stream = await _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/logs", queryParameters, null, null, cancellationToken) + .ConfigureAwait(false); + + return new MultiplexedStream(stream, !containerInspectResponse.Config.Tty); } - public async Task> InspectChangesAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + public async Task> InspectChangesAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -176,7 +164,7 @@ public Task GetContainerStatsAsync(string id, ContainerStatsParameters p return _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/stats", queryParameters, null, null, cancellationToken); } - public Task GetContainerStatsAsync(string id, ContainerStatsParameters parameters, IProgress progress, CancellationToken cancellationToken = default(CancellationToken)) + public Task GetContainerStatsAsync(string id, ContainerStatsParameters parameters, IProgress progress, CancellationToken cancellationToken = default) { return StreamUtil.MonitorStreamForMessagesAsync( GetContainerStatsAsync(id, parameters, cancellationToken), @@ -185,7 +173,7 @@ public Task GetContainerStatsAsync(string id, ContainerStatsParameters p progress); } - public Task ResizeContainerTtyAsync(string id, ContainerResizeParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public Task ResizeContainerTtyAsync(string id, ContainerResizeParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -201,7 +189,7 @@ public Task GetContainerStatsAsync(string id, ContainerStatsParameters p return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/resize", queryParameters, cancellationToken); } - public async Task StartContainerAsync(string id, ContainerStartParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task StartContainerAsync(string id, ContainerStartParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -214,7 +202,7 @@ public Task GetContainerStatsAsync(string id, ContainerStatsParameters p return result ?? throw new InvalidOperationException(); } - public async Task StopContainerAsync(string id, ContainerStopParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task StopContainerAsync(string id, ContainerStopParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -234,7 +222,7 @@ public Task GetContainerStatsAsync(string id, ContainerStatsParameters p return result ?? throw new InvalidOperationException(); } - public Task RestartContainerAsync(string id, ContainerRestartParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public Task RestartContainerAsync(string id, ContainerRestartParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -253,7 +241,7 @@ public Task GetContainerStatsAsync(string id, ContainerStatsParameters p return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/restart", queryParameters, null, null, TimeSpan.FromMilliseconds(Timeout.Infinite), cancellationToken); } - public Task KillContainerAsync(string id, ContainerKillParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public Task KillContainerAsync(string id, ContainerKillParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -280,7 +268,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/rename", queryParameters, cancellationToken); } - public Task PauseContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + public Task PauseContainerAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -290,7 +278,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/pause", cancellationToken); } - public Task UnpauseContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + public Task UnpauseContainerAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -300,7 +288,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/unpause", cancellationToken); } - public async Task AttachContainerAsync(string id, bool tty, ContainerAttachParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task AttachContainerAsync(string id, ContainerAttachParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -313,11 +301,17 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters } var queryParameters = new QueryString(parameters); - var result = await _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/attach", queryParameters, null, null, cancellationToken).ConfigureAwait(false); - return new MultiplexedStream(result, !tty); + + var containerInspectResponse = await InspectContainerAsync(id, cancellationToken) + .ConfigureAwait(false); + + var stream = await _client.MakeRequestForHijackedStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/attach", queryParameters, null, null, cancellationToken) + .ConfigureAwait(false); + + return new MultiplexedStream(stream, !containerInspectResponse.Config.Tty); } - public async Task WaitContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + public async Task WaitContainerAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -327,7 +321,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters return await _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/wait", null, null, null, TimeSpan.FromMilliseconds(Timeout.Infinite), cancellationToken).ConfigureAwait(false); } - public Task RemoveContainerAsync(string id, ContainerRemoveParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public Task RemoveContainerAsync(string id, ContainerRemoveParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -343,7 +337,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Delete, $"containers/{id}", queryParameters, cancellationToken); } - public async Task GetArchiveFromContainerAsync(string id, GetArchiveFromContainerParameters parameters, bool statOnly, CancellationToken cancellationToken = default(CancellationToken)) + public async Task GetArchiveFromContainerAsync(string id, GetArchiveFromContainerParameters parameters, bool statOnly, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -372,7 +366,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters }; } - public Task ExtractArchiveToContainerAsync(string id, ContainerPathStatParameters parameters, Stream stream, CancellationToken cancellationToken = default(CancellationToken)) + public Task ExtractArchiveToContainerAsync(string id, ContainerPathStatParameters parameters, Stream stream, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { @@ -396,7 +390,7 @@ public async Task PruneContainersAsync(ContainersPruneP return await _client.MakeRequestAsync(_client.NoErrorHandlers, HttpMethod.Post, "containers/prune", queryParameters, cancellationToken).ConfigureAwait(false); } - public async Task UpdateContainerAsync(string id, ContainerUpdateParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + public async Task UpdateContainerAsync(string id, ContainerUpdateParameters parameters, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) { diff --git a/src/Docker.DotNet/Endpoints/ExecOperations.cs b/src/Docker.DotNet/Endpoints/ExecOperations.cs index 7fd70461..db961bc3 100644 --- a/src/Docker.DotNet/Endpoints/ExecOperations.cs +++ b/src/Docker.DotNet/Endpoints/ExecOperations.cs @@ -55,9 +55,9 @@ public async Task StartContainerExecAsync(string id, Containe var data = new JsonRequestContent(parameters, DockerClient.JsonSerializer); - var result = await _client.MakeRequestForStreamAsync([NoSuchContainerHandler], HttpMethod.Post, $"exec/{id}/start", null, data, null, cancellationToken) + var stream = await _client.MakeRequestForHijackedStreamAsync([NoSuchContainerHandler], HttpMethod.Post, $"exec/{id}/start", null, data, null, cancellationToken) .ConfigureAwait(false); - return new MultiplexedStream(result, !parameters.Tty); + return new MultiplexedStream(stream, !parameters.Tty); } } \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/IContainerOperations.cs b/src/Docker.DotNet/Endpoints/IContainerOperations.cs index cb21bc3f..90e4f633 100644 --- a/src/Docker.DotNet/Endpoints/IContainerOperations.cs +++ b/src/Docker.DotNet/Endpoints/IContainerOperations.cs @@ -15,7 +15,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task> ListContainersAsync(ContainersListParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task> ListContainersAsync(ContainersListParameters parameters, CancellationToken cancellationToken = default); /// /// Creates a new container from an image. @@ -28,7 +28,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid, there was a conflict with another container, or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task CreateContainerAsync(CreateContainerParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateContainerAsync(CreateContainerParameters parameters, CancellationToken cancellationToken = default); /// /// Retrieves low-level information about a container. @@ -41,7 +41,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task InspectContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task InspectContainerAsync(string id, CancellationToken cancellationToken = default); /// /// Retrieves low-level information about a container with additional options. @@ -55,7 +55,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task InspectContainerAsync(string id, ContainerInspectParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task InspectContainerAsync(string id, ContainerInspectParameters parameters, CancellationToken cancellationToken = default); /// /// Retrieves a list of processes running within the container. @@ -70,57 +70,44 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task ListProcessesAsync(string id, ContainerListProcessesParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task ListProcessesAsync(string id, ContainerListProcessesParameters parameters, CancellationToken cancellationToken = default); /// - /// Gets stdout and stderr logs from a container created with a TTY. + /// Gets stdout and stderr logs from a container. /// + /// + /// The corresponding commands in the Docker CLI are docker inspect and docker container inspect. + /// /// The ID or name of the container. /// Specifics of how to perform the operation. + /// Provides a callback for reporting log entries as they're read. Every reported string represents one log line, with its terminating newline removed. /// When triggered, the operation will stop at the next available time, if possible. - /// A that resolves to a , which provides the log information. - /// This method works only for containers with the json-file or journald logging driver. - ///
The corresponding commands in the Docker CLI are docker logs and docker container logs.
- /// No such container was found. - /// One or more of the inputs was . + /// + /// A that will complete once all log lines have been read, or once the container has exited if Follow is set to . + /// + /// One or more of the inputs were . + /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout. /// The input is invalid or the daemon experienced an error. - /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - [Obsolete("The stream returned by this method won't be demultiplexed properly if the container was created without a TTY. Use GetContainerLogsAsync(string, bool, ContainerLogsParameters, CancellationToken) instead")] - Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Gets stdout and stderr logs from a container that was created with a TTY. - /// - /// The ID or name of the container. - /// Specifics of how to perform the operation. - /// When triggered, the operation will stop at the next available time, if possible. - /// Provides a callback for reporting log entries as they're read. Every reported string represents one log line, with its terminating newline removed. - /// A that will complete once all log lines have been read, or once the container has exited if Follow is set to . - /// This method is only suited for containers created with a TTY. For containers created without a TTY, use - /// instead. - ///
- /// The corresponding commands in the Docker CLI are docker inspect and docker container inspect.
/// No such container was found. - /// One or more of the inputs was . - /// The input is invalid or the daemon experienced an error. - /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken, IProgress progress); + Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, IProgress progress, CancellationToken cancellationToken = default); /// /// Gets stdout and stderr logs from a container. /// + /// + /// The corresponding commands in the Docker CLI are docker inspect and docker container inspect. + /// /// The ID or name of the container. - /// Indicates whether the container was created with a TTY. If , the returned stream is multiplexed. /// Specifics of how to perform the operation. /// When triggered, the operation will stop at the next available time, if possible. - /// A that resolves to a , which provides the log information. - /// If the container wasn't created with a TTY, this stream is multiplexed. - /// The corresponding commands in the Docker CLI are docker inspect and docker container inspect. - /// No such container was found. - /// One or more of the inputs was . + /// + /// A that resolves to a , which provides the log information. + /// + /// One or more of the inputs were . + /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout. /// The input is invalid or the daemon experienced an error. - /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task GetContainerLogsAsync(string id, bool tty, ContainerLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + /// No such container was found. + Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken = default); /// /// Reports which files in a container's filesystem have been added, deleted, or modified. @@ -132,7 +119,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task> InspectChangesAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task> InspectChangesAsync(string id, CancellationToken cancellationToken = default); /// /// Exports the contents of a container as a tarball. @@ -145,7 +132,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task ExportContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task ExportContainerAsync(string id, CancellationToken cancellationToken = default); /// /// Retrieves a live, raw stream of the container's resource usage statistics. @@ -153,7 +140,7 @@ public interface IContainerOperations /// The ID or name of the container. /// Specifics of how to perform the operation. /// When triggered, the operation will stop at the next available time, if possible. - /// A that resolves to a , which can be used to read the frames of statistics. For details + /// A that resolves to a , which can be used to read the frames of statistics. For details /// on the format, refer to the Docker Engine API documentation. /// /// The corresponding commands in the Docker CLI are docker stats and docker container stats. @@ -177,7 +164,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task GetContainerStatsAsync(string id, ContainerStatsParameters parameters, IProgress progress, CancellationToken cancellationToken = default(CancellationToken)); + Task GetContainerStatsAsync(string id, ContainerStatsParameters parameters, IProgress progress, CancellationToken cancellationToken = default); /// /// Resizes a container's TTY. @@ -192,7 +179,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task ResizeContainerTtyAsync(string id, ContainerResizeParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task ResizeContainerTtyAsync(string id, ContainerResizeParameters parameters, CancellationToken cancellationToken = default); /// /// Starts a container. @@ -209,7 +196,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task StartContainerAsync(string id, ContainerStartParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task StartContainerAsync(string id, ContainerStartParameters parameters, CancellationToken cancellationToken = default); /// /// Stops a container. @@ -227,7 +214,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task StopContainerAsync(string id, ContainerStopParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task StopContainerAsync(string id, ContainerStopParameters parameters, CancellationToken cancellationToken = default); /// /// Stops and then restarts a container. @@ -241,7 +228,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task RestartContainerAsync(string id, ContainerRestartParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task RestartContainerAsync(string id, ContainerRestartParameters parameters, CancellationToken cancellationToken = default); /// /// Sends a POSIX signal to a container--typically to kill it. @@ -257,7 +244,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The container was not running, the input is invalid, or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task KillContainerAsync(string id, ContainerKillParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task KillContainerAsync(string id, ContainerKillParameters parameters, CancellationToken cancellationToken = default); /// /// Changes the name of a container. @@ -279,7 +266,7 @@ public interface IContainerOperations /// When triggered, the operation will stop at the next available time, if possible. /// A that resolves when the operation is complete. /// - /// This uses the freeze cgroup to suspend all processes in the container. The processes are unaware that they are being + /// This uses the freeze cgroup to suspend all processes in the container. The processes are unaware that they are being /// suspended (e.g., they cannot capture a SIGSTOP signal). /// /// The corresponding commands in the Docker CLI are docker pause and docker container pause. @@ -288,7 +275,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task PauseContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task PauseContainerAsync(string id, CancellationToken cancellationToken = default); /// /// Resumes a container that was suspended. @@ -302,44 +289,40 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task UnpauseContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task UnpauseContainerAsync(string id, CancellationToken cancellationToken = default); /// /// Attaches to a container to read its output and send it input. /// + /// + /// The corresponding commands in the Docker CLI are docker attach and docker container attach. + /// /// The ID or name of the container. - /// Indicates whether the stream is a TTY stream. When , stdout and stderr are - /// combined into a single, undifferentiated stream. When , the stream is multiplexed. /// Specifics of how to perform the operation. /// When triggered, the operation will stop at the next available time, if possible. - /// A that resolves to a , which contains the - /// container's stdout and stderr content and which can be used to write to the container's stdin. - /// The format of the stream various, in part by the parameter's value. See the - /// Docker Engine API reference for details on - /// the format. - /// The corresponding commands in the Docker CLI are docker attach and docker container attach. - /// No such container was found. - /// One or more of the inputs was . + /// + /// A that resolves to a , which contains the container's + /// stdout and stderr content and which can be used to write to the container's stdin. + /// + /// One or more of the inputs were . + /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation, or timeout. /// The input is invalid or the daemon experienced an error. - /// The transport is unsuitable for the operation. - /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task AttachContainerAsync(string id, bool tty, ContainerAttachParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); - - // TODO: Attach Web Socket + /// No such container was found. + Task AttachContainerAsync(string id, ContainerAttachParameters parameters, CancellationToken cancellationToken = default); /// /// Waits for a container to stop. /// /// The ID or name of the container. /// When triggered, the operation will stop at the next available time, if possible. - /// A that resolves to a when the container has + /// A that resolves to a when the container has /// stopped. /// The corresponding commands in the Docker CLI are docker wait and docker container wait. /// No such container was found. /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task WaitContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task WaitContainerAsync(string id, CancellationToken cancellationToken = default); /// /// Deletes a container. @@ -353,7 +336,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// There is a conflict, the input is invalid, or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task RemoveContainerAsync(string id, ContainerRemoveParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task RemoveContainerAsync(string id, ContainerRemoveParameters parameters, CancellationToken cancellationToken = default); /// /// Gets information about the filesystem in a container. This may be either a listing of files or a complete @@ -361,7 +344,7 @@ public interface IContainerOperations /// /// The ID or name of the container. /// Specifics of how to perform the operation. - /// If , the method will only return file information; otherwise, it will return a + /// If , the method will only return file information; otherwise, it will return a /// stream of the filesystem as a tarball. /// When triggered, the operation will stop at the next available time, if possible. /// A that resolves to a , which holds @@ -370,7 +353,7 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task GetArchiveFromContainerAsync(string id, GetArchiveFromContainerParameters parameters, bool statOnly, CancellationToken cancellationToken = default(CancellationToken)); + Task GetArchiveFromContainerAsync(string id, GetArchiveFromContainerParameters parameters, bool statOnly, CancellationToken cancellationToken = default); /// /// Extracts a tar archive into a container's filesystem. @@ -382,23 +365,23 @@ public interface IContainerOperations /// A that resolves when the operation completes. /// No such container was found, or the path does not exist inside the container. /// One or more of the inputs was . - /// Permission is denied (the volume or container rootfs is marked read-only), + /// Permission is denied (the volume or container rootfs is marked read-only), /// the input is invalid, or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task ExtractArchiveToContainerAsync(string id, ContainerPathStatParameters parameters, Stream stream, CancellationToken cancellationToken = default(CancellationToken)); + Task ExtractArchiveToContainerAsync(string id, ContainerPathStatParameters parameters, Stream stream, CancellationToken cancellationToken = default); /// /// Deletes stopped containers. /// /// Specifics of how to perform the operation. /// When triggered, the operation will stop at the next available time, if possible. - /// A that resolves to a , which details which containers + /// A that resolves to a , which details which containers /// were removed. /// The corresponding command in the Docker CLI is docker container prune. /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task PruneContainersAsync(ContainersPruneParameters parameters = null, CancellationToken cancellationToken = default(CancellationToken)); + Task PruneContainersAsync(ContainersPruneParameters parameters = null, CancellationToken cancellationToken = default); /// /// Changes configuration options of a container without recreating it. @@ -413,5 +396,5 @@ public interface IContainerOperations /// One or more of the inputs was . /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. - Task UpdateContainerAsync(string id, ContainerUpdateParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + Task UpdateContainerAsync(string id, ContainerUpdateParameters parameters, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/StreamUtil.cs b/src/Docker.DotNet/Endpoints/StreamUtil.cs index 8efe8799..3e6feab0 100644 --- a/src/Docker.DotNet/Endpoints/StreamUtil.cs +++ b/src/Docker.DotNet/Endpoints/StreamUtil.cs @@ -2,19 +2,65 @@ namespace Docker.DotNet.Models; internal static class StreamUtil { - internal static async Task MonitorStreamAsync(Task streamTask, DockerClient client, CancellationToken cancellationToken, IProgress progress) + internal static async Task MonitorStreamAsync(MultiplexedStream multiplexedStream, IProgress progress, CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); + var buffer = new byte[8192]; - using (var stream = await streamTask) - using (var reader = new StreamReader(stream, new UTF8Encoding(false))) - using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken))) + using var memoryStream = new MemoryStream(); + + using var streamReader = new StreamReader(memoryStream, new UTF8Encoding(false), false, buffer.Length, true); + + while (true) { - string line; - while ((line = await await Task.WhenAny(reader.ReadLineAsync(), tcs.Task)) != null) + // TODO: + // Make sure that split multi-byte characters across read operations don't + // cause exceptions or get misinterpreted. + + var bytesRead = await multiplexedStream.ReadOutputAsync(buffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + + if (bytesRead.Count == 0) + { + break; + } + + await memoryStream.WriteAsync(buffer, 0, bytesRead.Count, cancellationToken) + .ConfigureAwait(false); + + // Read each line from the stream. If no complete line is available, it may be because + // the line is incomplete and the remaining bytes will be read in the next operation. + // We need to retain any leftover bytes to process them in the next iteration. + + memoryStream.Seek(0, SeekOrigin.Begin); + + long lastReadPosition; + + while (true) { - progress.Report(line); + var line = await streamReader.ReadLineAsync() + .ConfigureAwait(false); + + if (line == null) + { + lastReadPosition = memoryStream.Position; + break; + } + else + { + progress.Report(line); + } } + + var remainingBytes = memoryStream.Length - lastReadPosition; + + if (remainingBytes > 0) + { + var internalBuffer = memoryStream.GetBuffer(); + Array.Copy(internalBuffer, lastReadPosition, internalBuffer, 0, remainingBytes); + } + + memoryStream.Position = remainingBytes; + memoryStream.SetLength(remainingBytes); } } diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs index 56ecc36a..0c766b42 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -61,7 +61,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - if (Interlocked.Decrement(ref _bufferRefCount) == 0) + if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) { ArrayPool.Shared.Return(_buffer); } @@ -159,91 +159,45 @@ public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available public async Task ReadLineAsync(CancellationToken cancellationToken) { - const char nullChar = '\0'; + var line = new StringBuilder(_buffer.Length); - const char cr = '\r'; + var crIndex = -1; - const char lf = '\n'; + var lfIndex = -1; - if (_bufferCount == 0) - { - var bufferInUse = Interlocked.Increment(ref _bufferRefCount) > 1; - - try - { - if (bufferInUse) - { - _bufferOffset = 0; + bool crlfFound; - _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) - .ConfigureAwait(false); - } - } - catch (Exception e) - { - _logger.LogCritical(e, "Failed to read from buffer."); - throw; - } - finally + do + { + if (_bufferCount == 0) { - var bufferReleased = Interlocked.Decrement(ref _bufferRefCount) == 0; + _bufferOffset = 0; - if (bufferInUse && bufferReleased) - { - ArrayPool.Shared.Return(_buffer); - } + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); } - } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - var content = Encoding.ASCII.GetString(_buffer.TakeWhile(value => value != nullChar).ToArray()); - content = content.Replace("\r", ""); - content = content.Replace("\n", ""); - _logger.LogDebug("Raw buffer content: '{Content}'.", content); - } - - var start = _bufferOffset; - var end = -1; + var c = (char)_buffer[_bufferOffset]; + line.Append(c); - for (var i = _bufferOffset; i < _buffer.Length; i++) - { - // If a null terminator is found, skip the rest of the buffer. - if (_buffer[i] == nullChar) - { - _logger.LogDebug("Null terminator found at position: {Position}.", i); - end = i; - break; - } + _bufferOffset++; + _bufferCount--; - // Check if current byte is CR and the next byte is LF. - if (_buffer[i] == cr && i + 1 < _buffer.Length && _buffer[i + 1] == lf) + switch (c) { - _logger.LogDebug("CRLF found at positions {CR} and {LF}.", i, i + 1); - end = i; - break; + case '\r': + crIndex = line.Length; + break; + case '\n': + lfIndex = line.Length; + break; } - } - // No CRLF found, process the entire remaining buffer. - if (end == -1) - { - end = _buffer.Length; - _logger.LogDebug("No CRLF found. Setting end position to buffer length: {End}.", end); + crlfFound = crIndex + 1 == lfIndex; } - else - { - _bufferCount -= end - start + 2; - _bufferOffset = end + 2; - _logger.LogDebug("CRLF found. Consumed {Consumed} bytes. New offset: {Offset}, Remaining count: {RemainingBytes}.", end - start + 2, _bufferOffset, _bufferCount); - } - - var length = end - start; - var line = Encoding.ASCII.GetString(_buffer, start, length); + while (!crlfFound); - _logger.LogDebug("String from positions {Start} to {End} (length {Length}): '{Line}'.", start, end, length, line); - return line; + return line.ToString(0, line.Length - 2); } private int ReadBuffer(byte[] buffer, int offset, int count) diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs index aba3d8fc..37dc69b2 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs @@ -2,7 +2,7 @@ namespace Microsoft.Net.Http.Client; internal sealed class HttpConnection : IDisposable { - // private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; + private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; public HttpConnection(BufferedReadStream transport) { @@ -147,10 +147,29 @@ private HttpResponseMessage CreateResponseMessage(List responseLines) } } - // var isStream = content.Headers.TryGetValues("Content-Type", out var headerValues) - // && headerValues.Any(header => DockerStreamHeaders.Contains(header)); + // TODO: We'll need to refactor this in the future. + // + // Depending on the request and response (headers), we need to handle the response + // differently. We need to distinguish between four types of responses: + // + // 1. Chunked transfer encoding + // 2. HTTP with a `Content-Length` header + // 3. Hijacked TCP connections (using the connection upgrade headers) + // - `/containers/{id}/attach` + // - `/exec/{id}/start` + // 4. Streams without the connection upgrade headers + // - `/containers/{id}/logs` + + var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) + && responseHeaderValues.Any(header => "tcp".Equals(header)); + + var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) + && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); + + var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); + + content.ResolveResponseStream(chunked: isChunkedTransferEncoding); - content.ResolveResponseStream(chunked: response.Headers.TransferEncodingChunked.HasValue && response.Headers.TransferEncodingChunked.Value); return response; } diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs index a37b9b29..2522ac87 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -26,7 +26,6 @@ internal void ResolveResponseStream(bool chunked) } else { - // Raw, read until end and close _responseStream = _connection.Transport; } } diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index 0532f916..164a2f9b 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -60,9 +60,8 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( Timestamps = true, Follow = true }, - containerLogsCts.Token, - new Progress(m => _testOutputHelper.WriteLine(m)) - ); + new Progress(m => _testOutputHelper.WriteLine(m)), + containerLogsCts.Token); await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, @@ -106,8 +105,8 @@ await _testFixture.DockerClient.Containers.GetContainerLogsAsync( Timestamps = true, Follow = false }, - _testFixture.Cts.Token, - new Progress(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }) + new Progress(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }), + _testFixture.Cts.Token ); await _testFixture.DockerClient.Containers.StopContainerAsync( @@ -153,8 +152,8 @@ await _testFixture.DockerClient.Containers.GetContainerLogsAsync( Timestamps = true, Follow = false }, - _testFixture.Cts.Token, - new Progress(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }) + new Progress(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }), + _testFixture.Cts.Token ); await _testFixture.DockerClient.Containers.StopContainerAsync( @@ -191,7 +190,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - await Assert.ThrowsAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( + await Assert.ThrowsAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -200,8 +199,8 @@ await Assert.ThrowsAsync(() => _testFixture.DockerClient. Timestamps = true, Follow = true }, - containerLogsCts.Token, - new Progress(m => _testOutputHelper.WriteLine(m)) + new Progress(m => _testOutputHelper.WriteLine(m)), + containerLogsCts.Token )); } @@ -237,11 +236,11 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( Timestamps = true, Follow = true }, - containerLogsCts.Token, - new Progress(m => _testOutputHelper.WriteLine(m)) + new Progress(m => _testOutputHelper.WriteLine(m)), + containerLogsCts.Token ); - await Assert.ThrowsAsync(() => containerLogsTask); + await Assert.ThrowsAsync(() => containerLogsTask); } [Fact] @@ -277,8 +276,8 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( Timestamps = true, Follow = true }, - containerLogsCts.Token, - new Progress(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }) + new Progress(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }), + containerLogsCts.Token ); await Task.Delay(TimeSpan.FromSeconds(5)); @@ -289,7 +288,7 @@ await _testFixture.DockerClient.Containers.StopContainerAsync( _testFixture.Cts.Token ); - await Assert.ThrowsAsync(() => containerLogsTask); + await Assert.ThrowsAsync(() => containerLogsTask); _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); @@ -708,10 +707,49 @@ public async Task CreateImageAsync_NonExistingImage_ThrowsDockerImageNotFoundExc await Assert.ThrowsAsync(op); } - [Fact(Skip = "Refactor IExecOperations operations and writing/reading to/from stdin and stdout. It does not work reliably.")] - public async Task MultiplexedStreamWriteAsync_DoesNotThrowAnException() + [Fact] + public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_CompletesPid1Process() + { + // Given + var linefeedByte = new byte[] { 10 }; + + var createContainerParameters = new CreateContainerParameters(); + createContainerParameters.Image = _testFixture.Image.ID; + createContainerParameters.Entrypoint = new[] { "/bin/sh", "-c" }; + createContainerParameters.Cmd = new[] { "read line; echo Done" }; + createContainerParameters.OpenStdin = true; + + var containerAttachParameters = new ContainerAttachParameters(); + containerAttachParameters.Stdin = true; + containerAttachParameters.Stdout = true; + containerAttachParameters.Stderr = true; + containerAttachParameters.Logs = true; + containerAttachParameters.Stream = true; + + // When + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + + using var stream = await _testFixture.DockerClient.Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters); + + await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var (stdout, _) = await stream.ReadOutputToEndAsync(cts.Token); + + var containerInspectResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync(createContainerResponse.ID, _testFixture.Cts.Token); + + // Then + Assert.Equal(0, containerInspectResponse.State.ExitCode); + Assert.Equal("Done\n", stdout); + } + + [Fact] + public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_CompletesExecProcess() { // Given + var linefeedByte = new byte[] { 10 }; + var createContainerParameters = new CreateContainerParameters(); createContainerParameters.Image = _testFixture.Image.ID; createContainerParameters.Entrypoint = CommonCommands.SleepInfinity; @@ -731,10 +769,15 @@ public async Task MultiplexedStreamWriteAsync_DoesNotThrowAnException() var containerExecCreateResponse = await _testFixture.DockerClient.Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters); using var stream = await _testFixture.DockerClient.Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); - var buffer = new byte[] { 10 }; - var exception = await Record.ExceptionAsync(() => stream.WriteAsync(buffer, 0, buffer.Length, _testFixture.Cts.Token)); + await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var (stdout, _) = await stream.ReadOutputToEndAsync(cts.Token); + + var containerExecInspectResponse = await _testFixture.DockerClient.Exec.InspectContainerExecAsync(containerExecCreateResponse.ID, _testFixture.Cts.Token); // Then - Assert.Null(exception); + Assert.Equal(0, containerExecInspectResponse.ExitCode); + Assert.Equal("Done\n", stdout); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 8ae4634b..b32ecd6e 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -101,7 +101,7 @@ await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImag } catch { - this.LogDebug("Couldn't init a new swarm, the node should take part of an existing one."); + this.LogInformation("Couldn't init a new swarm, the node should take part of an existing one."); _hasInitializedSwarm = false; } @@ -178,7 +178,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// public bool IsEnabled(LogLevel logLevel) { - return logLevel >= MinLogLevel; + return logLevel == MinLogLevel; } /// @@ -191,7 +191,7 @@ public IDisposable BeginScope(TState state) where TState : notnull protected override void OnReport(JSONMessage value) { var message = JsonSerializer.Instance.Serialize(value); - this.LogDebug("Progress: '{Progress}'.", message); + this.LogInformation("Progress: '{Progress}'.", message); } private sealed class Disposable : IDisposable diff --git a/test/Docker.DotNet.Tests/xunit.runner.json b/test/Docker.DotNet.Tests/xunit.runner.json index 0e2fef59..262e03ef 100644 --- a/test/Docker.DotNet.Tests/xunit.runner.json +++ b/test/Docker.DotNet.Tests/xunit.runner.json @@ -1,4 +1,4 @@ { "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "diagnosticMessages" : true + "diagnosticMessages" : false } \ No newline at end of file diff --git a/version.json b/version.json index 423da8f4..a7718d1c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.126.1", + "version": "3.127.0", "nugetPackageVersion": { "semVer": 2 },