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
This commit is contained in:
Phillip Webb
2015-06-01 13:24:01 -07:00
parent 2123b267aa
commit bdf7663a9a
9 changed files with 491 additions and 5 deletions

View File

@@ -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()));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}
}