001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.util;
030
031import ch.qos.logback.core.joran.spi.NoAutoStart;
032import ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy;
033import ch.qos.logback.core.rolling.RolloverFailure;
034
035import java.io.File;
036import java.io.IOException;
037
038import java.nio.file.Files;
039import java.nio.file.Path;
040import java.nio.file.Paths;
041
042import java.util.Arrays;
043import java.util.Comparator;
044
045/**
046 * This is a Logback {@literal "triggering policy"} that forces a log
047 * {@literal "roll"} upon starting McIDAS-V. This policy will also attempt to
048 * move the old {@literal "log"} directory to {@literal "archived_logs"} as well
049 * as attempting to remove the oldest {@literal "archived log files"}.
050 *
051 * <p>Credit for the initial implementation belongs to
052 * <a href="http://stackoverflow.com/a/12408445">this StackOverflow post</a>.</p>
053 */
054@NoAutoStart
055public class StartupTriggeringPolicy<E>
056        extends DefaultTimeBasedFileNamingAndTriggeringPolicy<E> {
057
058    /**
059     * Responsible for determining what to do about the {@literal "logs"} and
060     * {@literal "archived_logs"} subdirectory situation.
061     */
062    private void renameOldLogDirectory() {
063        String userpath = System.getProperty("mcv.userpath");
064        if (userpath != null) {
065            Path oldLogPath = Paths.get(userpath, "logs");
066            Path newLogPath = Paths.get(userpath, "archived_logs");
067            File oldDirectory = oldLogPath.toFile();
068            File newDirectory = newLogPath.toFile();
069
070            // T F = rename
071            // F F = attempt to create
072            // T T = remove old dir
073            // F T = noop
074            if (oldDirectory.exists() && !newDirectory.exists()) {
075                oldDirectory.renameTo(newDirectory);
076            } else if (!oldDirectory.exists() && !newDirectory.exists()) {
077                if (!newDirectory.mkdir()) {
078                    addWarn("Could not create '"+newLogPath+'\'');
079                } else {
080                    addInfo("Created '"+newLogPath+'\'');
081                }
082            } else if (oldDirectory.exists() && newDirectory.exists()) {
083                addWarn("Both log directories exist; moving files from '" + oldLogPath + "' and attempting to delete");
084                removeOldLogDirectory(oldDirectory, newDirectory);
085            } else if (!oldDirectory.exists() && newDirectory.exists()) {
086                addInfo('\''+oldLogPath.toString()+"' does not exist; no cleanup is required");
087            } else {
088                addWarn("Unknown state! oldDirectory.exists()='"+oldDirectory.exists()+"' newDirectory.exists()='"+newDirectory.exists()+'\'');
089            }
090        }
091    }
092
093    /**
094     * Fires off a thread that moves all files within {@code oldDirectory}
095     * into {@code newDirectory}, and then attempts to remove
096     * {@code oldDirectory}.
097     *
098     * @param oldDirectory {@literal "Old"} log file directory. Be aware that
099     * any files within this directory will be relocated to
100     * {@code newDirectory} and this directory will then be removed. Cannot be
101     * {@code null}.
102     * @param newDirectory Destination for any files within
103     * {@code oldDirectory}. Cannot be {@code null}.
104     */
105    private void removeOldLogDirectory(File oldDirectory, File newDirectory) {
106        File[] files = oldDirectory.listFiles();
107        new Thread(asyncClearFiles(oldDirectory, newDirectory, files)).start();
108    }
109
110    /**
111     * Moves all files within {@code oldDirectory} into {@code newDirectory},
112     * and then removes {@code oldDirectory}.
113     *
114     * @param oldDirectory {@literal "Old"} log file directory. Cannot be
115     * {@code null}.
116     * @param newDirectory {@literal "New"} log file directory. Cannot be
117     * {@code null}.
118     * @param files {link File Files} within {@code oldDirectory} that should
119     * be moved to {@code newDirectory}. Cannot be {@code null}.
120     *
121     * @return Thread that will attempt to relocate any files within
122     * {@code oldDirectory} to {@code newDirectory} and then attempt removal
123     * of {@code oldDirectory}. Be aware that this thread has not yet had
124     * {@literal "start"} called.
125     */
126    private Runnable asyncClearFiles(final File oldDirectory,
127                                     final File newDirectory,
128                                     final File[] files)
129    {
130        return new Runnable() {
131            public void run() {
132                boolean success = true;
133                for (File f : files) {
134                    File newPath = new File(newDirectory, f.getName());
135                    if (f.renameTo(newPath)) {
136                        addInfo("Moved '"+f.getAbsolutePath()+"' to '"+newPath.getAbsolutePath()+'\'');
137                    } else {
138                        success = false;
139                        addWarn("Could not move '"+f.getAbsolutePath()+"' to '"+newPath.getAbsolutePath()+'\'');
140                    }
141                }
142                if (success) {
143                    if (oldDirectory.delete()) {
144                        addInfo("Removed '"+oldDirectory.getAbsolutePath()+'\'');
145                    } else {
146                        addWarn("Could not remove '"+oldDirectory.getAbsolutePath()+'\'');
147                    }
148                }
149            }
150        };
151    }
152
153    /**
154     * Finds the archived log files and determines whether or not {@link #asyncCleanFiles(int, java.io.File[])}
155     * should be called (and if it should, this method calls it).
156     *
157     * @param keepFiles Number of archived log files to keep around.
158     */
159    private void cleanupArchivedLogs(int keepFiles) {
160        String userpath = System.getProperty("mcv.userpath");
161        if (userpath != null) {
162            Path logDirectory = Paths.get(userpath, "archived_logs");
163            File[] files = logDirectory.toFile().listFiles();
164            if ((files != null) && (files.length > keepFiles)) {
165                new Thread(asyncCleanFiles(keepFiles, files)).start();
166            }
167            new Thread(asyncCleanReallyOldFiles()).start();
168        }
169    }
170
171    /**
172     * Removes log files archived by a very preliminary version of our Logback
173     * configuration. These files reside within the userpath, and are named
174     * {@literal "mcidasv.1.log.zip"}, {@literal "mcidasv.2.log.zip"}, and
175     * {@literal "mcidasv.3.log.zip"}.
176     *
177     * @return Thread that will attempt to remove the three archived log files.
178     */
179    private Runnable asyncCleanReallyOldFiles() {
180        return new Runnable() {
181            public void run() {
182                String userpath = System.getProperty("mcv.userpath");
183                if (userpath != null) {
184                    Path userDirectory = Paths.get(userpath);
185                    removePath(userDirectory.resolve("mcidasv.1.log.zip"));
186                    removePath(userDirectory.resolve("mcidasv.2.log.zip"));
187                    removePath(userDirectory.resolve("mcidasv.3.log.zip"));
188                }
189            }
190        };
191    }
192
193    /**
194     * Convenience method that attempts to delete {@code pathToRemove}.
195     *
196     * @param pathToRemove {@code Path} to the file to delete.
197     * Cannot be {@code null}.
198     */
199    private void removePath(Path pathToRemove) {
200        try {
201            if (Files.deleteIfExists(pathToRemove)) {
202                addInfo("removing '"+pathToRemove+'\'');
203            }
204        } catch (IOException e) {
205            addError("An exception occurred while trying to remove '"+pathToRemove+'\'', e);
206        }
207    }
208
209    /**
210     * Creates a thread that attempts to remove all but the {@code keep} oldest
211     * files in {@code files} (by using the last modified times).
212     *
213     * @param keep Number of archived log files to keep around.
214     * @param files Archived log files. Cannot be {@code null}.
215     *
216     * @return Thread that will attempt to remove everything except the
217     * specified number of archived log files. Be aware that this thread has
218     * not yet had {@literal "start"} called.
219     */
220    private Runnable asyncCleanFiles(final int keep, final File[] files) {
221        return new Runnable() {
222            public void run() {
223                Arrays.sort(files, new Comparator<File>() {
224                    public int compare(File f1, File f2) {
225                        return Long.valueOf(f2.lastModified()).compareTo(f1.lastModified());
226                    }
227                });
228                for (int i = keep-1; i < files.length; i++) {
229                    addInfo("removing '"+files[i]+'\'');
230                    try {
231                        Files.deleteIfExists(files[i].toPath());
232                    } catch (IOException e) {
233                        addWarn("An exception occurred while trying to remove '"+files[i]+'\'');
234                    }
235                }
236            }
237        };
238    }
239
240    /**
241     * Triggers a {@literal "logback rollover"} and calls
242     * {@link #cleanupArchivedLogs(int)}.
243     */
244    @Override public void start() {
245        renameOldLogDirectory();
246        super.start();
247        nextCheck = 0L;
248        isTriggeringEvent(null, null);
249        try {
250            tbrp.rollover();
251            int maxHistory = tbrp.getMaxHistory();
252            if (maxHistory > 0) {
253                addInfo("keep "+maxHistory+" most recent archived logs");
254                cleanupArchivedLogs(maxHistory);
255            } else {
256                addInfo("maxHistory not set; not cleaning archiving logs");
257            }
258        } catch (RolloverFailure e) {
259            addError("could not perform rollover of log file", e);
260        }
261    }
262}