blob: 84f5c086f1c3f42bc95587d710039be8771b59f1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.masterfs.watcher.windows;
import org.netbeans.modules.masterfs.providers.Notifier;
import com.sun.jna.FromNativeContext;
import com.sun.jna.IntegerType;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.PointerType;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.ptr.ByReference;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import java.io.InterruptedIOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.lookup.ServiceProvider;
/**
* A {@link Notifier} implementation using Win32 API ReadDirectoryChangesW.
* Based on JNA examples and platform library stubs.
*
* @author nenik
*/
// This must come before NioNotifier to avoid dangling directory references.
// See [NETBEANS-2785], [NETBEANS-3769]
@ServiceProvider(service=Notifier.class, position=150)
public final class WindowsNotifier extends Notifier<Void> {
static final Logger LOG = Logger.getLogger(WindowsNotifier.class.getName());
public static final class HANDLE extends PointerType {
private boolean immutable;
public HANDLE() { }
public HANDLE(Pointer p) {
setPointer(p);
immutable = true;
}
/** Override to the appropriate object for INVALID_HANDLE_VALUE. */
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
Object o = super.fromNative(nativeValue, context);
if (INVALID_HANDLE_VALUE.equals(o))
return INVALID_HANDLE_VALUE;
return o;
}
@Override
public void setPointer(Pointer p) {
if (immutable) {
throw new UnsupportedOperationException("immutable reference");
}
super.setPointer(p);
}
}
public static class ULONG_PTR extends IntegerType {
public ULONG_PTR() {
this(0);
}
public ULONG_PTR(long value) {
super(Native.POINTER_SIZE, value);
}
}
@SuppressWarnings("PublicField")
@FieldOrder({"Internal", "InternalHigh", "Offset", "OffsetHigh", "hEvent"})
public static class OVERLAPPED extends Structure {
public ULONG_PTR Internal;
public ULONG_PTR InternalHigh;
public int Offset;
public int OffsetHigh;
public HANDLE hEvent;
}
public static HANDLE INVALID_HANDLE_VALUE = new HANDLE(Pointer.createConstant(
Native.POINTER_SIZE == 8 ? -1 : 0xFFFFFFFFL));
public static class HANDLEByReference extends ByReference {
public HANDLEByReference() {
this(null);
}
public HANDLEByReference(HANDLE h) {
super(Native.POINTER_SIZE);
setValue(h);
}
public void setValue(HANDLE h) {
getPointer().setPointer(0, h != null ? h.getPointer() : null);
}
public HANDLE getValue() {
Pointer p = getPointer().getPointer(0);
if (p == null)
return null;
if (INVALID_HANDLE_VALUE.getPointer().equals(p))
return INVALID_HANDLE_VALUE;
HANDLE h = new HANDLE();
h.setPointer(p);
return h;
}
}
@SuppressWarnings("PublicField")
@FieldOrder({"NextEntryOffset", "Action", "FileNameLength", "FileName"})
public static class FILE_NOTIFY_INFORMATION extends Structure {
public int NextEntryOffset;
public int Action;
public int FileNameLength;
// filename is not nul-terminated, so we can't use a String/WString
public char[] FileName = new char[1];
private FILE_NOTIFY_INFORMATION() {}
public FILE_NOTIFY_INFORMATION(int size) {
if (size < size()) {
throw new IllegalArgumentException("Size must greater than "
+ size() + ", requested " + size);
}
allocateMemory(size);
}
/** WARNING: this filename may be either the short or long form of the filename. */
public String getFilename() {
return new String(FileName, 0, FileNameLength/2);
}
@Override
public void read() {
// avoid reading filename until we know how long it is
FileName = new char[0];
super.read();
FileName = getPointer().getCharArray(12, FileNameLength/2);
}
public FILE_NOTIFY_INFORMATION next() {
if (NextEntryOffset == 0)
return null;
FILE_NOTIFY_INFORMATION next = new FILE_NOTIFY_INFORMATION();
next.useMemory(getPointer(), NextEntryOffset);
next.read();
return next;
}
}
@SuppressWarnings("PublicField")
@FieldOrder({"nLength", "lpSecurityDescriptor", "bInheritHandle"})
public static class SECURITY_ATTRIBUTES extends Structure {
public final int nLength = size();
public Pointer lpSecurityDescriptor;
public boolean bInheritHandle;
}
interface Kernel32 extends StdCallLibrary {
HANDLE CreateFile(String lpFileName, int dwDesiredAccess, int dwShareMode,
SECURITY_ATTRIBUTES lpSecurityAttributes, int dwCreationDisposition,
int dwFlagsAndAttributes, HANDLE hTemplateFile);
HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort,
Pointer CompletionKey, int NumberOfConcurrentThreads);
int GetLastError();
boolean GetQueuedCompletionStatus(HANDLE CompletionPort,
IntByReference lpNumberOfBytes, ByReference lpCompletionKey,
PointerByReference lpOverlapped, int dwMilliseconds);
boolean PostQueuedCompletionStatus(HANDLE CompletionPort,
int dwNumberOfBytesTransferred, Pointer dwCompletionKey,
OVERLAPPED lpOverlapped);
boolean CloseHandle(HANDLE hObject);
interface OVERLAPPED_COMPLETION_ROUTINE extends StdCallCallback {
void callback(int errorCode, int nBytesTransferred,
OVERLAPPED overlapped);
}
public boolean ReadDirectoryChangesW(HANDLE directory,
FILE_NOTIFY_INFORMATION info, int length, boolean watchSubtree,
int notifyFilter, IntByReference bytesReturned, OVERLAPPED overlapped,
OVERLAPPED_COMPLETION_ROUTINE completionRoutine);
}
static final Kernel32 KERNEL32 = Native.load("kernel32", Kernel32.class, W32APIOptions.UNICODE_OPTIONS);
public WindowsNotifier() { // prepare port, start thread?
}
public @Override void removeWatch(Void key) throws IOException {}
public @Override String nextEvent() throws IOException, InterruptedException {
return events.take();
}
public static final int INFINITE = 0xFFFFFFFF;
public static final int FILE_NOTIFY_CHANGE_NAME = 0x00000003;
public static final int FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004;
public static final int FILE_NOTIFY_CHANGE_SIZE = 0x00000008;
public static final int FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010;
public static final int FILE_NOTIFY_CHANGE_CREATION = 0x00000040;
public static final int FILE_NOTIFY_CHANGE_SECURITY = 0x00000100;
private static final int NOTIFY_MASK =
FILE_NOTIFY_CHANGE_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE |
FILE_NOTIFY_CHANGE_LAST_WRITE |
FILE_NOTIFY_CHANGE_CREATION |
FILE_NOTIFY_CHANGE_SECURITY;
public static final int FILE_LIST_DIRECTORY = 0x00000001;
public static final int OPEN_EXISTING = 3;
public static final int FILE_SHARE_READ = 0x00000001;
public static final int FILE_SHARE_WRITE = 0x00000002;
public static final int FILE_SHARE_DELETE = 0x00000004;
public static final int FILE_FLAG_OVERLAPPED = 0x40000000;
public static final int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
private class FileInfo {
public final String path;
public final HANDLE handle;
public final FILE_NOTIFY_INFORMATION info = new FILE_NOTIFY_INFORMATION(BUFFER_SIZE);
public final IntByReference infoLength = new IntByReference();
public final OVERLAPPED overlapped = new OVERLAPPED();
public FileInfo(String path, HANDLE h) {
this.path = path;
this.handle = h;
}
}
private static int watcherThreadID;
private Thread watcher;
private HANDLE port;
private final Map<String, FileInfo> rootMap = new HashMap<String, FileInfo>();
private final Map<HANDLE, FileInfo> handleMap = new HashMap<HANDLE, FileInfo>();
private final BlockingQueue<String> events = new LinkedBlockingQueue<String>();
public @Override Void addWatch(String path) throws IOException {
if (path.isEmpty()) {
return null;
}
String root = null;
if (path.length() >= 3 && path.charAt(1) == ':') { // classic drive letter (e.g. C:\)
root = path.substring(0, 3).replace('/', '\\');
if (root.charAt(2) != '\\') {
throw new IOException("wrong path: " + path);
} else if (path.length() == 3) { // Can be suspicious. #236930.
LOG.log(Level.INFO, "Adding listener for drive {0}", path); //NOI18N
}
} else { // check if it is unc path
String normedPath = path.replace('/', '\\');
if (normedPath.startsWith("\\\\")) {
int thirdBackslash = normedPath.indexOf('\\', 3);
if (thirdBackslash != -1) {
int endOfRoot = normedPath.indexOf('\\', thirdBackslash + 1);
if (endOfRoot == -1) {
endOfRoot = normedPath.length();
}
root = normedPath.substring(0, endOfRoot);
} else {
throw new IOException("wrong path: " + path);
}
}
}
if (rootMap.containsKey(root)) return null; // already listening
path = root; // listen once on the rootpath instead
int mask = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
int flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED;
HANDLE handle = KERNEL32.CreateFile(path,
FILE_LIST_DIRECTORY,
mask, null, OPEN_EXISTING,
flags, null);
if (INVALID_HANDLE_VALUE.equals(handle)) {
throw new IOException("Unable to open " + path + ": "
+ KERNEL32.GetLastError());
}
FileInfo finfo = new FileInfo(path, handle);
rootMap.put(path, finfo);
handleMap.put(handle, finfo);
// Existing port is returned
port = KERNEL32.CreateIoCompletionPort(handle, port, handle.getPointer(), 0);
if (INVALID_HANDLE_VALUE.equals(port)) {
throw new IOException("Unable to create/use I/O Completion port "
+ "for " + path + ": " + KERNEL32.GetLastError());
}
if (!KERNEL32.ReadDirectoryChangesW(handle, finfo.info, finfo.info.size(),
true, NOTIFY_MASK, finfo.infoLength,
finfo.overlapped, null)) {
int err = KERNEL32.GetLastError();
throw new IOException("ReadDirectoryChangesW failed on "
+ finfo.path + ", handle " + handle
+ ": " + err);
}
if (watcher == null) {
Thread t = new Thread("W32 File Monitor") {
@Override
public void run() {
FileInfo finfo;
while (watcher != null) {
finfo = waitForChange();
if (finfo == null) continue;
try {
handleChanges(finfo);
} catch(IOException e) {
LOG.log(Level.INFO, "handleChanges", e);
}
}
}
};
t.setDaemon(true);
t.start();
watcher = t;
}
return null;
}
@Override
protected void start() throws IOException {
}
@Override
public void stop() throws IOException {
try {
Thread w = watcher;
if (w == null) {
return;
}
watcher = null;
w.interrupt();
w.join(2000);
} catch (InterruptedException ex) {
throw (IOException)new InterruptedIOException().initCause(ex);
}
}
private void notify(File file) {
events.add(file.getPath());
}
private static final int BUFFER_SIZE = 4096;
private void handleChanges(FileInfo finfo) throws IOException {
FILE_NOTIFY_INFORMATION fni = finfo.info;
// Lazily fetch the data from native to java - asynchronous update
fni.read();
do {
File file = new File(finfo.path, fni.getFilename());
notify(file);
fni = fni.next();
} while (fni != null);
if (!KERNEL32.ReadDirectoryChangesW(finfo.handle, finfo.info,
finfo.info.size(), true, NOTIFY_MASK,
finfo.infoLength, finfo.overlapped, null)) {
int err = KERNEL32.GetLastError();
throw new IOException("ReadDirectoryChangesW failed on "
+ finfo.path + ": " + err);
}
}
private FileInfo waitForChange() {
IntByReference rcount = new IntByReference();
HANDLEByReference rkey = new HANDLEByReference();
PointerByReference roverlap = new PointerByReference();
KERNEL32.GetQueuedCompletionStatus(port, rcount, rkey, roverlap, INFINITE);
synchronized (this) {
return handleMap.get(rkey.getValue());
}
}
}