001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.io.input; 018 019import static org.apache.commons.io.IOUtils.EOF; 020 021import java.io.ByteArrayOutputStream; 022import java.io.File; 023import java.io.FileNotFoundException; 024import java.io.IOException; 025import java.io.RandomAccessFile; 026import java.nio.charset.Charset; 027 028import org.apache.commons.io.FileUtils; 029import org.apache.commons.io.IOUtils; 030 031/** 032 * Simple implementation of the unix "tail -f" functionality. 033 * 034 * <h2>1. Create a TailerListener implementation</h2> 035 * <p> 036 * First you need to create a {@link TailerListener} implementation 037 * ({@link TailerListenerAdapter} is provided for convenience so that you don't have to 038 * implement every method). 039 * </p> 040 * 041 * <p>For example:</p> 042 * <pre> 043 * public class MyTailerListener extends TailerListenerAdapter { 044 * public void handle(String line) { 045 * System.out.println(line); 046 * } 047 * }</pre> 048 * 049 * <h2>2. Using a Tailer</h2> 050 * 051 * <p> 052 * You can create and use a Tailer in one of three ways: 053 * </p> 054 * <ul> 055 * <li>Using one of the static helper methods: 056 * <ul> 057 * <li>{@link Tailer#create(File, TailerListener)}</li> 058 * <li>{@link Tailer#create(File, TailerListener, long)}</li> 059 * <li>{@link Tailer#create(File, TailerListener, long, boolean)}</li> 060 * </ul> 061 * </li> 062 * <li>Using an {@link java.util.concurrent.Executor}</li> 063 * <li>Using an {@link Thread}</li> 064 * </ul> 065 * 066 * <p> 067 * An example of each of these is shown below. 068 * </p> 069 * 070 * <h3>2.1 Using the static helper method</h3> 071 * 072 * <pre> 073 * TailerListener listener = new MyTailerListener(); 074 * Tailer tailer = Tailer.create(file, listener, delay);</pre> 075 * 076 * <h3>2.2 Using an Executor</h3> 077 * 078 * <pre> 079 * TailerListener listener = new MyTailerListener(); 080 * Tailer tailer = new Tailer(file, listener, delay); 081 * 082 * // stupid executor impl. for demo purposes 083 * Executor executor = new Executor() { 084 * public void execute(Runnable command) { 085 * command.run(); 086 * } 087 * }; 088 * 089 * executor.execute(tailer); 090 * </pre> 091 * 092 * 093 * <h3>2.3 Using a Thread</h3> 094 * <pre> 095 * TailerListener listener = new MyTailerListener(); 096 * Tailer tailer = new Tailer(file, listener, delay); 097 * Thread thread = new Thread(tailer); 098 * thread.setDaemon(true); // optional 099 * 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()}. 109 * </p> 110 * <pre> 111 * thread.interrupt(); 112 * </pre> 113 * <p> 114 * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}. 115 * </p> 116 * <p> 117 * The file is read using the default charset; this can be overridden if necessary. 118 * </p> 119 * @see TailerListener 120 * @see TailerListenerAdapter 121 * @since 2.0 122 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()} 123 */ 124public class Tailer implements Runnable { 125 126 private static final int DEFAULT_DELAY_MILLIS = 1000; 127 128 private static final String RAF_MODE = "r"; 129 130 // The default charset used for reading files 131 private static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); 132 133 /** 134 * Buffer on top of RandomAccessFile. 135 */ 136 private final byte[] inbuf; 137 138 /** 139 * The file which will be tailed. 140 */ 141 private final File file; 142 143 /** 144 * The character set that will be used to read the file. 145 */ 146 private final Charset charset; 147 148 /** 149 * The amount of time to wait for the file to be updated. 150 */ 151 private final long delayMillis; 152 153 /** 154 * Whether to tail from the end or start of file 155 */ 156 private final boolean end; 157 158 /** 159 * The listener to notify of events when tailing. 160 */ 161 private final TailerListener listener; 162 163 /** 164 * Whether to close and reopen the file whilst waiting for more input. 165 */ 166 private final boolean reOpen; 167 168 /** 169 * The tailer will run as long as this value is true. 170 */ 171 private volatile boolean run = true; 172 173 /** 174 * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s. 175 * @param file The file to follow. 176 * @param listener the TailerListener to use. 177 */ 178 public Tailer(final File file, final TailerListener listener) { 179 this(file, listener, DEFAULT_DELAY_MILLIS); 180 } 181 182 /** 183 * Creates a Tailer for the given file, starting from the beginning. 184 * @param file the file to follow. 185 * @param listener the TailerListener to use. 186 * @param delayMillis the delay between checks of the file for new content in milliseconds. 187 */ 188 public Tailer(final File file, final TailerListener listener, final long delayMillis) { 189 this(file, listener, delayMillis, false); 190 } 191 192 /** 193 * Creates a Tailer for the given file, with a delay other than the default 1.0s. 194 * @param file the file to follow. 195 * @param listener the TailerListener to use. 196 * @param delayMillis the delay between checks of the file for new content in milliseconds. 197 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 198 */ 199 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) { 200 this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE); 201 } 202 203 /** 204 * Creates a Tailer for the given file, with a delay other than the default 1.0s. 205 * @param file the file to follow. 206 * @param listener the TailerListener to use. 207 * @param delayMillis the delay between checks of the file for new content in milliseconds. 208 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 209 * @param reOpen if true, close and reopen the file between reading chunks 210 */ 211 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, 212 final boolean reOpen) { 213 this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE); 214 } 215 216 /** 217 * Creates a Tailer for the given file, with a specified buffer size. 218 * @param file the file to follow. 219 * @param listener the TailerListener to use. 220 * @param delayMillis the delay between checks of the file for new content in milliseconds. 221 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 222 * @param bufSize Buffer size 223 */ 224 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, 225 final int bufSize) { 226 this(file, listener, delayMillis, end, false, bufSize); 227 } 228 229 /** 230 * Creates a Tailer for the given file, with a specified buffer size. 231 * @param file the file to follow. 232 * @param listener the TailerListener to use. 233 * @param delayMillis the delay between checks of the file for new content in milliseconds. 234 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 235 * @param reOpen if true, close and reopen the file between reading chunks 236 * @param bufSize Buffer size 237 */ 238 public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, 239 final boolean reOpen, final int bufSize) { 240 this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize); 241 } 242 243 /** 244 * Creates a Tailer for the given file, with a specified buffer size. 245 * @param file the file to follow. 246 * @param charset the Charset to be used for reading the file 247 * @param listener the TailerListener to use. 248 * @param delayMillis the delay between checks of the file for new content in milliseconds. 249 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 250 * @param reOpen if true, close and reopen the file between reading chunks 251 * @param bufSize Buffer size 252 */ 253 public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, 254 final boolean end, final boolean reOpen 255 , final int bufSize) { 256 this.file = file; 257 this.delayMillis = delayMillis; 258 this.end = end; 259 260 this.inbuf = new byte[bufSize]; 261 262 // Save and prepare the listener 263 this.listener = listener; 264 listener.init(this); 265 this.reOpen = reOpen; 266 this.charset = charset; 267 } 268 269 /** 270 * Creates and starts a Tailer for the given file. 271 * 272 * @param file the file to follow. 273 * @param listener the TailerListener to use. 274 * @param delayMillis the delay between checks of the file for new content in milliseconds. 275 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 276 * @param bufSize buffer size. 277 * @return The new tailer 278 */ 279 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, 280 final boolean end, final int bufSize) { 281 return create(file, listener, delayMillis, end, false, bufSize); 282 } 283 284 /** 285 * Creates and starts a Tailer for the given file. 286 * 287 * @param file the file to follow. 288 * @param listener the TailerListener to use. 289 * @param delayMillis the delay between checks of the file for new content in milliseconds. 290 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 291 * @param reOpen whether to close/reopen the file between chunks 292 * @param bufSize buffer size. 293 * @return The new tailer 294 */ 295 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, 296 final boolean end, final boolean reOpen, 297 final int bufSize) { 298 return create(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufSize); 299 } 300 301 /** 302 * Creates and starts a Tailer for the given file. 303 * 304 * @param file the file to follow. 305 * @param charset the character set to use for reading the file 306 * @param listener the TailerListener to use. 307 * @param delayMillis the delay between checks of the file for new content in milliseconds. 308 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 309 * @param reOpen whether to close/reopen the file between chunks 310 * @param bufSize buffer size. 311 * @return The new tailer 312 */ 313 public static Tailer create(final File file, final Charset charset, final TailerListener listener, 314 final long delayMillis, final boolean end, final boolean reOpen 315 ,final int bufSize) { 316 final Tailer tailer = new Tailer(file, charset, listener, delayMillis, end, reOpen, bufSize); 317 final Thread thread = new Thread(tailer); 318 thread.setDaemon(true); 319 thread.start(); 320 return tailer; 321 } 322 323 /** 324 * Creates and starts a Tailer for the given file with default buffer size. 325 * 326 * @param file the file to follow. 327 * @param listener the TailerListener to use. 328 * @param delayMillis the delay between checks of the file for new content in milliseconds. 329 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 330 * @return The new tailer 331 */ 332 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, 333 final boolean end) { 334 return create(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE); 335 } 336 337 /** 338 * Creates and starts a Tailer for the given file with default buffer size. 339 * 340 * @param file the file to follow. 341 * @param listener the TailerListener to use. 342 * @param delayMillis the delay between checks of the file for new content in milliseconds. 343 * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file. 344 * @param reOpen whether to close/reopen the file between chunks 345 * @return The new tailer 346 */ 347 public static Tailer create(final File file, final TailerListener listener, final long delayMillis, 348 final boolean end, final boolean reOpen) { 349 return create(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE); 350 } 351 352 /** 353 * Creates and starts a Tailer for the given file, starting at the beginning of the file 354 * 355 * @param file the file to follow. 356 * @param listener the TailerListener to use. 357 * @param delayMillis the delay between checks of the file for new content in milliseconds. 358 * @return The new tailer 359 */ 360 public static Tailer create(final File file, final TailerListener listener, final long delayMillis) { 361 return create(file, listener, delayMillis, false); 362 } 363 364 /** 365 * Creates and starts a Tailer for the given file, starting at the beginning of the file 366 * with the default delay of 1.0s 367 * 368 * @param file the file to follow. 369 * @param listener the TailerListener to use. 370 * @return The new tailer 371 */ 372 public static Tailer create(final File file, final TailerListener listener) { 373 return create(file, listener, DEFAULT_DELAY_MILLIS, false); 374 } 375 376 /** 377 * Return the file. 378 * 379 * @return the file 380 */ 381 public File getFile() { 382 return file; 383 } 384 385 /** 386 * Gets whether to keep on running. 387 * 388 * @return whether to keep on running. 389 * @since 2.5 390 */ 391 protected boolean getRun() { 392 return run; 393 } 394 395 /** 396 * Return the delay in milliseconds. 397 * 398 * @return the delay in milliseconds. 399 */ 400 public long getDelay() { 401 return delayMillis; 402 } 403 404 /** 405 * Follows changes in the file, calling the TailerListener's handle method for each new line. 406 */ 407 @Override 408 public void run() { 409 RandomAccessFile reader = null; 410 try { 411 long last = 0; // The last time the file was checked for changes 412 long position = 0; // position within the file 413 // Open the file 414 while (getRun() && reader == null) { 415 try { 416 reader = new RandomAccessFile(file, RAF_MODE); 417 } catch (final FileNotFoundException e) { 418 listener.fileNotFound(); 419 } 420 if (reader == null) { 421 Thread.sleep(delayMillis); 422 } else { 423 // The current position in the file 424 position = end ? file.length() : 0; 425 last = file.lastModified(); 426 reader.seek(position); 427 } 428 } 429 while (getRun()) { 430 final boolean newer = FileUtils.isFileNewer(file, last); // IO-279, must be done first 431 // Check the file length to see if it was rotated 432 final long length = file.length(); 433 if (length < position) { 434 // File was rotated 435 listener.fileRotated(); 436 // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it 437 // successfully 438 try (RandomAccessFile save = reader) { 439 reader = new RandomAccessFile(file, RAF_MODE); 440 // At this point, we're sure that the old file is rotated 441 // Finish scanning the old file and then we'll start with the new one 442 try { 443 readLines(save); 444 } catch (final IOException ioe) { 445 listener.handle(ioe); 446 } 447 position = 0; 448 } catch (final FileNotFoundException e) { 449 // in this case we continue to use the previous reader and position values 450 listener.fileNotFound(); 451 Thread.sleep(delayMillis); 452 } 453 continue; 454 } 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 if (reOpen && reader != null) { 474 reader.close(); 475 } 476 Thread.sleep(delayMillis); 477 if (getRun() && reOpen) { 478 reader = new RandomAccessFile(file, RAF_MODE); 479 reader.seek(position); 480 } 481 } 482 } catch (final InterruptedException e) { 483 Thread.currentThread().interrupt(); 484 listener.handle(e); 485 } catch (final Exception e) { 486 listener.handle(e); 487 } finally { 488 try { 489 if (reader != null) { 490 reader.close(); 491 } 492 } 493 catch (final IOException e) { 494 listener.handle(e); 495 } 496 stop(); 497 } 498 } 499 500 /** 501 * Allows the tailer to complete its current loop and return. 502 */ 503 public void stop() { 504 this.run = false; 505 } 506 507 /** 508 * Read new lines. 509 * 510 * @param reader The file to read 511 * @return The new position after the lines have been read 512 * @throws java.io.IOException if an I/O error occurs. 513 */ 514 private long readLines(final RandomAccessFile reader) throws IOException { 515 try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) { 516 long pos = reader.getFilePointer(); 517 long rePos = pos; // position to re-read 518 int num; 519 boolean seenCR = false; 520 while (getRun() && ((num = reader.read(inbuf)) != EOF)) { 521 for (int i = 0; i < num; i++) { 522 final byte ch = inbuf[i]; 523 switch ( ch ) { 524 case '\n': 525 seenCR = false; // swallow CR before LF 526 listener.handle(new String(lineBuf.toByteArray(), charset)); 527 lineBuf.reset(); 528 rePos = pos + i + 1; 529 break; 530 case '\r': 531 if (seenCR) { 532 lineBuf.write('\r'); 533 } 534 seenCR = true; 535 break; 536 default: 537 if (seenCR) { 538 seenCR = false; // swallow final CR 539 listener.handle(new String(lineBuf.toByteArray(), charset)); 540 lineBuf.reset(); 541 rePos = pos + i + 1; 542 } 543 lineBuf.write(ch); 544 } 545 } 546 pos = reader.getFilePointer(); 547 } 548 549 reader.seek(rePos); // Ensure we can re-read if necessary 550 551 if (listener instanceof TailerListenerAdapter) { 552 ((TailerListenerAdapter) listener).endOfFileReached(); 553 } 554 555 return rePos; 556 } 557 } 558}