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-3087pull/3077/merge
parent
2123b267aa
commit
bdf7663a9a
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
@ -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<String> 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<String> 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<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);
|
||||
}
|
||||
|
||||
@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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue