Skip to content

Commit 1fe6c1f

Browse files
committed
More robust watching
1 parent 4c400e9 commit 1fe6c1f

File tree

5 files changed

+172
-108
lines changed

5 files changed

+172
-108
lines changed

src/license/THIRD-PARTY.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# Please fill the missing licenses for dependencies :
1414
#
1515
#
16-
#Fri Nov 24 11:35:19 CET 2017
16+
#Fri Nov 24 15:15:14 CET 2017
1717
classworlds--classworlds--1.1-alpha-2=
1818
commons-collections--commons-collections--3.1=
1919
dom4j--dom4j--1.6.1=

src/main/java/org/seedstack/maven/WatchMojo.java

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.HashSet;
2929
import java.util.List;
3030
import java.util.Set;
31+
import java.util.concurrent.ArrayBlockingQueue;
32+
import java.util.concurrent.Semaphore;
3133
import org.apache.maven.plugin.MojoExecutionException;
3234
import org.apache.maven.plugin.MojoFailureException;
3335
import org.apache.maven.plugins.annotations.Execute;
@@ -43,8 +45,8 @@
4345
import org.seedstack.maven.classloader.ReloadingClassLoader;
4446
import org.seedstack.maven.livereload.LRServer;
4547
import org.seedstack.maven.runnables.DefaultLauncherRunnable;
46-
import org.seedstack.maven.watcher.AggregatingFileChangeListener;
4748
import org.seedstack.maven.watcher.DirectoryWatcher;
49+
import org.seedstack.maven.watcher.FileChangeListener;
4850
import org.seedstack.maven.watcher.FileEvent;
4951

