From e26975a646f5acc091c19e9bfd8b059af22d7e22 Mon Sep 17 00:00:00 2001 From: "mert.akdut" Date: Sat, 24 Oct 2026 00:42:37 +0300 Subject: [PATCH] Connect to the Windows named pipe without an existence pre-check --- .../DockerClientProviderStrategy.java | 5 +- .../DockerClientProviderStrategyTest.java | 113 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyTest.java diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java index 7b0aaafc169..fbc187870d7 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java @@ -183,7 +183,10 @@ protected boolean test() { break; case "unix": case "npipe": - if (!new File(dockerHost.getPath()).exists()) { + // Windows answers File.exists() on a named pipe by opening an instance of it, which fails with + // ERROR_PIPE_BUSY while every instance is taken, so a busy pipe would read as absent. + // NamedPipeSocket.connect waits for a busy pipe and fails on an absent one, so it decides alone. + if ("unix".equals(dockerHost.getScheme()) && !new File(dockerHost.getPath()).exists()) { log.debug("DOCKER_HOST socket file '{}' does not exist", dockerHost.getPath()); return false; } diff --git a/core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyTest.java b/core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyTest.java new file mode 100644 index 00000000000..de3a421ad4a --- /dev/null +++ b/core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyTest.java @@ -0,0 +1,113 @@ +package org.testcontainers.dockerclient; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.win32.StdCallLibrary; +import com.sun.jna.win32.W32APIOptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import java.io.File; +import java.net.URI; +import java.nio.channels.FileChannel; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests {@link DockerClientProviderStrategy#test()} against Windows named pipes the test creates itself. A pipe with + * a single instance that one client holds is busy, as Docker Desktop's pipe is while every instance of it is taken + * by other processes, so the busy state needs no race to reproduce. + */ +class DockerClientProviderStrategyTest { + + private static final int PIPE_ACCESS_DUPLEX = 0x00000003; + + private static final int PIPE_NOWAIT = 0x00000001; + + @Test + @EnabledOnOs(OS.WINDOWS) + void testPassesWhenNamedPipeIsBusyAndFreesWithinPingTimeout() throws Exception { + String pipePath = uniquePipePath(); + Pointer pipe = Kernel32.INSTANCE.CreateNamedPipe(pipePath, PIPE_ACCESS_DUPLEX, PIPE_NOWAIT, 1, 0, 0, 0, null); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + try ( + FileChannel holder = FileChannel.open( + Paths.get(pipePath), + StandardOpenOption.READ, + StandardOpenOption.WRITE + ) + ) { + assertThat(new File(pipePath).exists()).as("File.exists() on the busy pipe").isFalse(); + + ScheduledFuture release = executor.schedule( + () -> { + holder.close(); + Kernel32.INSTANCE.DisconnectNamedPipe(pipe); + Kernel32.INSTANCE.ConnectNamedPipe(pipe, null); + return null; + }, + 500, + TimeUnit.MILLISECONDS + ); + + assertThat(strategyFor(pipePath).test()).isTrue(); + release.get(); + } finally { + executor.shutdownNow(); + Kernel32.INSTANCE.CloseHandle(pipe); + } + } + + @Test + @EnabledOnOs(OS.WINDOWS) + void testFailsWhenNamedPipeDoesNotExist() { + assertThat(strategyFor(uniquePipePath()).test()).isFalse(); + } + + private static String uniquePipePath() { + return "//./pipe/testcontainers-" + UUID.randomUUID(); + } + + private static DockerClientProviderStrategy strategyFor(String pipePath) { + return new DockerClientProviderStrategy() { + @Override + public String getDescription() { + return "named pipe " + pipePath; + } + + @Override + public TransportConfig getTransportConfig() { + return TransportConfig.builder().dockerHost(URI.create("npipe://" + pipePath)).build(); + } + }; + } + + interface Kernel32 extends StdCallLibrary { + Kernel32 INSTANCE = Native.load("kernel32", Kernel32.class, W32APIOptions.DEFAULT_OPTIONS); + + Pointer CreateNamedPipe( + String name, + int openMode, + int pipeMode, + int maxInstances, + int outBufferSize, + int inBufferSize, + int defaultTimeOut, + Pointer securityAttributes + ); + + boolean ConnectNamedPipe(Pointer pipe, Pointer overlapped); + + boolean DisconnectNamedPipe(Pointer pipe); + + boolean CloseHandle(Pointer handle); + } +}