001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2022, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.classic.joran;
015
016import java.io.File;
017import java.io.FileInputStream;
018import java.net.URL;
019import java.util.List;
020import java.util.concurrent.ScheduledFuture;
021
022import ch.qos.logback.classic.LoggerContext;
023import ch.qos.logback.core.CoreConstants;
024import ch.qos.logback.core.joran.spi.ConfigurationWatchList;
025import ch.qos.logback.core.joran.spi.JoranException;
026import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
027import ch.qos.logback.core.model.Model;
028import ch.qos.logback.core.model.ModelUtil;
029import ch.qos.logback.core.spi.ConfigurationEvent;
030import ch.qos.logback.core.spi.ContextAwareBase;
031import ch.qos.logback.core.status.StatusUtil;
032
033import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
034import static ch.qos.logback.core.spi.ConfigurationEvent.*;
035
036public class ReconfigureOnChangeTask extends ContextAwareBase implements Runnable {
037
038    public static final String DETECTED_CHANGE_IN_CONFIGURATION_FILES = "Detected change in configuration files.";
039    static final String RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION = "Re-registering previous fallback configuration once more as a fallback configuration point";
040    static final String FALLING_BACK_TO_SAFE_CONFIGURATION = "Given previous errors, falling back to previously registered safe configuration.";
041
042    long birthdate = System.currentTimeMillis();
043    List<ReconfigureOnChangeTaskListener> listeners = null;
044
045    ScheduledFuture<?> scheduledFuture;
046
047    @Override
048    public void run() {
049        context.fireConfigurationEvent(newConfigurationChangeDetectorRunningEvent(this));
050
051        ConfigurationWatchList configurationWatchList = ConfigurationWatchListUtil.getConfigurationWatchList(context);
052        if (configurationWatchList == null) {
053            addWarn("Empty ConfigurationWatchList in context");
054            return;
055        }
056
057        List<File> filesToWatch = configurationWatchList.getCopyOfFileWatchList();
058        if (filesToWatch == null || filesToWatch.isEmpty()) {
059            addInfo("Empty watch file list. Disabling ");
060            return;
061        }
062        File changedFile = configurationWatchList.changeDetected();
063        if (changedFile == null) {
064            return;
065        }
066        context.fireConfigurationEvent(ConfigurationEvent.newConfigurationChangeDetectedEvent(this));
067
068        addInfo(DETECTED_CHANGE_IN_CONFIGURATION_FILES);
069        addInfo(CoreConstants.RESET_MSG_PREFIX + "named [" + context.getName() + "]");
070
071        if(changedFile.getName().endsWith(PROPERTIES_FILE_EXTENSION)) {
072            runPropertiesConfigurator(changedFile);
073            // no further processing
074            return;
075        }
076
077        // ======== fuller processing below
078
079        cancelFutureInvocationsOfThisTaskInstance();
080        URL mainConfigurationURL = configurationWatchList.getMainURL();
081
082        LoggerContext lc = (LoggerContext) context;
083        if (mainConfigurationURL.toString().endsWith("xml")) {
084            performXMLConfiguration(lc, mainConfigurationURL);
085        } else if (mainConfigurationURL.toString().endsWith("groovy")) {
086            addError("Groovy configuration disabled due to Java 9 compilation issues.");
087        }
088        //fireDoneReconfiguring();
089    }
090
091    private void runPropertiesConfigurator(File changedFile) {
092        addInfo("Will run PropertyConfigurator on "+changedFile.getAbsolutePath());
093        PropertyConfigurator propertyConfigurator = new PropertyConfigurator();
094        propertyConfigurator.setContext(context);
095        try {
096            propertyConfigurator.doConfigure(changedFile);
097            context.fireConfigurationEvent(newPartialConfigurationEndedSuccessfullyEvent(this));
098        } catch (JoranException e) {
099            addError("Failed to reload "+ changedFile);
100        }
101    }
102
103    private void cancelFutureInvocationsOfThisTaskInstance() {
104        boolean result = scheduledFuture.cancel(false);
105        if(!result) {
106            addWarn("could not cancel "+ this.toString());
107        }
108    }
109
110    private void performXMLConfiguration(LoggerContext lc, URL mainConfigurationURL) {
111        JoranConfigurator jc = new JoranConfigurator();
112        jc.setContext(context);
113        StatusUtil statusUtil = new StatusUtil(context);
114        Model failsafeTop = jc.recallSafeConfiguration();
115        URL mainURL = ConfigurationWatchListUtil.getMainWatchURL(context);
116        addInfo("Resetting loggerContext ["+lc.getName()+"]");
117        lc.reset();
118        long threshold = System.currentTimeMillis();
119        try {
120            jc.doConfigure(mainConfigurationURL);
121            // e.g. IncludeAction will add a status regarding XML parsing errors but no exception will reach here
122            if (statusUtil.hasXMLParsingErrors(threshold)) {
123                fallbackConfiguration(lc, failsafeTop, mainURL);
124            }
125        } catch (JoranException e) {
126            addWarn("Exception occurred during reconfiguration", e);
127            fallbackConfiguration(lc, failsafeTop, mainURL);
128        }
129    }
130
131    private void fallbackConfiguration(LoggerContext lc, Model failsafeTop, URL mainURL) {
132        // failsafe events are used only in case of errors. Therefore, we must *not*
133        // invoke file inclusion since the included files may be the cause of the error.
134
135        // List<SaxEvent> failsafeEvents = removeIncludeEvents(eventList);
136        JoranConfigurator joranConfigurator = new JoranConfigurator();
137        joranConfigurator.setContext(context);
138        ConfigurationWatchList oldCWL = ConfigurationWatchListUtil.getConfigurationWatchList(context);
139        ConfigurationWatchList newCWL = oldCWL.buildClone();
140
141        if (failsafeTop == null) {
142            addWarn("No previous configuration to fall back on.");
143            return;
144        } else {
145            addWarn(FALLING_BACK_TO_SAFE_CONFIGURATION);
146            addInfo("Safe model "+failsafeTop);
147            try {
148                lc.reset();
149                ConfigurationWatchListUtil.registerConfigurationWatchList(context, newCWL);
150                ModelUtil.resetForReuse(failsafeTop);
151                joranConfigurator.processModel(failsafeTop);
152                addInfo(RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION);
153                joranConfigurator.registerSafeConfiguration(failsafeTop);
154                context.fireConfigurationEvent(newConfigurationEndedSuccessfullyEvent(this));
155            } catch (Exception e) {
156                addError("Unexpected exception thrown by a configuration considered safe.", e);
157            }
158        }
159    }
160
161    @Override
162    public String toString() {
163        return "ReconfigureOnChangeTask(born:" + birthdate + ")";
164    }
165
166
167    public void setScheduredFuture(ScheduledFuture<?> aScheduledFuture) {
168        this.scheduledFuture = aScheduledFuture;
169    }
170}