Skip to content

Commit 138d4eb

Browse files
authored
Merge pull request #151 from badmannersteam/single-instance-manager-configuration
Configuration for the SingleInstanceManager
2 parents b456a31 + 02d436b commit 138d4eb

2 files changed

Lines changed: 74 additions & 17 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,22 @@ if (!isSingleInstance) {
188188

189189
In this example, the `SingleInstanceManager` will check if an instance is already running. If not, it will acquire the lock and start watching for restore requests. If an instance is already running, it will send a restore request to bring the existing window to the foreground, allowing you to focus on the already-running application rather than starting a new instance.
190190

191+
#### Configuration
192+
193+
`SingleInstanceManager` can be configured:
194+
```kotlin
195+
SingleInstanceManager.configuration = Configuration(
196+
lockFilesDir = Paths.get("path/to/your/app/data/dir/single_instance_manager"),
197+
appIdentifier = "app_id"
198+
)
199+
```
200+
This is useful when you need single-instance management but want finer-grained control.
201+
202+
By specifying a custom `lockFilesDir`, you limit the scope of single-instance management
203+
from every instance of your app on the whole system to only those that share the specified data directory.
204+
205+
Setting the custom `appIdentifier` can be used for even more granular control.
206+
191207
### 📌 Tray Position Detection
192208

193209
The `getTrayPosition()` function allows you to determine the current position of the system tray on the screen. This information can be useful for aligning application windows relative to the tray icon.

src/commonMain/kotlin/com/kdroid/composetray/utils/SingleInstanceManager.kt

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package com.kdroid.composetray.utils
2+
23
import com.kdroid.kmplog.Log
34
import com.kdroid.kmplog.d
45
import com.kdroid.kmplog.e
@@ -7,7 +8,11 @@ import java.io.RandomAccessFile
78
import java.nio.channels.FileChannel
89
import java.nio.channels.FileLock
910
import java.nio.channels.OverlappingFileLockException
10-
import java.nio.file.*
11+
import java.nio.file.FileSystems
12+
import java.nio.file.Files
13+
import java.nio.file.Path
14+
import java.nio.file.Paths
15+
import java.nio.file.StandardWatchEventKinds
1116

1217

1318
/**
@@ -17,13 +22,43 @@ import java.nio.file.*
1722
* and provides a mechanism to notify the running instance when another instance attempts to start.
1823
*/
1924
object SingleInstanceManager {
25+
26+
private const val TAG = "SingleInstanceChecker"
27+
28+
/**
29+
* Don't inline to [Configuration] initializer to prevent multiple calls with the different stack depth.
30+
*/
31+
private val APP_IDENTIFIER = getAppIdentifier()
32+
33+
/**
34+
* Configuration for a locking mechanism.
35+
*
36+
* @property lockFilesDir The directory where lock files will be stored. Defaults to the system's temporary directory.
37+
* @property lockIdentifier The lock identifier that will be used for generating lock files names.
38+
*/
39+
data class Configuration(
40+
val lockFilesDir: Path = Paths.get(System.getProperty("java.io.tmpdir")),
41+
val lockIdentifier: String = APP_IDENTIFIER
42+
) {
43+
val lockFileName: String = "$lockIdentifier.lock"
44+
val restoreRequestFileName: String = "$lockIdentifier.restore_request"
45+
}
46+
47+
var configuration: Configuration = Configuration()
48+
set(value) {
49+
check(fileChannel == null) { "Configuration can be changed only before first call to isSingleInstance()!" }
50+
field = value
51+
}
52+
2053
private var fileChannel: FileChannel? = null
2154
private var fileLock: FileLock? = null
22-
private val APP_IDENTIFIER = getAppIdentifier()
23-
private val LOCK_FILE_NAME = "$APP_IDENTIFIER.lock"
24-
private val MESSAGE_FILE_NAME = "${APP_IDENTIFIER}_restore_request"
25-
private const val TAG = "SingleInstanceChecker"
2655
private var isWatching = false
56+
57+
/**
58+
* Checks if the current process is the single running instance.
59+
*
60+
* @param onRestoreRequest A function to be executed if a restore request is received from another instance.
61+
*/
2762
fun isSingleInstance(onRestoreRequest: () -> Unit): Boolean {
2863
// If the lock is already acquired by this process, we are the first instance
2964
if (fileLock != null) {
@@ -64,17 +99,19 @@ object SingleInstanceManager {
6499
false
65100
}
66101
}
102+
67103
private fun createLockFile(): File {
68-
val lockFilePath = System.getProperty("java.io.tmpdir") + File.separator + LOCK_FILE_NAME
69-
return File(lockFilePath)
104+
val lockFile = configuration.lockFilesDir.resolve(configuration.lockFileName).toFile()
105+
lockFile.parentFile.mkdirs()
106+
return lockFile
70107
}
108+
71109
private fun watchForRestoreRequests(onRestoreRequest: () -> Unit) {
72110
Thread {
73111
try {
74-
val tmpDir = Paths.get(System.getProperty("java.io.tmpdir"))
75112
val watchService = FileSystems.getDefault().newWatchService()
76-
tmpDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE)
77-
Log.d(TAG, "Watching directory: $tmpDir for restore requests")
113+
configuration.lockFilesDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE)
114+
Log.d(TAG, "Watching directory: ${configuration.lockFilesDir} for restore requests")
78115
while (true) {
79116
val key = watchService.take()
80117
for (event in key.pollEvents()) {
@@ -83,7 +120,7 @@ object SingleInstanceManager {
83120
continue
84121
}
85122
val filename = event.context() as Path
86-
if (filename.toString() == MESSAGE_FILE_NAME) {
123+
if (filename.toString() == configuration.restoreRequestFileName) {
87124
Log.d(TAG, "Restore request file detected")
88125
onRestoreRequest()
89126
// Remove the request file after processing
@@ -100,24 +137,27 @@ object SingleInstanceManager {
100137
}
101138
}.start()
102139
}
140+
103141
private fun sendRestoreRequest() {
104142
try {
105-
val restoreRequestFile = Paths.get(System.getProperty("java.io.tmpdir"), MESSAGE_FILE_NAME)
106-
Files.createFile(restoreRequestFile)
107-
Log.d(TAG, "Restore request file created: $restoreRequestFile")
143+
val restoreRequestFilePath = configuration.lockFilesDir.resolve(configuration.restoreRequestFileName)
144+
Files.createFile(restoreRequestFilePath)
145+
Log.d(TAG, "Restore request file created: $restoreRequestFilePath")
108146
} catch (e: Exception) {
109147
Log.e(TAG, "Error while sending restore request", e)
110148
}
111149
}
150+
112151
private fun deleteRestoreRequestFile() {
113152
try {
114-
val restoreRequestFile = Paths.get(System.getProperty("java.io.tmpdir"), MESSAGE_FILE_NAME)
115-
Files.deleteIfExists(restoreRequestFile)
116-
Log.d(TAG, "Restore request file deleted")
153+
val restoreRequestFilePath = configuration.lockFilesDir.resolve(configuration.restoreRequestFileName)
154+
Files.deleteIfExists(restoreRequestFilePath)
155+
Log.d(TAG, "Restore request file deleted: $restoreRequestFilePath")
117156
} catch (e: Exception) {
118157
Log.e(TAG, "Error while deleting restore request file", e)
119158
}
120159
}
160+
121161
private fun releaseLock() {
122162
try {
123163
fileLock?.release()
@@ -127,6 +167,7 @@ object SingleInstanceManager {
127167
Log.e(TAG, "Error while releasing the lock", e)
128168
}
129169
}
170+
130171
private fun getAppIdentifier(): String {
131172
val callerClassName = Thread.currentThread().stackTrace[3].className
132173
return callerClassName.replace(".", "_")

0 commit comments

Comments
 (0)