Skip to content

Commit 576966c

Browse files
committed
fix(macOS): write tray state to writable tmp and namespace by app ID to avoid conflicts
- Persist tray_position.properties to: {java.io.tmpdir}/ComposeNativeTray/<appId>/tray_position.properties - Update SingleInstanceManager to use AppIdProvider for lock/restore files - Backward-compatible reads from legacy CWD and old tmp locations - Resilient I/O (swallow exceptions) to avoid crashes on read-only filesystems
1 parent f28aad8 commit 576966c

3 files changed

Lines changed: 96 additions & 27 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.kdroid.composetray.utils
2+
3+
/**
4+
* Provides a unique, stable application identifier to namespace shared resources
5+
* (temp files, locks, properties) and avoid conflicts when multiple apps use
6+
* this library on the same machine.
7+
*
8+
* Resolution order (first non-empty wins):
9+
* 1) System property "compose.native.tray.appId"
10+
* 2) Environment variable "COMPOSE_NATIVE_TRAY_APP_ID"
11+
* 3) Main class from system property "sun.java.command" (first token)
12+
* 4) Fallback to "ComposeNativeTrayApp"
13+
*/
14+
object AppIdProvider {
15+
private val cached by lazy { computeAppId() }
16+
17+
fun appId(): String = cached
18+
19+
private fun computeAppId(): String {
20+
val sunCmd = System.getProperty("sun.java.command")?.trim().orEmpty()
21+
println("AppIdProvider: sunCmd: $sunCmd")
22+
23+
if (sunCmd.isNotEmpty()) {
24+
val firstToken = sunCmd.split(" ", limit = 2).firstOrNull().orEmpty()
25+
if (firstToken.isNotEmpty()) return sanitize(firstToken)
26+
}
27+
28+
// Fallback
29+
return "ComposeNativeTrayApp"
30+
}
31+
32+
private fun sanitize(raw: String): String {
33+
// Replace non-alphanumeric/._- with underscore; trim length if excessively long
34+
val cleaned = raw.replace(Regex("[^A-Za-z0-9._-]"), "_")
35+
return cleaned.take(128).ifEmpty { "ComposeNativeTrayApp" }
36+
}
37+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ object SingleInstanceManager {
166166
}
167167

168168
private fun getAppIdentifier(): String {
169-
val callerClassName = Thread.currentThread().stackTrace[3].className
170-
return callerClassName.replace(".", "_")
169+
// Use unified app ID provider to avoid cross-app conflicts and allow explicit override
170+
return AppIdProvider.appId()
171171
}
172172
}

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

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,38 +60,70 @@ private const val POSITION_KEY = "TrayPosition"
6060
private const val X_KEY = "TrayX"
6161
private const val Y_KEY = "TrayY"
6262

63+
// Use a writable tmp/cache directory to avoid read-only filesystem issues (e.g., macOS app bundle working dir)
64+
private fun trayPropertiesFile(): File {
65+
val tmpBase = System.getProperty("java.io.tmpdir") ?: "."
66+
val appDir = File(File(tmpBase, "ComposeNativeTray"), AppIdProvider.appId())
67+
// Best-effort create directory; if it fails, we will fallback to legacy file access patterns safely
68+
runCatching { if (!appDir.exists()) appDir.mkdirs() }.onFailure { /* ignore */ }
69+
return File(appDir, PROPERTIES_FILE)
70+
}
71+
72+
// Legacy properties file in working directory (for backward compatibility)
73+
private fun legacyPropertiesFile(): File = File(PROPERTIES_FILE)
74+
75+
// Previous tmp location before appId namespacing (for backward-compatible reads only)
76+
private fun oldTmpPropertiesFile(): File {
77+
val tmpBase = System.getProperty("java.io.tmpdir") ?: "."
78+
val oldDir = File(tmpBase, "ComposeNativeTray")
79+
return File(oldDir, PROPERTIES_FILE)
80+
}
81+
82+
private fun loadPropertiesFrom(file: File): Properties? {
83+
if (!file.exists()) return null
84+
return runCatching {
85+
Properties().apply { file.inputStream().use { load(it) } }
86+
}.getOrNull()
87+
}
88+
89+
private fun storePropertiesTo(file: File, props: Properties) {
90+
// Ensure parent exists if any
91+
file.parentFile?.let { runCatching { if (!it.exists()) it.mkdirs() } }
92+
runCatching { file.outputStream().use { props.store(it, null) } }
93+
// We intentionally swallow exceptions to avoid crashing if the FS is read-only
94+
}
95+
6396
internal fun saveTrayPosition(position: TrayPosition) {
64-
val properties = Properties()
65-
val file = File(PROPERTIES_FILE)
66-
if (file.exists()) {
67-
properties.load(file.inputStream())
68-
}
97+
val preferredFile = trayPropertiesFile()
98+
val properties = loadPropertiesFrom(preferredFile)
99+
?: loadPropertiesFrom(oldTmpPropertiesFile())
100+
?: loadPropertiesFrom(legacyPropertiesFile())
101+
?: Properties()
69102
properties.setProperty(POSITION_KEY, position.name)
70-
file.outputStream().use { properties.store(it, null) }
103+
storePropertiesTo(preferredFile, properties)
71104
}
72105

