From d81206fe2e99311ac623f5faa1fcc6dc738f85ed Mon Sep 17 00:00:00 2001 From: Jeremy Long Date: Sun, 18 Aug 2013 05:54:11 -0400 Subject: [PATCH] Added an implementation of a spin lock that can be used to lock a directory. Former-commit-id: 121a3d5d026f524698762b377c3582fbc9303bf2 --- .../concurrency/DirectoryLockException.java | 64 +++++ .../concurrency/DirectorySpinLock.java | 266 ++++++++++++++++++ .../InvalidDirectoryException.java | 64 +++++ .../concurrency/DirectorySpinLockTest.java | 116 ++++++++ .../concurrency/SpinLockTask.java | 82 ++++++ 5 files changed, 592 insertions(+) create mode 100644 dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectoryLockException.java create mode 100644 dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectorySpinLock.java create mode 100644 dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/InvalidDirectoryException.java create mode 100644 dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/DirectorySpinLockTest.java create mode 100644 dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/SpinLockTask.java diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectoryLockException.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectoryLockException.java new file mode 100644 index 000000000..5985152a1 --- /dev/null +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectoryLockException.java @@ -0,0 +1,64 @@ +/* + * This file is part of dependency-check-core. + * + * Dependency-check-core 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. + * + * Dependency-check-core 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 + * dependency-check-core. If not, see http://www.gnu.org/licenses/. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.concurrency; + +/** + * If thrown, indicates that a problem occurred when locking a directory. + * + * @author Jeremy Long (jeremy.long@owasp.org) + */ +public class DirectoryLockException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new Directory Lock Exception. + */ + public DirectoryLockException() { + super(); + } + + /** + * Constructs a new Directory Lock Exception. + * + * @param msg the message describing the exception + */ + public DirectoryLockException(String msg) { + super(msg); + } + + /** + * Constructs a new Directory Lock Exception. + * + * @param ex the cause of the exception + */ + public DirectoryLockException(Throwable ex) { + super(ex); + } + + /** + * Constructs a new Directory Lock Exception. + * + * @param msg the message describing the exception + * @param ex the cause of the exception + */ + public DirectoryLockException(String msg, Throwable ex) { + super(msg, ex); + } +} diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectorySpinLock.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectorySpinLock.java new file mode 100644 index 000000000..60de62900 --- /dev/null +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/DirectorySpinLock.java @@ -0,0 +1,266 @@ +/* + * This file is part of dependency-check-core. + * + * Dependency-check-core 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. + * + * Dependency-check-core 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 + * dependency-check-core. If not, see http://www.gnu.org/licenses/. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.concurrency; + +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.FileLockInterruptionException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.OverlappingFileLockException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implements a spin lock on a given directory. If the lock cannot be obtained, + * the process will "spin" waiting for an opportunity to obtain the lock + * requested. + * + * @author Jeremy Long (jeremy.long@owasp.org) + */ +public class DirectorySpinLock implements Closeable, AutoCloseable { + + /** + * The name of the lock file. + */ + public static final String LOCK_NAME = "data.lock"; + /** + * The maximum wait period used when attempting to obtain a lock. + */ + public static final int MAX_SPIN = 100; + /** + * The file channel used to perform the lock. + */ + private FileChannel channel = null; + /** + * The file used to perform the lock. + */ + private File lockFile = null; + /** + * The lock object. + */ + private FileLock lock = null; + /** + * The maximum number of seconds that the spin lock will wait while trying + * to obtain a lock. + */ + private long maxWait = MAX_SPIN; + + /** + * Get the maximum wait time, in seconds, that the spin lock will wait while + * trying to obtain a lock. + * + * @return the number of seconds the spin lock will wait + */ + public long getMaxWait() { + return maxWait / 2; //sleep is for 500, so / 2 + } + + /** + * Set the maximum wait time, in seconds, that the spin lock will wait while + * trying to obtain a lock. + * + * @param maxWait the number of seconds the spin lock will wait + */ + public void setMaxWait(long maxWait) { + this.maxWait = maxWait * 2; //sleep is for 500, so * 2 + } + + /** + * Constructs a new spin lock on the given directory. + * + * @param directory the directory to monitor/lock + * @throws InvalidDirectoryException thrown if there is an issue with the + * directory provided + * @throws DirectoryLockException thrown there is an issue obtaining a + * handle to the lock file + */ + public DirectorySpinLock(File directory) throws InvalidDirectoryException, DirectoryLockException { + checkDirectory(directory); + lockFile = new File(directory, LOCK_NAME); + RandomAccessFile file = null; + try { + file = new RandomAccessFile(lockFile, "rw"); + } catch (FileNotFoundException ex) { + throw new DirectoryLockException("Lock file not found", ex); + } + channel = file.getChannel(); + } + + /** + * Attempts to obtain an exclusive lock; an exception is thrown if the lock + * could not be obtained. This method may block for a few seconds if a lock + * cannot be obtained. + * + * @throws DirectoryLockException thrown if there is an exception obtaining + * the lock + */ + public void obtainSharedLock() throws DirectoryLockException { + obtainLock(true); + } + + /** + * Attempts to obtain an exclusive lock; an exception is thrown if the lock + * could not be obtained. This method may block for a few seconds if a lock + * cannot be obtained. + * + * @throws DirectoryLockException thrown if there is an exception obtaining + * the lock + */ + public void obtainExclusiveLock() throws DirectoryLockException { + obtainLock(false); + } + + /** + * Attempts to obtain a lock; an exception is thrown if the lock could not + * be obtained. This method may block for a few seconds if a lock cannot be + * obtained. + * + * @param shared true if the lock is shared, otherwise false + * @param maxWait the maximum time to wait, in seconds, while trying to + * obtain the lock + * @throws DirectoryLockException thrown if there is an exception obtaining + * the lock + */ + protected void obtainLock(boolean shared, long maxWait) throws DirectoryLockException { + setMaxWait(maxWait); + obtainLock(shared); + } + + /** + * Attempts to obtain a lock; an exception is thrown if the lock could not + * be obtained. This method may block for a few seconds if a lock cannot be + * obtained. + * + * @param shared true if the lock is shared, otherwise false + * @throws DirectoryLockException thrown if there is an exception obtaining + * the lock + */ + protected void obtainLock(boolean shared) throws DirectoryLockException { + if (lock != null) { + release(); + } + if (channel == null) { + throw new DirectoryLockException("Unable to create lock, no file channel exists"); + } + int count = 0; + Exception lastException = null; + while (lock == null && count++ < maxWait) { + try { + lock = channel.lock(0, Long.MAX_VALUE, shared); + } catch (AsynchronousCloseException ex) { + lastException = ex; + } catch (ClosedChannelException ex) { + lastException = ex; + } catch (FileLockInterruptionException ex) { + lastException = ex; + } catch (OverlappingFileLockException ex) { + lastException = ex; + } catch (NonWritableChannelException ex) { + lastException = ex; + } catch (IOException ex) { + lastException = ex; + } + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + if (lock == null) { + if (lastException == null) { + throw new DirectoryLockException("Unable to obtain lock"); + } else { + throw new DirectoryLockException("Unable to obtain lock", lastException); + } + } + } + + /** + * Performs a few simple rudimentary checks on the specified directory. + * Specifically, does the file exist and is it a directory. + * + * @param directory the File object to inspect + * @throws InvalidDirectoryException thrown if the directory is null or is + * not a directory + */ + private void checkDirectory(File directory) throws InvalidDirectoryException { + if (directory == null) { + throw new InvalidDirectoryException("Unable to obtain lock on a null File"); + } + if (!directory.isDirectory()) { + final String msg = String.format("File, '%s', does not exist or is not a directory", directory.getAbsolutePath()); + throw new InvalidDirectoryException(msg); + } + } + + /** + * Releases any locks and closes the underlying channel. + * + * @throws IOException + */ + @Override + public void close() throws IOException { + release(); + if (lock != null) { + try { + lock.close(); + } catch (IOException ex) { + Logger.getLogger(DirectorySpinLock.class.getName()).log(Level.FINEST, "Unable to close file lock due to IO Exception", ex); + } + } + if (channel != null) { + try { + channel.close(); + } catch (IOException ex) { + Logger.getLogger(DirectorySpinLock.class.getName()).log(Level.FINEST, "Unable to close the channel for the file lock", ex); + } + } + if (lockFile != null) { + if (lockFile.exists()) { + /* yes, this delete could fail which is totally fine. The other + * thread holding the lock while delete it. + */ + lockFile.delete(); + } + } + } + + /** + * Releases the lock. Any exceptions that are thrown by the underlying lock + * during the release are ignored. + */ + public void release() { + if (lock != null) { + try { + lock.release(); + } catch (ClosedChannelException ex) { + Logger.getLogger(DirectorySpinLock.class.getName()).log(Level.FINEST, "Uable to release file lock", ex); + } catch (IOException ex) { + Logger.getLogger(DirectorySpinLock.class.getName()).log(Level.FINEST, "Unable to release file lock due to IO Exception", ex); + } + } + } +} diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/InvalidDirectoryException.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/InvalidDirectoryException.java new file mode 100644 index 000000000..3faa54ffa --- /dev/null +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/concurrency/InvalidDirectoryException.java @@ -0,0 +1,64 @@ +/* + * This file is part of dependency-check-core. + * + * Dependency-check-core 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. + * + * Dependency-check-core 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 + * dependency-check-core. If not, see http://www.gnu.org/licenses/. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.concurrency; + +/** + * If thrown, indicates that there is a problem with a directory. + * + * @author Jeremy Long (jeremy.long@owasp.org) + */ +public class InvalidDirectoryException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new Invalid Directory Exception. + */ + public InvalidDirectoryException() { + super(); + } + + /** + * Constructs a new Invalid Directory Exception. + * + * @param msg the message describing the exception + */ + public InvalidDirectoryException(String msg) { + super(msg); + } + + /** + * Constructs a new Invalid Directory Exception. + * + * @param ex the cause of the exception + */ + public InvalidDirectoryException(Throwable ex) { + super(ex); + } + + /** + * Constructs a new Invalid Directory Exception. + * + * @param msg the message describing the exception + * @param ex the cause of the exception + */ + public InvalidDirectoryException(String msg, Throwable ex) { + super(msg, ex); + } +} diff --git a/dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/DirectorySpinLockTest.java b/dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/DirectorySpinLockTest.java new file mode 100644 index 000000000..ac1c95285 --- /dev/null +++ b/dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/DirectorySpinLockTest.java @@ -0,0 +1,116 @@ +/* + * This file is part of dependency-check-core. + * + * Dependency-check-core 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. + * + * Dependency-check-core 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 + * dependency-check-core. If not, see http://www.gnu.org/licenses/. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.concurrency; + +import java.io.File; +import java.net.URL; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Jeremy Long (jeremy.long@owasp.org) + */ +public class DirectorySpinLockTest { + + public DirectorySpinLockTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of obtainSharedLock method, of class DirectorySpinLock. + * Specifically, this test uses the SpinLockTask to obtain an exclusive lock + * that is held for 5 seconds. We then try to obtain a shared lock while + * that task is running. It should take longer then 5 seconds to obtain the + * shared lock. + */ + @Test + public void testObtainSharedLock_withContention() throws Exception { + URL location = this.getClass().getProtectionDomain().getCodeSource().getLocation(); + File directory = new File(location.getFile()); + DirectorySpinLock instance = new DirectorySpinLock(directory); + SpinLockTask task = new SpinLockTask(directory, 5000, false, 2); + long start = System.currentTimeMillis(); + task.run(); + instance.obtainSharedLock(); + long end = System.currentTimeMillis(); + instance.close(); + if (task.getException() != null) { + throw task.getException(); + } + long timeElapsed = end - start; + assertTrue("no lock contention occured?", timeElapsed >= 5000); + //no exceptions means everything worked. + } + + /** + * Test of obtainSharedLock method, of class DirectorySpinLock. This method + * obtains two shared locks by using the SpinLockTask to obtain a lock in + * another thread. + */ + @Test + public void testObtainSharedLock() throws Exception { + URL location = this.getClass().getProtectionDomain().getCodeSource().getLocation(); + File directory = new File(location.getFile()); + DirectorySpinLock instance = new DirectorySpinLock(directory); + SpinLockTask task = new SpinLockTask(directory, 1000, true, 2); + task.run(); + instance.obtainSharedLock(); + instance.close(); + if (task.getException() != null) { + throw task.getException(); + } + //no exceptions means everything worked. + } + + /** + * Test of obtainExclusiveLock method, of class DirectorySpinLock. + */ + @Test + public void testObtainExclusiveLock() throws Exception { + URL location = this.getClass().getProtectionDomain().getCodeSource().getLocation(); + File directory = new File(location.getFile()); + DirectorySpinLock instance = new DirectorySpinLock(directory); + SpinLockTask task = new SpinLockTask(directory, 1000, true, 1); + instance.obtainExclusiveLock(); + task.run(); + instance.close(); + assertNotNull("No exception thrown due to exclusive lock failure?", task.getException()); + assertEquals("Incorrect exception when obtaining exclusive lock", "Unable to obtain lock", task.getException().getMessage()); + } +} diff --git a/dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/SpinLockTask.java b/dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/SpinLockTask.java new file mode 100644 index 000000000..7e6ce2a35 --- /dev/null +++ b/dependency-check-core/src/test/java/org/owasp/dependencycheck/concurrency/SpinLockTask.java @@ -0,0 +1,82 @@ +/* + * This file is part of dependency-check-core. + * + * Dependency-check-core 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. + * + * Dependency-check-core 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 + * dependency-check-core. If not, see http://www.gnu.org/licenses/. + * + * Copyright (c) 2013 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.concurrency; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author Jeremy Long (jeremy.long@owasp.org) + */ +public class SpinLockTask implements Runnable { + + DirectorySpinLock lock = null; + int holdLockFor; + long maxWait; + boolean shared; + private Exception exception = null; + + /** + * Get the value of exception + * + * @return the value of exception + */ + public Exception getException() { + return exception; + } + + /** + * Set the value of exception + * + * @param exception new value of exception + */ + public void setException(Exception exception) { + this.exception = exception; + } + + public SpinLockTask(File directory, int holdLockFor, boolean shared, long maxWait) throws InvalidDirectoryException, DirectoryLockException { + this.holdLockFor = holdLockFor; + this.shared = shared; + this.maxWait = maxWait; + lock = new DirectorySpinLock(directory); + } + + @Override + public void run() { + try { + lock.obtainLock(shared, maxWait); + Thread.sleep(holdLockFor); + } catch (DirectoryLockException ex) { + exception = ex; + } catch (InterruptedException ex) { + exception = ex; + } finally { + if (lock != null) { + try { + lock.close(); + } catch (IOException ex) { + exception = ex; + } + } + } + } +}