Skip to content

Commit 2eefc83

Browse files
feat(wslc): add WSL Containers (wslc://) transport, lifecycle integration and auto-detection
WSL Containers (wslc) runs dockerd inside a lightweight VM that exposes no Windows named pipe or TCP port; the only host-visible channel is the stdio bridge `wslc system session run docker system dial-stdio`. This adds first-class wslc support to docker-java. Transport (docker-java-transport, -transport-httpclient5): - WslcSocket: a Socket whose streams are the wslc dial-stdio child process (mirrors NamedPipeSocket; java.util.logging only, no new dependency). - ApacheDockerHttpClientImpl: a `wslc` scheme case plus a WslcSocket branch. Hijacked exec/attach/log streams work unchanged at the HttpClient5 socket level. Lifecycle integration (docker-java-core): - wslc's Windows integration (127.0.0.1 port relay and Windows-path bind mounts) is wired by the wslc control plane only when a container is created/started through the native `wslc` CLI, not over the Docker API on the same dockerd. - WslcLifecycleDockerHttpClient decorates a Docker-API client and reconciles just the affected calls with the wslc CLI, delegating everything else unchanged: POST /containers/create -> wslc create, POST /containers/{id}/start -> wslc start (wires relay+bind), POST /networks/create -> wslc network create, and GET /containers/{id}/json with NetworkSettings.Ports overridden from `wslc list` (the start relay's real host port differs from the daemon's recorded one). - Activated transparently in DockerClientImpl.getInstance only when the host scheme is wslc, so unix/npipe/tcp are unaffected and no extra dependency is needed. Consumers that shade docker-java-core (e.g. Testcontainers) pick it up after a plain rebuild. Auto-detection (DefaultDockerClientConfig): - when DOCKER_HOST is unset, fall back to wslc://localhost on Windows only if the //./pipe/docker_engine pipe is absent and wslc is available (`wslc version` exit 0, WSLC_EXECUTABLE-overridable), so Docker Desktop/Podman keep winning. Tests run without a daemon or wslc CLI: WslcSocketTest (executable resolution and connect failure) and WslcLifecycleDockerHttpClientTest (request pass-through, inspect fallback when wslc is unavailable, create-failure handling). Refs: #2658 Signed-off-by: David Tavoularis <david.tavoularis@mycom-osi.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 002b6eb commit 2eefc83

7 files changed

Lines changed: 916 additions & 2 deletions

File tree

docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Objects;
2929
import java.util.Properties;
3030
import java.util.Set;
31+
import java.util.concurrent.TimeUnit;
3132

3233
import static org.apache.commons.lang3.BooleanUtils.isTrue;
3334

@@ -68,6 +69,11 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf
6869

6970
static final String WINDOWS_DEFAULT_DOCKER_HOST = "npipe:////./pipe/docker_engine";
7071

72+
static final String WSLC_DEFAULT_DOCKER_HOST = "wslc://localhost";
73+
74+
// wslc availability is constant per machine; probe at most once per JVM.
75+
private static volatile Boolean wslcAvailable;
76+
7177
static {
7278
CONFIG_KEYS.add(DOCKER_HOST);
7379
CONFIG_KEYS.add(DOCKER_TLS_VERIFY);
@@ -115,6 +121,60 @@ private URI checkDockerHostScheme(URI dockerHost) {
115121
return dockerHost;
116122
}
117123

124+
/**
125+
* Default DOCKER_HOST when none is configured. On Windows a real Docker/Podman named pipe is
126+
* preferred; only when that pipe is absent and the WSL Containers (wslc) CLI is available does it
127+
* fall back to {@code wslc://localhost}, so existing Docker Desktop / Podman setups keep winning.
128+
*/
129+
private static String defaultDockerHost() {
130+
if (!SystemUtils.IS_OS_WINDOWS) {
131+
return DEFAULT_DOCKER_HOST;
132+
}
133+
if (!new File("//./pipe/docker_engine").exists() && isWslcAvailable()) {
134+
return WSLC_DEFAULT_DOCKER_HOST;
135+
}
136+
return WINDOWS_DEFAULT_DOCKER_HOST;
137+
}
138+
139+
private static boolean isWslcAvailable() {
140+
Boolean cached = wslcAvailable;
141+
if (cached == null) {
142+
cached = probeWslc();
143+
wslcAvailable = cached;
144+
}
145+
return cached;
146+
}
147+
148+
// 'wslc version' is a cheap metadata call that does not start the container VM. The executable
149+
// can be overridden with the WSLC_EXECUTABLE environment variable.
150+
private static boolean probeWslc() {
151+
String executable = System.getenv("WSLC_EXECUTABLE");
152+
if (executable == null || executable.trim().isEmpty()) {
153+
executable = "wslc.exe";
154+
}
155+
Process process = null;
156+
try {
157+
process = new ProcessBuilder(executable, "version")
158+
.redirectErrorStream(true)
159+
.redirectOutput(ProcessBuilder.Redirect.to(new File("NUL")))
160+
.start();
161+
if (!process.waitFor(10, TimeUnit.SECONDS)) {
162+
process.destroyForcibly();
163+
return false;
164+
}
165+
return process.exitValue() == 0;
166+
} catch (IOException | InterruptedException | RuntimeException e) {
167+
if (e instanceof InterruptedException) {
168+
Thread.currentThread().interrupt();
169+
}
170+
return false;
171+
} finally {
172+
if (process != null && process.isAlive()) {
173+
process.destroyForcibly();
174+
}
175+
}
176+
}
177+
118178
private static Properties loadIncludedDockerProperties(Properties systemProperties) {
119179
Properties p = new Properties();
120180
p.putAll(DEFAULT_PROPERTIES);
@@ -485,7 +545,7 @@ public DefaultDockerClientConfig build() {
485545

486546
URI dockerHostUri = dockerHost != null
487547
? dockerHost
488-
: URI.create(SystemUtils.IS_OS_WINDOWS ? WINDOWS_DEFAULT_DOCKER_HOST : DEFAULT_DOCKER_HOST);
548+
: URI.create(defaultDockerHost());
489549

490550
return new DefaultDockerClientConfig(dockerHostUri, dockerConfigFile, dockerConfig, apiVersion, registryUrl, registryUsername,
491551
registryPassword, registryEmail, sslConfig);

docker-java-core/src/main/java/com/github/dockerjava/core/DockerClientImpl.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,19 @@ public static DockerClientImpl getInstance(DockerClientConfig dockerClientConfig
211211
}
212212

213213
public static DockerClient getInstance(DockerClientConfig dockerClientConfig, DockerHttpClient dockerHttpClient) {
214+
DockerHttpClient httpClient = dockerHttpClient;
215+
// WSL Containers (wslc): the Docker Engine API reaches the daemon over the dial-stdio bridge,
216+
// but the Windows port relay and host bind mounts are only wired when a container is created
217+
// and started through the `wslc` CLI. Transparently route just those lifecycle calls through
218+
// wslc so that published ports are reachable from Windows and host directories can be
219+
// bind-mounted. Doing it here means every docker-java consumer (including Testcontainers, which
220+
// shades this class) gets the behaviour without any extra dependency or client-side wiring.
221+
if (dockerClientConfig.getDockerHost() != null
222+
&& "wslc".equals(dockerClientConfig.getDockerHost().getScheme())) {
223+
httpClient = new WslcLifecycleDockerHttpClient(httpClient, null);
224+
}
214225
return new DockerClientImpl(dockerClientConfig)
215-
.withHttpClient(dockerHttpClient);
226+
.withHttpClient(httpClient);
216227
}
217228

218229
/**

0 commit comments

Comments
 (0)