View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.input;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.File;
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.io.RandomAccessFile;
24  import java.nio.charset.Charset;
25  
26  import org.apache.commons.io.FileUtils;
27  import org.apache.commons.io.IOUtils;
28  
29  /**
30   * Simple implementation of the unix "tail -f" functionality.
31   * 
32   * <h2>1. Create a TailerListener implementation</h2>
33   * <p>
34   * First you need to create a {@link TailerListener} implementation
35   * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to
36   * implement every method).
37   * </p>
38   *
39   * <p>For example:</p>
40   * <pre>
41   *  public class MyTailerListener extends TailerListenerAdapter {
42   *      public void handle(String line) {
43   *          System.out.println(line);
44   *      }
45   *  }</pre>
46   *
47   * <h2>2. Using a Tailer</h2>
48   *
49   * <p>
50   * You can create and use a Tailer in one of three ways:
51   * </p>
52   * <ul>
53   *   <li>Using one of the static helper methods:
54   *     <ul>
55   *       <li>{@link Tailer#create(File, TailerListener)}</li>
56   *       <li>{@link Tailer#create(File, TailerListener, long)}</li>
57   *       <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li>
58   *     </ul>
59   *   </li>
60   *   <li>Using an {@link java.util.concurrent.Executor}</li>
61   *   <li>Using an {@link Thread}</li>
62   * </ul>
63   *
64   * <p>
65   * An example of each of these is shown below.
66   * </p>
67   *
68   * <h3>2.1 Using the static helper method</h3>
69   *
70   * <pre>
71   *      TailerListener listener = new MyTailerListener();
72   *      Tailer tailer = Tailer.create(file, listener, delay);</pre>
73   *
74   * <h3>2.2 Using an Executor</h3>
75   *
76   * <pre>
77   *      TailerListener listener = new MyTailerListener();
78   *      Tailer tailer = new Tailer(file, listener, delay);
79   *
80   *      // stupid executor impl. for demo purposes
81   *      Executor executor = new Executor() {
82   *          public void execute(Runnable command) {
83   *              command.run();
84   *           }
85   *      };
86   *
87   *      executor.execute(tailer);
88   * </pre>
89   *
90   *
91   * <h3>2.3 Using a Thread</h3>
92   * <pre>
93   *      TailerListener listener = new MyTailerListener();
94   *      Tailer tailer = new Tailer(file, listener, delay);
95   *      Thread thread = new Thread(tailer);
96   *      thread.setDaemon(true); // optional
97   *      thread.start();</pre>
98   *
99   * <h2>3. Stopping a Tailer</h2>
100  * <p>Remember to stop the tailer when you have done with it:</p>
101  * <pre>
102  *      tailer.stop();
103  * </pre>
104  *
105  * <h2>4. Interrupting a Tailer</h2>
106  * <p>You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.</p>
107  * <pre>
108  *      thread.interrupt();
109  * </pre>
110  * <p>If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.</p>
111  *
112  * <p>The file is read using the default charset; this can be overriden if necessary</p>
113  * @see TailerListener
114  * @see TailerListenerAdapter
115  * @version $Id: Tailer.java 1563230 2014-01-31 19:51:21Z ggregory $
116  * @since 2.0
117  * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}
118  */
119 public class Tailer implements Runnable {
120 
121     private static final int DEFAULT_DELAY_MILLIS = 1000;
122 
123     private static final String RAF_MODE = "r";
124 
125     private static final int DEFAULT_BUFSIZE = 4096;
126 
127     // The default charset used for reading files
128     private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
129 
130     /**
131      * Buffer on top of RandomAccessFile.
132      */
133     private final byte inbuf[];
134 
135     /**
136      * The file which will be tailed.
137      */
138     private final File file;
139 
140     /**
141      * The character set that will be used to read the file.
142      */
143     private final Charset cset;
144 
145     /**
146      * The amount of time to wait for the file to be updated.
147      */
148     private final long delayMillis;
149 
150     /**
151      * Whether to tail from the end or start of file
152      */
153     private final boolean end;
154 
155     /**
156      * The listener to notify of events when tailing.
157      */
158     private final TailerListener listener;
159 
160     /**
161      * Whether to close and reopen the file whilst waiting for more input.
162      */
163     private final boolean reOpen;
164 
165     /**
166      * The tailer will run as long as this value is true.
167      */
168     private volatile boolean run = true;
169 
170     /**
171      * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
172      * @param file The file to follow.
173      * @param listener the TailerListener to use.
174      */
175     public Tailer(final File file, final TailerListener listener) {
176         this(file, listener, DEFAULT_DELAY_MILLIS);
177     }
178 
179     /**
180      * Creates a Tailer for the given file, starting from the beginning.
181      * @param file the file to follow.
182      * @param listener the TailerListener to use.
183      * @param delayMillis the delay between checks of the file for new content in milliseconds.
184      */
185     public Tailer(final File file, final TailerListener listener, final long delayMillis) {
186         this(file, listener, delayMillis, false);
187     }
188 
189     /**
190      * Creates a Tailer for the given file, with a delay other than the default 1.0s.
191      * @param file the file to follow.
192      * @param listener the TailerListener to use.
193      * @param delayMillis the delay between checks of the file for new content in milliseconds.
194      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
195      */
196     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
197         this(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
198     }
199 
200     /**
201      * Creates a Tailer for the given file, with a delay other than the default 1.0s.
202      * @param file the file to follow.
203      * @param listener the TailerListener to use.
204      * @param delayMillis the delay between checks of the file for new content in milliseconds.
205      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
206      * @param reOpen if true, close and reopen the file between reading chunks
207      */
208     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
209         this(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
210     }
211 
212     /**
213      * Creates a Tailer for the given file, with a specified buffer size.
214      * @param file the file to follow.
215      * @param listener the TailerListener to use.
216      * @param delayMillis the delay between checks of the file for new content in milliseconds.
217      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
218      * @param bufSize Buffer size
219      */
220     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufSize) {
221         this(file, listener, delayMillis, end, false, bufSize);
222     }
223 
224     /**
225      * Creates a Tailer for the given file, with a specified buffer size.
226      * @param file the file to follow.
227      * @param listener the TailerListener to use.
228      * @param delayMillis the delay between checks of the file for new content in milliseconds.
229      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
230      * @param reOpen if true, close and reopen the file between reading chunks
231      * @param bufSize Buffer size
232      */
233     public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufSize) {
234         this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
235     }
236 
237     /**
238      * Creates a Tailer for the given file, with a specified buffer size.
239      * @param file the file to follow.
240      * @param cset the Charset to be used for reading the file
241      * @param listener the TailerListener to use.
242      * @param delayMillis the delay between checks of the file for new content in milliseconds.
243      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
244      * @param reOpen if true, close and reopen the file between reading chunks
245      * @param bufSize Buffer size
246      */
247     public Tailer(final File file, final Charset cset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen
248             , final int bufSize) {
249         this.file = file;
250         this.delayMillis = delayMillis;
251         this.end = end;
252 
253         this.inbuf = new byte[bufSize];
254 
255         // Save and prepare the listener
256         this.listener = listener;
257         listener.init(this);
258         this.reOpen = reOpen;
259         this.cset = cset; 
260     }
261 
262     /**
263      * Creates and starts a Tailer for the given file.
264      *
265      * @param file the file to follow.
266      * @param listener the TailerListener to use.
267      * @param delayMillis the delay between checks of the file for new content in milliseconds.
268      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
269      * @param bufSize buffer size.
270      * @return The new tailer
271      */
272     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufSize) {
273         return create(file, listener, delayMillis, end, false, bufSize);
274     }
275 
276     /**
277      * Creates and starts a Tailer for the given file.
278      *
279      * @param file the file to follow.
280      * @param listener the TailerListener to use.
281      * @param delayMillis the delay between checks of the file for new content in milliseconds.
282      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
283      * @param reOpen whether to close/reopen the file between chunks
284      * @param bufSize buffer size.
285      * @return The new tailer
286      */
287     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
288             final int bufSize) {
289         return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize);
290     }
291 
292     /**
293      * Creates and starts a Tailer for the given file.
294      *
295      * @param file the file to follow.
296      * @param charset the character set to use for reading the file
297      * @param listener the TailerListener to use.
298      * @param delayMillis the delay between checks of the file for new content in milliseconds.
299      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
300      * @param reOpen whether to close/reopen the file between chunks
301      * @param bufSize buffer size.
302      * @return The new tailer
303      */
304     public static Tailer create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen
305             ,final int bufSize) {
306         final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize);
307         final Thread thread = new Thread(tailer);
308         thread.setDaemon(true);
309         thread.start();
310         return tailer;
311     }
312 
313     /**
314      * Creates and starts a Tailer for the given file with default buffer size.
315      *
316      * @param file the file to follow.
317      * @param listener the TailerListener to use.
318      * @param delayMillis the delay between checks of the file for new content in milliseconds.
319      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
320      * @return The new tailer
321      */
322     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
323         return create(file, listener, delayMillis, end, DEFAULT_BUFSIZE);
324     }
325 
326     /**
327      * Creates and starts a Tailer for the given file with default buffer size.
328      *
329      * @param file the file to follow.
330      * @param listener the TailerListener to use.
331      * @param delayMillis the delay between checks of the file for new content in milliseconds.
332      * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
333      * @param reOpen whether to close/reopen the file between chunks
334      * @return The new tailer
335      */
336     public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
337         return create(file, listener, delayMillis, end, reOpen, DEFAULT_BUFSIZE);
338     }
339 
340     /**
341      * Creates and starts a Tailer for the given file, starting at the beginning of the file
342      *
343      * @param file the file to follow.
344      * @param listener the TailerListener to use.
345      * @param delayMillis the delay between checks of the file for new content in milliseconds.
346      * @return The new tailer
347      */
348     public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
349         return create(file, listener, delayMillis, false);
350     }
351 
352     /**
353      * Creates and starts a Tailer for the given file, starting at the beginning of the file
354      * with the default delay of 1.0s
355      *
356      * @param file the file to follow.
357      * @param listener the TailerListener to use.
358      * @return The new tailer
359      */
360     public static Tailer create(final File file, final TailerListener listener) {
361         return create(file, listener, DEFAULT_DELAY_MILLIS, false);
362     }
363 
364     /**
365      * Return the file.
366      *
367      * @return the file
368      */
369     public File getFile() {
370         return file;
371     }
372 
373     /**
374      * Gets whether to keep on running.
375      *
376      * @return whether to keep on running.
377      * @since 2.5
378      */
379     protected boolean getRun() {
380         return run;
381     }
382 
383     /**
384      * Return the delay in milliseconds.
385      *
386      * @return the delay in milliseconds.
387      */
388     public long getDelay() {
389         return delayMillis;
390     }
391 
392     /**
393      * Follows changes in the file, calling the TailerListener's handle method for each new line.
394      */
395     public void run() {
396         RandomAccessFile reader = null;
397         try {
398             long last = 0; // The last time the file was checked for changes
399             long position = 0; // position within the file
400             // Open the file
401             while (getRun() && reader == null) {
402                 try {
403                     reader = new RandomAccessFile(file, RAF_MODE);
404                 } catch (final FileNotFoundException e) {
405                     listener.fileNotFound();
406                 }
407                 if (reader == null) {
408                     Thread.sleep(delayMillis);
409                 } else {
410                     // The current position in the file
411                     position = end ? file.length() : 0;
412                     last = file.lastModified();
413                     reader.seek(position);
414                 }
415             }
416             while (getRun()) {
417                 final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first
418                 // Check the file length to see if it was rotated
419                 final long length = file.length();
420                 if (length < position) {
421                     // File was rotated
422                     listener.fileRotated();
423                     // Reopen the reader after rotation
424                     try {
425                         // Ensure that the old file is closed iff we re-open it successfully
426                         final RandomAccessFile save = reader;
427                         reader = new RandomAccessFile(file, RAF_MODE);
428                         // At this point, we're sure that the old file is rotated
429                         // Finish scanning the old file and then we'll start with the new one
430                         try {
431                             readLines(save);
432                         }  catch (IOException ioe) {
433                             listener.handle(ioe);
434                         }
435                         position = 0;
436                         // close old file explicitly rather than relying on GC picking up previous RAF
437                         IOUtils.closeQuietly(save);
438                     } catch (final FileNotFoundException e) {
439                         // in this case we continue to use the previous reader and position values
440                         listener.fileNotFound();
441                     }
442                     continue;
443                 } else {
444                     // File was not rotated
445                     // See if the file needs to be read again
446                     if (length > position) {
447                         // The file has more content than it did last time
448                         position = readLines(reader);
449                         last = file.lastModified();
450                     } else if (newer) {
451                         /*
452                          * This can happen if the file is truncated or overwritten with the exact same length of
453                          * information. In cases like this, the file position needs to be reset
454                          */
455                         position = 0;
456                         reader.seek(position); // cannot be null here
457 
458                         // Now we can read new lines
459                         position = readLines(reader);
460                         last = file.lastModified();
461                     }
462                 }
463                 if (reOpen) {
464                     IOUtils.closeQuietly(reader);
465                 }
466                 Thread.sleep(delayMillis);
467                 if (getRun() && reOpen) {
468                     reader = new RandomAccessFile(file, RAF_MODE);
469                     reader.seek(position);
470                 }
471             }
472         } catch (final InterruptedException e) {
473             Thread.currentThread().interrupt();
474             stop(e);
475         } catch (final Exception e) {
476             stop(e);
477         } finally {
478             IOUtils.closeQuietly(reader);
479         }
480     }
481 
482     private void stop(final Exception e) {
483         listener.handle(e);
484         stop();
485     }
486 
487     /**
488      * Allows the tailer to complete its current loop and return.
489      */
490     public void stop() {
491         this.run = false;
492     }
493 
494     /**
495      * Read new lines.
496      *
497      * @param reader The file to read
498      * @return The new position after the lines have been read
499      * @throws java.io.IOException if an I/O error occurs.
500      */
501     private long readLines(final RandomAccessFile reader) throws IOException {
502         ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64);
503         long pos = reader.getFilePointer();
504         long rePos = pos; // position to re-read
505         int num;
506         boolean seenCR = false;
507         while (getRun() && ((num = reader.read(inbuf)) != -1)) {
508             for (int i = 0; i < num; i++) {
509                 final byte ch = inbuf[i];
510                 switch (ch) {
511                 case '\n':
512                     seenCR = false; // swallow CR before LF
513                     listener.handle(new String(lineBuf.toByteArray(), cset));
514                     lineBuf.reset();
515                     rePos = pos + i + 1;
516                     break;
517                 case '\r':
518                     if (seenCR) {
519                         lineBuf.write('\r');
520                     }
521                     seenCR = true;
522                     break;
523                 default:
524                     if (seenCR) {
525                         seenCR = false; // swallow final CR
526                         listener.handle(new String(lineBuf.toByteArray(), cset));
527                         lineBuf.reset();
528                         rePos = pos + i + 1;
529                     }
530                     lineBuf.write(ch);
531                 }
532             }
533             pos = reader.getFilePointer();
534         }
535         IOUtils.closeQuietly(lineBuf); // not strictly necessary
536         reader.seek(rePos); // Ensure we can re-read if necessary
537         return rePos;
538     }
539 
540 }