Thanks to visit codestin.com
Credit goes to github.com

Skip to content

IOException: Connection aborted or Pipe has been terminated during execStartCmd PTY stream on Windows + WSL 2 with httpclient5 transport #2426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Jagkodar opened this issue Apr 10, 2025 · 0 comments

Comments

@Jagkodar
Copy link

Environment:

OS: Windows [Windows 11 Home 24H2]
Docker: Docker Desktop [4.40.0] using WSL 2 backend (Ubuntu [Version 2])
Java: JDK 17 ([17.0.11])
docker-java Version: Tested with 3.3.6, 3.4.0, 3.5.0
Transport: docker-java-transport-httpclient5 (Issue persists across tested docker-java versions with this transport. Netty transport failed to resolve via Maven).
Docker Host URI (Detected by docker-java): npipe:////./pipe/dockerDesktopLinuxEngine

Problem Description:

When attempting to create an interactive terminal session using dockerClient.execCreateCmd(...).withTty(true).withAttachStdin(true)... followed by dockerClient.execStartCmd(...).exec(callback), the initial connection to the Docker daemon works (e.g., pingCmd succeeds), and the exec instance is created.

The ResultCallback.Adapter receives the onStart event and typically receives one or two onNext events containing the initial shell prompt data from the container's PTY.

However, immediately after receiving the first few bytes from the PTY stream, the connection is abruptly terminated, resulting in the onError callback being invoked with either:

java.io.IOException: An established connection was aborted by the software in your host computer (when running within a Spring Boot application using WebSockets to forward the stream)
java.io.IOException: java.io.IOException: Denna pipe har avslutats (This pipe has been terminated) (when running the minimal reproducible example below).
This happens consistently across different docker-java versions (3.3.6, 3.4.0, 3.5.0) when using the httpclient5 transport. Manual docker exec -it sh commands from the host terminal work without issue.

Troubleshooting Steps Taken:

Created a minimal reproducible example (code below) which isolates the issue outside of Spring Boot/WebSockets.
Tested docker-java versions 3.3.6, 3.4.0, and 3.5.0 with the httpclient5 transport – the PTY stream error persists.
Attempted to use the netty transport, but encountered Maven dependency resolution issues preventing its download.
Reinstalled Docker Desktop.
Ensured WSL 2 integration is enabled for the default distro (Ubuntu).
Disabled Windows Defender Firewall and all third-party antivirus software.
Tried connecting via the default named pipe and explicitly via tcp://localhost:2375 (after enabling insecure TCP exposure in Docker Desktop). While TCP allowed the initial pingCmd to succeed when the named pipe failed, the subsequent PTY stream still failed with the "Connection aborted" error. TCP exposure has since been disabled.

Minimal Reproducible Example (DockerTest.java):

package com.example.test;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

