diff --git a/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/server/IntegrityRemotingServer.java b/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/server/IntegrityRemotingServer.java index a2460c79b..524cdf7fb 100644 --- a/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/server/IntegrityRemotingServer.java +++ b/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/server/IntegrityRemotingServer.java @@ -79,7 +79,7 @@ public class IntegrityRemotingServer { * @param aHostIP * the host IP to listen on * @param aPort - * the port to listen on + * the port to listen on (0 = auto-choose a free port) * @param aListener * the listener * @param aClassLoader @@ -95,9 +95,9 @@ public IntegrityRemotingServer(String aHostIP, int aPort, IntegrityRemotingServe throw new IllegalArgumentException("A listener must be provided."); } listener = aListener; - port = aPort; isFork = anIsForkFlag; serverEndpoint = new ServerEndpoint(aHostIP, aPort, createProcessors(), aClassLoader, anIsForkFlag); + port = serverEndpoint.getPort(); } /** diff --git a/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/transport/ServerEndpoint.java b/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/transport/ServerEndpoint.java index f0ebe8d06..2a066fd1e 100644 --- a/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/transport/ServerEndpoint.java +++ b/de.gebit.integrity.remoting/src/de/gebit/integrity/remoting/transport/ServerEndpoint.java @@ -65,7 +65,7 @@ public class ServerEndpoint { * @param aHostIP * the host IP to listen on * @param aPort - * the port to bind to + * the port to bind to (0 = auto-choose a free port) * @param aProcessorMap * the map of processors to use for processing incoming messages * @param aClassLoader @@ -77,8 +77,7 @@ public class ServerEndpoint { */ public ServerEndpoint(String aHostIP, int aPort, Map, MessageProcessor> aProcessorMap, ClassLoader aClassLoader, - boolean anIsForkFlag) - throws UnknownHostException, IOException { + boolean anIsForkFlag) throws UnknownHostException, IOException { messageProcessors = aProcessorMap; serverSocket = new ServerSocket(aPort, 0, Inet4Address.getByName(aHostIP)); connectionWaiter = new ConnectionWaiter(aClassLoader); @@ -95,6 +94,15 @@ public boolean isActive() { return serverSocket.isBound() && !serverSocket.isClosed(); } + /** + * Returns the port number on which the server endpoint is listening. + * + * @return + */ + public int getPort() { + return serverSocket.getLocalPort(); + } + /** * Closes the server endpoint and all endpoints currently active. * @@ -122,7 +130,7 @@ public void closeAll(boolean anEmptyOutputQueueFlag) { if (shutdownHookThread != null) { try { - Runtime.getRuntime().removeShutdownHook(shutdownHookThread); + Runtime.getRuntime().removeShutdownHook(shutdownHookThread); } catch (IllegalStateException exc) { // ignored - may be thrown if this code is run during VM shutdown, but we don't care about that } diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/DefaultTestRunner.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/DefaultTestRunner.java index 522a3f384..6219753ae 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/DefaultTestRunner.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/DefaultTestRunner.java @@ -591,26 +591,58 @@ public void initialize(TestModel aModel, Map someParameterizedCo parameterizedConstantValues = someParameterizedConstants; commandLineArguments = someCommandLineArguments; + + performRemotingBinding(aRemotingPort, aRemotingBindHost); + + waitForSetListInjection(); + } + + /** + * Performs the remoting port binding. + * + * @param aRemotingPort + * the port to bind to, or null if no remoting is desired + * @param aRemotingBindHost + * the host/IP to bind to + * @throws IOException + */ + protected void performRemotingBinding(Integer aRemotingPort, String aRemotingBindHost) throws IOException { Integer tempRemotingPort = aRemotingPort; - String tempRemotingBindHost = aRemotingBindHost; - if (isFork()) { + String tempRemotingHost = aRemotingBindHost; + if (isFork() && System.getProperty(Forker.SYSPARAM_FORK_REMOTING_HOST) != null) { + tempRemotingHost = System.getProperty(Forker.SYSPARAM_FORK_REMOTING_HOST); + } + if (isFork() && System.getProperty(Forker.SYSPARAM_FORK_REMOTING_PORT) != null) { tempRemotingPort = Integer.parseInt(System.getProperty(Forker.SYSPARAM_FORK_REMOTING_PORT)); - tempRemotingBindHost = System.getProperty(Forker.SYSPARAM_FORK_REMOTING_HOST, aRemotingBindHost); } + if (tempRemotingPort != null) { remotingListener = new RemotingListener(); try { - remotingServer = new IntegrityRemotingServer(tempRemotingBindHost, tempRemotingPort, remotingListener, + remotingServer = new IntegrityRemotingServer(tempRemotingHost, tempRemotingPort, remotingListener, javaClassLoader, isFork()); } catch (BindException exc) { - System.err.println("FAILED TO BIND REMOTING SERVER TO " + aRemotingBindHost + ":" + aRemotingPort); + System.err.println("FAILED TO BIND REMOTING SERVER TO " + tempRemotingHost + ":" + tempRemotingPort); throw exc; } + + if (tempRemotingPort == 0) { + // This message is important! It is being parsed by the master to find out the auto-generated fork port + // (see de.gebit.integrity.runner.forking.Fork.FORK_HOST_AND_PORT_PATTERN). + System.out + .println("Integrity Test Runner bound to " + tempRemotingHost + ":" + remotingServer.getPort()); + } } + } + /** + * If this is a fork, we now need to wait for the setlist and test scripts to be injected by the master! Otherwise + * this method returns immediately. + * + * @throws IOException + */ + protected void waitForSetListInjection() throws IOException { if (isFork()) { - // If this is a fork, we now need to wait for the setlist and test scripts to be injected by - // the master! long tempTimeout = System.nanoTime() + TimeUnit.SECONDS.toNanos(getForkConnectionTimeout()); synchronized (setListWaiter) { diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/console/ConsoleTestExecutor.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/console/ConsoleTestExecutor.java index cae7e8b11..2ac410f26 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/console/ConsoleTestExecutor.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/console/ConsoleTestExecutor.java @@ -200,7 +200,7 @@ public int run(String[] someArgs) { "variant", "Specify the variant to execute (must be defined in the scripts!)", "[{-v,--variant}]"); SimpleCommandLineParser.BooleanOption tempNoremoteOption = new SimpleCommandLineParser.BooleanOption(null, "noremote", "Disables remoting", "[{--noremote}]"); - SimpleCommandLineParser.IntegerOption tempRemoteportOption = new SimpleCommandLineParser.IntegerOption("r", + SimpleCommandLineParser.IntegerOption tempRemotePortOption = new SimpleCommandLineParser.IntegerOption("r", "remoteport", "Set the port number to bind to for remoting (default is " + IntegrityRemotingConstants.DEFAULT_PORT + ")", "[{-r,--remoteport} port]"); @@ -222,7 +222,7 @@ public int run(String[] someArgs) { null, "noconsole", "Do not capture stdout & stderr for test XML/HTML output", "[{--noconsole}]"); tempParser.addOptions(tempConsoleOption, tempXmlOption, tempXsltOption, tempNameOption, tempVariantOption, - tempNoremoteOption, tempRemoteportOption, tempRemoteHostOption, tempWaitForPlayOption, + tempNoremoteOption, tempRemotePortOption, tempRemoteHostOption, tempWaitForPlayOption, tempSkipModelCheck, tempParameterizedConstantOption, tempSeedOption, tempExcludeConsoleStreamsOption); if (someArgs.length == 0) { @@ -301,7 +301,7 @@ public int run(String[] someArgs) { Integer tempRemotePort = null; String tempRemoteHost = null; if (!tempNoremoteOption.isSet()) { - tempRemotePort = tempRemoteportOption.getValue(IntegrityRemotingConstants.DEFAULT_PORT); + tempRemotePort = tempRemotePortOption.getValue(IntegrityRemotingConstants.DEFAULT_PORT); tempRemoteHost = tempRemoteHostOption.getValue("0.0.0.0"); } diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/DefaultForker.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/DefaultForker.java index 9b7f1b67d..41def5edc 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/DefaultForker.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/DefaultForker.java @@ -11,11 +11,6 @@ import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; -import java.net.DatagramSocket; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; @@ -38,10 +33,8 @@ public class DefaultForker implements Forker { @Override public ForkedProcess fork(String[] someCommandLineArguments, String aForkName, long aRandomSeed) throws ForkException { - int tempPortNumber = getPortToUse(); - - List tempArgs = createArgumentList(someCommandLineArguments, tempPortNumber, aForkName, aRandomSeed); - return createProcess(tempArgs, tempPortNumber); + List tempArgs = createArgumentList(someCommandLineArguments, aForkName, aRandomSeed); + return createProcess(tempArgs); } /** @@ -49,23 +42,20 @@ public ForkedProcess fork(String[] someCommandLineArguments, String aForkName, l * * @param someCommandLineArguments * the command line arguments provided to {@link #fork(String[], int, String)}. - * @param aPortNumber - * the port number to use by the fork * @param aForkName * the name for the new fork * @param aRandomSeed * the seed for the RNG of the fork * @return the argument list */ - protected List createArgumentList(String[] someCommandLineArguments, int aPortNumber, String aForkName, - long aRandomSeed) { + protected List createArgumentList(String[] someCommandLineArguments, String aForkName, long aRandomSeed) { List tempArgs = new ArrayList(); addJavaExecutable(tempArgs); addClassPath(tempArgs); - addForkInformation(tempArgs, aPortNumber, aForkName, aRandomSeed); + addForkInformation(tempArgs, aForkName, aRandomSeed); addJVMArguments(tempArgs); @@ -107,24 +97,41 @@ protected void addClassPath(List anArgumentList) { * * @param anArgumentList * the argument list to extend - * @param aPortNumber - * the port number for the fork * @param aForkName * the name for the fork * @param aRandomSeed * the seed for the RNG of the fork */ - protected void addForkInformation(List anArgumentList, int aPortNumber, String aForkName, - long aRandomSeed) { - String tempHostInterface = getHostInterfaceToUse(); - if (tempHostInterface != null) { - anArgumentList.add("-D" + Forker.SYSPARAM_FORK_REMOTING_HOST + "=" + tempHostInterface); + protected void addForkInformation(List anArgumentList, String aForkName, long aRandomSeed) { + if (getForkHost() != null) { + anArgumentList.add("-D" + Forker.SYSPARAM_FORK_REMOTING_HOST + "=" + getForkHost()); + } + if (getForkPort() != null) { + anArgumentList.add("-D" + Forker.SYSPARAM_FORK_REMOTING_PORT + "=" + getForkPort()); } - anArgumentList.add("-D" + Forker.SYSPARAM_FORK_REMOTING_PORT + "=" + aPortNumber); + anArgumentList.add("-D" + Forker.SYSPARAM_FORK_NAME + "=" + aForkName); anArgumentList.add("-D" + Forker.SYSPARAM_FORK_SEED + "=" + aRandomSeed); } + /** + * The host name / IP to which the fork should be bound. + * + * @return + */ + protected String getForkHost() { + return "localhost"; + } + + /** + * The port to which the fork should be bound. + * + * @return + */ + protected Integer getForkPort() { + return 0; + } + /** * Adds any random JVM configuration arguments to the argument list. Default implementation asks the * {@link RuntimeMXBean}. @@ -177,10 +184,10 @@ protected void addApplicationArguments(List anArgumentList, String[] som * @return the forked process * @throws ForkException */ - protected LocalForkedProcess createProcess(List anArgumentList, int aPortNumber) throws ForkException { + protected LocalForkedProcess createProcess(List anArgumentList) throws ForkException { ProcessBuilder tempBuilder = new ProcessBuilder(anArgumentList); try { - return new LocalForkedProcess(tempBuilder.start(), aPortNumber); + return new LocalForkedProcess(tempBuilder.start()); } catch (IOException exc) { throw new ForkException("Error forking process", exc); } @@ -202,79 +209,4 @@ protected String guessMainClassName() { "Could not determine main class name. You probably need to implement your own Forker in order to use forking!"); } - /** - * The maximum possible port number. - */ - private static final int MAX_PORT_NUMBER = 65535; - - /** - * The minimum possible port number. - */ - private static final int MIN_PORT_NUMBER = 1024; - - /** - * This determines the port to use by the fork to communicate with the master. The fork must be able to bind to this - * port. The default implementation finds a free port on the machine by randomly checking ports above - * {@link #MIN_PORT_NUMBER}. - * - * @return the port to use - */ - protected int getPortToUse() { - int tempPort = 0; - do { - tempPort = (int) Math.floor(Math.random() * (double) (MAX_PORT_NUMBER - MIN_PORT_NUMBER)) + MIN_PORT_NUMBER; - } while (!isPortAvailable(tempPort)); - return tempPort; - } - - /** - * Determines the host interface to bind the fork to. - * - * @return the host interface name (IP or hostname) - */ - protected String getHostInterfaceToUse() { - return "localhost"; - } - - /** - * Checks whether a given port is available on the local machine. - * - * @param aPort - * the port to check - * @return true if the port is available, false if not - */ - protected boolean isPortAvailable(int aPort) { - InetAddress tempInterface; - try { - tempInterface = Inet4Address.getByName("localhost"); - } catch (UnknownHostException exc1) { - // This is almost impossible to occur! - throw new RuntimeException(exc1); - } - ServerSocket tempServerSocket = null; - DatagramSocket tempDatagramSocket = null; - try { - tempServerSocket = new ServerSocket(aPort, 1, tempInterface); - tempServerSocket.setReuseAddress(true); - tempDatagramSocket = new DatagramSocket(aPort, tempInterface); - tempDatagramSocket.setReuseAddress(true); - return true; - } catch (IOException exc) { - // nothing to do - } finally { - if (tempDatagramSocket != null) { - tempDatagramSocket.close(); - } - - if (tempServerSocket != null) { - try { - tempServerSocket.close(); - } catch (IOException exc) { - // ignore - } - } - } - - return false; - } } diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Fork.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Fork.java index 7e1c2dc71..b41c3800b 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Fork.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Fork.java @@ -19,6 +19,8 @@ import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.google.inject.Inject; import com.google.inject.Provider; @@ -78,6 +80,16 @@ public class Fork { */ private ForkedProcess process; + /** + * The {@link FilteringStreamCopier} for STDOUT, if the forked process supplies it. + */ + private FilteringStreamCopier stdoutCopier; + + /** + * The {@link FilteringStreamCopier} for STDERR, if the forked process supplies it. + */ + private FilteringStreamCopier stderrCopier; + /** * A flag remembering whether the fork has been started once. */ @@ -227,6 +239,12 @@ public class Fork { */ private static final int FORK_TIMESYNC_TIMEOUT_DEFAULT = 30; + /** + * The pattern to use for filtering. + */ + private static final Pattern FORK_HOST_AND_PORT_PATTERN = Pattern + .compile("Integrity Test Runner bound to ([^ :]+):(\\d+)"); + /** * The fork timesync timeout, in seconds. */ @@ -297,13 +315,15 @@ public void start() throws ForkException { InputStream tempStdOut = process.getInputStream(); if (tempStdOut != null) { - new StreamCopier("\tFORK '" + definition.getName() + "': ", - "Integrity - stdout copy: " + definition.getName(), tempStdOut, false).start(); + stdoutCopier = new FilteringStreamCopier("\tFORK '" + definition.getName() + "': ", + "Integrity - stdout copy: " + definition.getName(), tempStdOut, false, true); + stdoutCopier.start(); } InputStream tempStdErr = process.getErrorStream(); if (tempStdErr != null) { - new StreamCopier("\tFORK '" + definition.getName() + "': ", - "Integrity - stderr copy: " + definition.getName(), tempStdErr, true).start(); + stderrCopier = new FilteringStreamCopier("\tFORK '" + definition.getName() + "': ", + "Integrity - stderr copy: " + definition.getName(), tempStdErr, true, false); + stderrCopier.start(); } } @@ -379,13 +399,29 @@ public boolean isConnected() { * the timeout after which the method shall return in milliseconds * @param aClassLoader * the classloader to use when deserializing objects - * @return true if successful, false if the timeout was hit + * @return true if successful, false if the timeout was hit or if the process cannot be connected to yet (in this + * case this method will return immediately) * @throws IOException */ public boolean connect(long aTimeout, ClassLoader aClassLoader) throws IOException { + // If the forked process is able to supply its STDOUT stream, it will be able to communicate its host and port + // via the stream. If not, the forked process is mandated to implement getHost() and getPort() and supply the + // host and port this way. + String tempHost = (stdoutCopier != null ? stdoutCopier.getForkHostName() : getProcess().getHost()); + if (tempHost == null) { + return false; + } + if ("0.0.0.0".equals(tempHost)) { + // The host communicated via STDOUT is a wildcard address. We cannot use that to connect to the host, so + // fall back to getting the host from the process, which has to be able to supply a host name in this case + // according to the interface documentation. + tempHost = getProcess().getHost(); + } + int tempPort = (stdoutCopier != null ? stdoutCopier.getForkPort() : getProcess().getPort()); + synchronized (this) { - IntegrityRemotingClient tempClient = new IntegrityRemotingClient(getProcess().getHost(), - getProcess().getPort(), new ForkRemotingClientListener(), aClassLoader); + IntegrityRemotingClient tempClient = new IntegrityRemotingClient(tempHost, tempPort, + new ForkRemotingClientListener(), aClassLoader); try { wait(aTimeout); @@ -723,7 +759,7 @@ public void onAbortExecution(String anAbortExecutionMessage, String anAbortExecu } } - private class StreamCopier extends Thread { + private class FilteringStreamCopier extends Thread { /** * The prefix to add in front of each line. @@ -740,11 +776,28 @@ private class StreamCopier extends Thread { */ private boolean stdErr; - StreamCopier(String aPrefix, String aThreadName, InputStream aSource, boolean anStdErrFlag) { + /** + * Whether the stream is to be scanned for host/port that the fork binds to. + */ + private boolean isFilteringForHostAndPort; + + /** + * The forks' host name, as filtered from the stream. + */ + private String forkHostName; + + /** + * The forks' port, as filtered from the stream. + */ + private Integer forkPort; + + FilteringStreamCopier(String aPrefix, String aThreadName, InputStream aSource, boolean anStdErrFlag, + boolean aFilterForHostAndPortFlag) { super(aThreadName); prefix = aPrefix; source = new BufferedReader(new InputStreamReader(aSource)); stdErr = anStdErrFlag; + isFilteringForHostAndPort = aFilterForHostAndPortFlag; } private void println(String aLine) { @@ -770,12 +823,28 @@ public void run() { if (tempLine == null) { break; } else { + if (isFilteringForHostAndPort) { + Matcher tempMatcher = FORK_HOST_AND_PORT_PATTERN.matcher(tempLine); + if (tempMatcher.matches()) { + forkPort = Integer.parseInt(tempMatcher.group(2)); + forkHostName = tempMatcher.group(1); + isFilteringForHostAndPort = false; + } + } println(prefix + tempLine); } } while (true); println(prefix + "Process terminated!"); } + + public String getForkHostName() { + return forkHostName; + } + + public Integer getForkPort() { + return forkPort; + } } private class ForkMonitor extends Thread { diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/ForkedProcess.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/ForkedProcess.java index 8f7ec3cd8..d7d17a77f 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/ForkedProcess.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/ForkedProcess.java @@ -60,15 +60,23 @@ public interface ForkedProcess { /** * Returns the network host name where this process is running. In case of local processes, "localhost" shall be * returned, but distributed processes on other machines might of course return different values. This is a - * mandatory method. + * mandatory method if the forks' STDOUT stream is not available (through which host and port would otherwise be + * communicated to the master) OR if the host communicated via STDOUT is a wildcard address, otherwise it is + * optional and will not be called. It may return "null" as long as the fork has not yet brought up its remoting + * network binding - in that case, the connection attempts are paused until remoting is available. * - * @return the network host name where the process is running + * @return the network host name where the process is running, or null if the process is not yet available for + * connection */ String getHost(); /** * Returns the port where this processes' Integrity Test Runner is open for management connections. This is a - * mandatory method. + * mandatory method if the forks' STDOUT stream is not available (through which host and port would otherwise be + * communicated to the master), otherwise it is optional and will not be called. If {@link #getHost()} returns a + * non-null value, this method must return a meaningful value as well (it will also not be called before + * {@link #getHost()} was called and returned a non-null value, so it doesn't matter what it returns before that + * point in time, or if it is even callable). * * @return the remoting port of the test runner */ diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Forker.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Forker.java index ae269b720..56e151ca7 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Forker.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/Forker.java @@ -24,7 +24,8 @@ public interface Forker { String SYSPARAM_FORK_REMOTING_HOST = "integrity.fork.host"; /** - * System parameter name for the remoting port to use to communicate with the fork. + * System parameter name for the remoting port to use to communicate with the fork. If this is set, it overrides the + * setting that is provided to the test runner for general remoting. */ String SYSPARAM_FORK_REMOTING_PORT = "integrity.fork.port"; diff --git a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/LocalForkedProcess.java b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/LocalForkedProcess.java index 5c298058b..a2d2a1f54 100644 --- a/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/LocalForkedProcess.java +++ b/de.gebit.integrity.runner/src/de/gebit/integrity/runner/forking/LocalForkedProcess.java @@ -22,20 +22,14 @@ public class LocalForkedProcess implements ForkedProcess { */ protected Process process; - /** - * The port at which the fork listens for remoting connections. - */ - private Integer port; - /** * Creates a new instance. * * @param aProcess * the process to wrap */ - public LocalForkedProcess(Process aProcess, Integer aPort) { + public LocalForkedProcess(Process aProcess) { process = aProcess; - port = aPort; } @Override @@ -66,12 +60,12 @@ public InputStream getErrorStream() { @Override public String getHost() { - return "localhost"; + throw new UnsupportedOperationException("Not supported; host/port is communicated via STDOUT."); } @Override public int getPort() { - return port; + throw new UnsupportedOperationException("Not supported; host/port is communicated via STDOUT."); } }