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}