public class DockerTest {

private static final Logger logger = LoggerFactory.getLogger(DockerTest.class);

public static void main(String[] args) {
    DockerClient dockerClient = null;
    String containerId = null;
    PipedOutputStream ptyStdin = null; // For potential input later
    PipedInputStream ptyStdinPipe = null; // For potential input later

    try {
        // --- 1. Initialize Docker Client ---
        logger.info("Attempting to initialize Docker client using default host detection...");
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
        logger.info("DockerClientConfig resolved DOCKER_HOST to: {}", config.getDockerHost());

        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
                .dockerHost(config.getDockerHost())
                .sslConfig(config.getSSLConfig())
                .connectionTimeout(Duration.ofSeconds(30))
                .responseTimeout(Duration.ofSeconds(45))
                .build();

        dockerClient = DockerClientImpl.getInstance(config, httpClient);
        logger.info("Pinging Docker daemon...");
        dockerClient.pingCmd().exec();
        logger.info("Docker Ping Successful!");

        // --- 2. Create and Start a Simple Container ---
        String imageName = "ubuntu:latest"; // Use a simple image
         logger.info("Pulling image {} if not present...", imageName);
         try {
             dockerClient.inspectImageCmd(imageName).exec();
             logger.info("Image {} already present.", imageName);
         } catch (Exception e) { // Catch generic Exception as NotFoundException might not be the only cause
             logger.info("Image {} not found or error inspecting, pulling...", imageName);
             dockerClient.pullImageCmd(imageName).start().awaitCompletion();
             logger.info("Image {} pulled.", imageName);
         }


        logger.info("Creating container from image {}...", imageName);
        HostConfig hostConfig = HostConfig.newHostConfig().withNetworkMode("none"); // No network needed
        CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
                .withHostConfig(hostConfig)
                .withCmd("sleep", "infinity") // Keep container running
                .withTty(true) // Keep TTY for potential exec later
                .withAttachStdin(false) // Stdin attached later via exec
                .withAttachStdout(true)
                .withAttachStderr(true)
                .exec();
        containerId = container.getId();
        logger.info("Created container: {}", containerId);

        dockerClient.startContainerCmd(containerId).exec();
        logger.info("Started container: {}", containerId);

        // --- 3. Create Exec Instance (Shell) ---
        logger.info("Creating exec instance in container {}...", containerId);
        ExecCreateCmdResponse execCreateResponse = dockerClient.execCreateCmd(containerId)
                .withAttachStdout(true)
                .withAttachStderr(true)
                .withAttachStdin(true) // Attach stdin
                .withTty(true) // Allocate TTY
                .withCmd("sh", "-i") // Interactive shell
                .exec();
        String execId = execCreateResponse.getId();
        logger.info("Created exec instance: {}", execId);

        // --- 4. Start Exec and Attach Streams ---
        // Setup pipes for stdin
        ptyStdin = new PipedOutputStream();
        ptyStdinPipe = new PipedInputStream(ptyStdin);

        logger.info("Starting exec instance {} and attaching streams...", execId);

        // Create final references for use in the inner class
        final PipedInputStream finalPtyStdinPipe = ptyStdinPipe;
        final PipedOutputStream finalPtyStdin = ptyStdin;

        ResultCallback.Adapter<Frame> callback = new ResultCallback.Adapter<Frame>() {
            @Override
            public void onStart(Closeable closeable) {
                logger.info("[EXEC CALLBACK] onStart");
            }

            @Override
            public void onNext(Frame frame) {
                logger.info("[EXEC CALLBACK] onNext: Type={}, Payload={}",
                        frame.getStreamType(),
                        new String(frame.getPayload(), StandardCharsets.UTF_8).trim());
                // Simulate the point where the original error occurred
                // If the connection aborts shortly after this log, it matches the pattern
            }

            @Override
            public void onError(Throwable throwable) {
                logger.error("[EXEC CALLBACK] onError:", throwable);
            }

            @Override
            public void onComplete() {
                logger.info("[EXEC CALLBACK] onComplete");
            }

            @Override
            public void close() throws IOException {
                 logger.info("[EXEC CALLBACK] close");
                 if (finalPtyStdinPipe != null) finalPtyStdinPipe.close(); // Use final reference
                 if (finalPtyStdin != null) finalPtyStdin.close(); // Use final reference
            }
        };

        // Start the exec command
        dockerClient.execStartCmd(execId)
                .withDetach(false)
                .withTty(true)
                .withStdIn(ptyStdinPipe) // Attach the input stream
                .exec(callback);

        logger.info("Exec instance started. Waiting for callback events...");

        // Keep the main thread alive briefly to see callbacks
        Thread.sleep(15000); // Wait 15 seconds

        logger.info("Minimal test finished waiting.");


    } catch (Exception e) {
        logger.error("An error occurred during the test:", e);
    } finally {
        // --- 5. Cleanup ---
        logger.info("Starting cleanup...");
        // Close streams using original variables (which might be null if setup failed)
        if (ptyStdin != null) try { ptyStdin.close(); } catch (IOException e) { /* ignore */ }
        if (ptyStdinPipe != null) try { ptyStdinPipe.close(); } catch (IOException e) { /* ignore */ }

        if (dockerClient != null) {
            if (containerId != null) {
                try {
                    logger.info("Stopping container {}...", containerId);
                    dockerClient.stopContainerCmd(containerId).withTimeout(5).exec();
                } catch (Exception e) {
                    logger.warn("Error stopping container {}: {}", containerId, e.getMessage());
                }
                try {
                    logger.info("Removing container {}...", containerId);
                    dockerClient.removeContainerCmd(containerId).withForce(true).exec();
                } catch (Exception e) {
                    logger.warn("Error removing container {}: {}", containerId, e.getMessage());
                }
            }
            try {
                logger.info("Closing Docker client...");
                dockerClient.close();
            } catch (IOException e) {
                logger.error("Error closing Docker client:", e);
            }
        }
        logger.info("Cleanup finished.");
    }
}

}

Relevant Logs from Minimal Test:

[docker-java-stream-...] INFO com.example.test.DockerTest - [EXEC CALLBACK] onStart
[docker-java-stream-...] INFO com.example.test.DockerTest - [EXEC CALLBACK] onNext: Type=RAW, Payload=#
[docker-java-stream-...] INFO com.example.test.DockerTest - [EXEC CALLBACK] onNext: Type=RAW, Payload=
[docker-java-stream-...] ERROR com.example.test.DockerTest - [EXEC CALLBACK] onError:
java.io.IOException: java.io.IOException: Denna pipe har avslutats
at java.base/java.nio.channels.Channels$2.read(Channels.java:240)
at org.apache.hc.core5.http.impl.io.SessionInputBufferImpl.read(SessionInputBufferImpl.java:195)
... (stack trace continues) ...
Caused by: java.io.IOException: Denna pipe har avslutats
at java.base/sun.nio.ch.Iocp.translateErrorToIOException(Iocp.java:299)
... (stack trace continues) ...
[docker-java-stream-...] INFO com.example.test.DockerTest - [EXEC CALLBACK] onComplete

Expected Behavior:

The execStartCmd callback should remain active, receiving further output from the container's shell as commands are (theoretically) sent via the ptyStdin stream. It should only call onComplete or close when the shell process inside the container exits or the Closeable from onStart is closed.

Actual Behavior:

The connection underlying the PTY stream is terminated by the host system almost immediately after the stream starts, triggering onError.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant