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..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 @@ -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"; @@ -989,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 @@ -1338,6 +1351,9 @@ else if (requestTracing[0].matches("\\D+")) { case logproperties: setLogPropertiesFile(new File(value)); break; + case enabledynamiclogging: + enableDynamicLogging = true; + break; case logo: generateLogo = true; break; @@ -1690,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); @@ -1774,7 +1792,6 @@ private void resetLogging() { LOGGER.log(Level.SEVERE, "Unable to reset the log manager", ex); } } - } private void configureCommandFiles() throws IOException { @@ -2195,6 +2212,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 +2398,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 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); + } + +}