From b13fe4b88dca52f3cc7212e91880a505dfc19ea5 Mon Sep 17 00:00:00 2001 From: Matthew Gill Date: Mon, 22 Jun 2020 17:54:10 +0100 Subject: [PATCH 1/3] FISH-205 Create File Watcher Service Created a file watching service that can be interacted with statically to allow the Payara Micro boot sequence to queue up paths to watch before HK2 starts. Works by allowing any classto register actions to complete when a specific file or path is modified. The static methods will still work post-startup as the loop checks the static list. Signed-off-by: Matthew Gill --- .../executorservice/PayaraFileWatcher.java | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 nucleus/payara-modules/payara-executor-service/src/main/java/fish/payara/nucleus/executorservice/PayaraFileWatcher.java diff --git a/nucleus/payara-modules/payara-executor-service/src/main/java/fish/payara/nucleus/executorservice/PayaraFileWatcher.java b/nucleus/payara-modules/payara-executor-service/src/main/java/fish/payara/nucleus/executorservice/PayaraFileWatcher.java new file mode 100644 index 00000000000..27dba7f33c7 --- /dev/null +++ b/nucleus/payara-modules/payara-executor-service/src/main/java/fish/payara/nucleus/executorservice/PayaraFileWatcher.java @@ -0,0 +1,231 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) [2020] Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.nucleus.executorservice; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.logging.Level.WARNING; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.glassfish.api.StartupRunLevel; +import org.glassfish.api.event.EventListener; +import org.glassfish.api.event.EventTypes; +import org.glassfish.api.event.Events; +import org.glassfish.hk2.runlevel.RunLevel; +import org.glassfish.internal.deployment.Deployment; +import org.jvnet.hk2.annotations.Service; + +@Service(name = "payara-file-watcher") +@RunLevel(StartupRunLevel.VAL) +public class PayaraFileWatcher implements EventListener { + + private static final Logger LOGGER = Logger.getLogger(PayaraFileWatcher.class.getName()); + + private static final Map LISTENER_MAP = new HashMap<>(); + private static final Set PATHS_TO_WATCH = new HashSet<>(); + + private volatile boolean running; + + private WatchService watcher; + + @Inject + private Events events; + + @Inject + private PayaraExecutorService executor; + + // Lifecycle methods + + @PostConstruct + protected void postConstruct() { + if (events != null) { + events.register(this); + } + initialise(); + } + + @PreDestroy + protected void preDestroy(){ + if (events != null) { + events.unregister(this); + } + terminate(); + } + + @Override + public void event(Event event) { + if (event.is(Deployment.ALL_APPLICATIONS_LOADED)) { + // Embedded containers can be started and stopped multiple times. + // Thus we need to initialize anytime the server instance is started. + initialise(); + } else if (event.is(EventTypes.SERVER_SHUTDOWN)) { + terminate(); + } + } + + // Private methods + + /** + * The event loop method + */ + private void run() { + registerQueuedPaths(); + try { + // Block, waiting for an event on a watched file + WatchKey key = watcher.take(); + + // Loop through the events + for (WatchEvent event : key.pollEvents()) { + // Find the absolute path of the modified file + final Path modifiedPath = ((Path) key.watchable()).resolve((Path) event.context()); + + // Loop through the watched paths to find the action associated with it + Iterator> watchIterator = LISTENER_MAP.entrySet().iterator(); + while (watchIterator.hasNext()) { + Entry watchEntry = watchIterator.next(); + + // If this entry corresponds to the modified file + if (modifiedPath.endsWith(watchEntry.getKey())) { + // Ignore overflow events + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + + File modifiedFile = modifiedPath.toFile(); + + // If it's been deleted, remove the file watcher + if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE && !modifiedFile.exists()) { + LOGGER.info(format("Watched file %s was deleted; removing the file watcher", modifiedPath)); + watchIterator.remove(); + } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY && modifiedFile.length() > 0) { + // Run the associated action + LOGGER.fine(format("Watched file %s modified, running the listener", modifiedPath)); + watchEntry.getValue().run(); + } + } + } + } + key.reset(); + } catch (InterruptedException ex) { + LOGGER.log(WARNING, "The file watcher thread was interrupted", ex); + } + } + + /** + * Empty the map of file listeners, registering each one with the watch service + */ + private void registerQueuedPaths() { + if (!PATHS_TO_WATCH.isEmpty()) { + + // Iterate through paths + Iterator pathIterator = PATHS_TO_WATCH.iterator(); + while (pathIterator.hasNext()) { + + // Get the directory of a registered path + Path path = pathIterator.next(); + if (path.toFile().isFile()) { + path = path.getParent(); + } + + try { + path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); + LOGGER.fine(format("Watching path: %s", path)); + } catch (IOException ex) { + LOGGER.log(WARNING, format("Failed to register path %s with the watch service", path), ex); + } + + pathIterator.remove(); + } + } + } + + private synchronized void initialise() { + if (!running && !PATHS_TO_WATCH.isEmpty()) { + try { + watcher = FileSystems.getDefault().newWatchService(); + executor.scheduleWithFixedDelay(this::run, 0, 1, SECONDS); + running = true; + LOGGER.info("Initialised the file watcher service"); + } catch (IOException ex) { + LOGGER.log(WARNING, "Failed to initialise the watch service", ex); + } + } + } + + + private synchronized void terminate() { + if (running) { + try { + watcher.close(); + running = false; + LOGGER.info("Terminated the file watcher service"); + } catch (IOException ex) { + LOGGER.log(WARNING, "Failed to terminate the watch service", ex); + } + } + } + + // Static methods + + public static void watch(Path path, Runnable runnable) { + PATHS_TO_WATCH.add(path); + LISTENER_MAP.put(path, runnable); + } + +} From b9989b8016757936b9a72602a84a557a5f939e76 Mon Sep 17 00:00:00 2001 From: Matthew Gill Date: Mon, 22 Jun 2020 17:58:44 +0100 Subject: [PATCH 2/3] FISH-205 Add enableDynamicLogging Micro flag The flag isn't currently linked to anything, but whilst its non-functional the groundwork should still be in place to work properly in Payara Micro. Signed-off-by: Matthew Gill --- .../micro/cmd/options/RUNTIME_OPTION.java | 1 + .../payara/micro/impl/PayaraMicroImpl.java | 46 +++++++++++-------- .../main/resources/commandoptions.properties | 3 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/cmd/options/RUNTIME_OPTION.java b/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/cmd/options/RUNTIME_OPTION.java index ef0a78b3682..24655a3ad35 100644 --- a/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/cmd/options/RUNTIME_OPTION.java +++ b/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/cmd/options/RUNTIME_OPTION.java @@ -85,6 +85,7 @@ public enum RUNTIME_OPTION { version(false), logtofile(true, new FileValidator(false, false, false)), logproperties(true, new FileValidator(true, true, false)), + enabledynamiclogging(false), accesslog(true, new DirectoryValidator(true, true, true)), accesslogformat(true), accessloginterval(true), diff --git a/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java b/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java index 566eb0f7638..b080bac890b 100644 --- a/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java +++ b/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java @@ -39,24 +39,6 @@ */ package fish.payara.micro.impl; -import com.sun.appserv.server.util.Version; -import com.sun.enterprise.glassfish.bootstrap.Constants; -import com.sun.enterprise.glassfish.bootstrap.GlassFishImpl; -import com.sun.enterprise.server.logging.ODLLogFormatter; -import fish.payara.appserver.rest.endpoints.config.admin.ListRestEndpointsCommand; -import fish.payara.boot.runtime.BootCommand; -import fish.payara.boot.runtime.BootCommands; -import fish.payara.deployment.util.GAVConvertor; -import fish.payara.micro.BootstrapException; -import fish.payara.micro.PayaraMicroRuntime; -import fish.payara.micro.boot.AdminCommandRunner; -import fish.payara.micro.boot.PayaraMicroBoot; -import fish.payara.micro.boot.loader.OpenURLClassLoader; -import fish.payara.micro.cmd.options.RUNTIME_OPTION; -import fish.payara.micro.cmd.options.RuntimeOptions; -import fish.payara.micro.cmd.options.ValidationException; -import fish.payara.micro.data.InstanceDescriptor; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -71,6 +53,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Path; import java.util.Arrays; import java.util.Enumeration; import java.util.LinkedHashMap; @@ -89,6 +72,12 @@ import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; + +import com.sun.appserv.server.util.Version; +import com.sun.enterprise.glassfish.bootstrap.Constants; +import com.sun.enterprise.glassfish.bootstrap.GlassFishImpl; +import com.sun.enterprise.server.logging.ODLLogFormatter; + import org.glassfish.embeddable.BootstrapProperties; import org.glassfish.embeddable.CommandRunner; import org.glassfish.embeddable.Deployer; @@ -98,6 +87,21 @@ import org.glassfish.embeddable.GlassFishProperties; import org.glassfish.embeddable.GlassFishRuntime; +import fish.payara.appserver.rest.endpoints.config.admin.ListRestEndpointsCommand; +import fish.payara.boot.runtime.BootCommand; +import fish.payara.boot.runtime.BootCommands; +import fish.payara.deployment.util.GAVConvertor; +import fish.payara.micro.BootstrapException; +import fish.payara.micro.PayaraMicroRuntime; +import fish.payara.micro.boot.AdminCommandRunner; +import fish.payara.micro.boot.PayaraMicroBoot; +import fish.payara.micro.boot.loader.OpenURLClassLoader; +import fish.payara.micro.cmd.options.RUNTIME_OPTION; +import fish.payara.micro.cmd.options.RuntimeOptions; +import fish.payara.micro.cmd.options.ValidationException; +import fish.payara.micro.data.InstanceDescriptor; +import fish.payara.nucleus.executorservice.PayaraFileWatcher; + /** * Main class for Bootstrapping Payara Micro Edition This class is used from * applications to create a full JavaEE runtime environment and deploy war @@ -146,6 +150,7 @@ public class PayaraMicroImpl implements PayaraMicroBoot { private boolean enableAccessLog = false; private boolean enableAccessLogFormat = false; private boolean logPropertiesFile = false; + private boolean enableDynamicLogging; private String userLogPropertiesFile = ""; private int autoBindRange = 50; private String bootImage = "MICRO-INF/domain/boot.txt"; @@ -1338,6 +1343,9 @@ else if (requestTracing[0].matches("\\D+")) { case logproperties: setLogPropertiesFile(new File(value)); break; + case enabledynamiclogging: + enableDynamicLogging = true; + break; case logo: generateLogo = true; break; @@ -2195,6 +2203,7 @@ private void setArgumentsFromSystemProperties() { userLogFile = getProperty("payaramicro.userLogFile"); enableAccessLog = getBooleanProperty("payaramicro.enableAccessLog"); enableAccessLogFormat = getBooleanProperty("payaramicro.logPropertiesFile"); + enableDynamicLogging = getBooleanProperty("payaramicro.enableDynamicLogging"); enableHealthCheck = getBooleanProperty("payaramicro.enableHealthCheck"); httpPort = getIntegerProperty("payaramicro.port", Integer.MIN_VALUE); sslPort = getIntegerProperty("payaramicro.sslPort", Integer.MIN_VALUE); @@ -2380,6 +2389,7 @@ private void packageUberJar() { props.setProperty("payaramicro.enableAccessLog", Boolean.toString(enableAccessLog)); props.setProperty("payaramicro.enableAccessLogFormat", Boolean.toString(enableAccessLogFormat)); props.setProperty("payaramicro.logPropertiesFile", Boolean.toString(logPropertiesFile)); + props.setProperty("payaramicro.enableDynamicLogging", Boolean.toString(enableDynamicLogging)); props.setProperty("payaramicro.noCluster", Boolean.toString(noCluster)); props.setProperty("payaramicro.hostAware", Boolean.toString(hostAware)); props.setProperty("payaramicro.disablePhoneHome", Boolean.toString(disablePhoneHome)); diff --git a/appserver/extras/payara-micro/payara-micro-core/src/main/resources/commandoptions.properties b/appserver/extras/payara-micro/payara-micro-core/src/main/resources/commandoptions.properties index 35660028dd0..e6a120098c5 100644 --- a/appserver/extras/payara-micro/payara-micro-core/src/main/resources/commandoptions.properties +++ b/appserver/extras/payara-micro/payara-micro-core/src/main/resources/commandoptions.properties @@ -1,7 +1,7 @@ # # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. # - # Copyright (c) 2016-2019 Payara Foundation and/or its affiliates. All rights reserved. + # Copyright (c) 2016-2020 Payara Foundation and/or its affiliates. All rights reserved. # # The contents of this file are subject to the terms of either the GNU # General Public License Version 2 only ("GPL") or the Common Development @@ -73,6 +73,7 @@ disablephonehome=Disables sending of usage tracking information version=Displays the version information logtofile= outputs all the Log entries to a user defined file logproperties= Allows user to set their own logging properties file +enabledynamiclogging=Watch the logging properties file for changes, and apply them dynamically accesslog= Sets user defined directory path for the access log accesslogformat=Sets user defined log format for the access log accessloginterval=Sets user defined log interval for the access log From b41215f69850e06cc9fbf311cd3c56b6bd306bf9 Mon Sep 17 00:00:00 2001 From: Matthew Gill Date: Tue, 23 Jun 2020 11:33:15 +0100 Subject: [PATCH 3/3] FISH-205 Implement dynamic logging functionality The --enableDynamicLogging option now makes use of the file watcher service to dynamically update logging properties. Signed-off-by: Matthew Gill --- .../payara/micro/impl/PayaraMicroImpl.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java b/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java index b080bac890b..99a377560c2 100644 --- a/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java +++ b/appserver/extras/payara-micro/payara-micro-core/src/main/java/fish/payara/micro/impl/PayaraMicroImpl.java @@ -994,7 +994,15 @@ public PayaraMicroRuntime bootStrap() throws BootstrapException { } catch (IOException | URISyntaxException ex) { throw new BootstrapException("Problem unpacking the Runtime", ex); } - resetLogging(); + final String loggingProperty = System.getProperty("java.util.logging.config.file"); + resetLogging(loggingProperty); + // If it's been enabled, watch the log file for changes + if (enableDynamicLogging) { + PayaraFileWatcher.watch(new File(loggingProperty).toPath(), () -> { + LOGGER.info("Logging file modified, resetting logging"); + resetLogging(loggingProperty); + }); + } runtimeDir.processDirectoryInformation(); // build the runtime @@ -1698,9 +1706,11 @@ public void run() { }); } - private void resetLogging() { - - String loggingProperty = System.getProperty("java.util.logging.config.file"); + /** + * Reset the logging properties from the given file. + * @param loggingProperty the location of the file to read from. + */ + private void resetLogging(String loggingProperty) { if (loggingProperty != null) { // we need to copy into the unpacked domain the specified logging.properties file File file = new File(loggingProperty); @@ -1782,7 +1792,6 @@ private void resetLogging() { LOGGER.log(Level.SEVERE, "Unable to reset the log manager", ex); } } - } private void configureCommandFiles() throws IOException {