From bdf7663a9a9d76b6f1e192a5b73ba035d5283d68 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 1 Jun 2015 13:24:01 -0700 Subject: [PATCH] Add remote debug tunnel auto-configuration Provide auto-configuration for remote debugging over a HTTP tunnel. The RemoteClientConfiguration provides a server that the local IDE can connect to. When a client connects the remote connection is established using the HTTP tunnel. See gh-3087 --- ...RemoteDeveloperToolsAutoConfiguration.java | 33 ++++ .../RemoteDeveloperToolsProperties.java | 38 +++++ .../LocalDebugPortAvailableCondition.java | 58 +++++++ .../client/LoggingTunnelClientListener.java | 45 ++++++ .../client/RemoteClientConfiguration.java | 39 ++++- ...eDeveloperToolsAutoConfigurationTests.java | 51 +++++++ .../HttpTunnelIntegrationTest.java | 144 ++++++++++++++++++ ...LocalDebugPortAvailableConditionTests.java | 73 +++++++++ .../RemoteClientConfigurationTests.java | 15 +- 9 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableCondition.java create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LoggingTunnelClientListener.java create mode 100644 spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/integrationtest/HttpTunnelIntegrationTest.java create mode 100644 spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableConditionTests.java diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java index 51b35ffdad..dfb2d3952a 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java @@ -23,6 +23,7 @@ import javax.servlet.Filter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -39,6 +40,10 @@ import org.springframework.boot.developertools.restart.server.DefaultSourceFolde import org.springframework.boot.developertools.restart.server.HttpRestartServer; import org.springframework.boot.developertools.restart.server.HttpRestartServerHandler; import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter; +import org.springframework.boot.developertools.tunnel.server.HttpTunnelServer; +import org.springframework.boot.developertools.tunnel.server.HttpTunnelServerHandler; +import org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider; +import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.ServerHttpRequest; @@ -109,4 +114,32 @@ public class RemoteDeveloperToolsAutoConfiguration { } + /** + * Configuration for remote debug HTTP tunneling. + */ + @ConditionalOnProperty(prefix = "spring.developertools.remote.debug", name = "enabled", matchIfMissing = true) + static class RemoteDebugTunnelConfiguration { + + @Autowired + private DeveloperToolsProperties properties; + + @Bean + @ConditionalOnMissingBean(name = "remoteDebugHanderMapper") + public UrlHandlerMapper remoteDebugHanderMapper( + @Qualifier("remoteDebugHttpTunnelServer") HttpTunnelServer server) { + String url = this.properties.getRemote().getContextPath() + "/debug"; + logger.warn("Listening for remote debug traffic on " + url); + Handler handler = new HttpTunnelServerHandler(server); + return new UrlHandlerMapper(url, handler); + } + + @Bean + @ConditionalOnMissingBean(name = "remoteDebugHttpTunnelServer") + public HttpTunnelServer remoteDebugHttpTunnelServer() { + return new HttpTunnelServer(new SocketTargetServerConnection( + new RemoteDebugPortProvider())); + } + + } + } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java index accff6afd4..eadd0ba47e 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java @@ -35,6 +35,8 @@ public class RemoteDeveloperToolsProperties { private Restart restart = new Restart(); + private Debug debug = new Debug(); + public String getContextPath() { return this.contextPath; } @@ -47,6 +49,10 @@ public class RemoteDeveloperToolsProperties { return this.restart; } + public Debug getDebug() { + return this.debug; + } + public static class Restart { /** @@ -64,4 +70,36 @@ public class RemoteDeveloperToolsProperties { } + public static class Debug { + + public static final Integer DEFAULT_LOCAL_PORT = 8000; + + /** + * Enable remote debug support. + */ + private boolean enabled = true; + + /** + * Local remote debug server port. + */ + private int localPort = DEFAULT_LOCAL_PORT; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getLocalPort() { + return this.localPort; + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + } + } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableCondition.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableCondition.java new file mode 100644 index 0000000000..c57b681827 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableCondition.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2015 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. + * 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 org.springframework.boot.developertools.remote.client; + +import javax.net.ServerSocketFactory; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Condition used to check that the actual local port is available. + */ +class LocalDebugPortAvailableCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( + context.getEnvironment(), "spring.developertools.remote.debug."); + Integer port = resolver.getProperty("local-port", Integer.class); + if (port == null) { + port = RemoteDeveloperToolsProperties.Debug.DEFAULT_LOCAL_PORT; + } + if (isPortAvailable(port)) { + return ConditionOutcome.match("Local debug port availble"); + } + return ConditionOutcome.noMatch("Local debug port unavailble"); + } + + private boolean isPortAvailable(int port) { + try { + ServerSocketFactory.getDefault().createServerSocket(port).close(); + return true; + } + catch (Exception ex) { + return false; + } + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LoggingTunnelClientListener.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LoggingTunnelClientListener.java new file mode 100644 index 0000000000..c5500ce5d3 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/LoggingTunnelClientListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2015 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. + * 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 org.springframework.boot.developertools.remote.client; + +import java.nio.channels.SocketChannel; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.developertools.tunnel.client.TunnelClientListener; + +/** + * {@link TunnelClientListener} to log open/close events. + * + * @author Phillip Webb + */ +class LoggingTunnelClientListener implements TunnelClientListener { + + private static final Log logger = LogFactory + .getLog(LoggingTunnelClientListener.class); + + @Override + public void onOpen(SocketChannel socket) { + logger.info("Remote debug connection opened"); + } + + @Override + public void onClose(SocketChannel socket) { + logger.info("Remote debug connection closed"); + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java index 10f8d15d28..81a38cf744 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java @@ -21,11 +21,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.PostConstruct; +import javax.servlet.Filter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -40,7 +42,11 @@ import org.springframework.boot.developertools.livereload.LiveReloadServer; import org.springframework.boot.developertools.restart.DefaultRestartInitializer; import org.springframework.boot.developertools.restart.RestartScope; import org.springframework.boot.developertools.restart.Restarter; +import org.springframework.boot.developertools.tunnel.client.HttpTunnelConnection; +import org.springframework.boot.developertools.tunnel.client.TunnelClient; +import org.springframework.boot.developertools.tunnel.client.TunnelConnection; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; @@ -79,8 +85,9 @@ public class RemoteClientConfiguration { @PostConstruct private void logWarnings() { RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote(); - if (!remoteProperties.getRestart().isEnabled()) { - logger.warn("Remote restart is not enabled."); + if (!remoteProperties.getDebug().isEnabled() + && !remoteProperties.getRestart().isEnabled()) { + logger.warn("Remote restart and debug are both disabled."); } } @@ -168,4 +175,32 @@ public class RemoteClientConfiguration { } + /** + * Client configuration for remote debug HTTP tunneling. + */ + @ConditionalOnProperty(prefix = "spring.developertools.remote.debug", name = "enabled", matchIfMissing = true) + @ConditionalOnClass(Filter.class) + @Conditional(LocalDebugPortAvailableCondition.class) + static class RemoteDebugTunnelClientConfiguration { + + @Autowired + private DeveloperToolsProperties properties; + + @Value("${remoteUrl}") + private String remoteUrl; + + @Bean + public TunnelClient remoteDebugTunnelClient( + ClientHttpRequestFactory requestFactory) { + RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote(); + String url = this.remoteUrl + remoteProperties.getContextPath() + "/debug"; + TunnelConnection connection = new HttpTunnelConnection(url, requestFactory); + int localPort = remoteProperties.getDebug().getLocalPort(); + TunnelClient client = new TunnelClient(localPort, connection); + client.addListener(new LoggingTunnelClientListener()); + return client; + } + + } + } diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java index 4c58459345..a56b797a09 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java @@ -30,6 +30,10 @@ import org.springframework.boot.developertools.remote.server.DispatcherFilter; import org.springframework.boot.developertools.restart.MockRestarter; import org.springframework.boot.developertools.restart.server.HttpRestartServer; import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter; +import org.springframework.boot.developertools.tunnel.server.HttpTunnelServer; +import org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider; +import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection; +import org.springframework.boot.developertools.tunnel.server.TargetServerConnection; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -110,6 +114,23 @@ public class RemoteDeveloperToolsAutoConfigurationTests { this.context.getBean("remoteRestartHanderMapper"); } + @Test + public void invokeTunnelWithDefaultSetup() throws Exception { + loadContext("spring.developertools.remote.enabled:true"); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug"); + filter.doFilter(this.request, this.response, this.chain); + assertTunnelInvoked(true); + } + + @Test + public void disableRemoteDebug() throws Exception { + loadContext("spring.developertools.remote.enabled:true", + "spring.developertools.remote.debug.enabled:false"); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean("remoteDebugHanderMapper"); + } + @Test public void developerToolsHealthReturns200() throws Exception { loadContext("spring.developertools.remote.enabled:true"); @@ -120,6 +141,11 @@ public class RemoteDeveloperToolsAutoConfigurationTests { assertThat(this.response.getStatus(), equalTo(200)); } + private void assertTunnelInvoked(boolean value) { + assertThat(this.context.getBean(MockHttpTunnelServer.class).invoked, + equalTo(value)); + } + private void assertRestartInvoked(boolean value) { assertThat(this.context.getBean(MockHttpRestartServer.class).invoked, equalTo(value)); @@ -138,6 +164,12 @@ public class RemoteDeveloperToolsAutoConfigurationTests { @Import(RemoteDeveloperToolsAutoConfiguration.class) static class Config { + @Bean + public HttpTunnelServer remoteDebugHttpTunnelServer() { + return new MockHttpTunnelServer(new SocketTargetServerConnection( + new RemoteDebugPortProvider())); + } + @Bean public HttpRestartServer remoteRestartHttpRestartServer() { SourceFolderUrlFilter sourceFolderUrlFilter = mock(SourceFolderUrlFilter.class); @@ -146,6 +178,25 @@ public class RemoteDeveloperToolsAutoConfigurationTests { } + /** + * Mock {@link HttpTunnelServer} implementation. + */ + static class MockHttpTunnelServer extends HttpTunnelServer { + + private boolean invoked; + + public MockHttpTunnelServer(TargetServerConnection serverConnection) { + super(serverConnection); + } + + @Override + public void handle(ServerHttpRequest request, ServerHttpResponse response) + throws IOException { + this.invoked = true; + } + + } + /** * Mock {@link HttpRestartServer} implementation. */ diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/integrationtest/HttpTunnelIntegrationTest.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/integrationtest/HttpTunnelIntegrationTest.java new file mode 100644 index 0000000000..653b2eff54 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/integrationtest/HttpTunnelIntegrationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-2015 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. + * 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 org.springframework.boot.developertools.integrationtest; + +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.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.boot.developertools.remote.server.AccessManager; +import org.springframework.boot.developertools.remote.server.Dispatcher; +import org.springframework.boot.developertools.remote.server.DispatcherFilter; +import org.springframework.boot.developertools.remote.server.HandlerMapper; +import org.springframework.boot.developertools.remote.server.UrlHandlerMapper; +import org.springframework.boot.developertools.tunnel.client.HttpTunnelConnection; +import org.springframework.boot.developertools.tunnel.client.TunnelClient; +import org.springframework.boot.developertools.tunnel.client.TunnelConnection; +import org.springframework.boot.developertools.tunnel.server.HttpTunnelServer; +import org.springframework.boot.developertools.tunnel.server.HttpTunnelServerHandler; +import org.springframework.boot.developertools.tunnel.server.PortProvider; +import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection; +import org.springframework.boot.developertools.tunnel.server.StaticPortProvider; +import org.springframework.boot.developertools.tunnel.server.TargetServerConnection; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.boot.test.WebIntegrationTest; +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.SpringJUnit4ClassRunner; +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; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.junit.Assert.assertEquals; + +/** + * Simple integration tests for HTTP tunneling. + * + * @author Phillip Webb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = HttpTunnelIntegrationTest.Config.class) +@WebIntegrationTest +public class HttpTunnelIntegrationTest { + + @Autowired + private Config config; + + @Test + public void httpServerDirect() throws Exception { + String url = "http://localhost:" + this.config.httpServerPort + "/hello"; + ResponseEntity entity = new TestRestTemplate().getForEntity(url, + String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals("Hello World", entity.getBody()); + } + + @Test + public void viaTunnel() throws Exception { + String url = "http://localhost:" + this.config.clientPort + "/hello"; + ResponseEntity entity = new TestRestTemplate().getForEntity(url, + String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals("Hello World", entity.getBody()); + } + + @Configuration + @EnableWebMvc + static class Config { + + private int clientPort = SocketUtils.findAvailableTcpPort(); + + private int httpServerPort = SocketUtils.findAvailableTcpPort(); + + @Bean + public EmbeddedServletContainerFactory container() { + return new TomcatEmbeddedServletContainerFactory(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 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); + } + + @Bean + public DispatcherServlet dispatcherServlet() { + return new DispatcherServlet(); + } + + @Bean + public MyController myController() { + return new MyController(); + } + + } + + @RestController + static class MyController { + + @RequestMapping("/hello") + public String hello() { + return "Hello World"; + } + + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableConditionTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableConditionTests.java new file mode 100644 index 0000000000..f526312826 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/LocalDebugPortAvailableConditionTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2015 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. + * 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 org.springframework.boot.developertools.remote.client; + +import java.net.ServerSocket; + +import javax.net.ServerSocketFactory; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.SocketUtils; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LocalDebugPortAvailableCondition}. + * + * @author Phillip Webb + */ +public class LocalDebugPortAvailableConditionTests { + + private int port = SocketUtils.findAvailableTcpPort(); + + private LocalDebugPortAvailableCondition condition = new LocalDebugPortAvailableCondition(); + + @Test + public void portAvailable() throws Exception { + ConditionOutcome outcome = getOutcome(); + assertThat(outcome.isMatch(), equalTo(true)); + assertThat(outcome.getMessage(), equalTo("Local debug port availble")); + } + + @Test + public void portInUse() throws Exception { + final ServerSocket serverSocket = ServerSocketFactory.getDefault() + .createServerSocket(this.port); + ConditionOutcome outcome = getOutcome(); + serverSocket.close(); + assertThat(outcome.isMatch(), equalTo(false)); + assertThat(outcome.getMessage(), equalTo("Local debug port unavailble")); + } + + private ConditionOutcome getOutcome() { + MockEnvironment environment = new MockEnvironment(); + EnvironmentTestUtils.addEnvironment(environment, + "spring.developertools.remote.debug.local-port:" + this.port); + ConditionContext context = mock(ConditionContext.class); + given(context.getEnvironment()).willReturn(environment); + ConditionOutcome outcome = this.condition.getMatchOutcome(context, null); + return outcome; + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java index efaade66d9..284f90f698 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java @@ -38,6 +38,7 @@ import org.springframework.boot.developertools.remote.server.Dispatcher; import org.springframework.boot.developertools.remote.server.DispatcherFilter; import org.springframework.boot.developertools.restart.MockRestarter; import org.springframework.boot.developertools.restart.RestartScopeInitializer; +import org.springframework.boot.developertools.tunnel.client.TunnelClient; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.OutputCapture; import org.springframework.context.annotation.Bean; @@ -82,10 +83,11 @@ public class RemoteClientConfigurationTests { } @Test - public void warnIfRestartDisabled() throws Exception { - configure("spring.developertools.remote.restart.enabled:false"); + public void warnIfDebugAndRestartDisabled() throws Exception { + configure("spring.developertools.remote.debug.enabled:false", + "spring.developertools.remote.restart.enabled:false"); assertThat(this.output.toString(), - containsString("Remote restart is not enabled")); + containsString("Remote restart and debug are both disabled")); } @Test @@ -122,6 +124,13 @@ public class RemoteClientConfigurationTests { this.context.getBean(ClassPathFileSystemWatcher.class); } + @Test + public void remoteDebugDisabled() throws Exception { + configure("spring.developertools.remote.debug.enabled:false"); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(TunnelClient.class); + } + private void configure(String... pairs) { configureWithRemoteUrl("http://localhost", pairs); }