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 java.io.File;
032import java.io.FileOutputStream;
033import java.io.IOException;
034
035import java.util.Date;
036import java.util.concurrent.Future;
037
038import ch.qos.logback.core.rolling.RolloverFailure;
039import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy;
040import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
041import ch.qos.logback.core.rolling.helper.ArchiveRemover;
042import ch.qos.logback.core.rolling.helper.CompressionMode;
043import ch.qos.logback.core.rolling.helper.Compressor;
044import ch.qos.logback.core.rolling.helper.FileFilterUtil;
045import ch.qos.logback.core.util.FileUtil;
046
047/**
048 * This Logback {@literal "rolling policy"} copies the contents of a log file
049 * (in this case, mcidasv.log) to the specified destination, and then
050 * {@literal "zeroes out"} the original log file. This approach allows McIDAS-V
051 * users to run a command like {@literal "tail -f mcidasv.log"} without any
052 * issue. Even on Windows.
053 */
054public class TailFriendlyRollingPolicy<E> extends TimeBasedRollingPolicy<E> {
055    
056    Future<?> future;
057
058    @Override public void rollover() throws RolloverFailure {
059
060        // when rollover is called the elapsed period's file has
061        // been already closed. This is a working assumption of this method.
062
063        TimeBasedFileNamingAndTriggeringPolicy timeBasedFileNamingAndTriggeringPolicy = getTimeBasedFileNamingAndTriggeringPolicy();
064        String elapsedPeriodsFileName =
065            timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
066
067        String elapsedPeriodStem =
068            FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
069
070        // yes, "==" is okay here. we're checking an enum.
071        if (getCompressionMode() == CompressionMode.NONE) {
072            String src = getParentsRawFileProperty();
073            if (src != null) {
074                if (isFileEmpty(src)) {
075                    addInfo("File '"+src+"' exists and is zero-length; avoiding copy");
076                } else {
077                    renameByCopying(src, elapsedPeriodsFileName);
078                }
079            }
080        } else {
081            if (getParentsRawFileProperty() == null) {
082                future = asyncCompress(elapsedPeriodsFileName,
083                                       elapsedPeriodsFileName,
084                                       elapsedPeriodStem);
085            } else {
086                future = renamedRawAndAsyncCompress(elapsedPeriodsFileName,
087                                                    elapsedPeriodStem);
088            }
089        }
090
091        ArchiveRemover archiveRemover =
092            getTimeBasedFileNamingAndTriggeringPolicy().getArchiveRemover();
093
094        if (archiveRemover != null) {
095            Date d =
096                new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
097            archiveRemover.clean(d);
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}