/*-
*******************************************************************************
* Copyright (c) 2011, 2016 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthew Gerring - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.dawnsci.remotedataset.server.event;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.List;
import org.eclipse.dawnsci.analysis.api.io.IDataHolder;
import org.eclipse.dawnsci.remotedataset.ServiceHolder;
import org.eclipse.dawnsci.remotedataset.server.DiagnosticInfo;
import org.eclipse.january.IMonitor;
import org.eclipse.january.dataset.DataEvent;
import org.eclipse.january.dataset.IDynamicDataset;
import org.eclipse.january.dataset.ILazyDataset;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import hdf.hdf5lib.exceptions.HDF5FunctionArgumentException;
public class FileMonitorSocket extends WebSocketAdapter {
private static final Logger logger = LoggerFactory.getLogger(FileMonitorSocket.class);
private static DiagnosticInfo diagInfo;
private boolean connected;
@Override
public void onWebSocketConnect(Session sess) {
connected = true;
final String spath = getFirstValue(sess, "path");
final String sset = getFirstValue(sess, "dataset");
final boolean writing = Boolean.parseBoolean(getFirstValue(sess, "writingExpected"));
final Path path = Paths.get(spath);
try {
WatchService myWatcher = path.getFileSystem().newWatchService();
QueueReader fileWatcher = new QueueReader(myWatcher, sess, spath, sset, writing);
// We may only monitor a directory
if (Files.isDirectory(path)) {
path.register(myWatcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
} else {
path.getParent().register(myWatcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
}
Thread th = new Thread(fileWatcher, path.getFileName()+" Watcher");
th.setDaemon(true);
th.setPriority(Thread.MAX_PRIORITY-2);
th.start();
if (diagInfo!=null) diagInfo.record("Start Thread", th.getName());
} catch (Exception ne) {
logger.error("Cannot watch "+path, ne);
try {
sess.getRemote().sendString(ne.getMessage());
} catch (IOException e) {
logger.warn("Cannot write to remote "+sess, e);
}
}
}
private String getFirstValue(Session sess, String name) {
final List<String> vals = sess.getUpgradeRequest().getParameterMap().get(name);
return vals!=null?vals.get(0):null;
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
super.onWebSocketClose(statusCode, reason);
connected = false;
}
private class QueueReader implements Runnable {
/** the watchService that is passed in from above */
private WatchService watcher;
private Session session;
private String spath;
private String sdataset;
private boolean writing;
public QueueReader(WatchService watcher, Session session, String path, String dataset, boolean writing) {
this.watcher = watcher;
this.session = session;
this.spath = path;
this.sdataset = dataset;
this.writing = writing;
}
/**
* In order to implement a file watcher, we loop forever
* ensuring requesting to take the next item from the file
* watchers queue.
*/
@Override
public void run() {
final Path path = Paths.get(spath);
try {
// We wait until the file we are told to monitor exists.
while(!Files.exists(path)) {
Thread.sleep(200);
}
// get the first event before looping
WatchKey key = null;
while(session.isOpen() && connected && (key = watcher.take()) != null) {
try {
if (!Files.exists(path)) continue;
for (WatchEvent<?> event : key.pollEvents()) {
if (!Files.exists(path)) continue;
Path epath = (Path)event.context();
if (!Files.isDirectory(path) && !path.endsWith(epath)) continue;
try {
// Data has changed, read its shape and publish the event using a web socket.
final IDataHolder holder = ServiceHolder.getLoaderService().getData(spath, new IMonitor.Stub());
if (holder == null) continue; // We do not stop if the loader got nothing.
final ILazyDataset lz = sdataset!=null && !"".equals(sdataset)
? holder.getLazyDataset(sdataset)
: holder.getLazyDataset(0);
if (lz == null) continue; // We do not stop if the loader got nothing.
if (lz instanceof IDynamicDataset) {
((IDynamicDataset)lz).refreshShape();
}
if (writing) {
ServiceHolder.getLoaderService().clearSoftReferenceCache(spath);
}
final DataEvent evt = new DataEvent(lz.getName(), lz.getShape());
evt.setFilePath(spath);
// We manually JSON the object because we
// do not want a dependency and object simple
String json = evt.encode();
session.getRemote().sendString(json);
if (diagInfo!=null) diagInfo.record("JSON Send", json);
} catch (HDF5FunctionArgumentException h5error) {
// This happens sometimes when the file is not ready to read.
logger.trace("Path might not be ready to read "+path);
continue;
} catch (Exception ne) {
logger.error("Exception getting data from "+path);
continue;
}
break;
}
} finally {
key.reset();
}
}
} catch (Exception e) {
logger.error("Exception monitoring "+path, e);
if (session.isOpen()) session.close(403, e.getMessage());
} finally {
if (diagInfo!=null) diagInfo.record("Close Thread", Thread.currentThread().getName());
try {
watcher.close();
} catch (IOException e) {
logger.error("Error closing watcher",e);
}
}
}
}
public static void setRecordThreads(boolean recordThreads) {
if (recordThreads) {
FileMonitorSocket.diagInfo = new DiagnosticInfo();
} else {
FileMonitorSocket.diagInfo = null;
}
}
public static DiagnosticInfo getDiagnosticInfo() {
return diagInfo;
}
}