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