Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 23 additions & 40 deletions composeApp/src/desktopMain/kotlin/org/ooni/probe/Main.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.ooni.probe

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -18,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.app_name
Expand All @@ -32,63 +32,46 @@ import ooniprobe.composeapp.generated.resources.tray_icon_windows_light_running
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.ooni.probe.data.DeepLinkHandler
import org.ooni.probe.data.models.DeepLink
import org.ooni.probe.data.models.RunBackgroundState
import org.ooni.probe.shared.DesktopOS
import org.ooni.probe.shared.Platform
import java.awt.Desktop
import org.ooni.probe.shared.DeepLinkParser
import org.ooni.probe.shared.InstanceManager

val APP_ID = "org.ooni.probe" // needs to be thesame as conveyor `app.rdns-name`
const val APP_ID = "org.ooni.probe" // needs to be the same as conveyor `app.rdns-name`

fun main(args: Array<String>) {
val autoLaunch = AutoLaunch(appPackageName = APP_ID)

val instanceManager = InstanceManager(dependencies.platformInfo)
val deepLinkFlow = MutableSharedFlow<DeepLink?>(extraBufferCapacity = 1)

var deepLinkHandler: DeepLinkHandler? = null

dependencies.platformInfo.platform.let { platform ->
if (platform is Platform.Desktop && platform.os == DesktopOS.Mac) {
Desktop.getDesktop().setOpenURIHandler { event ->
deepLinkFlow.tryEmit(DeepLink.AddDescriptor(event.uri.path.split("/").last()))
}
} else {
deepLinkHandler = DeepLinkHandler()
deepLinkHandler.initialize(args)
CoroutineScope(Dispatchers.IO).launch {
instanceManager.observeUrls().collectLatest {
deepLinkFlow.tryEmit(DeepLinkParser(it))
}
}

application {
var isWindowVisible by remember { mutableStateOf(!autoLaunch.isStartedViaAutostart()) }

val deepLink by deepLinkFlow.collectAsState(null)
instanceManager.initialize(args)

// start an hourly background task that calls startSingleRun
LaunchedEffect(Unit) {
while (true) {
delay(1000 * 60 * 60)
startSingleRun()
}
}
CoroutineScope(Dispatchers.Default).launch {
autoLaunch.enable()
}

LaunchedEffect(Unit) {
deepLinkHandler?.addMessageListener { message ->
message?.let { message ->
isWindowVisible = true
deepLinkFlow.tryEmit(message)
}
}
// start an hourly background task that calls startSingleRun
CoroutineScope(Dispatchers.IO).launch {
while (true) {
delay(1000 * 60 * 60)
startSingleRun()
}
}

LaunchedEffect(Unit) {
autoLaunch.enable()
}
application {
var isWindowVisible by remember { mutableStateOf(!autoLaunch.isStartedViaAutostart()) }
val deepLink by deepLinkFlow.collectAsState(null)

Window(
onCloseRequest = {
isWindowVisible = false
},
onCloseRequest = { isWindowVisible = false },
visible = isWindowVisible,
icon = painterResource(trayIcon()),
title = stringResource(Res.string.app_name),
Expand Down Expand Up @@ -123,8 +106,8 @@ fun main(args: Array<String>) {
Item(
"Exit",
onClick = {
deepLinkHandler?.shutdown()
exitApplication()
instanceManager.shutdown()
},
)
},
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.ooni.probe.shared

import co.touchlab.kermit.Logger
import org.ooni.probe.config.OrganizationConfig
import org.ooni.probe.data.models.DeepLink
import java.net.URI

object DeepLinkParser {
operator fun invoke(url: String): DeepLink {
val uri = try {
URI.create(url)
} catch (e: Exception) {
Logger.w("Invalid deep link: $url")
return DeepLink.Error
}

return if (
(uri.scheme == "ooni" && uri.host == "runv2") ||
uri.host == OrganizationConfig.ooniRunDomain
) {
uri.path.split("/").lastOrNull()?.let { id ->
DeepLink.AddDescriptor(id)
} ?: run {
Logger.w("Invalid deep link: $uri")
DeepLink.Error
}
} else if (uri.scheme == "http" || uri.scheme == "https") {
DeepLink.RunUrls(url)
} else {
Logger.w("Invalid deep link: $uri")
DeepLink.Error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.ooni.probe.shared

import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.ooni.probe.APP_ID
import tk.pratanumandal.unique4j.Unique4j
import tk.pratanumandal.unique4j.exception.Unique4jException
import java.awt.Desktop
import java.net.URI
import java.util.Date

// Ensures only one app instance is running
// and relays the URLs (deep links) it receives from the system
class InstanceManager(
private val platformInfo: PlatformInfo,
) {
private var unique: Unique4j? = null
private val urls = MutableSharedFlow<String>(extraBufferCapacity = 1)

fun initialize(args: Array<String>) {
// MacOS already only allows for one app instance
if (isMac) {
// setOpenURIHandler only supports Mac
Desktop.getDesktop().setOpenURIHandler { event ->
urls.tryEmit(event.uri.toString())
}
return
}

try {
unique = object : Unique4j(APP_ID) {
override fun receiveMessage(message: String) {
handleArgs(message.split(" ").toTypedArray())
}

override fun sendMessage(): String {
// Check for deep links in arguments
Logger.d("Application launched without deep link at ${Date()}")
return args.joinToString(" ")
}

override fun handleException(exception: Exception) {
Logger.e(exception) { "Exception occurred" }
}

override fun beforeExit() {
Logger.d("Exiting subsequent instance.")
}
}

// Try to acquire lock
val lockFlag = unique!!.acquireLock()
if (lockFlag) {
Logger.d("Application instance started successfully")
handleArgs(args)
} else {
Logger.d("Could not acquire lock - this should not happen")
}

// Register shutdown hook to free the lock when application exits
Runtime.getRuntime().addShutdownHook(
Thread {
unique?.freeLock()
},
)
} catch (e: Unique4jException) {
Logger.e("Failed to initialize Unique4j", e)
}
}

fun shutdown() {
unique?.freeLock()
}

fun observeUrls() = urls.asSharedFlow()

private fun handleArgs(args: Array<String>) {
findUrlInArgs(args)?.let {
urls.tryEmit(it)
}
}

private fun findUrlInArgs(args: Array<String>): String? {
args.forEach { arg ->
val sanitizedArg =
// Windows can sometimes add extra slashes
arg.replace(":////", "://")
.replace(":/\\", "://")

try {
URI.create(sanitizedArg) // parse URL
return sanitizedArg
} catch (_: Exception) {
}
}

// Windows can sometimes pass URLs with the protocol separated
if (args.size >= 2 && args.first().endsWith(":")) {
return args[0] + "//" + args[1]
}

return null
}

private val isMac get() = (platformInfo.platform as? Platform.Desktop)?.os == DesktopOS.Mac
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.ooni.probe.shared

import org.junit.Test
import org.ooni.probe.config.OrganizationConfig
import org.ooni.probe.data.models.DeepLink
import kotlin.test.assertEquals

class DeepLinkParserTest {
@Test
fun test() {
assertEquals(
DeepLink.RunUrls("https://example.org"),
DeepLinkParser("https://example.org"),
)
assertEquals(
DeepLink.AddDescriptor("10158"),
DeepLinkParser("https://${OrganizationConfig.ooniRunDomain}/v2/10158"),
)
assertEquals(
DeepLink.AddDescriptor("10158"),
DeepLinkParser("ooni://runv2/10158"),
)
assertEquals(
DeepLink.Error,
DeepLinkParser("ooni://invalid"),
)
assertEquals(
DeepLink.Error,
DeepLinkParser("invalid"),
)
}
}
Loading