Skip to content

Commit

Permalink
Merge pull request #43227 from gayaldassanayake/watch-debounce
Browse files Browse the repository at this point in the history
Add debouncing logic to the file watcher and improve error messages
  • Loading branch information
gayaldassanayake authored Aug 3, 2024
2 parents 0fc18a7 + b30d9c9 commit 5e8d4cf
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,11 @@ public void execute() {
ProjectWatcher projectWatcher = new ProjectWatcher(
this, Paths.get(this.projectPath.toString()), outStream);
projectWatcher.watch();
} catch (IOException | InterruptedException e) {
throw createLauncherException("unable to run in the watch mode:" + e.getMessage());
} catch (IOException e) {
throw createLauncherException("unable to watch the project:" + e.getMessage());
} catch (ProjectException e) {
CommandUtil.printError(this.errStream, e.getMessage(), runCmd, false);
CommandUtil.exitError(this.exitWhenFinish);
}
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import io.ballerina.cli.cmd.RunCommand;
import io.ballerina.projects.ProjectKind;
import io.ballerina.projects.internal.ProjectFiles;
import io.ballerina.projects.util.FileUtils;

import java.io.IOException;
Expand All @@ -35,7 +36,12 @@
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static io.ballerina.cli.launcher.LauncherUtils.createLauncherException;
import static io.ballerina.projects.util.ProjectConstants.BLANG_SOURCE_EXT;
import static io.ballerina.projects.util.ProjectConstants.DEPENDENCIES_TOML;
import static io.ballerina.projects.util.ProjectConstants.MODULES_ROOT;
Expand All @@ -58,6 +64,9 @@ public class ProjectWatcher {
private final PrintStream outStream;
private final Path projectPath;
private final ProjectKind projectKind;
private final ScheduledExecutorService scheduledExecutorService;
private final Map<Path, Long> debounceMap = new ConcurrentHashMap<>();
private static final long debounceTimeMillis = 250;

public ProjectWatcher(RunCommand runCommand, Path projectPath, PrintStream outStream) throws IOException {
this.fileWatcher = FileSystems.getDefault().newWatchService();
Expand All @@ -66,13 +75,15 @@ public ProjectWatcher(RunCommand runCommand, Path projectPath, PrintStream outSt
this.outStream = outStream;
this.watchKeys = new HashMap<>();
this.projectKind = deriveProjectKind();
validateProjectPath();
this.scheduledExecutorService = Executors.newScheduledThreadPool(1);
registerFileTree(projectPath);
}

public void watch() throws IOException, InterruptedException {
RunCommandExecutor thread = new RunCommandExecutor(runCommand, outStream);
thread.start();
while (thread.shouldWatch()) {
public void watch() throws IOException {
final RunCommandExecutor[] thread = {new RunCommandExecutor(runCommand, outStream)};
thread[0].start();
while (thread[0].shouldWatch()) {
WatchKey key;
key = fileWatcher.poll();
Path dir = watchKeys.get(key);
Expand All @@ -88,11 +99,25 @@ public void watch() throws IOException, InterruptedException {
Path changedFileName = pathWatchEvent.context();
Path changedFilePath = dir.resolve(changedFileName).toAbsolutePath();
if (isValidFileChange(changedFilePath)) {
outStream.println("\nDetected file changes. Re-running the project...");
thread.terminate();
thread.join();
thread = new RunCommandExecutor(runCommand, outStream);
thread.start();
long currentTime = System.currentTimeMillis();
debounceMap.put(changedFilePath, currentTime);
scheduledExecutorService.schedule(() -> {
Long lastModifiedTime = debounceMap.get(changedFilePath);
if (lastModifiedTime == null
|| (System.currentTimeMillis() - lastModifiedTime < debounceTimeMillis)) {
return;
}
outStream.println("\nDetected file changes. Re-running the project...");
thread[0].terminate();
try {
thread[0].join();
} catch (InterruptedException e) {
throw createLauncherException("unable to watch the project:" + e.getMessage());
}
thread[0] = new RunCommandExecutor(runCommand, outStream);
thread[0].start();
debounceMap.remove(changedFilePath);
}, debounceTimeMillis, TimeUnit.MILLISECONDS);
}
if (kind == ENTRY_CREATE && Files.isDirectory(changedFilePath)) {
registerFileTree(changedFilePath);
Expand Down Expand Up @@ -171,6 +196,14 @@ private void register(Path dir) throws IOException {
watchKeys.put(key, dir);
}

private void validateProjectPath() {
if (projectKind.equals(ProjectKind.SINGLE_FILE_PROJECT)) {
ProjectFiles.validateSingleFileProjectFilePath(projectPath);
} else {
ProjectFiles.validateBuildProjectDirPath(projectPath);
}
}

@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,59 +576,61 @@ private Manifest createTestManifest() {
private void copyJar(ZipArchiveOutputStream outStream, JarLibrary jarLibrary,
HashMap<String, JarLibrary> copiedEntries, HashMap<String,
StringBuilder> services) throws IOException {

ZipFile zipFile = new ZipFile(jarLibrary.path().toFile());
ZipArchiveEntryPredicate predicate = entry -> {
String entryName = entry.getName();
if (entryName.equals("META-INF/MANIFEST.MF")) {
return false;
}
if (entryName.equals("module-info.class")) {
return false;
}
if (entryName.startsWith("META-INF/services")) {
StringBuilder s = services.get(entryName);
if (s == null) {
s = new StringBuilder();
services.put(entryName, s);
if (Thread.currentThread().isInterrupted()) {
return;
}
try (ZipFile zipFile = new ZipFile(jarLibrary.path().toFile())) {
ZipArchiveEntryPredicate predicate = entry -> {
String entryName = entry.getName();
if (entryName.equals("META-INF/MANIFEST.MF")) {
return false;
}
char c = '\n';
if (entryName.equals("module-info.class")) {
return false;
}
if (entryName.startsWith("META-INF/services")) {
StringBuilder s = services.get(entryName);
if (s == null) {
s = new StringBuilder();
services.put(entryName, s);
}
char c = '\n';

int len;
try (BufferedInputStream inStream = new BufferedInputStream(zipFile.getInputStream(entry))) {
while ((len = inStream.read()) != -1) {
c = (char) len;
s.append(c);
int len;
try (BufferedInputStream inStream = new BufferedInputStream(zipFile.getInputStream(entry))) {
while ((len = inStream.read()) != -1) {
c = (char) len;
s.append(c);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (c != '\n') {
s.append('\n');
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (c != '\n') {
s.append('\n');
}

// Its not required to copy SPI entries in here as we'll be adding merged SPI related entries
// separately. Therefore the predicate should be set as false.
return false;
}
// Its not required to copy SPI entries in here as we'll be adding merged SPI related entries
// separately. Therefore the predicate should be set as false.
return false;
}

// Skip already copied files or excluded extensions.
if (isCopiedEntry(entryName, copiedEntries)) {
addConflictedJars(jarLibrary, copiedEntries, entryName);
return false;
}
if (isExcludedEntry(entryName)) {
return false;
}
// SPIs will be merged first and then put into jar separately.
copiedEntries.put(entryName, jarLibrary);
return true;
};
// Skip already copied files or excluded extensions.
if (isCopiedEntry(entryName, copiedEntries)) {
addConflictedJars(jarLibrary, copiedEntries, entryName);
return false;
}
if (isExcludedEntry(entryName)) {
return false;
}
// SPIs will be merged first and then put into jar separately.
copiedEntries.put(entryName, jarLibrary);
return true;
};

// Transfers selected entries from this zip file to the output stream, while preserving its compression and
// all the other original attributes.
zipFile.copyRawEntries(outStream, predicate);
zipFile.close();
// Transfers selected entries from this zip file to the output stream, while preserving its compression and
// all the other original attributes.
zipFile.copyRawEntries(outStream, predicate);
}
}

private static boolean isCopiedEntry(String entryName, HashMap<String, JarLibrary> copiedEntries) {
Expand Down

0 comments on commit 5e8d4cf

Please sign in to comment.