5052
/**
@@ -57,7 +59,6 @@ public class WatchMojo extends AbstractExecutableMojo {
5759
public static final int LIVE_RELOAD_PORT = 35729;
5860
private DirectoryWatcher directoryWatcher;
5961
private Thread watcherThread;
60-
private AggregatingFileChangeListener fileChangeListener;
6162
private List<String> compileSourceRoots;
6263
private ReloadingClassLoader reloadingClassLoader;
6364
private DefaultLauncherRunnable launcherRunnable;
@@ -66,9 +67,8 @@ public class WatchMojo extends AbstractExecutableMojo {
6667
@Override
6768
public void execute() throws MojoExecutionException, MojoFailureException {
6869
this.compileSourceRoots = Collections.unmodifiableList(getProject().getCompileSourceRoots());
69-
this.fileChangeListener = new WatchFileChangeListener();
7070
try {
71-
this.directoryWatcher = new DirectoryWatcher(getLog(), fileChangeListener);
71+
this.directoryWatcher = new DirectoryWatcher(getLog(), new WatchFileChangeListener());
7272
} catch (IOException e) {
7373
throw new MojoExecutionException("Unable to create directory watcher", e);
7474
}
@@ -92,7 +92,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
9292
Runtime.getRuntime().addShutdownHook(new Thread("watcher") {
9393
@Override
9494
public void run() {
95-
fileChangeListener.stop();
9695
directoryWatcher.stop();
9796
watcherThread.interrupt();
9897
try {
@@ -135,32 +134,74 @@ public ReloadingClassLoader run() {
135134
return reloadingClassLoader;
136135
}
137136

138-
private class WatchFileChangeListener extends AggregatingFileChangeListener {
137+
private class WatchFileChangeListener implements FileChangeListener {
138+
private final Semaphore semaphore = new Semaphore(1);
139+
private final ArrayBlockingQueue<FileEvent> pending = new ArrayBlockingQueue<>(10000);
140+
139141
@Override
140-
public void onAggregatedChanges(Set<FileEvent> fileEvents) {
141-
try {
142-
if (getLog().isDebugEnabled()) {
143-
for (FileEvent fileEvent : fileEvents) {
144-
getLog().debug(fileEvent.getKind().name() + ": " + fileEvent.getFile().getAbsolutePath());
142+
public void onChange(Set<FileEvent> fileEvents) {
143+
pending.addAll(fileEvents);
144+
while (!pending.isEmpty()) {
145+
boolean permit = false;
146+
try {
147+
permit = semaphore.tryAcquire();
148+
if (permit) {
149+
HashSet<FileEvent> fileEventsToProcess = new HashSet<>();
150+
pending.drainTo(fileEventsToProcess);
151+
refresh(fileEventsToProcess);
152+
} else {
153+
pending.addAll(fileEvents);
154+
}
155+
} finally {
156+
if (permit) {
157+
semaphore.release();
145158
}
146159
}
160+
}
161+
}
147162

163+
private void refresh(Set<FileEvent> fileEvents) {
164+
try {
148165
Set<File> compiledFilesToRemove = new HashSet<>();
149166
Set<File> compiledFilesToUpdate = new HashSet<>();
150167

151-
analyzeEvents(fileEvents, compiledFilesToRemove, compiledFilesToUpdate);
168+
boolean newFiles = analyzeEvents(fileEvents, compiledFilesToRemove, compiledFilesToUpdate);
169+
170+
if (newFiles || !compiledFilesToRemove.isEmpty() || !compiledFilesToUpdate.isEmpty()) {
171+
getLog().info("Source changes detected");
152172

153-
if (!compiledFilesToRemove.isEmpty() || !compiledFilesToUpdate.isEmpty()) {
154-
reloadingClassLoader.invalidateClasses(
155-
analyzeClasses(compiledFilesToRemove, compiledFilesToUpdate)
156-
);
173+
try {
174+
// Invalidate classes from source files that are gone
175+
reloadingClassLoader.invalidateClasses(analyzeClasses(compiledFilesToRemove));
176+
} catch (MojoExecutionException e) {
177+
getLog().info("Cannot detect removed classes, invalidating all classes");
178+
reloadingClassLoader.invalidateAllClasses();
179+
}
180+
181+
// Remove compiled files for source files that are gone
157182
removeFiles(compiledFilesToRemove);
183+
184+
try {
185+
// Invalidate classes from source files that have changed
186+
reloadingClassLoader.invalidateClasses(analyzeClasses(compiledFilesToUpdate));
187+
} catch (MojoExecutionException e) {
188+
getLog().info("Cannot detect changed classes, invalidating all classes");
189+
reloadingClassLoader.invalidateAllClasses();
190+
}
191+
192+
// Recompile the sources
158193
recompile();
194+
195+
// Refresh the app
159196
launcherRunnable.refresh();
197+
198+
// Trigger LiveReload
160199
if (lrServer != null) {
161200
getLog().info("Triggering LiveReload");
162201
lrServer.notifyChange("/");
163202
}
203+
204+
getLog().info("Refresh complete");
164205
}
165206
} catch (Exception e) {
166207
Throwable toLog = e.getCause();
@@ -172,29 +213,32 @@ public void onAggregatedChanges(Set<FileEvent> fileEvents) {
172213
}
173214
}
174215

175-
private void analyzeEvents(Set<FileEvent> fileEvents, Set<File> compiledFilesToRemove,
216+
private boolean analyzeEvents(Set<FileEvent> fileEvents, Set<File> compiledFilesToRemove,
176217
Set<File> compiledFilesToUpdate) throws MojoExecutionException {
218+
boolean newFiles = false;
177219
for (FileEvent fileEvent : fileEvents) {
178220
File changedFile = fileEvent.getFile();
179221
if (!changedFile.isDirectory()) {
180222
for (String compileSourceRoot : compileSourceRoots) {
181223
try {
182-
String changedFilePath = changedFile.getCanonicalPath();
224+
String canonicalChangedFile = changedFile.getCanonicalPath();
183225
String sourceRootPath = new File(compileSourceRoot).getCanonicalPath();
184-
if (changedFilePath.startsWith(sourceRootPath + File.separator)
185-
&& changedFilePath.endsWith(".java")) {
186-
String compiledFile = changedFilePath.substring(sourceRootPath.length());
187-
compiledFile = compiledFile.replaceAll("\\.java$", ".class");
188-
File fullCompiledFile = new File(getClassesDirectory(), compiledFile);
189-
switch (fileEvent.getKind()) {
190-
case MODIFY:
191-
compiledFilesToUpdate.add(fullCompiledFile);
192-
break;
193-
case DELETE:
194-
compiledFilesToRemove.add(fullCompiledFile);
195-
break;
196-
default:
197-
// nothing to do
226+
if (canonicalChangedFile.startsWith(sourceRootPath + File.separator)
227+
&& canonicalChangedFile.endsWith(".java")) {
228+
if (fileEvent.getKind() == FileEvent.Kind.CREATE) {
229+
if (changedFile.length() > 0) {
230+
// ignore empty files (not relevant for app refresh)
231+
getLog().info("NEW: " + canonicalChangedFile);
232+
newFiles = true;
233+
}
234+
} else if (fileEvent.getKind() == FileEvent.Kind.MODIFY) {
235+
getLog().info("MODIFIED: " + canonicalChangedFile);
236+
compiledFilesToUpdate.add(resolveCompiledFile(sourceRootPath,
237+
canonicalChangedFile));
238+
} else if (fileEvent.getKind() == FileEvent.Kind.DELETE) {
239+
getLog().info("DELETED: " + canonicalChangedFile);
240+
compiledFilesToRemove.add(resolveCompiledFile(sourceRootPath,
241+
canonicalChangedFile));
198242
}
199243
}
200244
} catch (IOException e) {
@@ -204,34 +248,38 @@ private void analyzeEvents(Set<FileEvent> fileEvents, Set<File> compiledFilesToR
204248
}
205249
}
206250
}
251+
return newFiles;
252+
}
253+
254+
private File resolveCompiledFile(String sourceRootPath, String changedFilePath) {
255+
return new File(getClassesDirectory(), changedFilePath
256+
.substring(sourceRootPath.length())
257+
.replaceAll("\\.java$", ".class"));
207258
}
208259

209260
private void removeFiles(Set<File> compiledFilesToRemove) throws MojoExecutionException {
210261
for (File file : compiledFilesToRemove) {
211-
if (!file.delete()) {
262+
if (file.exists() && !file.delete()) {
212263
throw new MojoExecutionException("Unable to remove compiled file " + file.getAbsolutePath());
213264
}
214265
}
215266
}
216267

217-
private Set<String> analyzeClasses(Set<File> compiledFilesToRemove, Set<File> compiledFilesToUpdate) {
268+
private Set<String> analyzeClasses(Set<File> classFiles) throws MojoExecutionException {
218269
Set<String> classNamesToInvalidate = new HashSet<>();
219-
for (File file : compiledFilesToRemove) {
220-
classNamesToInvalidate.addAll(collectClassNames(file));
221-
}
222-
for (File file : compiledFilesToUpdate) {
270+
for (File file : classFiles) {
223271
classNamesToInvalidate.addAll(collectClassNames(file));
224272
}
225273
return classNamesToInvalidate;
226274
}
227275

228-
private Set<String> collectClassNames(File classFile) {
276+
private Set<String> collectClassNames(File classFile) throws MojoExecutionException {
229277
Set<String> classNames = new HashSet<>();
230278
try (FileInputStream is = new FileInputStream(classFile)) {
231279
ClassReader classReader = new ClassReader(is);
232280
classReader.accept(new ClassNameCollector(classNames), 0);
233281
} catch (IOException e) {
234-
getLog().warn("Unable to parse class file " + classFile.getAbsolutePath());
282+
throw new MojoExecutionException("Unable to analyze class file " + classFile.getAbsolutePath());
235283
}
236284
return classNames;
237285
}

src/main/java/org/seedstack/maven/classloader/ReloadingClassLoader.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* License, v. 2.0. If a copy of the MPL was not distributed with this
66
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
77
*/
8+
89
package org.seedstack.maven.classloader;
910

1011
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -71,6 +72,13 @@ public void invalidateClasses(Set<String> classNamesToInvalidate) {
7172
}
7273
}
7374

75+
public void invalidateAllClasses() {
76+
synchronized (classLoaders) {
77+
classLoaders.clear();
78+
log.debug("All classes will be reloaded on next access");
79+
}
80+
}
81+
7482
private DisposableClassLoader createDisposableClassLoader(final String name) throws ClassNotFoundException {
7583
final DisposableClassLoader result;
7684
try {

src/main/java/org/seedstack/maven/watcher/AggregatingFileChangeListener.java

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)