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 }