From faf630dfc3964f660a108bfd27ace35dd4cc2059 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Thu, 30 May 2024 19:05:25 -0600 Subject: [PATCH] #28593 testing multithreaded global Push approach --- .../cli/docs/content-type-push.adoc | 5 +- tools/dotcms-cli/cli/docs/files-push.adoc | 8 +- tools/dotcms-cli/cli/docs/language-push.adoc | 9 +- tools/dotcms-cli/cli/docs/push.adoc | 5 +- tools/dotcms-cli/cli/docs/site-push.adoc | 8 +- .../client/util/DirectoryWatcherService.java | 146 ++++++++++++++++++ .../com/dotcms/cli/command/PushCommand.java | 36 ++++- .../command/contenttype/ContentTypePush.java | 2 +- .../dotcms/cli/command/files/FilesPush.java | 136 ++++++++++------ .../cli/command/language/LanguagePush.java | 4 +- .../com/dotcms/cli/command/site/SitePush.java | 2 +- .../java/com/dotcms/cli/common/PushMixin.java | 20 ++- 12 files changed, 314 insertions(+), 67 deletions(-) create mode 100644 tools/dotcms-cli/cli/src/main/java/com/dotcms/api/client/util/DirectoryWatcherService.java diff --git a/tools/dotcms-cli/cli/docs/content-type-push.adoc b/tools/dotcms-cli/cli/docs/content-type-push.adoc index fa4f8afb9627..c9d21a19cb02 100644 --- a/tools/dotcms-cli/cli/docs/content-type-push.adoc +++ b/tools/dotcms-cli/cli/docs/content-type-push.adoc @@ -19,7 +19,7 @@ content-type-push - *Push content types* // tag::picocli-generated-man-section-synopsis[] == Synopsis -*content-type push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-rct*] +*content-type push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-rct*] [*-w*[=_watch_]] [*--dotcms-url*=__] [*--retry-attempts*=__] [*-tk*=__] [_path_] @@ -60,6 +60,9 @@ This command enables the pushing of content types to the server. It accommodates *-tk, --token*=__:: A dotCMS token to use for authentication. +*-w*, *--watch*[=_watch_]:: + When this option is enabled the tool watches for file changes within the push path If a change is detected the push operation gets triggered. The default watch interval is 2 seconds, but this can set passing an int value to this option. + // end::picocli-generated-man-section-options[] // tag::picocli-generated-man-section-arguments[] diff --git a/tools/dotcms-cli/cli/docs/files-push.adoc b/tools/dotcms-cli/cli/docs/files-push.adoc index 31f5534d01fc..c750ea55bcc5 100644 --- a/tools/dotcms-cli/cli/docs/files-push.adoc +++ b/tools/dotcms-cli/cli/docs/files-push.adoc @@ -19,8 +19,9 @@ files-push - *dotCMS Files push* // tag::picocli-generated-man-section-synopsis[] == Synopsis -*files push* [*-h*] [*--dry-run*] [*-ff*] [*-ra*] [*-rf*] [*--dotcms-url*=__] - [*--retry-attempts*=__] [*-tk*=__] [_path_] +*files push* [*-h*] [*--dry-run*] [*-ff*] [*-ra*] [*-rf*] [*-w*[=_watch_]] + [*--dotcms-url*=__] [*--retry-attempts*=__] + [*-tk*=__] [_path_] // end::picocli-generated-man-section-synopsis[] @@ -59,6 +60,9 @@ files-push - *dotCMS Files push* *-tk, --token*=__:: A dotCMS token to use for authentication. +*-w*, *--watch*[=_watch_]:: + When this option is enabled the tool watches for file changes within the push path If a change is detected the push operation gets triggered. The default watch interval is 2 seconds, but this can set passing an int value to this option. + // end::picocli-generated-man-section-options[] // tag::picocli-generated-man-section-arguments[] diff --git a/tools/dotcms-cli/cli/docs/language-push.adoc b/tools/dotcms-cli/cli/docs/language-push.adoc index 8afd0ccd86f1..9cb08d8d3771 100644 --- a/tools/dotcms-cli/cli/docs/language-push.adoc +++ b/tools/dotcms-cli/cli/docs/language-push.adoc @@ -19,9 +19,9 @@ language-push - *Push languages* // tag::picocli-generated-man-section-synopsis[] == Synopsis -*language push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-rl*] [*--byIso*=__] - [*--dotcms-url*=__] [*--retry-attempts*=__] - [*-tk*=__] [_path_] +*language push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-rl*] [*-w*[=_watch_]] + [*--byIso*=__] [*--dotcms-url*=__] + [*--retry-attempts*=__] [*-tk*=__] [_path_] // end::picocli-generated-man-section-synopsis[] @@ -63,6 +63,9 @@ This command enables the pushing of languages to the server. It accommodates the *-tk, --token*=__:: A dotCMS token to use for authentication. +*-w*, *--watch*[=_watch_]:: + When this option is enabled the tool watches for file changes within the push path If a change is detected the push operation gets triggered. The default watch interval is 2 seconds, but this can set passing an int value to this option. + // end::picocli-generated-man-section-options[] // tag::picocli-generated-man-section-arguments[] diff --git a/tools/dotcms-cli/cli/docs/push.adoc b/tools/dotcms-cli/cli/docs/push.adoc index ab974fa48123..734633ca6cc6 100644 --- a/tools/dotcms-cli/cli/docs/push.adoc +++ b/tools/dotcms-cli/cli/docs/push.adoc @@ -19,7 +19,7 @@ push - *dotCMS global push* // tag::picocli-generated-man-section-synopsis[] == Synopsis -*push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*--dotcms-url*=__] +*push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-w*[=_watch_]] [*--dotcms-url*=__] [*--retry-attempts*=__] [*-tk*=__] [_path_] // end::picocli-generated-man-section-synopsis[] @@ -56,6 +56,9 @@ push - *dotCMS global push* *-tk, --token*=__:: A dotCMS token to use for authentication. +*-w*, *--watch*[=_watch_]:: + When this option is enabled the tool watches for file changes within the push path If a change is detected the push operation gets triggered. The default watch interval is 2 seconds, but this can set passing an int value to this option. + // end::picocli-generated-man-section-options[] // tag::picocli-generated-man-section-arguments[] diff --git a/tools/dotcms-cli/cli/docs/site-push.adoc b/tools/dotcms-cli/cli/docs/site-push.adoc index bcccf5b2c394..8aae6d717102 100644 --- a/tools/dotcms-cli/cli/docs/site-push.adoc +++ b/tools/dotcms-cli/cli/docs/site-push.adoc @@ -19,8 +19,9 @@ site-push - *Push sites* // tag::picocli-generated-man-section-synopsis[] == Synopsis -*site push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-fse*] [*-rs*] [*--dotcms-url*=__] - [*--retry-attempts*=__] [*-tk*=__] [_path_] +*site push* [*-h*] [*-dau*] [*--dry-run*] [*-ff*] [*-fse*] [*-rs*] [*-w*[=_watch_]] + [*--dotcms-url*=__] [*--retry-attempts*=__] + [*-tk*=__] [_path_] // end::picocli-generated-man-section-synopsis[] @@ -62,6 +63,9 @@ This command enables the pushing of sites to the server. It accommodates the spe *-tk, --token*=__:: A dotCMS token to use for authentication. +*-w*, *--watch*[=_watch_]:: + When this option is enabled the tool watches for file changes within the push path If a change is detected the push operation gets triggered. The default watch interval is 2 seconds, but this can set passing an int value to this option. + // end::picocli-generated-man-section-options[] // tag::picocli-generated-man-section-arguments[] 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 new file mode 100644 index 000000000000..16c589fa5634 --- /dev/null +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/api/client/util/DirectoryWatcherService.java @@ -0,0 +1,146 @@ +package com.dotcms.api.client.util; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.jboss.logging.Logger; + +public class DirectoryWatcherService { + + private final Logger logger = Logger.getLogger(DirectoryWatcherService.class); + + private final Queue> eventQueue = new ConcurrentLinkedQueue<>(); + private final Map keys = new HashMap<>(); + private WatchService watchService; + private ScheduledExecutorService scheduler; + + @SuppressWarnings("unchecked") + public void watch(final Path path, final long pollInterval, final boolean onlyShowLastEvent, final WatchEventConsumer eventConsumer) throws IOException, InterruptedException { + watchService = FileSystems.getDefault().newWatchService(); + registerAll(path); + + logger.debug("Watching directory: " + path); + + scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate(() -> { + try { + processEvents(onlyShowLastEvent, eventConsumer); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + } + }, pollInterval, pollInterval, TimeUnit.SECONDS); + + while (true) { + WatchKey key = watchService.take(); + Path dir = keys.get(key); + + if (dir == null) { + logger.error("WatchKey not recognized!!"); + continue; + } + + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + if (kind == StandardWatchEventKinds.OVERFLOW) { + continue; + } + + WatchEvent ev = (WatchEvent) event; + Path name = ev.context(); + Path child = dir.resolve(name); + + eventQueue.add(event); + logger.debug(kind.name() + ": " + child); + + if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) { + registerAll(child); + } + } + } + + boolean valid = key.reset(); + if (!valid) { + keys.remove(key); + if (keys.isEmpty()) { + logger.debug("All directories are inaccessible, stopping watch service."); + watchService.close(); + break; + } + } + } + } + + private void registerAll(final Path start) throws IOException { + Files.walkFileTree(start, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + register(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + private void register(Path dir) throws IOException { + WatchKey key = dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); + keys.put(key, dir); + logger.debug("Registered: " + dir); + } + + @SuppressWarnings("unchecked") + private void processEvents(final boolean onlyShowLastEvent, WatchEventConsumer eventConsumer) + throws IOException, InterruptedException, ExecutionException { + if (onlyShowLastEvent) { + WatchEvent lastEvent = null; + WatchEvent event; + while ((event = eventQueue.poll()) != null) { + lastEvent = event; + } + if (lastEvent != null) { + eventConsumer.accept((WatchEvent) lastEvent); + } + } else { + WatchEvent event; + while ((event = eventQueue.poll()) != null) { + eventConsumer.accept((WatchEvent) event); + } + } + } + + public void stopWatching() throws IOException { + if (scheduler != null) { + scheduler.shutdown(); + } + if (watchService != null) { + watchService.close(); + } + } + + @FunctionalInterface + public interface WatchEventConsumer { + void accept(WatchEvent event) + throws IOException, InterruptedException, ExecutionException; + } +} + + 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 961c74ada322..79b429de74a2 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 @@ -8,7 +8,12 @@ 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; @@ -16,6 +21,7 @@ import picocli.CommandLine; /** + * Global Push Command * Represents a push command that is used to push Sites, Content Types, Languages, and Files to the * server. */ @@ -74,18 +80,34 @@ public Integer call() throws Exception { .sorted(Comparator.comparingInt(DotPush::getOrder)) .collect(Collectors.toList()); - // Process each subcommand - for (var subCommand : pushCommandsSorted) { + // Usa ExecutorService for parallel execution of the subcommands + final ExecutorService executorService = Executors.newFixedThreadPool(pushCommandsSorted.size()); + final List> futures = new ArrayList<>(); - var cmdLine = createCommandLine(subCommand); + for (var subCommand : pushCommandsSorted) { + Callable task = () -> { + var cmdLine = createCommandLine(subCommand); + return cmdLine.execute(args); + }; + futures.add(executorService.submit(task)); + } - // Use execute to parse the parameters with the subcommand - int exitCode = cmdLine.execute(args); - if (exitCode != CommandLine.ExitCode.OK) { - return exitCode; + // 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); } } + executorService.shutdown(); + 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 200b838271f1..7ab7e43104ce 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 @@ -80,7 +80,7 @@ public Integer call() throws Exception { if (workspace.isEmpty()) { throw new IllegalArgumentException( String.format("No valid workspace found at path: [%s]", - this.getPushMixin().path.toPath())); + this.getPushMixin().pushPath.toPath())); } File inputFile = this.getPushMixin().path().toFile(); 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 ba1af2c62c1f..2e3ac67b4f53 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 @@ -7,6 +7,7 @@ import com.dotcms.api.client.files.PushService; import com.dotcms.api.client.files.traversal.PushTraverseParams; import com.dotcms.api.client.files.traversal.TraverseResult; +import com.dotcms.api.client.util.DirectoryWatcherService; import com.dotcms.api.traversal.TreeNode; import com.dotcms.api.traversal.TreeNodePushInfo; import com.dotcms.cli.command.DotCommand; @@ -76,50 +77,97 @@ public Integer call() throws Exception { } // Validating and resolving the workspace and path - var resolvedWorkspaceAndPath = resolveWorkspaceAndPath(); - - File finalInputFile = resolvedWorkspaceAndPath.getRight(); - CompletableFuture> - folderTraversalFuture = executor.supplyAsync( - () -> - // Service to handle the traversal of the folder - pushService.traverseLocalFolders( - output, resolvedWorkspaceAndPath.getLeft().root().toFile(), - finalInputFile, filesPushMixin.removeAssets, - filesPushMixin.removeFolders, false, pushMixin.failFast) - ); - - // ConsoleLoadingAnimation instance to handle the waiting "animation" - ConsoleLoadingAnimation consoleLoadingAnimation = new ConsoleLoadingAnimation( - output, - folderTraversalFuture - ); - - CompletableFuture animationFuture = executor.runAsync( - consoleLoadingAnimation - ); - - // Waits for the completion of both the folder traversal and console loading animation tasks. - // This line blocks the current thread until both CompletableFuture instances - // (folderTraversalFuture and animationFuture) have completed. - CompletableFuture.allOf(folderTraversalFuture, animationFuture).join(); - final var result = folderTraversalFuture.get(); - - if (result == null) { - output.error(String.format( - "Error occurred while pushing folder info: [%s].", - pushMixin.path().toAbsolutePath())); - return CommandLine.ExitCode.SOFTWARE; - } - - if (result.isEmpty()) { - output.info(String.format("\r%n" - + " ──────%n" - + " No changes in %s to push%n%n", "Files")); - } else { - pushChangesIfAny(resolvedWorkspaceAndPath.getLeft().root(), result); - } - + var resolvedWorkspaceAndPath = resolveWorkspaceAndPath(); + + if(pushMixin.isWatchOn()){ + new DirectoryWatcherService().watch(pushMixin.path(),2L, true, event -> { + System.out.println("File changed: " + event.context() + " : " + event.kind()); + + CompletableFuture> + folderTraversalFuture = executor.supplyAsync( + () -> + // Service to handle the traversal of the folder + pushService.traverseLocalFolders( + output, resolvedWorkspaceAndPath.getLeft().root().toFile(), + pushMixin.path().toFile(), filesPushMixin.removeAssets, + filesPushMixin.removeFolders, false, pushMixin.failFast) + ); + + // ConsoleLoadingAnimation instance to handle the waiting "animation" + ConsoleLoadingAnimation consoleLoadingAnimation = new ConsoleLoadingAnimation( + output, + folderTraversalFuture + ); + + CompletableFuture animationFuture = executor.runAsync( + consoleLoadingAnimation + ); + + // Waits for the completion of both the folder traversal and console loading animation tasks. + // This line blocks the current thread until both CompletableFuture instances + // (folderTraversalFuture and animationFuture) have completed. + CompletableFuture.allOf(folderTraversalFuture, animationFuture).join(); + final var result = folderTraversalFuture.get(); + if (result == null) { + output.error(String.format( + "Error occurred while pushing folder info: [%s].", + pushMixin.path().toAbsolutePath())); + + } else { + if (result.isEmpty()) { + output.info(String.format("\r%n" + + " ──────%n" + + " No changes in %s to push%n%n", "Files")); + } else { + pushChangesIfAny(resolvedWorkspaceAndPath.getLeft().root(), result); + } + } + + }); + } else { + + File finalInputFile = resolvedWorkspaceAndPath.getRight(); + CompletableFuture> + folderTraversalFuture = executor.supplyAsync( + () -> + // Service to handle the traversal of the folder + pushService.traverseLocalFolders( + output, resolvedWorkspaceAndPath.getLeft().root().toFile(), + finalInputFile, filesPushMixin.removeAssets, + filesPushMixin.removeFolders, false, pushMixin.failFast) + ); + + // ConsoleLoadingAnimation instance to handle the waiting "animation" + ConsoleLoadingAnimation consoleLoadingAnimation = new ConsoleLoadingAnimation( + output, + folderTraversalFuture + ); + + CompletableFuture animationFuture = executor.runAsync( + consoleLoadingAnimation + ); + + // Waits for the completion of both the folder traversal and console loading animation tasks. + // This line blocks the current thread until both CompletableFuture instances + // (folderTraversalFuture and animationFuture) have completed. + CompletableFuture.allOf(folderTraversalFuture, animationFuture).join(); + final var result = folderTraversalFuture.get(); + + if (result == null) { + output.error(String.format( + "Error occurred while pushing folder info: [%s].", + pushMixin.path().toAbsolutePath())); + return CommandLine.ExitCode.SOFTWARE; + } + + if (result.isEmpty()) { + output.info(String.format("\r%n" + + " ──────%n" + + " No changes in %s to push%n%n", "Files")); + } else { + pushChangesIfAny(resolvedWorkspaceAndPath.getLeft().root(), result); + } + } return CommandLine.ExitCode.OK; } 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 e94339a79a48..5c6d2b908009 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 @@ -101,9 +101,9 @@ public Integer call() throws Exception { if (workspace.isEmpty()) { var message = "No valid workspace found"; - if (this.getPushMixin().path != null) { + if (this.getPushMixin().pushPath != null) { message = String.format("No valid workspace found at path: [%s]", - this.getPushMixin().path.toPath()); + this.getPushMixin().pushPath.toPath()); } throw new IllegalArgumentException(message); diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/site/SitePush.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/site/SitePush.java index 5fc101db17c7..e4a9f558f3ef 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/site/SitePush.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/command/site/SitePush.java @@ -87,7 +87,7 @@ private int push() throws Exception { if (workspace.isEmpty()) { throw new IllegalArgumentException( String.format("No valid workspace found at path: [%s]", - this.getPushMixin().path.toPath())); + this.getPushMixin().pushPath.toPath())); } File inputFile = this.getPushMixin().path().toFile(); diff --git a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/PushMixin.java b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/PushMixin.java index 310aa185269f..9104ad33e8fb 100644 --- a/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/PushMixin.java +++ b/tools/dotcms-cli/cli/src/main/java/com/dotcms/cli/common/PushMixin.java @@ -14,7 +14,7 @@ public class PushMixin { @CommandLine.Parameters(index = "0", arity = "0..1", paramLabel = "path", description = "local directory or file to push") - public File path; + public File pushPath; @CommandLine.Option(names = {"--dry-run"}, defaultValue = "false", description = @@ -35,6 +35,16 @@ public class PushMixin { + "and the command will not retry on error.") public int retryAttempts; + @CommandLine.Option(names = {"-w","--watch"}, + arity = "0..1", + paramLabel = "watch", + fallbackValue = "2", + description = + "When this option is enabled the tool watches for file changes within the push path" + + " If a change is detected the push operation gets triggered. " + + "The default watch interval is 2 seconds, but this can set passing an int value to this option.") + public Integer interval; + @CommandLine.Option(names = {"--noValidateUnmatchedArguments"}, description = "Allows to skip the the validation of the unmatched arguments. " + "Useful for internal use when a push sub-command is called from the global push.", @@ -48,10 +58,14 @@ public class PushMixin { * @return The path of the file. */ public Path path() { - if (null == path) { + if (null == pushPath) { return Path.of("").toAbsolutePath(); } - return path.toPath(); + return pushPath.toPath(); + } + + public boolean isWatchOn(){ + return null != interval; } }