73106
internal fun saveTrayClickPosition(x: Int, y: Int, position: TrayPosition) {
74-
val properties = Properties()
75-
val file = File(PROPERTIES_FILE)
76-
if (file.exists()) {
77-
properties.load(file.inputStream())
78-
}
107+
val preferredFile = trayPropertiesFile()
108+
val properties = loadPropertiesFrom(preferredFile)
109+
?: loadPropertiesFrom(oldTmpPropertiesFile())
110+
?: loadPropertiesFrom(legacyPropertiesFile())
111+
?: Properties()
79112
properties.setProperty(POSITION_KEY, position.name)
80113
properties.setProperty(X_KEY, x.toString())
81114
properties.setProperty(Y_KEY, y.toString())
82-
file.outputStream().use { properties.store(it, null) }
115+
storePropertiesTo(preferredFile, properties)
83116
}
84117

85118
internal fun loadTrayClickPosition(): TrayClickPosition? {
86-
val file = File(PROPERTIES_FILE)
87-
if (!file.exists()) return null
88-
89-
val properties = Properties()
90-
properties.load(file.inputStream())
119+
// Prefer new location, fallback to old tmp location, then legacy working dir
120+
val props = loadPropertiesFrom(trayPropertiesFile())
121+
?: loadPropertiesFrom(oldTmpPropertiesFile())
122+
?: loadPropertiesFrom(legacyPropertiesFile()) ?: return null
91123

92-
val positionStr = properties.getProperty(POSITION_KEY) ?: return null
93-
val x = properties.getProperty(X_KEY)?.toIntOrNull() ?: return null
94-
val y = properties.getProperty(Y_KEY)?.toIntOrNull() ?: return null
124+
val positionStr = props.getProperty(POSITION_KEY) ?: return null
125+
val x = props.getProperty(X_KEY)?.toIntOrNull() ?: return null
126+
val y = props.getProperty(Y_KEY)?.toIntOrNull() ?: return null
95127

96128
return try {
97129
TrayClickPosition(x, y, TrayPosition.valueOf(positionStr))
@@ -150,11 +182,11 @@ fun getTrayPosition(): TrayPosition {
150182
}
151183

152184
// Legacy fallback - just position without coordinates
153-
val properties = Properties()
154-
val file = File(PROPERTIES_FILE)
155-
if (file.exists()) {
156-
properties.load(file.inputStream())
157-
val position = properties.getProperty(POSITION_KEY, null)
185+
run {
186+
val props = loadPropertiesFrom(trayPropertiesFile())
187+
?: loadPropertiesFrom(oldTmpPropertiesFile())
188+
?: loadPropertiesFrom(legacyPropertiesFile())
189+
val position = props?.getProperty(POSITION_KEY, null)
158190
if (position != null) {
159191
return try {
160192
TrayPosition.valueOf(position)

0 commit comments

Comments
 (0)