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 java.io.File; 032import java.io.FileOutputStream; 033import java.io.IOException; 034 035import java.time.Instant; 036import java.util.Date; 037import java.util.concurrent.Future; 038 039import ch.qos.logback.core.rolling.RolloverFailure; 040import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy; 041import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; 042import ch.qos.logback.core.rolling.helper.ArchiveRemover; 043import ch.qos.logback.core.rolling.helper.CompressionMode; 044import ch.qos.logback.core.rolling.helper.Compressor; 045import ch.qos.logback.core.rolling.helper.FileFilterUtil; 046import ch.qos.logback.core.util.FileUtil; 047 048/** 049 * This Logback {@literal "rolling policy"} copies the contents of a log file 050 * (in this case, mcidasv.log) to the specified destination, and then 051 * {@literal "zeroes out"} the original log file. This approach allows McIDAS-V 052 * users to run a command like {@literal "tail -f mcidasv.log"} without any 053 * issue. Even on Windows. 054 */ 055public class TailFriendlyRollingPolicy<E> extends TimeBasedRollingPolicy<E> { 056 057 Future<?> future; 058 059 @Override public void rollover() throws RolloverFailure { 060 061 // when rollover is called the elapsed period's file has 062 // been already closed. This is a working assumption of this method. 063 064 TimeBasedFileNamingAndTriggeringPolicy timeBasedFileNamingAndTriggeringPolicy = getTimeBasedFileNamingAndTriggeringPolicy(); 065 String elapsedPeriodsFileName = 066 timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName(); 067 068 String elapsedPeriodStem = 069 FileFilterUtil.afterLastSlash(elapsedPeriodsFileName); 070 071 // yes, "==" is okay here. we're checking an enum. 072 if (getCompressionMode() == CompressionMode.NONE) { 073 String src = getParentsRawFileProperty(); 074 if (src != null) { 075 if (isFileEmpty(src)) { 076 addInfo("File '"+src+"' exists and is zero-length; avoiding copy"); 077 } else { 078 renameByCopying(src, elapsedPeriodsFileName); 079 } 080 } 081 } else { 082 if (getParentsRawFileProperty() == null) { 083 future = asyncCompress(elapsedPeriodsFileName, 084 elapsedPeriodsFileName, 085 elapsedPeriodStem); 086 } else { 087 future = renamedRawAndAsyncCompress(elapsedPeriodsFileName, 088 elapsedPeriodStem); 089 } 090 } 091 092 ArchiveRemover archiveRemover = 093 getTimeBasedFileNamingAndTriggeringPolicy().getArchiveRemover(); 094 095 if (archiveRemover != null) { 096 Instant instant = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime()); 097 archiveRemover.clean(instant); 098 } 099 } 100 101 Future<?> asyncCompress(String uncompressedPath, 102 String compressedPath, String innerEntryName) 103 throws RolloverFailure 104 { 105 Compressor compressor = new Compressor(getCompressionMode()); 106 return compressor.asyncCompress(uncompressedPath, 107 compressedPath, 108 innerEntryName); 109 } 110 111 Future<?> renamedRawAndAsyncCompress(String nameOfCompressedFile, 112 String innerEntryName) 113 throws RolloverFailure 114 { 115 String parentsRawFile = getParentsRawFileProperty(); 116 String tmpTarget = parentsRawFile + System.nanoTime() + ".tmp"; 117 renameByCopying(parentsRawFile, tmpTarget); 118 return asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName); 119 } 120 121 /** 122 * Copies the contents of {@code src} into {@code target}, and then 123 * {@literal "zeroes out"} {@code src}. 124 * 125 * @param src Path to the file to be copied. Cannot be {@code null}. 126 * @param target Path to the destination file. Cannot be {@code null}. 127 * 128 * @throws RolloverFailure if copying failed. 129 */ 130 public void renameByCopying(String src, String target) 131 throws RolloverFailure 132 { 133 FileUtil fileUtil = new FileUtil(getContext()); 134 fileUtil.copy(src, target); 135 // using "ignored" this way is intentional; it's what takes care of the 136 // zeroing out. 137 try (FileOutputStream ignored = new FileOutputStream(src)) { 138 addInfo("zeroing out " + src); 139 } catch (IOException e) { 140 addError("Could not reset " + src, e); 141 } 142 } 143 144 /** 145 * Determine if the file at the given path is zero length. 146 * 147 * @param filepath Path to the file to be tested. Cannot be {@code null}. 148 * 149 * @return {@code true} if {@code filepath} exists and is empty. 150 */ 151 private static boolean isFileEmpty(String filepath) { 152 File f = new File(filepath); 153 return f.exists() && (f.length() == 0L); 154 } 155}