    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.io.input;
    018
    019import static org.apache.commons.io.IOUtils.EOF;
    020
    021import java.io.ByteArrayOutputStream;
    022import java.io.File;
    023import java.io.FileNotFoundException;
    024import java.io.IOException;
    025import java.io.RandomAccessFile;
    026import java.nio.charset.Charset;
    027
    028import org.apache.commons.io.FileUtils;
    029import org.apache.commons.io.IOUtils;
    030
    031/**
    032 * Simple implementation of the unix "tail -f" functionality.
    033 *
    034 * <h2>1. Create a TailerListener implementation</h2>
    035 * <p>
    036 * First you need to create a {@link TailerListener} implementation
    037 * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
    038 * implement every method).
    039 * </p>
    040 *
    041 * <p>For example:</p>
    042 * <pre>
    043 *  public class MyTailerListener extends TailerListenerAdapter {
    044 *      public void handle(String line) {
    045 *          System.out.println(line);
    046 *      }
    047 *  }</pre>
    048 *
    049 * <h2>2. Using a Tailer</h2>
    050 *
    051 * <p>
    052 * You can create and use a Tailer in one of three ways:
    053 * </p>
    054 * <ul>
    055 *   <li>Using one of the static helper methods:
    056 *     <ul>
    057 *       <li>{@link Tailer#create(File, TailerListener)}</li>
    058 *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
    059 *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
    060 *     </ul>
    061 *   </li>
    062 *   <li>Using an {@link java.util.concurrent.Executor}</li>
    063 *   <li>Using an {@link Thread}</li>
    064 * </ul>
    065 *
    066 * <p>
    067 * An example of each of these is shown below.
    068 * </p>
    069 *
    070 * <h3>2.1 Using the static helper method</h3>
    071 *
    072 * <pre>
    073 *      TailerListener listener = new MyTailerListener();
    074 *      Tailer tailer = Tailer.create(file, listener, delay);</pre>
    075 *
    076 * <h3>2.2 Using an Executor</h3>
    077 *
    078 * <pre>
    079 *      TailerListener listener = new MyTailerListener();
    080 *      Tailer tailer = new Tailer(file, listener, delay);
    081 *
    082 *      // stupid executor impl. for demo purposes
    083 *      Executor executor = new Executor() {
    084 *          public void execute(Runnable command) {
    085 *              command.run();
    086 *           }
    087 *      };
    088 *
    089 *      executor.execute(tailer);
    090 * </pre>
    091 *
    092 *
    093 * <h3>2.3 Using a Thread</h3>
    094 * <pre>
    095 *      TailerListener listener = new MyTailerListener();
    096 *      Tailer tailer = new Tailer(file, listener, delay);
    097 *      Thread thread = new Thread(tailer);
    098 *      thread.setDaemon(true); // optional
    099 *      thread.start();</pre>
    100 *
    101 * <h2>3. Stopping a Tailer</h2>
    102 * <p>Remember to stop the tailer when you have done with it:</p>
    103 * <pre>
    104 *      tailer.stop();
    105 * </pre>
    106 *
    107 * <h2>4. Interrupting a Tailer</h2>
    108 * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.
    109 * </p>
    110 * <pre>
    111 *      thread.interrupt();
    112 * </pre>
    113 * <p>
    114 * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.
    115 * </p>
    116 * <p>
    117 * The file is read using the default charset; this can be overridden if necessary.
    118 * </p>
    119 * @see TailerListener
    120 * @see TailerListenerAdapter
    121 * @since 2.0
    122 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}
    123 */
    124public class Tailer implements Runnable {
    125
    126    private static final int DEFAULT_DELAY_MILLIS = 1000;
    127
    128    private static final String RAF_MODE = "r";
    129
    130    // The default charset used for reading files
    131    private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
    132
    133    /**
    134     * Buffer on top of RandomAccessFile.
    135     */
    136    private final byte[] inbuf;
    137
    138    /**
    139     * The file which will be tailed.
    140     */
    141    private final File file;
    142
    143    /**
    144     * The character set that will be used to read the file.
    145     */
    146    private final Charset charset;
    147
    148    /**
    149     * The amount of time to wait for the file to be updated.
    150     */
    151    private final long delayMillis;
    152
    153    /**
    154     * Whether to tail from the end or start of file
    155     */
    156    private final boolean end;
    157
    158    /**
    159     * The listener to notify of events when tailing.
    160     */
    161    private final TailerListener listener;
    162
    163    /**
    164     * Whether to close and reopen the file whilst waiting for more input.
    165     */
    166    private final boolean reOpen;
    167
    168    /**
    169     * The tailer will run as long as this value is true.
    170     */
    171    private volatile boolean run = true;
    172
    173    /**
    174     * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
    175     * @param file The file to follow.
    176     * @param listener the TailerListener to use.
    177     */
    178    public Tailer(final File file, final TailerListener listener) {
    179        this(file, listener, DEFAULT_DELAY_MILLIS);
    180    }
    181
    182    /**
    183     * Creates a Tailer for the given file, starting from the beginning.
    184     * @param file the file to follow.
    185     * @param listener the TailerListener to use.
    186     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    187     */
    188    public Tailer(final File file, final TailerListener listener, final long delayMillis) {
    189        this(file, listener, delayMillis, false);
    190    }
    191
    192    /**
    193     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    194     * @param file the file to follow.
    195     * @param listener the TailerListener to use.
    196     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    197     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    198     */
    199    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
    200        this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE);
    201    }
    202
    203    /**
    204     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
    205     * @param file the file to follow.
    206     * @param listener the TailerListener to use.
    207     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    208     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    209     * @param reOpen if true, close and reopen the file between reading chunks
    210     */
    211    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
    212                  final boolean reOpen) {
    213        this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE);
    214    }
    215
    216    /**
    217     * Creates a Tailer for the given file, with a specified buffer size.
    218     * @param file the file to follow.
    219     * @param listener the TailerListener to use.
    220     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    221     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    222     * @param bufSize Buffer size
    223     */
    224    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
    225                  final int bufSize) {
    226        this(file, listener, delayMillis, end, false, bufSize);
    227    }
    228
    229    /**
    230     * Creates a Tailer for the given file, with a specified buffer size.
    231     * @param file the file to follow.
    232     * @param listener the TailerListener to use.
    233     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    234     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    235     * @param reOpen if true, close and reopen the file between reading chunks
    236     * @param bufSize Buffer size
    237     */
    238    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end,
    239                  final boolean reOpen, final int bufSize) {
    240        this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
    241    }
    242
    243    /**
    244     * Creates a Tailer for the given file, with a specified buffer size.
    245     * @param file the file to follow.
    246     * @param charset the Charset to be used for reading the file
    247     * @param listener the TailerListener to use.
    248     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    249     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    250     * @param reOpen if true, close and reopen the file between reading chunks
    251     * @param bufSize Buffer size
    252     */
    253    public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis,
    254                  final boolean end, final boolean reOpen
    255            , final int bufSize) {
    256        this.file = file;
    257        this.delayMillis = delayMillis;
    258        this.end = end;
    259
    260        this.inbuf = new byte[bufSize];
    261
    262        // Save and prepare the listener
    263        this.listener = listener;
    264        listener.init(this);
    265        this.reOpen = reOpen;
    266        this.charset = charset;
    267    }
    268
    269    /**
    270     * Creates and starts a Tailer for the given file.
    271     *
    272     * @param file the file to follow.
    273     * @param listener the TailerListener to use.
    274     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    275     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    276     * @param bufSize buffer size.
    277     * @return The new tailer
    278     */
    279    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    280                                final boolean end, final int bufSize) {
    281        return create(file, listener, delayMillis, end, false, bufSize);
    282    }
    283
    284    /**
    285     * Creates and starts a Tailer for the given file.
    286     *
    287     * @param file the file to follow.
    288     * @param listener the TailerListener to use.
    289     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    290     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    291     * @param reOpen whether to close/reopen the file between chunks
    292     * @param bufSize buffer size.
    293     * @return The new tailer
    294     */
    295    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    296                                final boolean end, final boolean reOpen,
    297            final int bufSize) {
    298        return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
    299    }
    300
    301    /**
    302     * Creates and starts a Tailer for the given file.
    303     *
    304     * @param file the file to follow.
    305     * @param charset the character set to use for reading the file
    306     * @param listener the TailerListener to use.
    307     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    308     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    309     * @param reOpen whether to close/reopen the file between chunks
    310     * @param bufSize buffer size.
    311     * @return The new tailer
    312     */
    313    public static Tailer create(final File file, final Charset charset, final TailerListener listener,
    314                                final long delayMillis, final boolean end, final boolean reOpen
    315            ,final int bufSize) {
    316        final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize);
    317        final Thread thread = new Thread(tailer);
    318        thread.setDaemon(true);
    319        thread.start();
    320        return tailer;
    321    }
    322
    323    /**
    324     * Creates and starts a Tailer for the given file with default buffer size.
    325     *
    326     * @param file the file to follow.
    327     * @param listener the TailerListener to use.
    328     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    329     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    330     * @return The new tailer
    331     */
    332    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    333                                final boolean end) {
    334        return create(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE);
    335    }
    336
    337    /**
    338     * Creates and starts a Tailer for the given file with default buffer size.
    339     *
    340     * @param file the file to follow.
    341     * @param listener the TailerListener to use.
    342     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    343     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
    344     * @param reOpen whether to close/reopen the file between chunks
    345     * @return The new tailer
    346     */
    347    public static Tailer create(final File file, final TailerListener listener, final long delayMillis,
    348                                final boolean end, final boolean reOpen) {
    349        return create(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE);
    350    }
    351
    352    /**
    353     * Creates and starts a Tailer for the given file, starting at the beginning of the file
    354     *
    355     * @param file the file to follow.
    356     * @param listener the TailerListener to use.
    357     * @param delayMillis the delay between checks of the file for new content in milliseconds.
    358     * @return The new tailer
    359     */
    360    public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
    361        return create(file, listener, delayMillis, false);
    362    }
    363
    364    /**
    365     * Creates and starts a Tailer for the given file, starting at the beginning of the file
    366     * with the default delay of 1.0s
    367     *
    368     * @param file the file to follow.
    369     * @param listener the TailerListener to use.
    370     * @return The new tailer
    371     */
    372    public static Tailer create(final File file, final TailerListener listener) {
    373        return create(file, listener, DEFAULT_DELAY_MILLIS, false);
    374    }
    375
    376    /**
    377     * Return the file.
    378     *
    379     * @return the file
    380     */
    381    public File getFile() {
    382        return file;
    383    }
    384
    385    /**
    386     * Gets whether to keep on running.
    387     *
    388     * @return whether to keep on running.
    389     * @since 2.5
    390     */
    391    protected boolean getRun() {
    392        return run;
    393    }
    394
    395    /**
    396     * Return the delay in milliseconds.
    397     *
    398     * @return the delay in milliseconds.
    399     */
    400    public long getDelay() {
    401        return delayMillis;
    402    }
    403
    404    /**
    405     * Follows changes in the file, calling the TailerListener's handle method for each new line.
    406     */
    407    @Override
    408    public void run() {
    409        RandomAccessFile reader = null;
    410        try {
    411            long last = 0; // The last time the file was checked for changes
    412            long position = 0; // position within the file
    413            // Open the file
    414            while (getRun() && reader == null) {
    415                try {
    416                    reader = new RandomAccessFile(file, RAF_MODE);
    417                } catch (final FileNotFoundException e) {
    418                    listener.fileNotFound();
    419                }
    420                if (reader == null) {
    421                    Thread.sleep(delayMillis);
    422                } else {
    423                    // The current position in the file
    424                    position = end ? file.length() : 0;
    425                    last = file.lastModified();
    426                    reader.seek(position);
    427                }
    428            }
    429            while (getRun()) {
    430                final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first
    431                // Check the file length to see if it was rotated
    432                final long length = file.length();
    433                if (length < position) {
    434                    // File was rotated
    435                    listener.fileRotated();
    436                    // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it
    437                    // successfully
    438                    try (RandomAccessFile save = reader) {
    439                        reader = new RandomAccessFile(file, RAF_MODE);
    440                        // At this point, we're sure that the old file is rotated
    441                        // Finish scanning the old file and then we'll start with the new one
    442                        try {
    443                            readLines(save);
    444                        }  catch (final IOException ioe) {
    445                            listener.handle(ioe);
    446                        }
    447                        position = 0;
    448                    } catch (final FileNotFoundException e) {
    449                        // in this case we continue to use the previous reader and position values
    450                        listener.fileNotFound();
    451                        Thread.sleep(delayMillis);
    452                    }
    453                    continue;
    454                }
    455                // File was not rotated
    456                // See if the file needs to be read again
    457                if (length > position) {
    458                    // The file has more content than it did last time
    459                    position = readLines(reader);
    460                    last = file.lastModified();
    461                } else if (newer) {
    462                    /*
    463                     * This can happen if the file is truncated or overwritten with the exact same length of
    464                     * information. In cases like this, the file position needs to be reset
    465                     */
    466                    position = 0;
    467                    reader.seek(position); // cannot be null here
    468
    469                    // Now we can read new lines
    470                    position = readLines(reader);
    471                    last = file.lastModified();
    472                }
    473                if (reOpen && reader != null) {
    474                    reader.close();
    475                }
    476                Thread.sleep(delayMillis);
    477                if (getRun() && reOpen) {
    478                    reader = new RandomAccessFile(file, RAF_MODE);
    479                    reader.seek(position);
    480                }
    481            }
    482        } catch (final InterruptedException e) {
    483            Thread.currentThread().interrupt();
    484            listener.handle(e);
    485        } catch (final Exception e) {
    486            listener.handle(e);
    487        } finally {
    488            try {
    489                if (reader != null) {
    490                    reader.close();
    491                }
    492            }
    493            catch (final IOException e) {
    494                listener.handle(e);
    495            }
    496            stop();
    497        }
    498    }
    499
    500    /**
    501     * Allows the tailer to complete its current loop and return.
    502     */
    503    public void stop() {
    504        this.run = false;
    505    }
    506
    507    /**
    508     * Read new lines.
    509     *
    510     * @param reader The file to read
    511     * @return The new position after the lines have been read
    512     * @throws java.io.IOException if an I/O error occurs.
    513     */
    514    private long readLines(final RandomAccessFile reader) throws IOException {
    515        try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) {
    516            long pos = reader.getFilePointer();
    517            long rePos = pos; // position to re-read
    518            int num;
    519            boolean seenCR = false;
    520            while (getRun() && ((num = reader.read(inbuf)) != EOF)) {
    521                for (int i = 0; i < num; i++) {
    522                    final byte ch = inbuf[i];
    523                    switch ( ch ) {
    524                        case '\n':
    525                            seenCR = false; // swallow CR before LF
    526                            listener.handle(new String(lineBuf.toByteArray(), charset));
    527                            lineBuf.reset();
    528                            rePos = pos + i + 1;
    529                            break;
    530                        case '\r':
    531                            if (seenCR) {
    532                                lineBuf.write('\r');
    533                            }
    534                            seenCR = true;
    535                            break;
    536                        default:
    537                            if (seenCR) {
    538                                seenCR = false; // swallow final CR
    539                                listener.handle(new String(lineBuf.toByteArray(), charset));
    540                                lineBuf.reset();
    541                                rePos = pos + i + 1;
    542                            }
    543                            lineBuf.write(ch);
    544                    }
    545                }
    546                pos = reader.getFilePointer();
    547            }
    548
    549            reader.seek(rePos); // Ensure we can re-read if necessary
    550
    551            if (listener instanceof TailerListenerAdapter) {
    552                ((TailerListenerAdapter) listener).endOfFileReached();
    553            }
    554
    555            return rePos;
    556        }
    557    }
    558}