Newer
Older
KeeperJerry_Launcher / Launcher / source / hasher / DirWatcher.java
@KeeperJerry KeeperJerry on 27 Jun 2020 5 KB Рефактор кода
package launcher.hasher;

import com.sun.nio.file.ExtendedWatchEventModifier;
import com.sun.nio.file.SensitivityWatchEventModifier;
import launcher.LauncherAPI;
import launcher.hasher.HashedEntry.Type;
import launcher.helper.IOHelper;
import launcher.helper.JVMHelper;
import launcher.helper.JVMHelper.OS;
import launcher.helper.LogHelper;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Objects;

public final class DirWatcher implements Runnable, AutoCloseable
{
    private static final boolean FILE_TREE_SUPPORTED = JVMHelper.OS_TYPE == OS.MUSTDIE;

    // Constants
    private static final Modifier[] MODIFIERS = {
            SensitivityWatchEventModifier.HIGH
    };
    private static final Modifier[] FILE_TREE_MODIFIERS = {
            ExtendedWatchEventModifier.FILE_TREE, SensitivityWatchEventModifier.HIGH
    };
    private static final Kind<?>[] KINDS = {
            StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE
    };

    // Instance
    private final Path dir;
    private final HashedDir hdir;
    private final FileNameMatcher matcher;
    private final WatchService service;
    private final boolean digest;

    @LauncherAPI
    public DirWatcher(Path dir, HashedDir hdir, FileNameMatcher matcher, boolean digest) throws IOException
    {
        this.dir = Objects.requireNonNull(dir, "dir");
        this.hdir = Objects.requireNonNull(hdir, "hdir");
        this.matcher = matcher;
        this.digest = digest;
        service = dir.getFileSystem().newWatchService();

        // Use FILE_TREE if supported
        if (FILE_TREE_SUPPORTED)
        {
            dir.register(service, KINDS, FILE_TREE_MODIFIERS);
            return;
        }

        // Register dirs recursively
        IOHelper.walk(dir, new RegisterFileVisitor(), true);
    }

    private static void handleError(Throwable e)
    {
        LogHelper.error(e);
        JVMHelper.halt0(0x0BADFEE1);
    }

    private static Deque<String> toPath(Iterable<Path> path)
    {
        Deque<String> result = new LinkedList<>();
        for (Path pe : path)
        {
            result.add(pe.toString());
        }
        return result;
    }

    @Override
    @LauncherAPI
    public void close() throws IOException
    {
        service.close();
    }

    @Override
    @LauncherAPI
    public void run()
    {
        try
        {
            processLoop();
        }
        catch (InterruptedException | ClosedWatchServiceException ignored)
        {
            // Do nothing (closed etc)
        }
        catch (Throwable exc)
        {
            handleError(exc);
        }
    }

    private void processKey(WatchKey key) throws IOException
    {
        Path watchDir = (Path) key.watchable();
        for (WatchEvent<?> event : key.pollEvents())
        {
            Kind<?> kind = event.kind();
            if (kind.equals(StandardWatchEventKinds.OVERFLOW))
            {
                if (Boolean.getBoolean("launcher.dirwatcher.ignoreOverflows"))
                {
                    continue; // Sometimes it's better to ignore than interrupt fair playing
                }
                throw new IOException("Overflow");
            }

            // Resolve paths and verify is not exclusion
            Path path = watchDir.resolve((Path) event.context());
            Deque<String> stringPath = toPath(dir.relativize(path));
            if (matcher != null && !matcher.shouldVerify(stringPath))
            {
                continue; // Exclusion; should not be verified
            }

            // Verify is REALLY modified (not just attributes)
            if (kind.equals(StandardWatchEventKinds.ENTRY_MODIFY))
            {
                HashedEntry entry = hdir.resolve(stringPath);
                if (entry != null && (entry.getType() != Type.FILE || ((HashedFile) entry).isSame(path, digest)))
                {
                    continue; // Modified attributes, not need to worry :D
                }
            }

            // Forbidden modification!
            throw new SecurityException(String.format("Forbidden modification (%s, %d times): '%s'", kind, event.count(), path));
        }
        key.reset();
    }

    private void processLoop() throws IOException, InterruptedException
    {
        while (!Thread.interrupted())
        {
            processKey(service.take());
        }
    }

    private final class RegisterFileVisitor extends SimpleFileVisitor<Path>
    {
        private final Deque<String> path = new LinkedList<>();

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException
        {
            FileVisitResult result = super.postVisitDirectory(dir, exc);
            if (!DirWatcher.this.dir.equals(dir))
            {
                path.removeLast();
            }
            return result;
        }

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
        {
            FileVisitResult result = super.preVisitDirectory(dir, attrs);
            if (DirWatcher.this.dir.equals(dir))
            {
                dir.register(service, KINDS, MODIFIERS);
                return result;
            }

            // Maybe it's unnecessary to go deeper
            path.add(IOHelper.getFileName(dir));
            if (matcher != null && !matcher.shouldVerify(path))
            {
                return FileVisitResult.SKIP_SUBTREE;
            }

            // Register
            dir.register(service, KINDS, MODIFIERS);
            return result;
        }
    }
}