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