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}