/**
* Copyright 2013 the original author or authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.WebSocketAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Math.round;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.regex.Pattern.compile;
import static org.apache.commons.lang3.math.NumberUtils.toFloat;
/**
* Implements the tailing of logfiles provided by the {@link LogFiles}.
*
* @author Olaf Otto
*/
public class TailSocket extends WebSocketAdapter {
private static final Pattern TAIL_COMMAND = compile("tail:(([0-9]+\\.)?[0-9]+)mb:(.+)");
private final Logger logger = LoggerFactory.getLogger(getClass());
private ExecutorService executorService = newSingleThreadExecutor();
private final LogFiles logFiles;
private Tail tail;
TailSocket(LogFiles logFiles) {
this.logFiles = logFiles;
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
stopTail();
this.executorService.shutdownNow();
super.onWebSocketClose(statusCode, reason);
}
/**
* @param message a tail command as specified by {@link #TAIL_COMMAND}.
* immediately sends the last <code>n</code> bytes of a specified
* log file, if present, and begins tailing the logfile thereafter.
*/
@Override
public void onWebSocketText(String message) {
if (isPing(message)) {
sendPong();
return;
}
Matcher m = TAIL_COMMAND.matcher(message);
if (!m.matches()) {
logger.warn("Unsupported command format '" + message + "', must match " + TAIL_COMMAND.pattern() + ", ignoring the command.");
return;
}
try {
float including = toFloat(m.group(1));
String path = m.group(m.groupCount());
File file = resolveLogFile(path);
if (file == null) {
return;
}
long bytesToTail = round(including * 1024L * 1024L);
tail(file, bytesToTail);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void sendPong() {
getRemote().sendStringByFuture("pong");
}
private boolean isPing(String message) {
return "ping".equals(message);
}
private void tail(File file, long bytesToTail) throws IOException {
stopTail();
this.tail = new Tail(getRemote(), file, bytesToTail);
this.executorService.execute(this.tail);
}
private void stopTail() {
if (this.tail != null) {
synchronized (this.tail) {
this.tail.stop();
this.tail.notify();
this.tail = null;
}
}
}
private File resolveLogFile(String path) throws IOException {
return this.logFiles.resolveLogFiles()
.stream()
.filter(f -> f.getAbsolutePath().equals(path))
.findFirst()
.orElse(null);
}
}