jdk/src/windows/classes/sun/nio/fs/WindowsWatchService.java
author alanb
Mon, 09 May 2011 18:45:33 +0100
changeset 9679 d98ae8bc45fc
parent 9035 1255eb81cc2f
child 17715 9d2234cba0ff
permissions -rw-r--r--
7042979: Rename StandardSocketOption and StandardWatchEventKind Reviewed-by: forax, chegar

/*
 * Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.nio.fs;

import java.nio.file.*;
import java.io.IOException;
import java.util.*;
import com.sun.nio.file.ExtendedWatchEventModifier;
import sun.misc.Unsafe;

import static sun.nio.fs.WindowsNativeDispatcher.*;
import static sun.nio.fs.WindowsConstants.*;

/*
 * Win32 implementation of WatchService based on ReadDirectoryChangesW.
 */

class WindowsWatchService
    extends AbstractWatchService
{
    private final Unsafe unsafe = Unsafe.getUnsafe();

    // background thread to service I/O completion port
    private final Poller poller;

    /**
     * Creates an I/O completion port and a daemon thread to service it
     */
    WindowsWatchService(WindowsFileSystem fs) throws IOException {
        // create I/O completion port
        long port = 0L;
        try {
            port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0);
        } catch (WindowsException x) {
            throw new IOException(x.getMessage());
        }

        this.poller = new Poller(fs, this, port);
        this.poller.start();
    }

    @Override
    WatchKey register(Path path,
                      WatchEvent.Kind<?>[] events,
                      WatchEvent.Modifier... modifiers)
         throws IOException
    {
        // delegate to poller
        return poller.register(path, events, modifiers);
    }

    @Override
    void implClose() throws IOException {
        // delegate to poller
        poller.close();
    }

    /**
     * Windows implementation of WatchKey.
     */
    private class WindowsWatchKey extends AbstractWatchKey {
        // file key (used to detect existing registrations)
        private FileKey fileKey;

        // handle to directory
        private volatile long handle = INVALID_HANDLE_VALUE;

        // interest events
        private Set<? extends WatchEvent.Kind<?>> events;

        // subtree
        private boolean watchSubtree;

        // buffer for change events
        private NativeBuffer buffer;

        // pointer to bytes returned (in buffer)
        private long countAddress;

        // pointer to overlapped structure (in buffer)
        private long overlappedAddress;

        // completion key (used to map I/O completion to WatchKey)
        private int completionKey;

        WindowsWatchKey(Path dir,
                        AbstractWatchService watcher,
                        FileKey fileKey)
        {
            super(dir, watcher);
            this.fileKey = fileKey;
        }

        WindowsWatchKey init(long handle,
                             Set<? extends WatchEvent.Kind<?>> events,
                             boolean watchSubtree,
                             NativeBuffer buffer,
                             long countAddress,
                             long overlappedAddress,
                             int completionKey)
        {
            this.handle = handle;
            this.events = events;
            this.watchSubtree = watchSubtree;
            this.buffer = buffer;
            this.countAddress = countAddress;
            this.overlappedAddress = overlappedAddress;
            this.completionKey = completionKey;
            return this;
        }

        long handle() {
            return handle;
        }

        Set<? extends WatchEvent.Kind<?>> events() {
            return events;
        }

        void setEvents(Set<? extends WatchEvent.Kind<?>> events) {
            this.events = events;
        }

        boolean watchSubtree() {
            return watchSubtree;
        }

        NativeBuffer buffer() {
            return buffer;
        }

        long countAddress() {
            return countAddress;
        }

        long overlappedAddress() {
            return overlappedAddress;
        }

        FileKey fileKey() {
            return fileKey;
        }

        int completionKey() {
            return completionKey;
        }

        // close directory and release buffer
        void releaseResources() {
            CloseHandle(handle);
            buffer.cleaner().clean();
        }

        // Invalidate key by closing directory and releasing buffer
        void invalidate() {
            releaseResources();
            handle = INVALID_HANDLE_VALUE;
            buffer = null;
            countAddress = 0;
            overlappedAddress = 0;
        }

        @Override
        public boolean isValid() {
            return handle != INVALID_HANDLE_VALUE;
        }

        @Override
        public void cancel() {
            if (isValid()) {
                // delegate to poller
                poller.cancel(this);
            }
        }
    }

    // file key to unique identify (open) directory
    private static class FileKey {
        private final int volSerialNumber;
        private final int fileIndexHigh;
        private final int fileIndexLow;

        FileKey(int volSerialNumber, int fileIndexHigh, int fileIndexLow) {
            this.volSerialNumber = volSerialNumber;
            this.fileIndexHigh = fileIndexHigh;
            this.fileIndexLow = fileIndexLow;
        }

        @Override
        public int hashCode() {
            return volSerialNumber ^ fileIndexHigh ^ fileIndexLow;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this)
                return true;
            if (!(obj instanceof FileKey))
                return false;
            FileKey other = (FileKey)obj;
            if (this.volSerialNumber != other.volSerialNumber) return false;
            if (this.fileIndexHigh != other.fileIndexHigh) return false;
            if (this.fileIndexLow != other.fileIndexLow) return false;
            return true;
        }
    }

    // all change events
    private static final int ALL_FILE_NOTIFY_EVENTS =
        FILE_NOTIFY_CHANGE_FILE_NAME |
        FILE_NOTIFY_CHANGE_DIR_NAME |
        FILE_NOTIFY_CHANGE_ATTRIBUTES  |
        FILE_NOTIFY_CHANGE_SIZE |
        FILE_NOTIFY_CHANGE_LAST_WRITE |
        FILE_NOTIFY_CHANGE_CREATION |
        FILE_NOTIFY_CHANGE_SECURITY;

    /**
     * Background thread to service I/O completion port.
     */
    private class Poller extends AbstractPoller {
        /*
         * typedef struct _OVERLAPPED {
         *     DWORD  Internal;
         *     DWORD  InternalHigh;
         *     DWORD  Offset;
         *     DWORD  OffsetHigh;
         *     HANDLE hEvent;
         * } OVERLAPPED;
         */
        private static final short SIZEOF_DWORD         = 4;
        private static final short SIZEOF_OVERLAPPED    = 32; // 20 on 32-bit

        /*
         * typedef struct _FILE_NOTIFY_INFORMATION {
         *     DWORD NextEntryOffset;
         *     DWORD Action;
         *     DWORD FileNameLength;
         *     WCHAR FileName[1];
         * } FileNameLength;
         */
        private static final short OFFSETOF_NEXTENTRYOFFSET = 0;
        private static final short OFFSETOF_ACTION          = 4;
        private static final short OFFSETOF_FILENAMELENGTH  = 8;
        private static final short OFFSETOF_FILENAME        = 12;

        // size of per-directory buffer for events (FIXME - make this configurable)
        private static final int CHANGES_BUFFER_SIZE    = 16 * 1024;

        private final WindowsFileSystem fs;
        private final WindowsWatchService watcher;
        private final long port;

        // maps completion key to WatchKey
        private final Map<Integer,WindowsWatchKey> int2key;

        // maps file key to WatchKey
        private final Map<FileKey,WindowsWatchKey> fk2key;

        // unique completion key for each directory
        private int lastCompletionKey;

        Poller(WindowsFileSystem fs, WindowsWatchService watcher, long port) {
            this.fs = fs;
            this.watcher = watcher;
            this.port = port;
            this.int2key = new HashMap<Integer,WindowsWatchKey>();
            this.fk2key = new HashMap<FileKey,WindowsWatchKey>();
            this.lastCompletionKey = 0;
        }

        @Override
        void wakeup() throws IOException {
            try {
                PostQueuedCompletionStatus(port, 0);
            } catch (WindowsException x) {
                throw new IOException(x.getMessage());
            }
        }

        /**
         * Register a directory for changes as follows:
         *
         * 1. Open directory
         * 2. Read its attributes (and check it really is a directory)
         * 3. Assign completion key and associated handle with completion port
         * 4. Call ReadDirectoryChangesW to start (async) read of changes
         * 5. Create or return existing key representing registration
         */
        @Override
        Object implRegister(Path obj,
                            Set<? extends WatchEvent.Kind<?>> events,
                            WatchEvent.Modifier... modifiers)
        {
            WindowsPath dir = (WindowsPath)obj;
            boolean watchSubtree = false;

            // FILE_TREE modifier allowed
            for (WatchEvent.Modifier modifier: modifiers) {
                if (modifier == ExtendedWatchEventModifier.FILE_TREE) {
                    watchSubtree = true;
                    continue;
                } else {
                    if (modifier == null)
                        return new NullPointerException();
                    if (modifier instanceof com.sun.nio.file.SensitivityWatchEventModifier)
                        continue; // ignore
                    return new UnsupportedOperationException("Modifier not supported");
                }
            }

            // open directory
            long handle = -1L;
            try {
                handle = CreateFile(dir.getPathForWin32Calls(),
                                    FILE_LIST_DIRECTORY,
                                    (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE),
                                    OPEN_EXISTING,
                                    FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED);
            } catch (WindowsException x) {
                return x.asIOException(dir);
            }

            boolean registered = false;
            try {
                // read attributes and check file is a directory
                WindowsFileAttributes attrs = null;
                try {
                    attrs = WindowsFileAttributes.readAttributes(handle);
                } catch (WindowsException x) {
                    return x.asIOException(dir);
                }
                if (!attrs.isDirectory()) {
                    return new NotDirectoryException(dir.getPathForExceptionMessage());
                }

                // check if this directory is already registered
                FileKey fk = new FileKey(attrs.volSerialNumber(),
                                         attrs.fileIndexHigh(),
                                         attrs.fileIndexLow());
                WindowsWatchKey existing = fk2key.get(fk);

                // if already registered and we're not changing the subtree
                // modifier then simply update the event and return the key.
                if (existing != null && watchSubtree == existing.watchSubtree()) {
                    existing.setEvents(events);
                    return existing;
                }

                // unique completion key (skip 0)
                int completionKey = ++lastCompletionKey;
                if (completionKey == 0)
                    completionKey = ++lastCompletionKey;

                // associate handle with completion port
                try {
                    CreateIoCompletionPort(handle, port, completionKey);
                } catch (WindowsException x) {
                    return new IOException(x.getMessage());
                }

                // allocate memory for events, including space for other structures
                // needed to do overlapped I/O
                int size = CHANGES_BUFFER_SIZE + SIZEOF_DWORD + SIZEOF_OVERLAPPED;
                NativeBuffer buffer = NativeBuffers.getNativeBuffer(size);

                long bufferAddress = buffer.address();
                long overlappedAddress = bufferAddress + size - SIZEOF_OVERLAPPED;
                long countAddress = overlappedAddress - SIZEOF_DWORD;

                // start async read of changes to directory
                try {
                    ReadDirectoryChangesW(handle,
                                          bufferAddress,
                                          CHANGES_BUFFER_SIZE,
                                          watchSubtree,
                                          ALL_FILE_NOTIFY_EVENTS,
                                          countAddress,
                                          overlappedAddress);
                } catch (WindowsException x) {
                    buffer.release();
                    return new IOException(x.getMessage());
                }

                WindowsWatchKey watchKey;
                if (existing == null) {
                    // not registered so create new watch key
                    watchKey = new WindowsWatchKey(dir, watcher, fk)
                        .init(handle, events, watchSubtree, buffer, countAddress,
                              overlappedAddress, completionKey);
                    // map file key to watch key
                    fk2key.put(fk, watchKey);
                } else {
                    // directory already registered so need to:
                    // 1. remove mapping from old completion key to existing watch key
                    // 2. release existing key's resources (handle/buffer)
                    // 3. re-initialize key with new handle/buffer
                    int2key.remove(existing.completionKey());
                    existing.releaseResources();
                    watchKey = existing.init(handle, events, watchSubtree, buffer,
                        countAddress, overlappedAddress, completionKey);
                }
                // map completion map to watch key
                int2key.put(completionKey, watchKey);

                registered = true;
                return watchKey;

            } finally {
                if (!registered) CloseHandle(handle);
            }
        }

        // cancel single key
        @Override
        void implCancelKey(WatchKey obj) {
            WindowsWatchKey key = (WindowsWatchKey)obj;
            if (key.isValid()) {
                fk2key.remove(key.fileKey());
                int2key.remove(key.completionKey());
                key.invalidate();
            }
        }

        // close watch service
        @Override
        void implCloseAll() {
            // cancel all keys
            for (Map.Entry<Integer,WindowsWatchKey> entry: int2key.entrySet()) {
                entry.getValue().invalidate();
            }
            fk2key.clear();
            int2key.clear();

            // close I/O completion port
            CloseHandle(port);
        }

        // Translate file change action into watch event
        private WatchEvent.Kind<?> translateActionToEvent(int action)
        {
            switch (action) {
                case FILE_ACTION_MODIFIED :
                    return StandardWatchEventKinds.ENTRY_MODIFY;

                case FILE_ACTION_ADDED :
                case FILE_ACTION_RENAMED_NEW_NAME :
                    return StandardWatchEventKinds.ENTRY_CREATE;

                case FILE_ACTION_REMOVED :
                case FILE_ACTION_RENAMED_OLD_NAME :
                    return StandardWatchEventKinds.ENTRY_DELETE;

                default :
                    return null;  // action not recognized
            }
        }

        // process events (list of FILE_NOTIFY_INFORMATION structures)
        private void processEvents(WindowsWatchKey key, int size) {
            long address = key.buffer().address();

            int nextOffset;
            do {
                int action = unsafe.getInt(address + OFFSETOF_ACTION);

                // map action to event
                WatchEvent.Kind<?> kind = translateActionToEvent(action);
                if (key.events().contains(kind)) {
                    // copy the name
                    int nameLengthInBytes = unsafe.getInt(address + OFFSETOF_FILENAMELENGTH);
                    if ((nameLengthInBytes % 2) != 0) {
                        throw new AssertionError("FileNameLength.FileNameLength is not a multiple of 2");
                    }
                    char[] nameAsArray = new char[nameLengthInBytes/2];
                    unsafe.copyMemory(null, address + OFFSETOF_FILENAME, nameAsArray,
                        Unsafe.ARRAY_CHAR_BASE_OFFSET, nameLengthInBytes);

                    // create FileName and queue event
                    WindowsPath name = WindowsPath
                        .createFromNormalizedPath(fs, new String(nameAsArray));
                    key.signalEvent(kind, name);
                }

                // next event
                nextOffset = unsafe.getInt(address + OFFSETOF_NEXTENTRYOFFSET);
                address += (long)nextOffset;
            } while (nextOffset != 0);
        }

        /**
         * Poller main loop
         */
        @Override
        public void run() {
            for (;;) {
                CompletionStatus info = null;
                try {
                    info = GetQueuedCompletionStatus(port);
                } catch (WindowsException x) {
                    // this should not happen
                    x.printStackTrace();
                    return;
                }

                // wakeup
                if (info.completionKey() == 0) {
                    boolean shutdown = processRequests();
                    if (shutdown) {
                        return;
                    }
                    continue;
                }

                // map completionKey to get WatchKey
                WindowsWatchKey key = int2key.get(info.completionKey());
                if (key == null) {
                    // We get here when a registration is changed. In that case
                    // the directory is closed which causes an event with the
                    // old completion key.
                    continue;
                }

                // ReadDirectoryChangesW failed
                if (info.error() != 0) {
                    // buffer overflow
                    if (info.error() == ERROR_NOTIFY_ENUM_DIR) {
                        key.signalEvent(StandardWatchEventKinds.OVERFLOW, null);
                    } else {
                        // other error so cancel key
                        implCancelKey(key);
                        key.signal();
                    }
                    continue;
                }

                // process the events
                if (info.bytesTransferred() > 0) {
                    processEvents(key, info.bytesTransferred());
                } else {
                    // insufficient buffer size
                    key.signalEvent(StandardWatchEventKinds.OVERFLOW, null);
                }

                // start read for next batch of changes
                try {
                    ReadDirectoryChangesW(key.handle(),
                                          key.buffer().address(),
                                          CHANGES_BUFFER_SIZE,
                                          key.watchSubtree(),
                                          ALL_FILE_NOTIFY_EVENTS,
                                          key.countAddress(),
                                          key.overlappedAddress());
                } catch (WindowsException x) {
                    // no choice but to cancel key
                    implCancelKey(key);
                    key.signal();
                }
            }
        }
    }
}