/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.io.input; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; /** * Simple implementation of the unix "tail -f" functionality. * <p> * <h2>1. Create a TailerListener implementation</h3> * <p> * First you need to create a {@link TailerListener} implementation * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to * implement every method). * </p> * * <p>For example:</p> * <pre> * public class MyTailerListener extends TailerListenerAdapter { * public void handle(String line) { * System.out.println(line); * } * } * </pre> * * <h2>2. Using a Tailer</h2> * * You can create and use a Tailer in one of three ways: * <ul> * <li>Using one of the static helper methods: * <ul> * <li>{@link Tailer#create(File, TailerListener)}</li> * <li>{@link Tailer#create(File, TailerListener, long)}</li> * <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li> * </ul> * </li> * <li>Using an {@link java.util.concurrent.Executor}</li> * <li>Using an {@link Thread}</li> * </ul> * * An example of each of these is shown below. * * <h3>2.1 Using the static helper method</h3> * * <pre> * TailerListener listener = new MyTailerListener(); * Tailer tailer = Tailer.create(file, listener, delay); * </pre> * * <h3>2.2 Use an Executor</h3> * * <pre> * TailerListener listener = new MyTailerListener(); * Tailer tailer = new Tailer(file, listener, delay); * * // stupid executor impl. for demo purposes * Executor executor = new Executor() { * public void execute(Runnable command) { * command.run(); * } * }; * * executor.execute(tailer); * </pre> * * * <h3>2.3 Use a Thread</h3> * <pre> * TailerListener listener = new MyTailerListener(); * Tailer tailer = new Tailer(file, listener, delay); * Thread thread = new Thread(tailer); * thread.setDaemon(true); // optional * thread.start(); * </pre> * * <h2>3. Stop Tailing</h3> * <p>Remember to stop the tailer when you have done with it:</p> * <pre> * tailer.stop(); * </pre> * * @see TailerListener * @see TailerListenerAdapter * @version $Id: Tailer.java 1348698 2012-06-11 01:09:58Z ggregory $ * @since 2.0 */ public class Tailer implements Runnable { private static final int DEFAULT_DELAY_MILLIS = 1000; private static final String RAF_MODE = "r"; private static final int DEFAULT_BUFSIZE = 4096; /** * Buffer on top of RandomAccessFile. */ private final byte inbuf[]; /** * The file which will be tailed. */ private final File file; /** * The amount of time to wait for the file to be updated. */ private final long delayMillis; /** * Whether to tail from the end or start of file */ private final boolean end; /** * The listener to notify of events when tailing. */ private final TailerListener listener; /** * Whether to close and reopen the file whilst waiting for more input. */ private final boolean reOpen; /** * The tailer will run as long as this value is true. */ private volatile boolean run = true; /** * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s. * @param file The file to follow. * @param listener the TailerListener to use. */ public Tailer(File file, TailerListener listener) { this(file, listener, DEFAULT_DELAY_MILLIS); } /** * Creates a Tailer for the given file, starting from the beginning. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. */ public Tailer(File file, TailerListener listener, long delayMillis) { this(file, listener, delayMillis, false); } /** * Creates a Tailer for the given file, with a delay other than the default 1.0s. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. */ public Tailer(File file, TailerListener listener, long delayMillis, boolean end) { this(file, listener, delayMillis, end, DEFAULT_BUFSIZE); } /** * Creates a Tailer for the given file, with a delay other than the default 1.0s. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen if true, close and reopen the file between reading chunks */ public Tailer(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen) { this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE); } /** * Creates a Tailer for the given file, with a specified buffer size. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param bufSize Buffer size */ public Tailer(File file, TailerListener listener, long delayMillis, boolean end, int bufSize) { this(file, listener, delayMillis, end, false, bufSize); } /** * Creates a Tailer for the given file, with a specified buffer size. * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen if true, close and reopen the file between reading chunks * @param bufSize Buffer size */ public Tailer(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, int bufSize) { this.file = file; this.delayMillis = delayMillis; this.end = end; this.inbuf = new byte[bufSize]; // Save and prepare the listener this.listener = listener; listener.init(this); this.reOpen = reOpen; } /** * Creates and starts a Tailer for the given file. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param bufSize buffer size. * @return The new tailer */ public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, int bufSize) { Tailer tailer = new Tailer(file, listener, delayMillis, end, bufSize); Thread thread = new Thread(tailer); thread.setDaemon(true); thread.start(); return tailer; } /** * Creates and starts a Tailer for the given file. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen whether to close/reopen the file between chunks * @param bufSize buffer size. * @return The new tailer */ public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen, int bufSize) { Tailer tailer = new Tailer(file, listener, delayMillis, end, reOpen, bufSize); Thread thread = new Thread(tailer); thread.setDaemon(true); thread.start(); return tailer; } /** * Creates and starts a Tailer for the given file with default buffer size. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @return The new tailer */ public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end) { return create(file, listener, delayMillis, end, DEFAULT_BUFSIZE); } /** * Creates and starts a Tailer for the given file with default buffer size. * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. * @param reOpen whether to close/reopen the file between chunks * @return The new tailer */ public static Tailer create(File file, TailerListener listener, long delayMillis, boolean end, boolean reOpen) { return create(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE); } /** * Creates and starts a Tailer for the given file, starting at the beginning of the file * * @param file the file to follow. * @param listener the TailerListener to use. * @param delayMillis the delay between checks of the file for new content in milliseconds. * @return The new tailer */ public static Tailer create(File file, TailerListener listener, long delayMillis) { return create(file, listener, delayMillis, false); } /** * Creates and starts a Tailer for the given file, starting at the beginning of the file * with the default delay of 1.0s * * @param file the file to follow. * @param listener the TailerListener to use. * @return The new tailer */ public static Tailer create(File file, TailerListener listener) { return create(file, listener, DEFAULT_DELAY_MILLIS, false); } /** * Return the file. * * @return the file */ public File getFile() { return file; } /** * Return the delay in milliseconds. * * @return the delay in milliseconds. */ public long getDelay() { return delayMillis; } /** * Follows changes in the file, calling the TailerListener's handle method for each new line. */ public void run() { RandomAccessFile reader = null; try { long last = 0; // The last time the file was checked for changes long position = 0; // position within the file // Open the file while (run && reader == null) { try { reader = new RandomAccessFile(file, RAF_MODE); } catch (FileNotFoundException e) { listener.fileNotFound(); } if (reader == null) { try { Thread.sleep(delayMillis); } catch (InterruptedException e) { } } else { // The current position in the file position = end ? file.length() : 0; last = System.currentTimeMillis(); reader.seek(position); } } while (run) { boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first // Check the file length to see if it was rotated long length = file.length(); if (length < position) { // File was rotated listener.fileRotated(); // Reopen the reader after rotation try { // Ensure that the old file is closed iff we re-open it successfully RandomAccessFile save = reader; reader = new RandomAccessFile(file, RAF_MODE); position = 0; // close old file explicitly rather than relying on GC picking up previous RAF IOUtils.closeQuietly(save); } catch (FileNotFoundException e) { // in this case we continue to use the previous reader and position values listener.fileNotFound(); } continue; } else { // File was not rotated // See if the file needs to be read again if (length > position) { // The file has more content than it did last time position = readLines(reader); last = System.currentTimeMillis(); } else if (newer) { /* * This can happen if the file is truncated or overwritten with the exact same length of * information. In cases like this, the file position needs to be reset */ position = 0; reader.seek(position); // cannot be null here // Now we can read new lines position = readLines(reader); last = System.currentTimeMillis(); } } if (reOpen) { IOUtils.closeQuietly(reader); } try { Thread.sleep(delayMillis); } catch (InterruptedException e) { } if (run && reOpen) { reader = new RandomAccessFile(file, RAF_MODE); reader.seek(position); } } } catch (Exception e) { listener.handle(e); } finally { IOUtils.closeQuietly(reader); } } /** * Allows the tailer to complete its current loop and return. */ public void stop() { this.run = false; } /** * Read new lines. * * @param reader The file to read * @return The new position after the lines have been read * @throws java.io.IOException if an I/O error occurs. */ private long readLines(RandomAccessFile reader) throws IOException { StringBuilder sb = new StringBuilder(); long pos = reader.getFilePointer(); long rePos = pos; // position to re-read int num; boolean seenCR = false; while (run && ((num = reader.read(inbuf)) != -1)) { for (int i = 0; i < num; i++) { byte ch = inbuf[i]; switch (ch) { case '\n': seenCR = false; // swallow CR before LF listener.handle(sb.toString()); sb.setLength(0); rePos = pos + i + 1; break; case '\r': if (seenCR) { sb.append('\r'); } seenCR = true; break; default: if (seenCR) { seenCR = false; // swallow final CR listener.handle(sb.toString()); sb.setLength(0); rePos = pos + i + 1; } sb.append((char) ch); // add character, not its ascii value } } pos = reader.getFilePointer(); } reader.seek(rePos); // Ensure we can re-read if necessary return rePos; } }