/*
* Copyright 2004 - 2009 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id: Folder.java 10493 2009-11-18 23:24:26Z tot $
*/
package de.dal33t.powerfolder.disk;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import net.contentobjects.jnotify.JNotify;
import net.contentobjects.jnotify.JNotifyException;
import net.contentobjects.jnotify.JNotifyListener;
import de.dal33t.powerfolder.ConfigurationEntry;
import de.dal33t.powerfolder.PFComponent;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.light.FileInfoFactory;
import de.dal33t.powerfolder.util.FileUtils;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.Util;
import de.dal33t.powerfolder.util.os.OSUtil;
/**
* TRAC #711: Automatic change detection by watching the filesystem.
* <p>
* Does NOT watch Meta Folders.
*
* @author sprajc
*/
public class FolderWatcher extends PFComponent {
private static final boolean UNREGISTER_WATCHERS = true;
private static Boolean LIB_LOADED;
private Folder folder;
private int watchID = -1;
private NotifyListener listener;
private Map<String, FileInfo> dirtyFiles = Util.createConcurrentHashMap();
private volatile boolean ignoreAll;
private Map<FileInfo, FileInfo> ignoreFiles = Util
.createConcurrentHashMap();
private AtomicBoolean scheduled = new AtomicBoolean(false);
private ReentrantLock scannerLock = new ReentrantLock();
private long delay;
FolderWatcher(Folder folder) {
super(folder.getController());
this.folder = folder;
this.listener = new NotifyListener();
reconfigure(folder.getSyncProfile());
}
public boolean isSupported() {
return ConfigurationEntry.FOLDER_WATCHER_ENABLED
.getValueBoolean(getController()) && isLibLoaded();
}
/**
* Adds a file to the ingore list. Files won't get scanned by FolderWatcher
* until they get removed.
*
* @param fInfo
*/
void addIgnoreFile(FileInfo fInfo) {
if (!isSupported()) {
return;
}
Reject.ifNull(fInfo, "FileInfo");
ignoreFiles.put(fInfo, fInfo);
if (isFiner()) {
logFiner("Added to ignore: " + fInfo.toDetailString());
}
}
/**
* Removes a file from the ignore list. Files afterwards get automatically
* scanned if a file system change event occurs.
*
* @param fInfo
*/
void removeIgnoreFile(final FileInfo fInfo) {
if (!isSupported()) {
return;
}
Reject.ifNull(fInfo, "FileInfo");
// Delay the removal by ~1000 ms because file system events occur
// delayed.
getController().schedule(new TimerTask() {
@Override
public void run() {
ignoreFiles.remove(fInfo.getRelativeName());
if (isFiner()) {
logFiner("Removed from ignore: " + fInfo.toDetailString());
}
}
}, 1000);
}
/**
* @param ignoreAll
* if ignore all file system events. Basically suspends the
* FolderWatcher.
*/
public void setIngoreAll(boolean ignoreAll) {
this.ignoreAll = ignoreAll;
}
public synchronized static boolean isLibLoaded() {
if (LIB_LOADED == null) {
try {
// PFC-2162:
System.setProperty("file.encoding", "UTF8");
LIB_LOADED = OSUtil.loadLibrary(JNotify.class, "jnotify");
} catch (Error e) {
LIB_LOADED = false;
}
}
return LIB_LOADED;
}
synchronized void remove() {
if (!isLibLoaded()) {
return;
}
if (watchID >= 0) {
if (!UNREGISTER_WATCHERS) {
logWarning("NOT unregistering filesystem watcher from "
+ folder
+ " to prevent crash. Ignoring further filesystem events");
watchID = -1;
return;
}
try {
JNotify.removeWatch(watchID);
} catch (JNotifyException e) {
logWarning(e);
} finally {
watchID = -1;
}
}
}
synchronized void reconfigure(SyncProfile syncProfile) {
if (folder.isEncrypted()) {
return;
}
if (!isSupported()) {
return;
}
if (!syncProfile.isInstantSync()) {
remove();
return;
}
if (folder.getInfo().isMetaFolder()) {
remove();
return;
}
if (folder.checkIfDeviceDisconnected()) {
remove();
return;
}
String path = folder.getLocalBase().getAbsolutePath();
if (path.startsWith("\\")) {
// Don't watch on UNC paths
remove();
return;
}
delay = 1000L * ConfigurationEntry.FOLDER_WATCHER_DELAY
.getValueInt(getController());
boolean watchSubtree = true;
try {
watchID = JNotify.addWatch(path, JNotify.FILE_ANY, watchSubtree,
listener);
logFine("Initialized filesystem watch on " + path + " / " + folder);
} catch (JNotifyException e) {
logSevere("Unable to initialize filesystem watch for " + folder
+ ". " + e);
logFiner(e);
watchID = -1;
}
}
// Logger methods *********************************************************
@Override
public String getLoggerName() {
return super.getLoggerName() + " '" + folder.getName() + '\'';
}
private class DirtyFilesScanner implements Runnable {
public void run() {
if (!scannerLock.tryLock()) {
// Already locked
return;
}
if (dirtyFiles.isEmpty()) {
return;
}
if (ignoreAll) {
return;
}
FileInfo dirtyFile = null;
try {
List<FileInfo> fileInfos = new LinkedList<FileInfo>();
if (folder.checkIfDeviceDisconnected()) {
logFine("Device disconnected while scanning " + folder
+ ": " + folder.getLocalBase());
dirtyFiles.clear();
return;
}
for (Entry<String, FileInfo> entry : dirtyFiles.entrySet()) {
dirtyFile = entry.getValue();
if (ignoreAll) {
return;
}
if (ignoreFiles.containsKey(dirtyFile)) {
// Ignore.
continue;
}
fileInfos.add(dirtyFile);
}
dirtyFiles.clear();
if (!fileInfos.isEmpty()) {
folder.scanChangedFiles(fileInfos);
for (FileInfo fileInfo : fileInfos) {
if (!fileInfo.isLookupInstance()
&& fileInfo.isDiretory())
{
folder.recommendScanOnNextMaintenance();
}
}
}
if (fileInfos.size() > 0 && isFine()) {
logFine("Scanned " + fileInfos.size() + " changed files");
}
} catch (Exception e) {
logSevere("Unable to scan changed file: " + dirtyFile + ". "
+ e, e);
} finally {
scannerLock.unlock();
}
}
}
private class NotifyListener implements JNotifyListener {
public void fileRenamed(int wd, String rootPath, String oldName,
String newName)
{
fileChanged(rootPath, oldName);
fileChanged(rootPath, newName);
}
public void fileModified(int wd, String rootPath, String name) {
fileChanged(rootPath, name);
}
public void fileDeleted(int wd, String rootPath, String name) {
fileChanged(rootPath, name);
}
public void fileCreated(int wd, String rootPath, String name) {
fileChanged(rootPath, name);
}
private void fileChanged(String rootPath, String name) {
if (watchID < 0) {
// Illegal / Useless
return;
}
if (!isSupported()) {
// No supported
return;
}
if (!folder.scanAllowedNow()) {
// Not allowed
return;
}
if (!FileUtils.isScannable(name, folder.getInfo())) {
return;
}
if (ignoreAll) {
return;
}
if (OSUtil.isMacOS() && name.contains("?")) {
// Skip
return;
}
// For linux
if (name.endsWith("/")) {
name = name.substring(0, name.length() - 1);
}
name = FileInfoFactory.decodeIllegalChars(name);
if (dirtyFiles.containsKey(name)) {
// Skipping already dirty file
return;
}
try {
FileInfo lookup = lookupInstance(rootPath, name);
if (ignoreFiles.containsKey(lookup)) {
// Skipping ignored file
return;
}
dirtyFiles.put(name, lookup);
if (!scannerLock.isLocked()) {
if (scheduled.compareAndSet(false, true)) {
getController().schedule(new Runnable() {
public void run() {
getController().getIOProvider().startIO(
new DirtyFilesScanner());
scheduled.set(false);
}
}, delay);
}
}
} catch (Exception e) {
logSevere("Unable to enqueue changed file for scan: "
+ rootPath + ", " + name + ". " + e, e);
}
}
private FileInfo lookupInstance(String rootPath, String rawName) {
String name = rawName;
if (name.contains("\\")) {
name = name.replace('\\', '/');
}
if (name.contains("//")) {
name = name.replace("//", "/");
}
if (name.startsWith("/")) {
name = name.substring(1);
}
return FileInfoFactory.lookupInstance(folder.getInfo(), name);
}
}
}