diff --git a/client-py/build.gradle.kts b/client-py/build.gradle.kts new file mode 100644 index 0000000..0b281ee --- /dev/null +++ b/client-py/build.gradle.kts @@ -0,0 +1,92 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.SharedLibrary +import xyz.xfqlittlefan.fhraise.buildsrc.projectVersion + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +group = "xyz.xfqlittlefan.fhraise" +project.version = projectVersion + +kotlin { + val linuxTargets = mutableListOf() + val mingwTargets = mutableListOf() + + linuxTargets += linuxArm64() + linuxTargets += linuxX64() + + mingwTargets += mingwX64() + + val defaultSharedLibConfigure: SharedLibrary.() -> Unit = { + export(project(":py")) + } + + configure(linuxTargets) { + binaries { + sharedLib { + baseName = "fhraisepy" + defaultSharedLibConfigure() + } + } + } + + configure(mingwTargets) { + binaries { + sharedLib { + baseName = "libfhraisepy" + defaultSharedLibConfigure() + } + } + } + + applyDefaultHierarchyTemplate() + + sourceSets { + val nativeMain by getting { + dependencies { + api(project(":py")) + implementation(libs.kotlinx.datetime) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.serialization.kotlinx.cbor) + } + } + + val nonMingwMain by creating { + dependsOn(nativeMain) + dependencies { + implementation(libs.ktor.client.cio) + } + } + + val mingwMain by getting { + dependencies { + implementation(libs.ktor.client.winhttp) + } + } + + val linuxMain by getting { + dependsOn(nonMingwMain) + } + } +} diff --git a/client-py/src/nativeMain/kotlin/Client.kt b/client-py/src/nativeMain/kotlin/Client.kt new file mode 100644 index 0000000..1a82a43 --- /dev/null +++ b/client-py/src/nativeMain/kotlin/Client.kt @@ -0,0 +1,110 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +import io.ktor.client.* +import io.ktor.client.plugins.websocket.* +import io.ktor.serialization.kotlinx.* +import kotlinx.cinterop.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.Cbor +import kotlin.coroutines.resume +import kotlin.experimental.ExperimentalNativeApi + +class Client(private val host: String, private val port: UShort) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val messageChannel = Channel(3) + + @OptIn(ExperimentalForeignApi::class) + private val messageErrorChannel = Channel>(3) + private val resultChannel = Channel(3) + + @OptIn(ExperimentalSerializationApi::class) + private val client = HttpClient { + install(WebSockets) { + contentConverter = KotlinxWebsocketSerializationConverter(Cbor) + } + } + + @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) + fun connect(throwable: CPointer>?) = runBlocking { + logger.debug("Connecting to $host:$port.") + suspendCancellableCoroutine { continuation -> + logger.debug("Launching coroutine.") + scope.launch(Dispatchers.IO) { + runCatching(throwable) { + logger.debug("Starting connection.") + client.webSocket(host = host, port = port.toInt(), path = pyWsPath) { + continuation.resume(true) + logger.debug("Connected.") + while (true) { + logger.debug("Waiting for message...") + val receiveResult = + runCatching(null) { messageChannel.send(receiveDeserialized()) }.onFailure { + logger.error("Failed to receive message.") + it as ThrowableWrapper + messageErrorChannel.send(it.throwable) + messageChannel.send(null) + } + if (receiveResult.isFailure) continue + logger.debug("Waiting for result...") + sendSerialized(resultChannel.receive()) + } + } + }.onFailure { + if (continuation.isActive) { + logger.error("Failed to connect.") + continuation.resume(false) + } else { + logger.error("Uncaught error in $this.") + throw it + } + } + } + } + } + + @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) + fun receive( + type: CPointer>, + ref: CPointer, + throwable: CPointer>?, + getResult: CPointer CPointer<*>>> + ): Boolean { + val message = runBlocking { messageChannel.receive() } + + if (message == null) { + return runBlocking { + throwable?.pointed?.value = messageErrorChannel.receive() + false + } + } + + type.pointed.value = message::class.qualifiedName!!.cstrPtr + ref.pointed.value = StableRef.create(message).asCPointer() + + return runBlocking { + runCatching(throwable) { + resultChannel.send(getResult().asStableRef().get()) + }.isSuccess + } + } +} diff --git a/client-py/src/nativeMain/kotlin/Logger.kt b/client-py/src/nativeMain/kotlin/Logger.kt new file mode 100644 index 0000000..a2940b2 --- /dev/null +++ b/client-py/src/nativeMain/kotlin/Logger.kt @@ -0,0 +1,50 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +import kotlinx.datetime.Clock + +class Logger internal constructor(private val tag: Any) { + @Deprecated("This constructor is for calling from C code only.", level = DeprecationLevel.HIDDEN) + constructor(tag: String) : this(" $tag" as Any) + + fun debug(message: String) { + println("Debug", message) + } + + fun info(message: String) { + println("Info", message) + } + + fun warn(message: String) { + println("Warn", message) + } + + fun error(message: String) { + println("Error", message) + } + + private fun println(level: String, message: String) { + message.split("\n").forEach { + println("${Clock.System.now()} [$tag] $level: $it") + } + } +} + +internal val Any.logger: Logger get() = Logger(this::class.let { it.qualifiedName ?: it }) diff --git a/client-py/src/nativeMain/kotlin/Throwable.kt b/client-py/src/nativeMain/kotlin/Throwable.kt new file mode 100644 index 0000000..3cc76bd --- /dev/null +++ b/client-py/src/nativeMain/kotlin/Throwable.kt @@ -0,0 +1,95 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +import kotlinx.cinterop.* +import kotlin.experimental.ExperimentalNativeApi + +@OptIn(ExperimentalForeignApi::class) +class ThrowableVar(rawPtr: NativePtr) : CStructVar(rawPtr) { + @Suppress("DEPRECATION") + companion object : Type(40, 8) + + var type: CArrayPointer? + get() = memberAt>(0).value + set(value) { + memberAt>(0).value = value + } + + var ref: COpaquePointer? + get() = memberAt(8).value + set(value) { + memberAt(8).value = value + } + + var message: CArrayPointer? + get() = memberAt>(16).value + set(value) { + memberAt>(16).value = value + } + + var stacktrace: CArrayPointer>? + get() = memberAt>>(24).value + set(value) { + memberAt>>(24).value = value + } + + var stacktraceSize: Int? + get() = memberAt(32).value + set(value) { + memberAt(32).value = value ?: 0 + } +} + +@ExperimentalForeignApi +internal class ThrowableWrapper(val throwable: CPointer) : Throwable() + +@ExperimentalNativeApi +@ExperimentalForeignApi +internal fun Throwable.wrapToC(): CPointer { + logger.debug("Wrapping throwable.") + logger.warn(getStackTrace().joinToString("\n")) + + val throwable = nativeHeap.alloc() + throwable.type = this::class.qualifiedName?.cstrPtr + throwable.ref = StableRef.create(this).asCPointer() + throwable.message = this.message?.cstrPtr + val stacktraceList = mutableListOf() + this.getStackTrace().forEach { + stacktraceList.add(it.cstrPtr.pointed) + } + val stacktraceArray = nativeHeap.allocArrayOfPointersTo(stacktraceList) + throwable.stacktrace = stacktraceArray + throwable.stacktraceSize = stacktraceList.size + return throwable.ptr +} + +@ExperimentalNativeApi +@ExperimentalForeignApi +internal inline fun runCatching( + throwablePtr: CPointer>?, block: () -> R +) = runCatching(block).fold(onSuccess = { + Result.success(it) +}, onFailure = { + val throwableVarPtr = it.wrapToC() + if (throwablePtr != null && throwablePtr.rawValue != nativeNullPtr) { + throwablePtr.pointed.value = throwableVarPtr + } + Result.failure(ThrowableWrapper(throwableVarPtr)) +}) diff --git a/client-py/src/nativeMain/kotlin/Types.kt b/client-py/src/nativeMain/kotlin/Types.kt new file mode 100644 index 0000000..fe61a10 --- /dev/null +++ b/client-py/src/nativeMain/kotlin/Types.kt @@ -0,0 +1,30 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +import kotlinx.cinterop.* + +@ExperimentalForeignApi +internal val String.cstrPtr: CPointer + get() { + val cstr = cstr + val ptr = nativeHeap.allocArray(cstr.size) + cstr.place(ptr) + return ptr + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfeebf8..01d9592 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ kotlin = "1.9.23" kotlinx-atomicfu = "0.23.2" kotlinx-coroutines = "1.8.0" kotlinx-datetime = "0.5.0" +kotlinx-serilization = "1.6.3" agp = "8.2.2" androidx-core-splashscreen = "1.0.1" androidx-window = "1.2.0" @@ -30,6 +31,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serilization" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } @@ -42,6 +44,7 @@ decompose-extensions-compose = { module = "com.arkivanov.decompose:extensions-co ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } ktor-client-resources = { module = "io.ktor:ktor-client-resources", version.ref = "ktor" } ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } diff --git a/py/build.gradle.kts b/py/build.gradle.kts new file mode 100644 index 0000000..28da710 --- /dev/null +++ b/py/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +import xyz.xfqlittlefan.fhraise.buildsrc.projectVersion + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +group = "xyz.xfqlittlefan.fhraise" +project.version = projectVersion + +kotlin { + jvm() + + linuxArm64() + linuxX64() + mingwX64() + + applyDefaultHierarchyTemplate() + + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.kotlinx.serialization.core) + } + } + } +} diff --git a/py/src/commonMain/kotlin/Constants.kt b/py/src/commonMain/kotlin/Constants.kt new file mode 100644 index 0000000..462da57 --- /dev/null +++ b/py/src/commonMain/kotlin/Constants.kt @@ -0,0 +1,21 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +const val pyWsPath = "/internal/py/ws" diff --git a/py/src/commonMain/kotlin/Message.kt b/py/src/commonMain/kotlin/Message.kt new file mode 100644 index 0000000..636dd95 --- /dev/null +++ b/py/src/commonMain/kotlin/Message.kt @@ -0,0 +1,54 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +import kotlinx.serialization.Serializable + +@Serializable +sealed class Message { + @Serializable + sealed class Register : Message() { + @Serializable + data class Frame(val callId: String, val content: ByteArray) : Register() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Frame + + if (callId != other.callId) return false + if (!content.contentEquals(other.content)) return false + + return true + } + + override fun hashCode(): Int { + var result = callId.hashCode() + result = 31 * result + content.contentHashCode() + return result + } + } + + @Serializable + sealed class Result : Register() { + @Serializable + data object Success : Result() + } + } +} diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 84d86eb..ece21b1 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -34,6 +34,7 @@ application { } dependencies { + implementation(project(":py")) implementation(projects.shared) implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.server.core) diff --git a/server/src/main/kotlin/Application.kt b/server/src/main/kotlin/Application.kt index cc59701..84adbb2 100644 --- a/server/src/main/kotlin/Application.kt +++ b/server/src/main/kotlin/Application.kt @@ -41,6 +41,7 @@ import xyz.xfqlittlefan.fhraise.api.registerAppCodeVerification import xyz.xfqlittlefan.fhraise.auth.cleanupUnverifiedUsersPreDay import xyz.xfqlittlefan.fhraise.models.cleanupVerificationCodes import xyz.xfqlittlefan.fhraise.proxy.proxyKeycloak +import xyz.xfqlittlefan.fhraise.py.py fun main() { embeddedServer(CIO, port = defaultServerPort, host = "0.0.0.0", module = Application::module).start(wait = true) @@ -72,6 +73,7 @@ fun Application.module() { cleanupVerificationCodes() routing { + py() proxyKeycloak() apiAuth() apiOAuth() diff --git a/server/src/main/kotlin/py/Py.kt b/server/src/main/kotlin/py/Py.kt new file mode 100644 index 0000000..3448c7d --- /dev/null +++ b/server/src/main/kotlin/py/Py.kt @@ -0,0 +1,29 @@ +/* + * This file is part of Fhraise. + * Copyright (c) 2024 HSAS Foodies. All Rights Reserved. + * + * Fhraise is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Fhraise is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Fhraise. If not, see . + */ + +package xyz.xfqlittlefan.fhraise.py + +import io.ktor.server.routing.* +import io.ktor.server.websocket.* + +fun Route.py() { + webSocket(pyWsPath) { + sendSerialized(Message.Register.Frame("id", "hello".encodeToByteArray())) + receiveDeserialized() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c84498..2d6b78f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,7 +35,11 @@ dependencyResolutionManagement { } } +include(":shared") + include(":compose-app") include(":server") -include(":shared") +include(":py") +include(":client-py") + include(":keycloak-spi")