Skip to content
1 change: 0 additions & 1 deletion src/Docker.DotNet/Docker.DotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETStandard'">
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
Expand Down
16 changes: 15 additions & 1 deletion src/Docker.DotNet/DockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,20 @@ internal async Task<WriteClosableStream> 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<string, string>();
}

headers.Add("Connection", "tcp");
headers.Add("Upgrade", "Upgrade");

var response = await PrivateMakeRequestAsync(timeout, HttpCompletionOption.ResponseHeadersRead, method, path, queryString, headers, body, cancellationToken)
.ConfigureAwait(false);

Expand Down Expand Up @@ -455,7 +469,7 @@ private HttpRequestMessage PrepareRequest(HttpMethod method, string path, IQuery

private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpResponseMessage response, IEnumerable<ApiResponseErrorHandlingDelegate> handlers)
{
var isErrorResponse = statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest;
var isErrorResponse = (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest) && statusCode != HttpStatusCode.SwitchingProtocols;

string responseBody = null;

Expand Down
9 changes: 2 additions & 7 deletions src/Docker.DotNet/DockerPipeStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

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)
{
Expand All @@ -26,7 +26,6 @@
public override long Position
{
get { throw new NotImplementedException(); }

set { throw new NotImplementedException(); }
}

Expand All @@ -37,23 +36,19 @@
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()
{
// The Docker daemon expects a write of zero bytes to signal the end of writes. Use native
// calls to achieve this since CoreCLR ignores a zero-byte write.
var overlapped = new NativeOverlapped();

Check warning on line 45 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

This call site is reachable on all platforms. 'NativeOverlapped' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 45 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

This call site is reachable on all platforms. 'NativeOverlapped' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 45 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net9.0)

This call site is reachable on all platforms. 'NativeOverlapped' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 45 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net9.0)

This call site is reachable on all platforms. 'NativeOverlapped' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

#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.
overlapped.EventHandle = (IntPtr)(handle.DangerousGetHandle().ToInt64() | 1);

Check warning on line 51 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

This call site is reachable on all platforms. 'NativeOverlapped.EventHandle' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 51 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

This call site is reachable on all platforms. 'NativeOverlapped.EventHandle' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 51 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net9.0)

This call site is reachable on all platforms. 'NativeOverlapped.EventHandle' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 51 in src/Docker.DotNet/DockerPipeStream.cs

View workflow job for this annotation

GitHub Actions / build (net9.0)

This call site is reachable on all platforms. 'NativeOverlapped.EventHandle' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)
if (WriteFile(_stream.SafePipeHandle, IntPtr.Zero, 0, IntPtr.Zero, ref overlapped) == 0)
{
const int ERROR_IO_PENDING = 997;
Expand Down
88 changes: 41 additions & 47 deletions src/Docker.DotNet/Endpoints/ContainerOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal ContainerOperations(DockerClient client)
_client = client;
}

