diff --git a/app/src/desktopMain/kotlin/com/crosspaste/DesktopMouseModule.kt b/app/src/desktopMain/kotlin/com/crosspaste/DesktopMouseModule.kt index 0ec44db15..5a5e8e6ae 100644 --- a/app/src/desktopMain/kotlin/com/crosspaste/DesktopMouseModule.kt +++ b/app/src/desktopMain/kotlin/com/crosspaste/DesktopMouseModule.kt @@ -1,6 +1,7 @@ package com.crosspaste import com.crosspaste.config.DesktopConfigManager +import com.crosspaste.config.DevConfig import com.crosspaste.mouse.AwtLocalScreensProvider import com.crosspaste.mouse.LocalScreensProvider import com.crosspaste.mouse.MacosLocalScreensProvider @@ -12,9 +13,11 @@ import com.crosspaste.mouse.MouseIpcProtocol import com.crosspaste.mouse.MouseLayoutStore import com.crosspaste.mouse.Position import com.crosspaste.mouse.asDaemonHandle +import com.crosspaste.path.AppPathProvider import com.crosspaste.platform.Platform import com.crosspaste.sync.SyncManager import com.crosspaste.ui.mouse.ScreenArrangementViewModel +import com.crosspaste.utils.getAppEnvUtils import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -116,9 +119,38 @@ fun desktopMouseModule(): Module = // SyncManager's StateFlow is the canonical multi-cast view. syncRuntimeInfosFlow = get().realTimeSyncRuntimeInfos, clientFactory = { + val platform = get() + val pathProvider = get() + val isDevelopment = getAppEnvUtils().isDevelopment() + + // Dev: developer-supplied path from development.properties. + // Prod: bundled binary sits next to the app's runtime under pasteAppExePath. + val candidates = + if (isDevelopment) { + listOfNotNull(DevConfig.mouseBinaryPath) + } else { + listOf( + pathProvider.pasteAppExePath.resolve(MouseDaemonBinary.binaryName(platform)).toString(), + ) + } + val binary = - MouseDaemonBinary.resolve() - ?: throw IllegalStateException("crosspaste-mouse binary not found") + MouseDaemonBinary.resolve(candidatePaths = candidates) + ?: throw IllegalStateException( + buildString { + append("crosspaste-mouse binary not found. Tried: ") + append(if (candidates.isEmpty()) "(no candidates)" else candidates.joinToString()) + if (isDevelopment) { + append(". Set mouseBinaryPath in development.properties, ") + append("or set CROSSPASTE_MOUSE_BIN / -Dcrosspaste.mouse.binary.") + } else { + append( + ". The bundled binary is missing — please reinstall the app or " + + "report this issue.", + ) + } + }, + ) MouseDaemonClient(MouseDaemonProcess.spawn(binary).asDaemonHandle()) }, ) diff --git a/app/src/desktopMain/kotlin/com/crosspaste/config/DevConfig.kt b/app/src/desktopMain/kotlin/com/crosspaste/config/DevConfig.kt index 50e4c9e1a..a20495f7c 100644 --- a/app/src/desktopMain/kotlin/com/crosspaste/config/DevConfig.kt +++ b/app/src/desktopMain/kotlin/com/crosspaste/config/DevConfig.kt @@ -11,4 +11,6 @@ object DevConfig { val pasteUserPath: String? = development.getProperty("pasteUserPath") val marketingMode: Boolean = development.getProperty("marketingMode")?.toBoolean() == true + + val mouseBinaryPath: String? = development.getProperty("mouseBinaryPath")?.takeIf { it.isNotBlank() } } diff --git a/app/src/desktopMain/kotlin/com/crosspaste/mouse/MouseDaemonBinary.kt b/app/src/desktopMain/kotlin/com/crosspaste/mouse/MouseDaemonBinary.kt index cf4486ff6..922807409 100644 --- a/app/src/desktopMain/kotlin/com/crosspaste/mouse/MouseDaemonBinary.kt +++ b/app/src/desktopMain/kotlin/com/crosspaste/mouse/MouseDaemonBinary.kt @@ -1,5 +1,6 @@ package com.crosspaste.mouse +import com.crosspaste.platform.Platform import java.io.File object MouseDaemonBinary { @@ -8,27 +9,29 @@ object MouseDaemonBinary { /** * Resolution order: - * 1. -Dcrosspaste.mouse.binary= - * 2. $CROSSPASTE_MOUSE_BIN - * 3. Any path in `candidatePaths` (e.g. bundled in resources/bin/...) - * Returns null if nothing points at an existing regular file. + * 1. -Dcrosspaste.mouse.binary= (per-launch override) + * 2. $CROSSPASTE_MOUSE_BIN (per-shell override) + * 3. Any path in [candidatePaths] (caller-supplied, env-aware) + * + * The caller is responsible for assembling [candidatePaths] from the + * dev-mode `DevConfig.mouseBinaryPath` or the prod-mode + * `pasteAppExePath / binaryName(platform)`. Returns null if nothing + * points at an existing regular file. */ fun resolve( + candidatePaths: List = emptyList(), envLookup: (String) -> String? = System::getenv, - candidatePaths: List = defaultCandidatePaths(), ): File? { System.getProperty(SYSTEM_PROPERTY)?.let { File(it).takeIf { f -> f.isFile } }?.let { return it } envLookup(ENV_VAR)?.let { File(it).takeIf { f -> f.isFile } }?.let { return it } return candidatePaths .asSequence() + .filter { it.isNotBlank() } .map(::File) .firstOrNull { it.isFile } } - private fun defaultCandidatePaths(): List { - // Production bundling is a follow-up plan; for now the only - // candidate is a dev-mode symlink next to the app jar. - val home = System.getProperty("user.home") ?: return emptyList() - return listOf("$home/crosspaste-mouse/target/release/crosspaste-mouse") - } + /** crosspaste-mouse build artifact name for the given platform. */ + fun binaryName(platform: Platform): String = + if (platform.isWindows()) "crosspaste-mouse.exe" else "crosspaste-mouse" } diff --git a/app/src/desktopMain/resources/development.properties.template b/app/src/desktopMain/resources/development.properties.template index d4cee8c34..9d7cf5ae9 100644 --- a/app/src/desktopMain/resources/development.properties.template +++ b/app/src/desktopMain/resources/development.properties.template @@ -1,2 +1,8 @@ pasteAppPath=. -pasteUserPath=.user \ No newline at end of file +pasteUserPath=.user + +# Mouse daemon binary path (development only). Set to the absolute path of +# your local crosspaste-mouse build. Leave empty to fall back to +# -Dcrosspaste.mouse.binary or $CROSSPASTE_MOUSE_BIN. +# Example: mouseBinaryPath=/Users/me/crosspaste-mouse/target/release/crosspaste-mouse +mouseBinaryPath= diff --git a/app/src/desktopTest/kotlin/com/crosspaste/mouse/MouseDaemonBinaryTest.kt b/app/src/desktopTest/kotlin/com/crosspaste/mouse/MouseDaemonBinaryTest.kt index 5160a28f0..d4f340f14 100644 --- a/app/src/desktopTest/kotlin/com/crosspaste/mouse/MouseDaemonBinaryTest.kt +++ b/app/src/desktopTest/kotlin/com/crosspaste/mouse/MouseDaemonBinaryTest.kt @@ -1,5 +1,6 @@ package com.crosspaste.mouse +import com.crosspaste.platform.Platform import java.nio.file.Files import kotlin.test.AfterTest import kotlin.test.Test @@ -50,4 +51,26 @@ class MouseDaemonBinaryTest { System.setProperty(propKey, "/definitely/does/not/exist/mouse") assertNull(MouseDaemonBinary.resolve(envLookup = { null }, candidatePaths = emptyList())) } + + @Test + fun `binaryName appends exe on windows only`() { + val windows = Platform(name = "Windows", arch = "x64", bitMode = 64, version = "10") + val macos = Platform(name = "Macos", arch = "arm64", bitMode = 64, version = "14") + val linux = Platform(name = "Linux", arch = "x64", bitMode = 64, version = "5.15") + assertEquals("crosspaste-mouse.exe", MouseDaemonBinary.binaryName(windows)) + assertEquals("crosspaste-mouse", MouseDaemonBinary.binaryName(macos)) + assertEquals("crosspaste-mouse", MouseDaemonBinary.binaryName(linux)) + } + + @Test + fun `candidate paths are used when overrides are absent`() { + System.clearProperty(propKey) + val file = Files.createTempFile("fake-daemon", "").toFile().apply { deleteOnExit() } + val result = + MouseDaemonBinary.resolve( + candidatePaths = listOf("", "/does/not/exist", file.absolutePath), + envLookup = { null }, + ) + assertEquals(file.absolutePath, result?.absolutePath) + } }