/** * Copyright 2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package io.neba.core.logviewer; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import static java.lang.Math.max; import static java.lang.Thread.sleep; import static java.nio.ByteBuffer.allocate; import static java.nio.file.Files.newByteChannel; import static java.nio.file.StandardOpenOption.READ; import static org.apache.commons.io.IOUtils.closeQuietly; /** * A non-blocking tail implementation allowing to read an arbitrary number of bytes from the end of a file * and follow changes to it. * * @author Olaf Otto */ public class Tail implements Runnable { private static final int AWAIT_FILE_ROTATION_MILLIS = 1000; private static final int TAIL_CHECK_INTERVAL_MILLIS = 500; private final Logger logger = LoggerFactory.getLogger(getClass()); private final RemoteEndpoint remoteEndpoint; private final File file; private final long bytesToTail; private boolean stopped = false; Tail(RemoteEndpoint remoteEndpoint, File file, long bytesToTail) { if (remoteEndpoint == null) { throw new IllegalArgumentException("constructor parameter remoteEndpoint must not be null"); } if (file == null) { throw new IllegalArgumentException("constructor parameter file must not be null"); } this.bytesToTail = bytesToTail; this.remoteEndpoint = remoteEndpoint; this.file = file; } @Override public void run() { SeekableByteChannel channel = null; try { channel = newByteChannel(this.file.toPath(), READ); long availableInByte = this.file.length(); long startingFromInByte = max(availableInByte - this.bytesToTail, 0); channel.position(startingFromInByte); long position = startingFromInByte; // Read up to this amount of data from the file at once. ByteBuffer readBuffer = allocate(4096); while (!this.stopped) { // The file might be temporarily gone during rotation. Wait, then decide // whether the file is considered gone permanently or whether a rotation has occurred. if (!this.file.exists()) { sleep(AWAIT_FILE_ROTATION_MILLIS); } if (!this.file.exists()) { this.remoteEndpoint.sendString("file not found"); return; } if (position > this.file.length()) { this.remoteEndpoint.sendString("file rotated"); position = 0; closeQuietly(channel); channel = newByteChannel(this.file.toPath(), READ); } int read = channel.read(readBuffer); if (read == -1) { sleep(TAIL_CHECK_INTERVAL_MILLIS); continue; } position = channel.position(); readBuffer.flip(); this.remoteEndpoint.sendBytes(readBuffer); readBuffer.clear(); } } catch (IOException e) { this.logger.error("Unable to tail " + this.file.getAbsolutePath() + ".", e); } catch (InterruptedException e) { if (!this.stopped) { this.logger.error("Stopped tailing " + this.file.getAbsolutePath() + ", got interrupted.", e); } } finally { closeQuietly(channel); } } public void stop() { this.stopped = true; } }