001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2025
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;
044import java.util.concurrent.atomic.AtomicLong;
045
046/**
047 * This is a Logback {@literal "triggering policy"} that forces a log
048 * {@literal "roll"} upon starting McIDAS-V. This policy will also attempt to
049 * move the old {@literal "log"} directory to {@literal "archived_logs"} as well
050 * as attempting to remove the oldest {@literal "archived log files"}.
051 *
052 * <p>Credit for the initial implementation belongs to
053 * <a href="http://stackoverflow.com/a/12408445">this StackOverflow post</a>.</p>
054 */
055@NoAutoStart
056public class StartupTriggeringPolicy<E>
057        extends DefaultTimeBasedFileNamingAndTriggeringPolicy<E> {
058
059    /**
060     * Responsible for determining what to do about the {@literal "logs"} and
061     * {@literal "archived_logs"} subdirectory situation.
062     */
063    private void renameOldLogDirectory() {
064        String userpath = System.getProperty("mcv.userpath");
065        if (userpath != null) {
066            Path oldLogPath = Paths.get(userpath, "logs");
067            Path newLogPath = Paths.get(userpath, "archived_logs");
068            File oldDirectory = oldLogPath.toFile();
069            File newDirectory = newLogPath.toFile();
070
071            // T F = rename
072            // F F = attempt to create
073            // T T = remove old dir
074            // F T = noop
075            if (oldDirectory.exists() && !newDirectory.exists()) {
076                oldDirectory.renameTo(newDirectory);
077            } else if (!oldDirectory.exists() && !newDirectory.exists()) {
078                if (!newDirectory.mkdir()) {
079                    addWarn("Could not create '"+newLogPath+'\'');
080                } else {
081                    addInfo("Created '"+newLogPath+'\'');
082                }
083            } else if (oldDirectory.exists() && newDirectory.exists()) {
084                addWarn("Both log directories exist; moving files from '" + oldLogPath + "' and attempting to delete");
085                removeOldLogDirectory(oldDirectory, newDirectory);
086            } else if (!oldDirectory.exists() && newDirectory.exists()) {
087                addInfo('\''+oldLogPath.toString()+"' does not exist; no cleanup is required");
088            } else {
089                addWarn("Unknown state! oldDirectory.exists()='"+oldDirectory.exists()+"' newDirectory.exists()='"+newDirectory.exists()+'\'');
090            }
091        }
092    }
093
094    /**
095     * Fires off a thread that moves all files within {@code oldDirectory}
096     * into {@code newDirectory}, and then attempts to remove
097     * {@code oldDirectory}.
098     *
099     * @param oldDirectory {@literal "Old"} log file directory. Be aware that
100     * any files within this directory will be relocated to
101     * {@code newDirectory} and this directory will then be removed. Cannot be
102     * {@code null}.
103     * @param newDirectory Destination for any files within
104     * {@code oldDirectory}. Cannot be {@code null}.
105     */
106    private void removeOldLogDirectory(File oldDirectory, File newDirectory) {
107        File[] files = oldDirectory.listFiles();
108        new Thread(asyncClearFiles(oldDirectory, newDirectory, files)).start();
109    }
110
111    /**
112     * Moves all files within {@code oldDirectory} into {@code newDirectory},
113     * and then removes {@code oldDirectory}.
114     *
115     * @param oldDirectory {@literal "Old"} log file directory. Cannot be
116     * {@code null}.
117     * @param newDirectory {@literal "New"} log file directory. Cannot be
118     * {@code null}.
119     * @param files {link File Files} within {@code oldDirectory} that should
120     * be moved to {@code newDirectory}. Cannot be {@code null}.
121     *
122     * @return Thread that will attempt to relocate any files within
123     * {@code oldDirectory} to {@code newDirectory} and then attempt removal
124     * of {@code oldDirectory}. Be aware that this thread has not yet had
125     * {@literal "start"} called.
126     */
127    private Runnable asyncClearFiles(final File oldDirectory,
128                                     final File newDirectory,
129                                     final File[] files)
130    {
131        return new Runnable() {
132            public void run() {
133                boolean success = true;
134                for (File f : files) {
135                    File newPath = new File(newDirectory, f.getName());
136                    if (f.renameTo(newPath)) {
137                        addInfo("Moved '"+f.getAbsolutePath()+"' to '"+newPath.getAbsolutePath()+'\'');
138                    } else {
139                        success = false;
140                        addWarn("Could not move '"+f.getAbsolutePath()+"' to '"+newPath.getAbsolutePath()+'\'');
141                    }
142                }
143                if (success) {
144                    if (oldDirectory.delete()) {
145                        addInfo("Removed '"+oldDirectory.getAbsolutePath()+'\'');
146                    } else {
147                        addWarn("Could not remove '"+oldDirectory.getAbsolutePath()+'\'');
148                    }
149                }
150            }
151        };
152    }
153
154    /**
155     * Finds the archived log files and determines whether or not {@link #asyncCleanFiles(int, java.io.File[])}
156     * should be called (and if it should, this method calls it).
157     *
158     * @param keepFiles Number of archived log files to keep around.
159     */
160    private void cleanupArchivedLogs(int keepFiles) {
161        String userpath = System.getProperty("mcv.userpath");
162        if (userpath != null) {
163            Path logDirectory = Paths.get(userpath, "archived_logs");
164            File[] files = logDirectory.toFile().listFiles();
165            if ((files != null) && (files.length > keepFiles)) {
166                new Thread(asyncCleanFiles(keepFiles, files)).start();
167            }
168            new Thread(asyncCleanReallyOldFiles()).start();
169        }
170    }
171
172    /**
173     * Removes log files archived by a very preliminary version of our Logback
174     * configuration. These files reside within the userpath, and are named
175     * {@literal "mcidasv.1.log.zip"}, {@literal "mcidasv.2.log.zip"}, and
176     * {@literal "mcidasv.3.log.zip"}.
177     *
178     * @return Thread that will attempt to remove the three archived log files.
179     */
180    private Runnable asyncCleanReallyOldFiles() {
181        return new Runnable() {
182            public void run() {
183                String userpath = System.getProperty("mcv.userpath");
184                if (userpath != null) {
185                    Path userDirectory = Paths.get(userpath);
186                    removePath(userDirectory.resolve("mcidasv.1.log.zip"));
187                    removePath(userDirectory.resolve("mcidasv.2.log.zip"));
188                    removePath(userDirectory.resolve("mcidasv.3.log.zip"));
189                }
190            }
191        };
192    }
193
194    /**
195     * Convenience method that attempts to delete {@code pathToRemove}.
196     *
197     * @param pathToRemove {@code Path} to the file to delete.
198     * Cannot be {@code null}.
199     */
200    private void removePath(Path pathToRemove) {
201        try {
202            if (Files.deleteIfExists(pathToRemove)) {
203                addInfo("removing '"+pathToRemove+'\'');
204            }
205        } catch (IOException e) {
206            addError("An exception occurred while trying to remove '"+pathToRemove+'\'', e);
207        }
208    }
209
210    /**
211     * Creates a thread that attempts to remove all but the {@code keep} oldest
212     * files in {@code files} (by using the last modified times).
213     *
214     * @param keep Number of archived log files to keep around.
215     * @param files Archived log files. Cannot be {@code null}.
216     *
217     * @return Thread that will attempt to remove everything except the
218     * specified number of archived log files. Be aware that this thread has
219     * not yet had {@literal "start"} called.
220     */
221    private Runnable asyncCleanFiles(final int keep, final File[] files) {
222        return new Runnable() {
223            public void run() {
224                Arrays.sort(files, new Comparator<File>() {
225                    public int compare(File f1, File f2) {
226                        return Long.valueOf(f2.lastModified()).compareTo(f1.lastModified());
227                    }
228                });
229                for (int i = keep-1; i < files.length; i++) {
230                    addInfo("removing '"+files[i]+'\'');
231                    try {
232                        Files.deleteIfExists(files[i].toPath());
233                    } catch (IOException e) {
234                        addWarn("An exception occurred while trying to remove '"+files[i]+'\'');
235                    }
236                }
237            }
238        };
239    }
240
241    /**
242     * Triggers a {@literal "logback rollover"} and calls
243     * {@link #cleanupArchivedLogs(int)}.
244     */
245    @Override public void start() {
246        renameOldLogDirectory();
247        super.start();
248        this.atomicNextCheck = new AtomicLong(0L);
249        isTriggeringEvent(null, null);
250        try {
251            tbrp.rollover();
252            int maxHistory = tbrp.getMaxHistory();
253            if (maxHistory > 0) {
254                addInfo("keep "+maxHistory+" most recent archived logs");
255                cleanupArchivedLogs(maxHistory);
256            } else {
257                addInfo("maxHistory not set; not cleaning archiving logs");
258            }
259        } catch (RolloverFailure e) {
260            addError("could not perform rollover of log file", e);
261        }
262    }
263}