Minimise our usage of SocketUtils.findAvailableTcpPort

Closes gh-9382
This commit is contained in:
Andy Wilkinson
2017-06-08 10:37:40 +01:00
parent 10868519e1
commit f7e9ec5f42
26 changed files with 442 additions and 286 deletions

View File

@@ -112,13 +112,15 @@ public class LiveReloadServer {
/**
* Start the livereload server and accept incoming connections.
* @return the port on which the server is listening
* @throws IOException in case of I/O errors
*/
public void start() throws IOException {
public int start() throws IOException {
synchronized (this.monitor) {
Assert.state(!isStarted(), "Server already started");
logger.debug("Starting live reload server on port " + this.port);
this.serverSocket = new ServerSocket(this.port);
int localPort = this.serverSocket.getLocalPort();
this.listenThread = this.threadFactory.newThread(new Runnable() {
@Override
@@ -130,6 +132,7 @@ public class LiveReloadServer {
this.listenThread.setDaemon(true);
this.listenThread.setName("Live Reload Server");
this.listenThread.start();
return localPort;
}
}

View File

@@ -57,7 +57,7 @@ public class TunnelClient implements SmartInitializingSingleton {
private ServerThread serverThread;
public TunnelClient(int listenPort, TunnelConnection tunnelConnection) {
Assert.isTrue(listenPort > 0, "ListenPort must be positive");
Assert.isTrue(listenPort >= 0, "ListenPort must be greater than or equal to 0");
Assert.notNull(tunnelConnection, "TunnelConnection must not be null");
this.listenPort = listenPort;
this.tunnelConnection = tunnelConnection;
@@ -78,18 +78,20 @@ public class TunnelClient implements SmartInitializingSingleton {
}
/**
* Start the client and accept incoming connections on the port.
* Start the client and accept incoming connections.
* @return the port on which the client is listening
* @throws IOException in case of I/O errors
*/
public void start() throws IOException {
public int start() throws IOException {
synchronized (this.monitor) {
Assert.state(this.serverThread == null, "Server already started");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort));
logger.trace(
"Listening for TCP traffic to tunnel on port " + this.listenPort);
int port = serverSocketChannel.socket().getLocalPort();
logger.trace("Listening for TCP traffic to tunnel on port " + port);
this.serverThread = new ServerThread(serverSocketChannel);
this.serverThread.start();
return port;
}
}
@@ -100,7 +102,6 @@ public class TunnelClient implements SmartInitializingSingleton {
public void stop() throws IOException {
synchronized (this.monitor) {
if (this.serverThread != null) {
logger.trace("Closing tunnel client on port " + this.listenPort);
this.serverThread.close();
try {
this.serverThread.join(2000);
@@ -143,6 +144,8 @@ public class TunnelClient implements SmartInitializingSingleton {
}
public void close() throws IOException {
logger.trace("Closing tunnel client on port "
+ this.serverSocketChannel.socket().getLocalPort());
this.serverSocketChannel.close();
this.acceptConnections = false;
interrupt();

View File

@@ -53,7 +53,6 @@ import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -77,8 +76,6 @@ public class LocalDevToolsAutoConfigurationTests {
@Rule
public MockRestarter mockRestarter = new MockRestarter();
private int liveReloadPort = SocketUtils.findAvailableTcpPort();
private ConfigurableApplicationContext context;
@After
@@ -270,7 +267,7 @@ public class LocalDevToolsAutoConfigurationTests {
Map<String, Object> specifiedProperties) {
Map<String, Object> properties = new HashMap<>();
properties.put("spring.thymeleaf.check-template-location", false);
properties.put("spring.devtools.livereload.port", this.liveReloadPort);
properties.put("spring.devtools.livereload.port", 0);
properties.put("server.port", 0);
properties.putAll(specifiedProperties);
return properties;

View File

@@ -16,13 +16,14 @@
package org.springframework.boot.devtools.integrationtest;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.devtools.integrationtest.HttpTunnelIntegrationTests.TunnelConfiguration.TestTunnelClient;
import org.springframework.boot.devtools.remote.server.AccessManager;
import org.springframework.boot.devtools.remote.server.Dispatcher;
import org.springframework.boot.devtools.remote.server.DispatcherFilter;
@@ -33,22 +34,19 @@ import org.springframework.boot.devtools.tunnel.client.TunnelClient;
import org.springframework.boot.devtools.tunnel.client.TunnelConnection;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServer;
import org.springframework.boot.devtools.tunnel.server.HttpTunnelServerHandler;
import org.springframework.boot.devtools.tunnel.server.PortProvider;
import org.springframework.boot.devtools.tunnel.server.SocketTargetServerConnection;
import org.springframework.boot.devtools.tunnel.server.StaticPortProvider;
import org.springframework.boot.devtools.tunnel.server.TargetServerConnection;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.SocketUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.DispatcherServlet;
@@ -61,62 +59,48 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Phillip Webb
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HttpTunnelIntegrationTests {
@Autowired
private Config config;
@Test
public void httpServerDirect() throws Exception {
String url = "http://localhost:" + this.config.httpServerPort + "/hello";
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.register(ServerConfiguration.class);
context.refresh();
String url = "http://localhost:" + context.getWebServer().getPort() + "/hello";
ResponseEntity<String> entity = new TestRestTemplate().getForEntity(url,
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("Hello World");
context.close();
}
@Test
public void viaTunnel() throws Exception {
String url = "http://localhost:" + this.config.clientPort + "/hello";
AnnotationConfigServletWebServerApplicationContext serverContext = new AnnotationConfigServletWebServerApplicationContext();
serverContext.register(ServerConfiguration.class);
serverContext.refresh();
AnnotationConfigApplicationContext tunnelContext = new AnnotationConfigApplicationContext();
TestPropertyValues.of("server.port:" + serverContext.getWebServer().getPort())
.applyTo(tunnelContext);
tunnelContext.register(TunnelConfiguration.class);
tunnelContext.refresh();
String url = "http://localhost:"
+ tunnelContext.getBean(TestTunnelClient.class).port + "/hello";
ResponseEntity<String> entity = new TestRestTemplate().getForEntity(url,
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("Hello World");
serverContext.close();
tunnelContext.close();
}
@Configuration
@EnableWebMvc
static class Config {
private int clientPort = SocketUtils.findAvailableTcpPort();
private int httpServerPort = SocketUtils.findAvailableTcpPort();
static class ServerConfiguration {
@Bean
public ServletWebServerFactory container() {
return new TomcatServletWebServerFactory(this.httpServerPort);
}
@Bean
public DispatcherFilter filter() {
PortProvider port = new StaticPortProvider(this.httpServerPort);
TargetServerConnection connection = new SocketTargetServerConnection(port);
HttpTunnelServer server = new HttpTunnelServer(connection);
HandlerMapper mapper = new UrlHandlerMapper("/httptunnel",
new HttpTunnelServerHandler(server));
Collection<HandlerMapper> mappers = Collections.singleton(mapper);
Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers);
return new DispatcherFilter(dispatcher);
}
@Bean
public TunnelClient tunnelClient() {
String url = "http://localhost:" + this.httpServerPort + "/httptunnel";
TunnelConnection connection = new HttpTunnelConnection(url,
new SimpleClientHttpRequestFactory());
return new TunnelClient(this.clientPort, connection);
return new TomcatServletWebServerFactory(0);
}
@Bean
@@ -129,6 +113,47 @@ public class HttpTunnelIntegrationTests {
return new MyController();
}
@Bean
public DispatcherFilter filter(
AnnotationConfigServletWebServerApplicationContext context) {
TargetServerConnection connection = new SocketTargetServerConnection(
() -> context.getWebServer().getPort());
HttpTunnelServer server = new HttpTunnelServer(connection);
HandlerMapper mapper = new UrlHandlerMapper("/httptunnel",
new HttpTunnelServerHandler(server));
Collection<HandlerMapper> mappers = Collections.singleton(mapper);
Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers);
return new DispatcherFilter(dispatcher);
}
}
static class TunnelConfiguration {
@Bean
public TunnelClient tunnelClient(@Value("${server.port}") int serverPort) {
String url = "http://localhost:" + serverPort + "/httptunnel";
TunnelConnection connection = new HttpTunnelConnection(url,
new SimpleClientHttpRequestFactory());
return new TestTunnelClient(0, connection);
}
static class TestTunnelClient extends TunnelClient {
private int port;
TestTunnelClient(int listenPort, TunnelConnection tunnelConnection) {
super(listenPort, tunnelConnection);
}
@Override
public int start() throws IOException {
this.port = super.start();
return this.port;
}
}
}
@RestController

View File

@@ -31,7 +31,6 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.util.SocketUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PingMessage;
@@ -56,14 +55,14 @@ public class LiveReloadServerTests {
private static final String HANDSHAKE = "{command: 'hello', "
+ "protocols: ['http://livereload.com/protocols/official-7']}";
private int port = SocketUtils.findAvailableTcpPort();
private int port;
private MonitoredLiveReloadServer server;
@Before
public void setUp() throws Exception {
this.server = new MonitoredLiveReloadServer(this.port);
this.server.start();
this.server = new MonitoredLiveReloadServer(0);
this.port = this.server.start();
}
@After

View File

@@ -26,7 +26,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@@ -39,13 +38,11 @@ import static org.mockito.Mockito.mock;
*/
public class LocalDebugPortAvailableConditionTests {
private int port = SocketUtils.findAvailableTcpPort();
private LocalDebugPortAvailableCondition condition = new LocalDebugPortAvailableCondition();
@Test
public void portAvailable() throws Exception {
ConditionOutcome outcome = getOutcome();
ConditionOutcome outcome = getOutcome(0);
assertThat(outcome.isMatch()).isTrue();
assertThat(outcome.getMessage())
.isEqualTo("Local Debug Port Condition found local debug port");
@@ -53,19 +50,19 @@ public class LocalDebugPortAvailableConditionTests {
@Test
public void portInUse() throws Exception {
final ServerSocket serverSocket = ServerSocketFactory.getDefault()
.createServerSocket(this.port);
ConditionOutcome outcome = getOutcome();
ServerSocket serverSocket = ServerSocketFactory.getDefault()
.createServerSocket(0);
ConditionOutcome outcome = getOutcome(serverSocket.getLocalPort());
serverSocket.close();
assertThat(outcome.isMatch()).isFalse();
assertThat(outcome.getMessage())
.isEqualTo("Local Debug Port Condition did not find local debug port");
}
private ConditionOutcome getOutcome() {
private ConditionOutcome getOutcome(int port) {
MockEnvironment environment = new MockEnvironment();
TestPropertyValues.of(
"spring.devtools.remote.debug.local-port:" + this.port).applyTo(environment);
TestPropertyValues.of("spring.devtools.remote.debug.local-port:" + port)
.applyTo(environment);
ConditionContext context = mock(ConditionContext.class);
given(context.getEnvironment()).willReturn(environment);
ConditionOutcome outcome = this.condition.getMatchOutcome(context, null);

View File

@@ -43,11 +43,11 @@ import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -73,13 +73,16 @@ public class RemoteClientConfigurationTests {
private AnnotationConfigServletWebServerApplicationContext context;
private static int remotePort = SocketUtils.findAvailableTcpPort();
private AnnotationConfigApplicationContext clientContext;
@After
public void cleanup() {
if (this.context != null) {
this.context.close();
}
if (this.clientContext != null) {
this.clientContext.close();
}
}
@Test
@@ -114,12 +117,12 @@ public class RemoteClientConfigurationTests {
configure();
Set<ChangedFiles> changeSet = new HashSet<>();
ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false);
this.context.publishEvent(event);
LiveReloadConfiguration configuration = this.context
this.clientContext.publishEvent(event);
LiveReloadConfiguration configuration = this.clientContext
.getBean(LiveReloadConfiguration.class);
configuration.getExecutor().shutdown();
configuration.getExecutor().awaitTermination(2, TimeUnit.SECONDS);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
LiveReloadServer server = this.clientContext.getBean(LiveReloadServer.class);
verify(server).triggerReload();
}
@@ -150,17 +153,24 @@ public class RemoteClientConfigurationTests {
private void configure(String remoteUrl, boolean setSecret, String... pairs) {
this.context = new AnnotationConfigServletWebServerApplicationContext();
new RestartScopeInitializer().initialize(this.context);
this.context.register(Config.class, RemoteClientConfiguration.class);
String remoteUrlProperty = "remoteUrl:" + remoteUrl + ":"
+ RemoteClientConfigurationTests.remotePort;
TestPropertyValues.of(remoteUrlProperty).applyTo(this.context);
TestPropertyValues.of(pairs).applyTo(this.context);
this.context.register(Config.class);
if (setSecret) {
TestPropertyValues.of(
"spring.devtools.remote.secret:secret").applyTo(this.context);
TestPropertyValues.of("spring.devtools.remote.secret:secret")
.applyTo(this.context);
}
this.context.refresh();
this.clientContext = new AnnotationConfigApplicationContext();
TestPropertyValues.of(pairs).applyTo(this.clientContext);
new RestartScopeInitializer().initialize(this.clientContext);
this.clientContext.register(ClientConfig.class, RemoteClientConfiguration.class);
if (setSecret) {
TestPropertyValues.of("spring.devtools.remote.secret:secret")
.applyTo(this.clientContext);
}
String remoteUrlProperty = "remoteUrl:" + remoteUrl + ":"
+ this.context.getWebServer().getPort();
TestPropertyValues.of(remoteUrlProperty).applyTo(this.clientContext);
this.clientContext.refresh();
}
@Configuration
@@ -168,12 +178,7 @@ public class RemoteClientConfigurationTests {
@Bean
public TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(remotePort);
}
@Bean
public LiveReloadServer liveReloadServer() {
return mock(LiveReloadServer.class);
return new TomcatServletWebServerFactory(0);
}
@Bean
@@ -191,4 +196,14 @@ public class RemoteClientConfigurationTests {
}
@Configuration
static class ClientConfig {
@Bean
public LiveReloadServer liveReloadServer() {
return mock(LiveReloadServer.class);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,7 +36,6 @@ import org.springframework.boot.devtools.test.MockClientHttpRequestFactory;
import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection.TunnelChannel;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.http.HttpStatus;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@@ -59,8 +58,6 @@ public class HttpTunnelConnectionTests {
@Rule
public OutputCapture outputCapture = new OutputCapture();
private int port = SocketUtils.findAvailableTcpPort();
private String url;
private ByteArrayOutputStream incomingData;
@@ -75,7 +72,7 @@ public class HttpTunnelConnectionTests {
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.url = "http://localhost:" + this.port;
this.url = "http://localhost:12345";
this.incomingData = new ByteArrayOutputStream();
this.incomingChannel = Channels.newChannel(this.incomingData);
}
@@ -164,8 +161,7 @@ public class HttpTunnelConnectionTests {
TunnelChannel tunnel = openTunnel(true);
assertThat(tunnel.isOpen()).isFalse();
this.outputCapture.expect(containsString(
"Failed to connect to remote application at http://localhost:"
+ this.port));
"Failed to connect to remote application at http://localhost:12345"));
}
private void write(TunnelChannel channel, String string) throws IOException {

View File

@@ -29,8 +29,6 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -46,15 +44,13 @@ public class TunnelClientTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private int listenPort = SocketUtils.findAvailableTcpPort();
private MockTunnelConnection tunnelConnection = new MockTunnelConnection();
@Test
public void listenPortMustBePositive() throws Exception {
public void listenPortMustNotBeNegative() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ListenPort must be positive");
new TunnelClient(0, this.tunnelConnection);
this.thrown.expectMessage("ListenPort must be greater than or equal to 0");
new TunnelClient(-5, this.tunnelConnection);
}
@Test
@@ -66,10 +62,9 @@ public class TunnelClientTests {
@Test
public void typicalTraffic() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
TunnelClient client = new TunnelClient(0, this.tunnelConnection);
int port = client.start();
SocketChannel channel = SocketChannel.open(new InetSocketAddress(port));
channel.write(ByteBuffer.wrap("hello".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(5);
channel.read(buffer);
@@ -80,10 +75,9 @@ public class TunnelClientTests {
@Test
public void socketChannelClosedTriggersTunnelClose() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
TunnelClient client = new TunnelClient(0, this.tunnelConnection);
int port = client.start();
SocketChannel channel = SocketChannel.open(new InetSocketAddress(port));
Thread.sleep(200);
channel.close();
client.getServerThread().stopAcceptingConnections();
@@ -94,10 +88,9 @@ public class TunnelClientTests {
@Test
public void stopTriggersTunnelClose() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
TunnelClient client = new TunnelClient(0, this.tunnelConnection);
int port = client.start();
SocketChannel channel = SocketChannel.open(new InetSocketAddress(port));
Thread.sleep(200);
client.stop();
assertThat(this.tunnelConnection.getOpenedTimes()).isEqualTo(1);
@@ -107,12 +100,11 @@ public class TunnelClientTests {
@Test
public void addListener() throws Exception {
TunnelClient client = new TunnelClient(this.listenPort, this.tunnelConnection);
TunnelClient client = new TunnelClient(0, this.tunnelConnection);
TunnelClientListener listener = mock(TunnelClientListener.class);
client.addListener(listener);
client.start();
SocketChannel channel = SocketChannel
.open(new InetSocketAddress(this.listenPort));
int port = client.start();
SocketChannel channel = SocketChannel.open(new InetSocketAddress(port));
Thread.sleep(200);
channel.close();
client.getServerThread().stopAcceptingConnections();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,8 +27,6 @@ import java.nio.channels.SocketChannel;
import org.junit.Before;
import org.junit.Test;
import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@@ -41,18 +39,14 @@ public class SocketTargetServerConnectionTests {
private static final int DEFAULT_TIMEOUT = 1000;
private int port;
private MockServer server;
private SocketTargetServerConnection connection;
@Before
public void setup() throws IOException {
this.port = SocketUtils.findAvailableTcpPort();
this.server = new MockServer(this.port);
StaticPortProvider portProvider = new StaticPortProvider(this.port);
this.connection = new SocketTargetServerConnection(portProvider);
this.server = new MockServer();
this.connection = new SocketTargetServerConnection(() -> this.server.getPort());
}
@Test
@@ -107,9 +101,13 @@ public class SocketTargetServerConnectionTests {
private ServerThread thread;
MockServer(int port) throws IOException {
MockServer() throws IOException {
this.serverSocket = ServerSocketChannel.open();
this.serverSocket.bind(new InetSocketAddress(port));
this.serverSocket.bind(new InetSocketAddress(0));
}
int getPort() {
return this.serverSocket.socket().getLocalPort();
}
public void delay(int delay) {