Trigger livereload on remote updates

Add livereload support to RemoteClientConfiguration which is triggered
whenever updates are pushed to the remote application.

Closes gh-3086
pull/3077/merge
Phillip Webb 10 years ago
parent 05ea2d77ef
commit c27b63b354

@ -0,0 +1,116 @@
/*
* 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.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link Runnable} that waits to triggers live reload until the remote server has
* restarted.
*
* @author Phillip Webb
*/
class DelayedLiveReloadTrigger implements Runnable {
private static final long SHUTDOWN_TIME = 1000;
private static final long SLEEP_TIME = 500;
private static final long TIMEOUT = 30000;
private static final Log logger = LogFactory.getLog(DelayedLiveReloadTrigger.class);
private final OptionalLiveReloadServer liveReloadServer;
private final ClientHttpRequestFactory requestFactory;
private final URI uri;
private long shutdownTime = SHUTDOWN_TIME;
private long sleepTime = SLEEP_TIME;
private long timeout = TIMEOUT;
public DelayedLiveReloadTrigger(OptionalLiveReloadServer liveReloadServer,
ClientHttpRequestFactory requestFactory, String url) {
Assert.notNull(liveReloadServer, "LiveReloadServer must not be null");
Assert.notNull(requestFactory, "RequestFactory must not be null");
Assert.hasLength(url, "URL must not be empty");
this.liveReloadServer = liveReloadServer;
this.requestFactory = requestFactory;
try {
this.uri = new URI(url);
}
catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
}
protected void setTimings(long shutdown, long sleep, long timeout) {
this.shutdownTime = shutdown;
this.sleepTime = sleep;
this.timeout = timeout;
}
@Override
public void run() {
try {
Thread.sleep(this.shutdownTime);
long start = System.currentTimeMillis();
while (!isUp()) {
long runTime = System.currentTimeMillis() - start;
if (runTime > this.timeout) {
return;
}
Thread.sleep(this.sleepTime);
}
logger.info("Remote server has changed, triggering LiveReload");
this.liveReloadServer.triggerReload();
}
catch (InterruptedException ex) {
}
}
private boolean isUp() {
try {
ClientHttpRequest request = createRequest();
ClientHttpResponse response = request.execute();
return response.getStatusCode() == HttpStatus.OK;
}
catch (Exception ex) {
return false;
}
}
private ClientHttpRequest createRequest() throws IOException {
return this.requestFactory.createRequest(this.uri, HttpMethod.GET);
}
}

@ -17,6 +17,8 @@
package org.springframework.boot.developertools.remote.client; package org.springframework.boot.developertools.remote.client;
import java.net.URL; import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -24,16 +26,23 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.developertools.autoconfigure.DeveloperToolsProperties; import org.springframework.boot.developertools.autoconfigure.DeveloperToolsProperties;
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties; import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties;
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher; import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy; import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy; import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.developertools.livereload.LiveReloadServer;
import org.springframework.boot.developertools.restart.DefaultRestartInitializer; import org.springframework.boot.developertools.restart.DefaultRestartInitializer;
import org.springframework.boot.developertools.restart.RestartScope;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory;
@ -75,6 +84,52 @@ public class RemoteClientConfiguration {
} }
} }
/**
* LiveReload configuration.
*/
@ConditionalOnProperty(prefix = "spring.developertools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Autowired(required = false)
private LiveReloadServer liveReloadServer;
@Autowired
private ClientHttpRequestFactory clientHttpRequestFactory;
@Value("${remoteUrl}")
private String remoteUrl;
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Bean
@RestartScope
@ConditionalOnMissingBean
public LiveReloadServer liveReloadServer() {
return new LiveReloadServer(this.properties.getLivereload().getPort(),
Restarter.getInstance().getThreadFactory());
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
String url = this.remoteUrl + this.properties.getRemote().getContextPath();
this.executor.execute(new DelayedLiveReloadTrigger(
optionalLiveReloadServer(), this.clientHttpRequestFactory, url));
}
@Bean
public OptionalLiveReloadServer optionalLiveReloadServer() {
return new OptionalLiveReloadServer(this.liveReloadServer);
}
final ExecutorService getExecutor() {
return this.executor;
}
}
/** /**
* Client configuration for remote update and restarts. * Client configuration for remote update and restarts.
*/ */

