Skip to content

Commit

Permalink
Named-pipe implementation (WIP: some tests hang)
Browse files Browse the repository at this point in the history
  • Loading branch information
hazsetata committed Dec 13, 2017
1 parent c1c3e83 commit 446ef75
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 3 deletions.
13 changes: 11 additions & 2 deletions src/main/java/com/spotify/docker/client/DefaultDockerClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ public void progress(ProgressMessage message) throws DockerException {
// ==========================================================================

private static final String UNIX_SCHEME = "unix";
private static final String NAMED_PIPE_SCHEME = "npipe";

private static final Logger log = LoggerFactory.getLogger(DefaultDockerClient.class);

Expand Down Expand Up @@ -357,7 +358,10 @@ Client getNoTimeoutClient() {
* @param uri The docker rest api uri.
*/
public DefaultDockerClient(final String uri) {
this(URI.create(uri.replaceAll("^unix:///", "unix://localhost/")));
this(URI.create(uri.replaceAll("^unix:///", "unix://localhost/")
.replaceAll("^npipe:////\\.", "npipe://localhost/")
)
);
}

/**
Expand Down Expand Up @@ -396,6 +400,8 @@ protected DefaultDockerClient(final Builder builder) {

if (originalUri.getScheme().equals(UNIX_SCHEME)) {
this.uri = UnixConnectionSocketFactory.sanitizeUri(originalUri);
} else if (originalUri.getScheme().equals(NAMED_PIPE_SCHEME)) {
this.uri = WindowsNamedPipeSocketFactory.sanitizeUri(originalUri);
} else {
this.uri = originalUri;
}
Expand Down Expand Up @@ -485,6 +491,8 @@ private Registry<ConnectionSocketFactory> getSchemeRegistry(final Builder builde

if (builder.uri.getScheme().equals(UNIX_SCHEME)) {
registryBuilder.register(UNIX_SCHEME, new UnixConnectionSocketFactory(builder.uri));
} else if (builder.uri.getScheme().equals(NAMED_PIPE_SCHEME)) {
registryBuilder.register(NAMED_PIPE_SCHEME, new WindowsNamedPipeSocketFactory(builder.uri));
}

return registryBuilder.build();
Expand Down Expand Up @@ -2793,7 +2801,8 @@ public static Builder fromEnv() throws DockerCertificateException {
final Optional<DockerCertificatesStore> certs = DockerCertificates.builder()
.dockerCertPath(dockerCertPath).build();

if (endpoint.startsWith(UNIX_SCHEME + "://")) {
if (endpoint.startsWith(UNIX_SCHEME + "://")
|| endpoint.startsWith(NAMED_PIPE_SCHEME + "://")) {
builder.uri(endpoint);
} else {
final String stripped = endpoint.replaceAll(".*://", "");
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/spotify/docker/client/DockerHost.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public String getenv(final String name) {
private static SystemDelegate systemDelegate = defaultSystemDelegate;

private static final String DEFAULT_UNIX_ENDPOINT = "unix:///var/run/docker.sock";
private static final String DEFAULT_WINDOWS_ENDPOINT = "npipe:////./pipe/docker_engine";
private static final String DEFAULT_ADDRESS = "localhost";
private static final int DEFAULT_PORT = 2375;

Expand All @@ -72,7 +73,7 @@ public String getenv(final String name) {
private final String certPath;

private DockerHost(final String endpoint, final String certPath) {
if (endpoint.startsWith("unix://")) {
if (endpoint.startsWith("unix://") || endpoint.startsWith("npipe://")) {
this.port = 0;
this.address = DEFAULT_ADDRESS;
this.host = endpoint;
Expand Down Expand Up @@ -187,6 +188,8 @@ static String defaultDockerEndpoint() {
final String os = osName.toLowerCase(Locale.ENGLISH);
if (os.equalsIgnoreCase("linux") || os.contains("mac")) {
return DEFAULT_UNIX_ENDPOINT;
} else if (os.contains("windows")) {
return DEFAULT_WINDOWS_ENDPOINT;
} else {
return DEFAULT_ADDRESS + ":" + defaultPort();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*-
* -\-\-
* docker-client
* --
* Copyright (C) 2016 - 2017 Spotify AB
* --
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* -/-/-
*/

package com.spotify.docker.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.HttpHost;
import org.apache.http.annotation.Contract;
import org.apache.http.annotation.ThreadingBehavior;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.protocol.HttpContext;

/**
* Provides a ConnectionSocketFactory for connecting Apache HTTP clients to Windows named pipes.
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
public class WindowsNamedPipeSocketFactory implements ConnectionSocketFactory {
private static final Pattern PIPE_URI_PATTERN =
Pattern.compile("^npipe:/+(?<host>.+)/pipe/(?<name>.+)$");
private static final String PIPE_NAME_FORMAT = "\\\\%s\\pipe\\%s";

private final String pipeName;

public WindowsNamedPipeSocketFactory(final URI socketUri) {
Matcher uriMatcher = PIPE_URI_PATTERN.matcher(socketUri.toString());
if (uriMatcher.matches()) {
pipeName = String.format(
PIPE_NAME_FORMAT,
uriMatcher.group("host").equalsIgnoreCase("localhost") ? "." : uriMatcher.group("host"),
uriMatcher.group("name")
);
} else {
throw new IllegalArgumentException("Invalid named pipe URI.");
}
}

public static URI sanitizeUri(final URI uri) {
if (uri.getScheme().equals("npipe")) {
return URI.create("npipe://localhost:80");
} else {
return uri;
}
}

@Override
public Socket createSocket(HttpContext httpContext) throws IOException {
return new WindowsNamedPipeSocket(this.pipeName);
}

@Override
public Socket connectSocket(final int connectTimeout,
final Socket socket,
final HttpHost host,
final InetSocketAddress remoteAddress,
final InetSocketAddress localAddress,
final HttpContext context) throws IOException {
if (!(socket instanceof WindowsNamedPipeSocket)) {
throw new AssertionError("Unexpected socket: " + socket);
}

socket.connect(new InetSocketAddress(80)); // The SocketAddress implementation is ignored

return socket;
}

public class WindowsNamedPipeSocket extends Socket {
private String namedPipePath;

private RandomAccessFile namedPipeFile;
private InputStream inputStream;
private OutputStream outputStream;

public WindowsNamedPipeSocket(String namedPipePath) {
this.namedPipePath = namedPipePath;
}

@Override
public void connect(SocketAddress endpoint, int timeout) throws IOException {
namedPipeFile = new RandomAccessFile(namedPipePath, "rw");
inputStream = new InputStream() {
@Override
public int read() throws IOException {
return namedPipeFile.read();
}

@Override
public int read(byte[] bytes) throws IOException {
return namedPipeFile.read(bytes);
}

@Override
public int read(byte[] bytes, int off, int len) throws IOException {
return namedPipeFile.read(bytes, off, len);
}
};

outputStream = new OutputStream() {
@Override
public void write(int abyte) throws IOException {
namedPipeFile.write(abyte);
}

@Override
public void write(byte[] bytes) throws IOException {
namedPipeFile.write(bytes);
}

@Override
public void write(byte[] bytes, int off, int len) throws IOException {
namedPipeFile.write(bytes, off, len);
}
};
}

@Override
public InputStream getInputStream() throws IOException {
return inputStream;
}

@Override
public OutputStream getOutputStream() throws IOException {
return outputStream;
}

@Override
public void setTcpNoDelay(boolean on) throws SocketException {
// Do nothing
}

@Override
public boolean getTcpNoDelay() throws SocketException {
return true;
}

@Override
public void setSoLinger(boolean on, int linger) throws SocketException {
// Do nothing
}

@Override
public int getSoLinger() throws SocketException {
return -1; // Disabled
}

@Override
public void setOOBInline(boolean on) throws SocketException {
// Do nothing
}

@Override
public boolean getOOBInline() throws SocketException {
return false;
}

@Override
public synchronized void setSoTimeout(int timeout) throws SocketException {
// Do nothing
}

@Override
public synchronized int getSoTimeout() throws SocketException {
return 0;
}

@Override
public synchronized void setSendBufferSize(int size) throws SocketException {
// Do nothing
}

@Override
public synchronized int getSendBufferSize() throws SocketException {
return 0;
}

@Override
public synchronized void setReceiveBufferSize(int size) throws SocketException {
// Do nothing
}

@Override
public synchronized int getReceiveBufferSize() throws SocketException {
return 0;
}

@Override
public void setKeepAlive(boolean on) throws SocketException {
// Do nothing
}

@Override
public boolean getKeepAlive() throws SocketException {
return true;
}

@Override
public void setTrafficClass(int tc) throws SocketException {
// Do nothing
}

@Override
public int getTrafficClass() throws SocketException {
return 0;
}

@Override
public void setReuseAddress(boolean on) throws SocketException {
// Do nothing
}

@Override
public boolean getReuseAddress() throws SocketException {
return false;
}

@Override
public void shutdownInput() throws IOException {
// Do nothing
}

@Override
public void shutdownOutput() throws IOException {
// Do nothing
}

@Override
public synchronized void close() throws IOException {
if (namedPipeFile != null) {
namedPipeFile.close();
namedPipeFile = null;
inputStream = null;
outputStream = null;
}
}

@Override
public boolean isClosed() {
return (namedPipeFile == null);
}
}
}

0 comments on commit 446ef75

Please sign in to comment.