diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/api/client/util/DirectoryWatcherService.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/api/client/util/DirectoryWatcherService.java index 695226a1d405..9176119b7951 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/api/client/util/DirectoryWatcherService.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/api/client/util/DirectoryWatcherService.java @@ -33,12 +33,16 @@ public class DirectoryWatcherService { private final Logger logger = Logger.getLogger(DirectoryWatcherService.class); private final Set paths = ConcurrentHashMap.newKeySet(); private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicBoolean suspended = new AtomicBoolean(false); + public DirectoryWatcherService() throws IOException { watchService = FileSystems.getDefault().newWatchService(); scheduler = Executors.newScheduledThreadPool(1); } + + @SuppressWarnings("java:S1452") public BlockingQueue> watch(Path path, long pollIntervalSeconds) throws IOException { registerAll(path); if(running.compareAndSet(false, true)){ @@ -49,10 +53,11 @@ public BlockingQueue> watch(Path path, long pollIntervalSeconds) t private void processEvents() { try{ - pollEvents(); + if (!suspended.get()) { + pollEvents(); + } } catch (Exception e) { logger.error("Error processing events", e); - } } @@ -62,6 +67,7 @@ private void pollEvents() { if (key != null) { WatchEvent lastEvent = null; for (WatchEvent event : key.pollEvents()) { + System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context()); lastEvent = event; } if (lastEvent != null) { @@ -75,6 +81,18 @@ public boolean isRunning() { return running.get(); } + public boolean isSuspended() { + return suspended.get(); + } + + public void suspend() { + suspended.set(true); + } + + public void resume() { + suspended.set(false); + } + public void stop() { if (scheduler != null) { scheduler.shutdown(); diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/DotPush.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/DotPush.java index c78e3bdac5d8..cd880c453ee1 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/DotPush.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/DotPush.java @@ -10,7 +10,7 @@ * Furthermore, it can provide the name of a custom mixin it uses, which is useful for custom * command line configurations where specific logic may be associated with certain mixin names. */ -public interface DotPush extends DotCommand{ +public interface DotPush extends DotCommand { /** * Returns the {@link PushMixin} associated with the implementing class. This {@link PushMixin} diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/PushCommand.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/PushCommand.java index 246d373a4915..7f621bfb28e7 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/PushCommand.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/PushCommand.java @@ -1,6 +1,5 @@ package com.dotcms.cli.command; -import com.dotcms.api.client.util.DirectoryWatcherService; import com.dotcms.cli.common.AuthenticationMixin; import com.dotcms.cli.common.CommandInterceptor; import com.dotcms.cli.common.FullPushOptionsMixin; @@ -11,12 +10,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; -import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.stream.Collectors; import javax.enterprise.context.control.ActivateRequestContext; import javax.enterprise.inject.Instance; @@ -63,8 +57,6 @@ public class PushCommand implements Callable, DotPush { @Inject Instance pushCommands; - @Inject - DirectoryWatcherService directoryWatcherService; @Override @CommandInterceptor @@ -77,24 +69,6 @@ public Integer call() throws Exception { // Validate we have a workspace at the specified path checkValidWorkspace(pushMixin.path()); - /* - if(pushMixin.isWatchOn()){ - directoryWatcherService.watch(pushMixin.path(), pushMixin.interval, true, event -> { - execCommands(); - }); - return CommandLine.ExitCode.OK; - } - */ - - final Integer exitCode = execSubCommands(); - if (exitCode != null) { - return exitCode; - } - - return CommandLine.ExitCode.OK; - } - - private Integer execSubCommands() { // Preparing the list of arguments to be passed to the subcommands var expandedArgs = new ArrayList<>(spec.commandLine().getParseResult().expandedArgs()); expandedArgs.add("--noValidateUnmatchedArguments"); @@ -106,34 +80,19 @@ private Integer execSubCommands() { .sorted(Comparator.comparingInt(DotPush::getOrder)) .collect(Collectors.toList()); - // Usa ExecutorService for parallel execution of the subcommands - final ExecutorService executorService = Executors.newFixedThreadPool(pushCommandsSorted.size()); - final List> futures = new ArrayList<>(); - + // Process each subcommand for (var subCommand : pushCommandsSorted) { - Callable task = () -> { - var cmdLine = createCommandLine(subCommand); - return cmdLine.execute(args); - }; - futures.add(executorService.submit(task)); - } - // Wait for all subcommands to finish and check for errors - for (Future future : futures) { - try { - int exitCode = future.get(); - if (exitCode != CommandLine.ExitCode.OK) { - executorService.shutdownNow(); - return exitCode; - } - } catch (InterruptedException | ExecutionException e) { - executorService.shutdownNow(); - throw new RuntimeException("Error executing subcommand", e); + var cmdLine = createCommandLine(subCommand); + + // Use execute to parse the parameters with the subcommand + int exitCode = cmdLine.execute(args); + if (exitCode != CommandLine.ExitCode.OK) { + return exitCode; } } - executorService.shutdown(); - return null; + return CommandLine.ExitCode.OK; } /** diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/contenttype/ContentTypePush.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/contenttype/ContentTypePush.java index 7c877dcc8684..ee9af861e1a7 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/contenttype/ContentTypePush.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/contenttype/ContentTypePush.java @@ -7,6 +7,7 @@ import com.dotcms.cli.command.DotCommand; import com.dotcms.cli.command.DotPush; import com.dotcms.cli.common.ApplyCommandOrder; +import com.dotcms.cli.common.CommandInterceptor; import com.dotcms.cli.common.FullPushOptionsMixin; import com.dotcms.cli.common.OutputOptionMixin; import com.dotcms.cli.common.PushMixin; @@ -63,6 +64,7 @@ public class ContentTypePush extends AbstractContentTypeCommand implements Calla CommandLine.Model.CommandSpec spec; @Override + @CommandInterceptor public Integer call() throws Exception { // When calling from the global push we should avoid the validation of the unmatched diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/files/FilesPush.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/files/FilesPush.java index ce23842acdd2..74712705846a 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/files/FilesPush.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/files/FilesPush.java @@ -13,6 +13,7 @@ import com.dotcms.cli.command.DotPush; import com.dotcms.cli.command.PushContext; import com.dotcms.cli.common.ApplyCommandOrder; +import com.dotcms.cli.common.CommandInterceptor; import com.dotcms.cli.common.ConsoleLoadingAnimation; import com.dotcms.cli.common.OutputOptionMixin; import com.dotcms.cli.common.PushMixin; @@ -65,6 +66,7 @@ public class FilesPush extends AbstractFilesCommand implements Callable ManagedExecutor executor; @Override + @CommandInterceptor public Integer call() throws Exception { // When calling from the global push we should avoid the validation of the unmatched diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/language/LanguagePush.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/language/LanguagePush.java index 5f85d4e7c8e5..58a8ac09f13b 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/language/LanguagePush.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/language/LanguagePush.java @@ -9,6 +9,7 @@ import com.dotcms.cli.command.DotCommand; import com.dotcms.cli.command.DotPush; import com.dotcms.cli.common.ApplyCommandOrder; +import com.dotcms.cli.common.CommandInterceptor; import com.dotcms.cli.common.FullPushOptionsMixin; import com.dotcms.cli.common.OutputOptionMixin; import com.dotcms.cli.common.PushMixin; @@ -84,6 +85,7 @@ public class LanguagePush extends AbstractLanguageCommand implements Callable, CommandLine.Model.CommandSpec spec; @Override + @CommandInterceptor public Integer call() throws Exception { // When calling from the global push we should avoid the validation of the unmatched diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/CommandExecutionInterceptor.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/CommandExecutionInterceptor.java index bb5b99115725..3768bb411d71 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/CommandExecutionInterceptor.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/CommandExecutionInterceptor.java @@ -1,6 +1,7 @@ package com.dotcms.cli.common; import static java.nio.file.StandardWatchEventKinds.OVERFLOW; + import com.dotcms.api.client.util.DirectoryWatcherService; import com.dotcms.cli.command.DotPush; import java.nio.file.Path; @@ -22,28 +23,58 @@ public class CommandExecutionInterceptor { @Inject DirectoryWatcherService service; + // ThreadLocal to track the recursion depth for the interceptor + private static final ThreadLocal callDepth = ThreadLocal.withInitial(() -> 0); + @AroundInvoke public Object invoke(final InvocationContext context) throws Exception { - final Object target = context.getTarget(); - logger.debug("Executing command: " + context.getTarget()); - if (target instanceof DotPush) { - final DotPush push = (DotPush) target; - final PushMixin filesPushMixin = push.getPushMixin(); - if (filesPushMixin.isWatchMode()) { - Object result = null; - final Path path = filesPushMixin.path(); - final BlockingQueue> eventQueue = service.watch(path, filesPushMixin.interval); - while (service.isRunning()) { - final WatchEvent event = eventQueue.take(); - if (event.kind() == OVERFLOW) { - continue; + + try { + // Increment the call depth + callDepth.set(callDepth.get() + 1); + if (callDepth.get() > 1) { + // If the call depth is greater than 1, we are in a recursive call and should not intercept + return context.proceed(); + } + // otherwise, we are in the first call and should intercept + final Object target = context.getTarget(); + logger.debug("Executing command: " + context.getTarget()); + if (target instanceof DotPush) { + final DotPush push = (DotPush) target; + //Otherwise, we are in the first call and should intercept + final PushMixin filesPushMixin = push.getPushMixin(); + if (filesPushMixin.isWatchMode()) { + push.getOutput().info("Running in Watch Mode on " + filesPushMixin.path()); + Object result = null; + final Path path = filesPushMixin.path(); + final BlockingQueue> eventQueue = service.watch(path, filesPushMixin.interval); + while (service.isRunning()) { + final WatchEvent event = eventQueue.take(); + if (event.kind() == OVERFLOW) { + continue; + } + try{ + //Disengage the watch service to avoid recursion issues + //The command itself might trigger a file change + service.suspend(); + result = context.proceed(); + }finally { + //Re-engage the watch mode + service.resume(); + } } - result = context.proceed(); + return result; } - return result; + } + return context.proceed(); + } finally { + // Decrement the call depth + callDepth.set(callDepth.get() - 1); + // Clean up ThreadLocal if it's no longer needed + if (callDepth.get() == 0) { + callDepth.remove(); } } - return context.proceed(); }