public async Task<IList<ContainerListResponse>> ListContainersAsync(ContainersListParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<IList<ContainerListResponse>> ListContainersAsync(ContainersListParameters parameters, CancellationToken cancellationToken = default)
{
if (parameters == null)
{
Expand All @@ -36,7 +36,7 @@ internal ContainerOperations(DockerClient client)
return await _client.MakeRequestAsync<ContainerListResponse[]>(_client.NoErrorHandlers, HttpMethod.Get, "containers/json", queryParameters, cancellationToken).ConfigureAwait(false);
}

public async Task<CreateContainerResponse> CreateContainerAsync(CreateContainerParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<CreateContainerResponse> CreateContainerAsync(CreateContainerParameters parameters, CancellationToken cancellationToken = default)
{
IQueryString qs = null;

Expand All @@ -54,7 +54,7 @@ internal ContainerOperations(DockerClient client)
return await _client.MakeRequestAsync<CreateContainerResponse>(new[] { NoSuchImageHandler }, HttpMethod.Post, "containers/create", qs, data, cancellationToken).ConfigureAwait(false);
}

public async Task<ContainerInspectResponse> InspectContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ContainerInspectResponse> InspectContainerAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -64,7 +64,7 @@ internal ContainerOperations(DockerClient client)
return await _client.MakeRequestAsync<ContainerInspectResponse>(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/json", cancellationToken).ConfigureAwait(false);
}

public async Task<ContainerInspectResponse> InspectContainerAsync(string id, ContainerInspectParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ContainerInspectResponse> InspectContainerAsync(string id, ContainerInspectParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -80,7 +80,7 @@ internal ContainerOperations(DockerClient client)
return await _client.MakeRequestAsync<ContainerInspectResponse>(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/json", queryString, cancellationToken).ConfigureAwait(false);
}

public async Task<ContainerProcessesResponse> ListProcessesAsync(string id, ContainerListProcessesParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ContainerProcessesResponse> ListProcessesAsync(string id, ContainerListProcessesParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -96,32 +96,16 @@ internal ContainerOperations(DockerClient client)
return await _client.MakeRequestAsync<ContainerProcessesResponse>(new[] { NoSuchContainerHandler }, HttpMethod.Get, $"containers/{id}/top", queryParameters, cancellationToken).ConfigureAwait(false);
}

public Task<Stream> GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters, IProgress<string> 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<ContainerLogsParameters>(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<string> progress)
{
return StreamUtil.MonitorStreamAsync(
GetContainerLogsAsync(id, parameters, cancellationToken),
_client,
cancellationToken,
progress);
}

public async Task<MultiplexedStream> GetContainerLogsAsync(string id, bool tty, ContainerLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<MultiplexedStream> GetContainerLogsAsync(string id, ContainerLogsParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -133,14 +117,18 @@ public Task GetContainerLogsAsync(string id, ContainerLogsParameters parameters,
throw new ArgumentNullException(nameof(parameters));
}

IQueryString queryParameters = new QueryString<ContainerLogsParameters>(parameters);
var queryParameters = new QueryString<ContainerLogsParameters>(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<IList<ContainerFileSystemChangeResponse>> InspectChangesAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
public async Task<IList<ContainerFileSystemChangeResponse>> InspectChangesAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand Down Expand Up @@ -176,7 +164,7 @@ public Task<Stream> 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<ContainerStatsResponse> progress, CancellationToken cancellationToken = default(CancellationToken))
public Task GetContainerStatsAsync(string id, ContainerStatsParameters parameters, IProgress<ContainerStatsResponse> progress, CancellationToken cancellationToken = default)
{
return StreamUtil.MonitorStreamForMessagesAsync(
GetContainerStatsAsync(id, parameters, cancellationToken),
Expand All @@ -185,7 +173,7 @@ public Task<Stream> 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))
{
Expand All @@ -201,7 +189,7 @@ public Task<Stream> GetContainerStatsAsync(string id, ContainerStatsParameters p
return _client.MakeRequestAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/resize", queryParameters, cancellationToken);
}

public async Task<bool> StartContainerAsync(string id, ContainerStartParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<bool> StartContainerAsync(string id, ContainerStartParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -214,7 +202,7 @@ public Task<Stream> GetContainerStatsAsync(string id, ContainerStatsParameters p
return result ?? throw new InvalidOperationException();
}

public async Task<bool> StopContainerAsync(string id, ContainerStopParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<bool> StopContainerAsync(string id, ContainerStopParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -234,7 +222,7 @@ public Task<Stream> 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))
{
Expand All @@ -253,7 +241,7 @@ public Task<Stream> 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))
{
Expand All @@ -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))
{
Expand All @@ -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))
{
Expand All @@ -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<MultiplexedStream> AttachContainerAsync(string id, bool tty, ContainerAttachParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<MultiplexedStream> AttachContainerAsync(string id, ContainerAttachParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -313,11 +301,17 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters
}

var queryParameters = new QueryString<ContainerAttachParameters>(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<ContainerWaitResponse> WaitContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ContainerWaitResponse> WaitContainerAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand All @@ -327,7 +321,7 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters
return await _client.MakeRequestAsync<ContainerWaitResponse>(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))
{
Expand All @@ -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<GetArchiveFromContainerResponse> GetArchiveFromContainerAsync(string id, GetArchiveFromContainerParameters parameters, bool statOnly, CancellationToken cancellationToken = default(CancellationToken))
public async Task<GetArchiveFromContainerResponse> GetArchiveFromContainerAsync(string id, GetArchiveFromContainerParameters parameters, bool statOnly, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand Down Expand Up @@ -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))
{
Expand All @@ -396,7 +390,7 @@ public async Task<ContainersPruneResponse> PruneContainersAsync(ContainersPruneP
return await _client.MakeRequestAsync<ContainersPruneResponse>(_client.NoErrorHandlers, HttpMethod.Post, "containers/prune", queryParameters, cancellationToken).ConfigureAwait(false);
}

public async Task<ContainerUpdateResponse> UpdateContainerAsync(string id, ContainerUpdateParameters parameters, CancellationToken cancellationToken = default(CancellationToken))
public async Task<ContainerUpdateResponse> UpdateContainerAsync(string id, ContainerUpdateParameters parameters, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
Expand Down
4 changes: 2 additions & 2 deletions src/Docker.DotNet/Endpoints/ExecOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public async Task<MultiplexedStream> StartContainerExecAsync(string id, Containe

var data = new JsonRequestContent<ContainerExecStartParameters>(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);
}
}
Loading