1 /*
2  * Copyright 2019 The Android Open Source Project
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 package androidx.sqlite.util
17 
18 import android.util.Log
19 import androidx.annotation.RestrictTo
20 import java.io.File
21 import java.io.FileOutputStream
22 import java.io.IOException
23 import java.nio.channels.FileChannel
24 import java.util.concurrent.locks.Lock
25 import java.util.concurrent.locks.ReentrantLock
26 
27 /**
28  * Utility class for in-process and multi-process key-based lock mechanism for safely doing
29  * synchronized operations.
30  *
31  * Acquiring the lock will be quick if no other thread or process has a lock with the same key. But
32  * if the lock is already held then acquiring it will block, until the other thread or process
33  * releases the lock. Note that the key and lock directory must be the same to achieve
34  * synchronization.
35  *
36  * Locking is done via two levels:
37  * 1. Thread locking within the same JVM process is done via a map of String key to ReentrantLock
38  *    objects.
39  * 2. Multi-process locking is done via a lock file whose name contains the key and FileLock
40  *    objects.
41  *
42  * Creates a lock with `name` and using `lockDir` as the directory for the lock files.
43  *
44  * @param name the name of this lock.
45  * @param lockDir the directory where the lock files will be located.
46  * @param processLock whether to use file for process level locking or not by default. The behaviour
47  *   can be overridden via the [lock] method.
48  */
49 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
50 public class ProcessLock(name: String, lockDir: File?, private val processLock: Boolean) {
<lambda>null51     private val lockFile: File? = lockDir?.let { File(it, "$name.lck") }
52     private val threadLock: Lock = getThreadLock(name)
53     private var lockChannel: FileChannel? = null
54 
55     /**
56      * Attempts to grab the lock, blocking if already held by another thread or process.
57      *
58      * @param [processLock] whether to use file for process level locking or not.
59      */
locknull60     public fun lock(processLock: Boolean = this.processLock) {
61         threadLock.lock()
62         if (processLock) {
63             try {
64                 if (lockFile == null) {
65                     throw IOException("No lock directory was provided.")
66                 }
67                 // Verify parent dir
68                 val parentDir = lockFile.parentFile
69                 parentDir?.mkdirs()
70                 lockChannel = FileOutputStream(lockFile).channel.apply { lock() }
71             } catch (e: IOException) {
72                 lockChannel = null
73                 Log.w(TAG, "Unable to grab file lock.", e)
74             }
75         }
76     }
77 
78     /** Releases the lock. */
unlocknull79     public fun unlock() {
80         try {
81             lockChannel?.close()
82         } catch (ignored: IOException) {}
83         threadLock.unlock()
84     }
85 
86     private companion object {
87         private const val TAG = "SupportSQLiteLock"
88         // in-process lock map
89         private val threadLocksMap: MutableMap<String, Lock> = HashMap()
90 
getThreadLocknull91         private fun getThreadLock(key: String): Lock =
92             synchronized(threadLocksMap) {
93                 return threadLocksMap.getOrPut(key) { ReentrantLock() }
94             }
95     }
96 }
97