/******************************************************************************* * * Copyright (c) 2010-2011 Sonatype, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * * * *******************************************************************************/ package org.hudsonci.utils.tasks; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.RemoteInputStream; import hudson.remoting.RemoteOutputStream; import hudson.remoting.SocketInputStream; import hudson.remoting.SocketOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; /** * Opens a server socket on a node and facilitates accepting connections. * * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> * @since 2.1.0 */ public class OpenServerSocket implements Callable<OpenServerSocket.Acceptor, IOException> { private static final Logger log = LoggerFactory.getLogger(OpenServerSocket.class); public static final int DEFAULT_SO_TIMEOUT = 30 * 1000; /** * Used to accept a new connection on the remote server socket. */ public interface Acceptor extends Closeable { int getPort(); Connection accept(boolean close) throws IOException; Connection accept() throws IOException; } /** * Represents the accepted connection. */ public interface Connection extends Closeable { InputStream getInput(); OutputStream getOutput(); } public Acceptor call() throws IOException { return new AcceptorImpl(); } protected void customize(final ServerSocket serverSocket) throws SocketException { assert serverSocket != null; serverSocket.setSoTimeout(DEFAULT_SO_TIMEOUT); } // TODO: Document behavior on both sides of channel so its easier to comprehend whats going on here private class AcceptorImpl implements Acceptor, Serializable { private static final long serialVersionUID = 1L; private transient final ServerSocket serverSocket; public AcceptorImpl() throws IOException { serverSocket = new ServerSocket(); serverSocket.bind(null); customize(serverSocket); log.debug("Created acceptor"); } public Connection accept(final boolean close) throws IOException { log.debug("Accepting"); Socket socket = serverSocket.accept(); log.debug("Accepted: {}", socket); if (close) { close(); } return new ConnectionImpl(socket); } public Connection accept() throws IOException { return accept(false); } public void close() throws IOException { log.debug("Closing"); serverSocket.close(); } public int getPort() { return serverSocket.getLocalPort(); } /** * Executed on remote, returns a proxy. */ private Object writeReplace() { return Channel.current().export(Acceptor.class, this); } } private class ConnectionImpl implements Connection, Serializable { private static final long serialVersionUID = 1L; private InputStream input; private OutputStream output; public ConnectionImpl(final InputStream input, final OutputStream output) { assert input != null; this.input = input; assert output != null; this.output = output; log.debug("Created connection"); } private ConnectionImpl(final Socket socket) throws IOException { // assert socket != null this(new SocketInputStream(socket), new SocketOutputStream(socket)); // TODO: How to close the socket? } public InputStream getInput() { return input; } public OutputStream getOutput() { return output; } public void close() throws IOException { // FIXME: nop for now, trying to close the streams causes problems } /** * Executed on remote, returns connection wrapper with remote streams. */ private Object writeReplace() { return new ConnectionImpl(new RemoteInputStream(input), new RemoteOutputStream(output)); } /** * Re-establishes buffering. */ private Object readResolve() { this.input = new BufferedInputStream(input); this.output = new BufferedOutputStream(output); return this; } } }