@ -0,0 +1,133 @@
/*
* 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.io.IOException;
import java.net.URI;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link DelayedLiveReloadTrigger}.
*
* @author Phillip Webb
*/
public class DelayedLiveReloadTriggerTests {
private static final String URL = "http://localhost:8080";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private OptionalLiveReloadServer liveReloadServer;
@Mock
private ClientHttpRequestFactory requestFactory;
@Mock
private ClientHttpRequest errorRequest;
@Mock
private ClientHttpRequest okRequest;
@Mock
private ClientHttpResponse errorResponse;
@Mock
private ClientHttpResponse okResponse;
private DelayedLiveReloadTrigger trigger;
@Before
public void setup() throws IOException {
MockitoAnnotations.initMocks(this);
given(this.errorRequest.execute()).willReturn(this.errorResponse);
given(this.okRequest.execute()).willReturn(this.okResponse);
given(this.errorResponse.getStatusCode()).willReturn(
HttpStatus.INTERNAL_SERVER_ERROR);
given(this.okResponse.getStatusCode()).willReturn(HttpStatus.OK);
this.trigger = new DelayedLiveReloadTrigger(this.liveReloadServer,
this.requestFactory, URL);
}
@Test
public void liveReloadServerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("LiveReloadServer must not be null");
new DelayedLiveReloadTrigger(null, this.requestFactory, URL);
}
@Test
public void requestFactoryMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RequestFactory must not be null");
new DelayedLiveReloadTrigger(this.liveReloadServer, null, URL);
}
@Test
public void urlMostNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new DelayedLiveReloadTrigger(this.liveReloadServer, this.requestFactory, null);
}
@Test
public void urlMustNotBeEmpty() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("URL must not be empty");
new DelayedLiveReloadTrigger(this.liveReloadServer, this.requestFactory, "");
}
@Test
public void triggerReloadOnStatus() throws Exception {
given(this.requestFactory.createRequest(new URI(URL), HttpMethod.GET)).willThrow(
new IOException()).willReturn(this.errorRequest, this.okRequest);
long startTime = System.currentTimeMillis();
this.trigger.setTimings(10, 200, 30000);
this.trigger.run();
assertThat(System.currentTimeMillis() - startTime, greaterThan(300L));
verify(this.liveReloadServer).triggerReload();
}
@Test
public void timeout() throws Exception {
given(this.requestFactory.createRequest(new URI(URL), HttpMethod.GET)).willThrow(
new IOException());
this.trigger.setTimings(10, 0, 10);
this.trigger.run();
verify(this.liveReloadServer, never()).triggerReload();
}
}

@ -17,6 +17,9 @@
package org.springframework.boot.developertools.remote.client; package org.springframework.boot.developertools.remote.client;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.After; import org.junit.After;
import org.junit.Rule; import org.junit.Rule;
@ -25,7 +28,12 @@ import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher; import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.boot.developertools.livereload.LiveReloadServer;
import org.springframework.boot.developertools.remote.client.RemoteClientConfiguration.LiveReloadConfiguration;
import org.springframework.boot.developertools.remote.server.Dispatcher; import org.springframework.boot.developertools.remote.server.Dispatcher;
import org.springframework.boot.developertools.remote.server.DispatcherFilter; import org.springframework.boot.developertools.remote.server.DispatcherFilter;
import org.springframework.boot.developertools.restart.MockRestarter; import org.springframework.boot.developertools.restart.MockRestarter;
@ -44,6 +52,7 @@ import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/** /**
* Tests for {@link RemoteClientConfiguration}. * Tests for {@link RemoteClientConfiguration}.
@ -85,6 +94,27 @@ public class RemoteClientConfigurationTests {
assertThat(this.output.toString(), not(containsString("is insecure"))); assertThat(this.output.toString(), not(containsString("is insecure")));
} }
@Test
public void liveReloadOnClassPathChanged() throws Exception {
configure();
Set<ChangedFiles> changeSet = new HashSet<ChangedFiles>();
ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false);
this.context.publishEvent(event);
LiveReloadConfiguration configuration = this.context
.getBean(LiveReloadConfiguration.class);
configuration.getExecutor().shutdown();
configuration.getExecutor().awaitTermination(2, TimeUnit.SECONDS);
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
verify(server).triggerReload();
}
@Test
public void liveReloadDisabled() throws Exception {
configure("spring.developertools.livereload.enabled:false");
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(OptionalLiveReloadServer.class);
}
@Test @Test
public void remoteRestartDisabled() throws Exception { public void remoteRestartDisabled() throws Exception {
configure("spring.developertools.remote.restart.enabled:false"); configure("spring.developertools.remote.restart.enabled:false");
@ -115,6 +145,11 @@ public class RemoteClientConfigurationTests {
return new TomcatEmbeddedServletContainerFactory(remotePort); return new TomcatEmbeddedServletContainerFactory(remotePort);
} }
@Bean
public LiveReloadServer liveReloadServer() {
return mock(LiveReloadServer.class);
}
@Bean @Bean
public DispatcherFilter dispatcherFilter() throws IOException { public DispatcherFilter dispatcherFilter() throws IOException {
return new DispatcherFilter(dispatcher()); return new DispatcherFilter(dispatcher());

Loading…
Cancel
Save