/* * Copyright 2012-2013, CMM, University of Queensland. * * This file is part of Paul. * * Paul is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Paul is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Paul. If not, see <http://www.gnu.org/licenses/>. */ package au.edu.uq.cmm.paul.watcher; import java.io.File; import java.io.IOException; import java.net.UnknownHostException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import au.edu.uq.cmm.aclslib.config.FacilityConfig; import au.edu.uq.cmm.aclslib.service.MonitoredThreadServiceBase; import au.edu.uq.cmm.aclslib.service.Service; import au.edu.uq.cmm.aclslib.service.ServiceWrapper; import au.edu.uq.cmm.paul.Paul; import au.edu.uq.cmm.paul.PaulException; import au.edu.uq.cmm.paul.grabber.FileGrabber; import au.edu.uq.cmm.paul.status.Facility; import au.edu.uq.cmm.paul.status.FacilityStatus; import au.edu.uq.cmm.paul.status.FacilityStatusManager.Status; public class FileWatcher extends MonitoredThreadServiceBase { private static class WatcherEntry { private final Path dir; private final Facility facility; private final WatchKey key; private final WatcherEntry parent; private final Set<WatcherEntry> children; public WatcherEntry(WatchKey key, WatcherEntry parent, Path dir, Facility facility) { super(); this.dir = dir; this.facility = facility; this.key = key; this.parent = parent; this.children = new HashSet<WatcherEntry>(); if (parent != null) { parent.children.add(this); } } @Override public int hashCode() { return key.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; return key.equals(((WatcherEntry) obj).key); } } private static final Logger LOG = LoggerFactory.getLogger(FileWatcher.class); private Map<WatchKey, WatcherEntry> watchMap = new HashMap<WatchKey, WatcherEntry>(); private UncPathnameMapper uncNameMapper; private WatchService watcher; private Paul services; public FileWatcher(Paul services) throws UnknownHostException { this.services = services; this.uncNameMapper = Objects.requireNonNull(services.getUncNameMapper()); } @SuppressWarnings("unchecked") static <T> WatchEvent<T> cast(WatchEvent<?> event) { return (WatchEvent<T>)event; } @Override public void run() { watcher = null; try { startWatcher(); while (true) { WatchKey key = watcher.take(); WatcherEntry entry = watchMap.get(key); if (entry != null) { for (WatchEvent<?> event : key.pollEvents()) { processWatchEvent(entry, event); } } key.reset(); } } catch (IOException ex) { throw new PaulException("Unexpected IO error", ex); } catch (InterruptedException ex) { LOG.info("Interrupted ... we're done"); } finally { stopWatcher(); } } private void processWatchEvent(WatcherEntry entry, WatchEvent<?> event) throws IOException { Kind<?> kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) { LOG.error("Event overflow!"); return; } WatchEvent<Path> ev = cast(event); Path path = entry.dir.resolve(ev.context()); File file = path.toFile(); LOG.debug("Event for facility " + entry.facility.getFacilityName()); if (kind == StandardWatchEventKinds.ENTRY_CREATE) { LOG.debug("Created - " + path); if (file.isDirectory()) { addKeys(entry.facility, file, entry); } else { notifyEvent(entry.facility, file, true); } } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { LOG.debug("Modified - " + path); if (!file.isDirectory()) { notifyEvent(entry.facility, file, false); } } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { LOG.debug("Deleted - " + path); removeKeyForPath(entry, path); } } private void notifyEvent(Facility facility, File file, boolean create) { long now = System.currentTimeMillis(); FacilityStatus status = services.getFacilityStatusManager().getStatus(facility); FileGrabber grabber = status.getFileGrabber(); if (grabber != null) { grabber.eventOccurred(new FileWatcherEvent(facility, file, create, now, false)); } } private void startWatcher() throws IOException { FileSystem fs = FileSystems.getDefault(); watcher = fs.newWatchService(); for (FacilityConfig facility : services.getFacilityMapper().allFacilities()) { startFileWatching((Facility) facility); } } private void stopWatcher() { if (watcher == null) { return; } try { for (FacilityConfig facility : services.getFacilityMapper().allFacilities()) { stopFileWatching((Facility) facility); } } finally { try { watcher.close(); } catch (IOException ex) { LOG.debug("Exception in watcher close", ex); } finally { watcher = null; } } } public void startFileWatching(Facility facility) { // FIXME - file grabber start / stop is not properly synchronized. LOG.debug("StartFileWatching(" + facility.getFacilityName() + ")"); String name = facility.getFolderName(); File local; FacilityStatus status = services.getFacilityStatusManager().getStatus(facility); if (facility.isDisabled()) { status.setStatus(Status.DISABLED); } else if (getState() != Service.State.STARTED) { status.setStatus(Status.OFF); status.setMessage("File watcher service is not running"); } else if (name == null) { LOG.info("Facility's folder name is unset"); status.setStatus(Status.OFF); status.setMessage("Facility's folder name is unset"); } else if ((local = uncNameMapper.mapUncPathname(name)) == null) { LOG.info("Facility's folder name (" + name + ") isn't a Samba share on this host"); status.setStatus(Status.OFF); status.setMessage("Facility's folder name (" + name + ") isn't a Samba share on this host"); } else if (!local.exists()) { LOG.info("Facility folder name '" + name + "' maps to non-existent local path '" + local + "'"); status.setStatus(Status.OFF); status.setMessage("Facility folder name '" + name + "' maps to non-existent local path '" + local + "'"); } else { try { status.setLocalDirectory(local); FileGrabber grabber = new FileGrabber(services, facility); Service service = new ServiceWrapper(grabber); status.setFileGrabber(grabber); status.setFileGrabberService(service); service.startStartup(); addKeys(facility, local, null); status.setStatus(Status.ON); status.setMessage(""); } catch (IOException ex) { LOG.error("IOException occured while enabling watcher for '" + name + "'", ex); status.setStatus(Status.OFF); status.setMessage( "An IO exception occured while enabling watcher for '" + name + "' - see logs for details"); } } } public void stopFileWatching(Facility facility) { // FIXME - file grabber start / stop is not properly synchronized. LOG.debug("StopFileWatching(" + facility.getFacilityName() + ")"); try { FacilityStatus status = services.getFacilityStatusManager().getStatus(facility); if (status.getFileGrabberService() == null) { LOG.debug("No file grabber found"); } else { status.getFileGrabberService().shutdown(); status.setFileGrabberService(null); } for (WatcherEntry entry : watchMap.values()) { if (entry.facility == facility && entry.parent == null) { removeKey(entry, true); break; } } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } FacilityStatus status = services.getFacilityStatusManager().getStatus(facility); status.setStatus(facility.isDisabled() ? Status.DISABLED : Status.OFF); status.setMessage(""); } private void addKeys(Facility facility, File local, WatcherEntry parent) throws IOException { // If a directory is created while we are recursively adding // watcher keys, we may possibly miss it. However, I think // that we should get an event for the creation ... which would // allow us to add the key in the event processing code. Path dir = Paths.get(local.toURI()); WatchKey key = null; try { key = dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.OVERFLOW); } catch (IOException ex) { if (parent != null) { LOG.warn("Subdirectory " + local + " for facility " + facility.getFacilityName() + " is not readable: ignoring it", ex); return; } else { throw ex; } } LOG.debug("Added directory watcher for " + local + " for facility " + facility.getFacilityName()); WatcherEntry entry = new WatcherEntry(key, parent, dir, facility); watchMap.put(key, entry); // Recursively add keys for nested directories. for (File child : local.listFiles()) { if (child.isDirectory()) { addKeys(facility, child, entry); } } } private void removeKeyForPath(WatcherEntry entry, Path path) { // Potentially inefficient ... if directory corresponding to 'entry' // has many subdirectories. for (WatcherEntry child : entry.children) { if (child.dir.equals(path)) { removeKey(child, false); break; } } } private void removeKey(WatcherEntry entry, boolean force) { // (We don't want to cancel the watch key for a facility's // folder just because it has disappeared. I don't think // we'll get an event for that ... but this is just in case.) if (entry.parent != null || force) { watchMap.remove(entry.key); entry.key.cancel(); LOG.debug("Cancelled directory watcher for " + entry.dir + " for facility " + entry.facility.getFacilityName()); } for (WatcherEntry child : entry.children) { removeKey(child, true); } if (entry.parent != null) { entry.parent.children.remove(entry); } } }