Is it possible to have the CfD installer associate...
# compose-desktop
s
Is it possible to have the CfD installer associate a custom URI protocol with the app? I'm trying to implement an oauth/oidc flow and that seems to be the most appropriate way to get the response from the browser.
s
Maybe @Stefan Oltmann can help you? I think he implement it. https://kotlinlang.slack.com/archives/C01D6HTPATV/p1634299813089000
s
Hehe, yeah, I kinda solved it, but you may not like my solution. 🙈 I'm happy to share it. Thanks for bringing me in, @SrSouza For Mac it's really easy as
Copy code
Desktop.getDesktop().setOpenURIHandler { event ->
     // do something
}
but for Windows things get messy. My Windows solution goes like this: 1. Register a custom URI scheme on start via registry editor 2. Let this call a custom EXE I build with Kotlin Native that just writes everything given to it out as a text file 3. Have a File Watcher pick this change up and further process it. This code registers the URI scheme:
Copy code
private fun registerUriSchemeOnWindows() {

    try {

        /*
         * We look for a file in the JAR that must exist.
         */
        val resourceName = "icon.ico"

        val url = Thread.currentThread().contextClassLoader.getResource(resourceName)

        checkNotNull(url) { "Resource $resourceName was not found." }

        val trimSize = "!/$resourceName".length

        val jarPath = url.path.substring(0, url.path.length - trimSize)

        val pathToExe = Paths.get(URI(jarPath))
            .resolve("../resources/cmdargshelper.exe")
            .toFile()
            .canonicalPath

        <http://Log.info|Log.info>("App location: $pathToExe")

        val runtime = Runtime.getRuntime()

        val protocolPath = "HKEY_CURRENT_USER\\Software\\Classes\\my-app-name"

        var process = runtime.exec("reg add $protocolPath /t REG_SZ /d \"My App Name\" /f")
        process.waitFor()

        process = runtime.exec("reg add $protocolPath /v \"URL Protocol\" /t REG_SZ /d \"\" /f")
        process.waitFor()

        process = runtime.exec(
            "reg add $protocolPath\\shell /f"
        )
        process.waitFor()

        process = runtime.exec(
            "reg add $protocolPath\\shell\\open /f"
        )
        process.waitFor()

        process = runtime.exec(
            "reg add $protocolPath\\shell\\open\\command /t REG_SZ /d \"$pathToExe %1\" /f"
        )
        process.waitFor()

        <http://Log.info|Log.info>("Custom URI scheme registered.")

    } catch (ex: Exception) {
        Log.error("Could not register URI scheme.", ex)
    }
}
This is my cmdargshelper.exe:
Copy code
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toKString
import platform.posix.EOF
import platform.posix.fclose
import platform.posix.fopen
import platform.posix.fputs
import platform.posix.getenv
import platform.posix.mkdir
import kotlin.system.getTimeMillis

fun main(args: Array<String>) {

    if (args.size != 1) {
        println("USAGE: Must be called with one argument.")
        return
    }

    val argument = args[0]

    val appDataPath = getenv("LOCALAPPDATA")?.toKString()

    val millis = getTimeMillis()

    val dirPath = "$appDataPath\\My App Name\\cmdargs"
    val filePath = "$dirPath\\$millis.cmdargs"

    println("$argument >> $filePath")

    val mkdirResult = mkdir(dirPath)

    if (mkdirResult == 0)
        println("Directory 'cmdargs' created.")

    writeAllText(filePath, argument)
}

private fun writeAllText(filePath: String, text: String) {

    val file =
        fopen(filePath, "w") ?: throw IllegalArgumentException("Failed to open $filePath")

    try {

        memScoped {

            if (fputs(text, file) == EOF)
                error("File write error")
        }

    } finally {
        fclose(file)
    }
}
And in my main() I do this:
Copy code
if (isRunningOnWindows()) {

    /* Watch args.temp */
    fileWatchService = FileSystems.getDefault().newWatchService()

    launch(<http://Dispatchers.IO|Dispatchers.IO>) {

        val watchService = fileWatchService

        checkNotNull(watchService) { "WatchService was not initialized." }

        try {

            val argsTempPath = Paths.get(getLocalCachePath() + "/cmdargs")

            val argsTempPathFile = argsTempPath.toFile()

            if (!argsTempPathFile.exists())
                argsTempPathFile.mkdirs()

            argsTempPath.register(watchService, ENTRY_MODIFY)

            var poll = true

            while (poll) {

                val watchKey = watchService.take()

                for (event in watchKey.pollEvents()) {

                    val file = File(argsTempPathFile, event.context().toString())

                    val text = file.readText()

                    withContext(Dispatchers.Main) {

                        // Handle the URI here! I do this:
                        store.dispatch(AppAction.UriOpened(text))
                    }

                    /*
                     * Clean up with JVM exit.
                     *
                     * Immediate deletion can cause problems.
                     */
                    file.deleteOnExit()
                }

                poll = watchKey.reset()
            }

        } catch (ex: ClosedWatchServiceException) {
            Log.debug("Watch service ended.")
        } catch (ex: Exception) {
            Log.error("Error watching file system.", ex)
        }
    }
}
At the end of main() I need to call
fileWatchService?.close()
That's why my Main.kt holds
private var fileWatchService: WatchService? = null
It works for me, but I'm happy to replace it with a simpler solution if someone has one. 😄
s
Thanks! I was under the impression that Auth0 didn't support http callbacks. Their documentation implies it, but it worked for me. I'll keep this in mind if that circumstance changes. I wrote a short article detailing how to connect to Auth0 on the desktop because there was not a lot of information online for that. https://medium.com/@seanproctor/oauth-in-compose-for-desktop-with-auth0-9990075606a1
s
http Callbacks are mostly only allowed if working with localhost. Most services require https. For that I set a small service up that redirects all https://myservice.com/foobar calls to foobar:// - similar to what Microsoft Teams does with it's meeting links.
I really don't understand why it's that difficult on Windows 🙄
I did not go with your solution with the local webserver because I fear some personal firewall may block opening of local ports. So this may not always work.
It's about time that desktop operating systems integrate such a system like Android & iOS do natively.
s
Hrm, good point about the local firewall.