Skip to content

Commit ae63892

Browse files
committed
Merge main into development to resolve conflicts
2 parents e03d403 + b3a2a80 commit ae63892

File tree

3 files changed

+254
-1
lines changed

3 files changed

+254
-1
lines changed

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#
2+
3+
Archives containing JAR files are available as [releases](https://github.com/intisy/docker-java/releases).
4+
5+
## What is docker-java?
6+
7+
Docker-java provides a standalone Docker server for Java.
8+
9+
## Usage in private projects
10+
11+
* Maven (inside the file)
12+
```xml
13+
<repository>
14+
<id>github</id>
15+
<url>https://maven.pkg.github.com/intisy/docker-java</url>
16+
<snapshots><enabled>true</enabled></snapshots>
17+
</repository>
18+
<dependency>
19+
<groupId>io.github.intisy</groupId>
20+
<artifactId>docker-java</artifactId>
21+
<version>1.2.4.4</version>
22+
</dependency>
23+
```
24+
25+
* Maven (inside the file)
26+
```xml
27+
<servers>
28+
<server>
29+
<id>github</id>
30+
<username>your-username</username>
31+
<password>your-access-token</password>
32+
</server>
33+
</servers>
34+
```
35+
36+
* Gradle (inside the or file)
37+
```groovy
38+
repositories {
39+
maven {
40+
url "https://maven.pkg.github.com/intisy/docker-java"
41+
credentials {
42+
username = "<your-username>"
43+
password = "<your-access-token>"
44+
}
45+
}
46+
}
47+
dependencies {
48+
implementation 'io.github.intisy:docker-java:1.2.4.4'
49+
}
50+
```
51+
52+
## Usage in public projects
53+
54+
* Gradle (inside the or file)
55+
```groovy
56+
plugins {
57+
id "io.github.intisy.github-gradle" version "1.3.7"
58+
}
59+
dependencies {
60+
githubImplementation "intisy:docker-java:1.2.4.4"
61+
}
62+
```
63+
64+
Once you have it installed you can use it like so:
65+
66+
```
67+
DockerProvider dockerProvider = DockerProvider.get();
68+
dockerProvider.ensureInstalled();
69+
dockerProvider.start();
70+
DockerClient dockerClient = dockerProvider.getClient();
71+
```
72+
73+
Currently supported setups:
74+
* [x] - Linux root
75+
* [x] - Linux rootless
76+
* [x] - Windows Administrator
77+
* [x] - Windows non-Administrator
78+
* [x] - macOS root
79+
* [x] - macOS rootless
80+
81+
## License
82+
83+
[![Apache License 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)

src/main/java/io/github/intisy/docker/WindowsDockerProvider.java

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,10 +662,21 @@ private void startNativeDocker() throws IOException, InterruptedException {
662662
}
663663

664664
private int dockerPort;
665+
private boolean usingExistingDaemon = false;
666+
private static final Object EXISTING_DAEMON_LOCK = new Object();
667+
private static volatile boolean existingDaemonSetup = false;
668+
private static volatile int sharedDaemonPort = 0;
669+
private static volatile String sharedWslIp = null;
665670

666671
private void startWsl2Docker() throws IOException, InterruptedException {
667672
ensureInstalled();
668673

674+
if (tryConnectToExistingDaemon()) {
675+
log.info("Connected to existing Docker daemon in WSL2");
676+
usingExistingDaemon = true;
677+
return;
678+
}
679+
669680
dockerPort = 2375 + Math.abs(instanceId.hashCode() % 1000);
670681
wslSocketPath = "tcp://0.0.0.0:" + dockerPort;
671682
String wslLogFile = "/tmp/docker-java-" + instanceId + ".log";
@@ -731,7 +742,7 @@ private void startWsl2Docker() throws IOException, InterruptedException {
731742
String isolationFlags = "";
732743
if (otherDockerdRunning) {
733744
log.info("Another Docker daemon detected, using isolation flags to avoid conflicts");
734-
isolationFlags = " --iptables=false --bridge=none";
745+
isolationFlags = " --iptables=false";
735746
}
736747

737748
log.debug("Starting dockerd directly...");
@@ -830,6 +841,150 @@ private void printDockerdSudoManualInstructions() {
830841
log.error("");
831842
}
832843

844+
/**
845+
* Try to connect to an existing Docker daemon running in WSL2.
846+
* This checks if Docker is running, starts it if needed, and exposes it on TCP.
847+
* Uses synchronization to prevent race conditions when multiple threads call this.
848+
*/
849+
private boolean tryConnectToExistingDaemon() {
850+
synchronized (EXISTING_DAEMON_LOCK) {
851+
if (existingDaemonSetup && sharedDaemonPort > 0 && sharedWslIp != null) {
852+
dockerPort = sharedDaemonPort;
853+
wslIpAddress = sharedWslIp;
854+
log.info("Using existing Docker daemon connection on port {}", dockerPort);
855+
return true;
856+
}
857+
858+
try {
859+
String socketCheck = runWslCommand("test -S /var/run/docker.sock && echo yes || echo no", false, 5);
860+
if (!"yes".equals(socketCheck.trim())) {
861+
log.debug("Docker socket /var/run/docker.sock does not exist");
862+
863+
log.info("Docker daemon not running, attempting to start it...");
864+
String startResult = runWslCommand("sudo service docker start 2>&1", false, 30);
865+
log.debug("Docker service start result: {}", startResult);
866+
867+
Thread.sleep(3000);
868+
869+
socketCheck = runWslCommand("test -S /var/run/docker.sock && echo yes || echo no", false, 5);
870+
if (!"yes".equals(socketCheck.trim())) {
871+
log.debug("Docker socket still doesn't exist after service start");
872+
return false;
873+
}
874+
}
875+
876+
log.info("Found Docker daemon in WSL2, setting up TCP forwarding...");
877+
878+
String wslIp = runWslCommand("hostname -I | awk '{print $1}'", false, 5).trim();
879+
if (wslIp.isEmpty()) {
880+
log.info("Could not get WSL2 IP address, will use isolated daemon");
881+
return false;
882+
}
883+
wslIpAddress = wslIp;
884+
log.info("WSL2 IP: {}", wslIp);
885+
886+
String socatPath = runWslCommand("command -v socat 2>/dev/null || echo ''", false, 5).trim();
887+
if (socatPath.isEmpty()) {
888+
log.info("Installing socat for TCP forwarding...");
889+
runWslCommand("sudo apt-get update -qq && sudo apt-get install -y -qq socat 2>&1", false, 120);
890+
socatPath = runWslCommand("command -v socat 2>/dev/null || echo ''", false, 5).trim();
891+
if (socatPath.isEmpty()) {
892+
log.info("Failed to install socat, will use isolated daemon");
893+
return false;
894+
}
895+
}
896+
897+
dockerPort = 2375;
898+
899+
runWslCommand("sudo pkill -9 -f 'socat.*:" + dockerPort + "' 2>/dev/null; sleep 1", false, 10);
900+
901+
String socketPerms = runWslCommand("ls -la /var/run/docker.sock 2>&1", false, 5);
902+
log.info("Docker socket: {}", socketPerms.trim());
903+
904+
String socatCmd = String.format(
905+
"sudo nohup socat TCP-LISTEN:%d,bind=0.0.0.0,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock </dev/null >/tmp/socat-%d.log 2>&1 &",
906+
dockerPort, dockerPort);
907+
runWslCommand(socatCmd, false, 5);
908+
909+
Thread.sleep(2000);
910+
911+
String socatPid = runWslCommand("pgrep -f 'socat.*:" + dockerPort + "' 2>/dev/null | head -1 || echo ''", false, 5).trim();
912+
if (socatPid.isEmpty()) {
913+
String socatLog = runWslCommand("cat /tmp/socat-" + dockerPort + ".log 2>/dev/null | head -20 || echo '(no log)'", false, 5);
914+
log.info("socat failed to start. Log: {}", socatLog);
915+
return false;
916+
}
917+
log.info("socat running with PID: {}", socatPid);
918+
919+
String listenCheck = runWslCommand("ss -tlnp 2>/dev/null | grep ':" + dockerPort + " ' || echo 'not listening'", false, 5);
920+
log.info("Port {} status: {}", dockerPort, listenCheck.trim());
921+
922+
boolean connected = testDockerConnection(wslIp, dockerPort);
923+
if (!connected) {
924+
connected = testDockerConnection("localhost", dockerPort);
925+
if (connected) {
926+
wslIpAddress = "localhost";
927+
}
928+
}
929+
930+
if (connected) {
931+
sharedDaemonPort = dockerPort;
932+
sharedWslIp = wslIpAddress;
933+
existingDaemonSetup = true;
934+
log.info("Connected to Docker daemon via TCP at {}:{}", wslIpAddress, dockerPort);
935+
return true;
936+
}
937+
938+
String socatLog = runWslCommand("cat /tmp/socat-" + dockerPort + ".log 2>/dev/null | tail -10 || echo '(no log)'", false, 5);
939+
log.info("Connection failed, socat log: {}", socatLog);
940+
return false;
941+
} catch (Exception e) {
942+
log.info("Failed to connect to existing daemon: {}", e.getMessage());
943+
return false;
944+
}
945+
}
946+
}
947+
948+
/**
949+
* Test Docker connection from Windows by attempting a TCP socket connection and HTTP request.
950+
*/
951+
private boolean testDockerConnection(String host, int port) {
952+
try {
953+
log.info("Testing Docker connection to {}:{}...", host, port);
954+
955+
java.net.Socket socket = new java.net.Socket();
956+
socket.connect(new java.net.InetSocketAddress(host, port), 3000);
957+
socket.close();
958+
log.info("TCP socket connection successful to {}:{}", host, port);
959+
960+
java.net.URL url = new java.net.URL("http://" + host + ":" + port + "/version");
961+
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
962+
conn.setConnectTimeout(3000);
963+
conn.setReadTimeout(3000);
964+
int responseCode = conn.getResponseCode();
965+
conn.disconnect();
966+
967+
if (responseCode == 200) {
968+
log.info("Docker API responding at {}:{}", host, port);
969+
return true;
970+
}
971+
log.info("Docker API returned HTTP {} at {}:{}", responseCode, host, port);
972+
return false;
973+
} catch (java.net.ConnectException e) {
974+
log.info("Connection refused to {}:{} - socat may not be forwarding correctly", host, port);
975+
return false;
976+
} catch (java.net.SocketTimeoutException e) {
977+
log.info("Connection timeout to {}:{}", host, port);
978+
return false;
979+
} catch (IOException e) {
980+
log.info("Connection test failed for {}:{} - {}: {}", host, port, e.getClass().getSimpleName(), e.getMessage());
981+
return false;
982+
}
983+
}
984+
985+
/**
986+
* Check if passwordless sudo is available for dockerd.
987+
*/
833988
private boolean checkPasswordlessSudo() {
834989
try {
835990
ProcessBuilder pb = new ProcessBuilder("wsl", "-d", wslDistro, "-e", "bash", "-c",
@@ -1023,6 +1178,18 @@ public DockerClient getClient() {
10231178
public void stop() {
10241179
log.info("Stopping Docker daemon (instance: {})...", instanceId);
10251180

1181+
if (usingExistingDaemon && usingWsl2 && wslDistro != null) {
1182+
try {
1183+
ProcessBuilder pb = new ProcessBuilder("wsl", "-d", wslDistro, "-e", "bash", "-c",
1184+
"sudo -n pkill -f 'socat.*:" + dockerPort + "' 2>/dev/null || true");
1185+
pb.start().waitFor(5, TimeUnit.SECONDS);
1186+
log.info("Stopped socat TCP forwarder");
1187+
} catch (IOException | InterruptedException e) {
1188+
log.debug("Failed to stop socat: {}", e.getMessage());
1189+
}
1190+
return;
1191+
}
1192+
10261193
if (dockerProcess != null) {
10271194
if (usingWsl2 && wslDistro != null) {
10281195
try {

src/main/java/io/github/intisy/docker/transport/DockerHttpClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public DockerResponse post(String path) throws IOException {
6666
public DockerResponse post(String path, Map<String, String> queryParams, Object body) throws IOException {
6767
String fullPath = buildPathWithQuery(path, queryParams);
6868
String jsonBody = body != null ? gson.toJson(body) : null;
69+
if (jsonBody != null && path.contains("/containers/create")) {
70+
log.info("Creating container with JSON: {}", jsonBody);
71+
}
6972
return request("POST", fullPath, jsonBody);
7073
}
7174

0 commit comments

Comments
 (0)