View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2017 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.utils;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.RandomAccessFile;
23  import java.nio.channels.FileLock;
24  import java.security.SecureRandom;
25  import java.sql.Timestamp;
26  import java.util.Date;
27  import javax.annotation.concurrent.NotThreadSafe;
28  import org.owasp.dependencycheck.exception.WriteLockException;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  /**
33   * A lock file implementation; creates a custom lock file so that only a single
34   * instance of dependency-check can update the a given resource.
35   *
36   * @author Jeremy Long
37   */
38  @NotThreadSafe
39  public class WriteLock implements AutoCloseable {
40  
41      /**
42       * The logger.
43       */
44      private static final Logger LOGGER = LoggerFactory.getLogger(WriteLock.class);
45      /**
46       * Secure random number generator.
47       */
48      private static final SecureRandom SECURE_RANDOM = new SecureRandom();
49      /**
50       * How long to sleep waiting for the lock.
51       */
52      public static final int SLEEP_DURATION = 15000;
53      /**
54       * Max attempts to obtain a lock.
55       */
56      public static final int MAX_SLEEP_COUNT = 160;
57      /**
58       * The file lock.
59       */
60      private FileLock lock = null;
61      /**
62       * Reference to the file that we are locking.
63       */
64      private RandomAccessFile file = null;
65      /**
66       * The lock file.
67       */
68      private File lockFile = null;
69      /**
70       * The configured settings.
71       */
72      private final Settings settings;
73      /**
74       * A random string used to validate the lock.
75       */
76      private final String magic;
77      /**
78       * A flag indicating whether or not an resource is lockable.
79       */
80      private final boolean isLockable;
81      /**
82       * The name of the lock file.
83       */
84      private final String lockFileName;
85      /**
86       * The shutdown hook used to remove the lock file in case of an unexpected
87       * shutdown.
88       */
89      private WriteLockShutdownHook hook = null;
90  
91      /**
92       * Constructs a new Write Lock object with the configured settings.
93       *
94       * @param settings the configured settings
95       * @throws WriteLockException thrown if a lock could not be obtained
96       */
97      public WriteLock(Settings settings) throws WriteLockException {
98          this(settings, true);
99      }
100 
101     /**
102      * Constructs a new Write Lock object with the configured settings.
103      *
104      * @param settings the configured settings
105      * @param isLockable a flag indicating if a lock can be obtained for the
106      * resource; if false the lock does nothing. This is useful in the case of
107      * ODC where we need to lock for updates against H2 but we do not need to
108      * lock updates for other databases.
109      * @throws WriteLockException thrown if a lock could not be obtained
110      */
111     public WriteLock(Settings settings, boolean isLockable) throws WriteLockException {
112         this(settings, isLockable, "odc.update.lock");
113     }
114 
115     /**
116      * Constructs a new Write Lock object with the configured settings.
117      *
118      * @param settings the configured settings
119      * @param isLockable a flag indicating if a lock can be obtained for the
120      * resource; if false the lock does nothing. This is useful in the case of
121      * ODC where we need to lock for updates against H2 but we do not need to
122      * lock updates for other databases.
123      * @param lockFileName the name of the lock file; note the lock file will be
124      * in the ODC data directory.
125      * @throws WriteLockException thrown if a lock could not be obtained
126      */
127     public WriteLock(Settings settings, boolean isLockable, String lockFileName) throws WriteLockException {
128         this.settings = settings;
129         final byte[] random = new byte[16];
130         SECURE_RANDOM.nextBytes(random);
131         magic = Checksum.getHex(random);
132         this.isLockable = isLockable;
133         this.lockFileName = lockFileName;
134         lock();
135     }
136 
137     /**
138      * Obtains a lock on the resource.
139      *
140      * @throws WriteLockException thrown if a lock could not be obtained
141      */
142     public final void lock() throws WriteLockException {
143         if (!isLockable) {
144             return;
145         }
146         try {
147             final File dir = settings.getDataDirectory();
148             lockFile = new File(dir, lockFileName);
149             checkState();
150             int ctr = 0;
151             do {
152                 try {
153                     if (!lockFile.exists() && lockFile.createNewFile()) {
154                         file = new RandomAccessFile(lockFile, "rw");
155                         lock = file.getChannel().lock();
156                         file.writeBytes(magic);
157                         file.getChannel().force(true);
158                         Thread.sleep(20);
159                         file.seek(0);
160                         final String current = file.readLine();
161                         if (current != null && !current.equals(magic)) {
162                             lock.close();
163                             lock = null;
164                             LOGGER.debug("Another process obtained a lock first ({})", Thread.currentThread().getName());
165                         } else {
166                             addShutdownHook();
167                             final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
168                             LOGGER.debug("Lock file created ({}) {} @ {}", Thread.currentThread().getName(), magic, timestamp);
169                         }
170                     }
171                 } catch (InterruptedException ex) {
172                     Thread.currentThread().interrupt();
173                     LOGGER.trace("Expected error as another thread has likely locked the file", ex);
174                 } catch (IOException ex) {
175                     LOGGER.trace("Expected error as another thread has likely locked the file", ex);
176                 } finally {
177                     if (lock == null && file != null) {
178                         try {
179                             file.close();
180                             file = null;
181                         } catch (IOException ex) {
182                             LOGGER.trace("Unable to close the lock file", ex);
183                         }
184                     }
185                 }
186                 if (lock == null || !lock.isValid()) {
187                     try {
188                         final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
189                         LOGGER.debug("Sleeping thread {} ({}) for {} seconds because an exclusive lock on the database could not be obtained ({})",
190                                 Thread.currentThread().getName(), magic, SLEEP_DURATION / 1000, timestamp);
191                         Thread.sleep(SLEEP_DURATION);
192                     } catch (InterruptedException ex) {
193                         LOGGER.debug("sleep was interrupted.", ex);
194                         Thread.currentThread().interrupt();
195                     }
196                 }
197             } while (++ctr < MAX_SLEEP_COUNT && (lock == null || !lock.isValid()));
198             if (lock == null || !lock.isValid()) {
199                 throw new WriteLockException("Unable to obtain the update lock, skipping the database update. Skipping the database update.");
200             }
201         } catch (IOException ex) {
202             throw new WriteLockException(ex.getMessage(), ex);
203         }
204     }
205 
206     /**
207      * Releases the lock on the resource.
208      */
209     @Override
210     public void close() {
211         if (!isLockable) {
212             return;
213         }
214         if (lock != null) {
215             try {
216                 lock.release();
217                 lock = null;
218             } catch (IOException ex) {
219                 LOGGER.debug("Failed to release lock", ex);
220             }
221         }
222         if (file != null) {
223             try {
224                 file.close();
225                 file = null;
226             } catch (IOException ex) {
227                 LOGGER.debug("Unable to delete lock file", ex);
228             }
229         }
230         if (lockFile != null && lockFile.isFile()) {
231             final String msg = readLockFile();
232             if (msg != null && msg.equals(magic) && !lockFile.delete()) {
233                 LOGGER.error("Lock file '{}' was unable to be deleted. Please manually delete this file.", lockFile.toString());
234                 lockFile.deleteOnExit();
235             }
236         }
237         lockFile = null;
238         removeShutdownHook();
239         final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
240         LOGGER.debug("Lock released ({}) {} @ {}", Thread.currentThread().getName(), magic, timestamp);
241     }
242 
243     /**
244      * Checks the state of the custom write lock file and under some conditions
245      * will attempt to remove the lock file.
246      *
247      * @throws WriteLockException thrown if the lock directory does not exist
248      * and cannot be created
249      */
250     private void checkState() throws WriteLockException {
251         if (!lockFile.getParentFile().isDirectory() && !lockFile.mkdir()) {
252             throw new WriteLockException("Unable to create path to data directory.");
253         }
254         if (lockFile.isFile()) {
255             //TODO - this 30 minute check needs to be configurable.
256             if (getFileAge(lockFile) > 30) {
257                 LOGGER.debug("An old write lock file was found: {}", lockFile.getAbsolutePath());
258                 if (!lockFile.delete()) {
259                     LOGGER.warn("An old write lock file was found but the system was unable to delete "
260                             + "the file. Consider manually deleting {}", lockFile.getAbsolutePath());
261                 }
262             } else {
263                 LOGGER.info("Lock file found `{}`", lockFile);
264                 LOGGER.info("Existing update in progress; waiting for update to complete");
265             }
266         }
267     }
268 
269     /**
270      * Reads the first line from the lock file and returns the results as a
271      * string.
272      *
273      * @return the first line from the lock file; or null if the contents could
274      * not be read
275      */
276     private String readLockFile() {
277         String msg = null;
278         try (RandomAccessFile f = new RandomAccessFile(lockFile, "rw")) {
279             msg = f.readLine();
280         } catch (IOException ex) {
281             LOGGER.debug(String.format("Error reading lock file: %s", lockFile), ex);
282         }
283         return msg;
284     }
285 
286     /**
287      * Returns the age of the file in minutes.
288      *
289      * @param file the file to calculate the age
290      * @return the age of the file
291      */
292     private double getFileAge(File file) {
293         final Date d = new Date();
294         final long modified = file.lastModified();
295         final double time = (d.getTime() - modified) / 1000.0 / 60.0;
296         LOGGER.debug("Lock file age is {} minutes", time);
297         return time;
298     }
299 
300     /**
301      * Adds the shutdown hook to the JVM.
302      */
303     private void addShutdownHook() {
304         if (hook == null) {
305             hook = WriteLockShutdownHookFactory.getHook(settings);
306             hook.add(this);
307 
308         }
309     }
310 
311     /**
312      * Removes the shutdown hook.
313      */
314     private void removeShutdownHook() {
315         if (hook != null) {
316             hook.remove();
317             hook = null;
318         }
319     }
320 }