Skip to content

Commit

Permalink
Add apollo-debug-server (#5353)
Browse files Browse the repository at this point in the history
  • Loading branch information
BoD authored Nov 6, 2023
1 parent 9382034 commit ce77936
Show file tree
Hide file tree
Showing 20 changed files with 560 additions and 23 deletions.
13 changes: 9 additions & 4 deletions build-logic/src/main/kotlin/Mpp.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import org.gradle.api.Action
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
Expand Down Expand Up @@ -34,13 +33,19 @@ private val enableLinux = System.getenv("APOLLO_JVM_ONLY")?.toBoolean()?.not() ?
private val enableJs = System.getenv("APOLLO_JVM_ONLY")?.toBoolean()?.not() ?: true
private val enableApple = System.getenv("APOLLO_JVM_ONLY")?.toBoolean()?.not() ?: true

fun Project.configureMppDefaults(withJs: Boolean, withLinux: Boolean, withAndroid: Boolean) {
fun Project.configureMppDefaults(
withJvm: Boolean,
withJs: Boolean,
withLinux: Boolean,
withAndroid: Boolean,
withApple: Boolean,
) {
configureMpp(
withJvm = true,
withJvm = withJvm,
withJs = withJs,
browserTest = false,
withLinux = withLinux,
appleTargets = allAppleTargets,
appleTargets = if (!withApple) emptySet() else allAppleTargets,
withAndroid = withAndroid,
)
}
Expand Down
10 changes: 7 additions & 3 deletions build-logic/src/main/kotlin/api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ fun Project.apolloLibrary(
javaModuleName: String?,
withJs: Boolean = true,
withLinux: Boolean = true,
withApple: Boolean = true,
withJvm: Boolean = true,
publish: Boolean = true
) {
group = property("GROUP")!!
Expand Down Expand Up @@ -34,9 +36,11 @@ fun Project.apolloLibrary(

if (extensions.findByName("kotlin") is KotlinMultiplatformExtension) {
configureMppDefaults(
withJs,
withLinux,
extensions.findByName("android") != null
withJvm = withJvm,
withJs = withJs,
withLinux = withLinux,
withAndroid = extensions.findByName("android") != null,
withApple = withApple,
)
}

Expand Down
2 changes: 1 addition & 1 deletion gradle/libraries.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ androidx-sqlite = "2.3.1"
# This is used by the gradle integration tests to get the artifacts locally
apollo = "4.0.0-beta.3-SNAPSHOT"
# Used by the apollo-tooling project which uses a published version of Apollo
apollo-published = "4.0.0-alpha.3"
apollo-published = "4.0.0-beta.2"
cache = "2.0.2"
# See https://developer.android.com/jetpack/androidx/releases/compose-kotlin
compose-compiler = "1.5.4-dev-k1.9.20-50f08dfa4b4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public final class com/apollographql/apollo3/debugserver/ApolloDebugServer {
public static final field INSTANCE Lcom/apollographql/apollo3/debugserver/ApolloDebugServer;
public final fun registerApolloClient (Lcom/apollographql/apollo3/ApolloClient;Ljava/lang/String;)V
public static synthetic fun registerApolloClient$default (Lcom/apollographql/apollo3/debugserver/ApolloDebugServer;Lcom/apollographql/apollo3/ApolloClient;Ljava/lang/String;ILjava/lang/Object;)V
public final fun unregisterApolloClient (Lcom/apollographql/apollo3/ApolloClient;)V
}

7 changes: 7 additions & 0 deletions libraries/apollo-debug-server/api/jvm/apollo-debug-server.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public final class com/apollographql/apollo3/debugserver/ApolloDebugServer {
public static final field INSTANCE Lcom/apollographql/apollo3/debugserver/ApolloDebugServer;
public final fun registerApolloClient (Lcom/apollographql/apollo3/ApolloClient;Ljava/lang/String;)V
public static synthetic fun registerApolloClient$default (Lcom/apollographql/apollo3/debugserver/ApolloDebugServer;Lcom/apollographql/apollo3/ApolloClient;Ljava/lang/String;ILjava/lang/Object;)V
public final fun unregisterApolloClient (Lcom/apollographql/apollo3/ApolloClient;)V
}

87 changes: 87 additions & 0 deletions libraries/apollo-debug-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import com.android.build.gradle.tasks.BundleAar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
alias(libs.plugins.apollo.published)
id("com.google.devtools.ksp")
}

apolloLibrary(
javaModuleName = "com.apollographql.apollo3.debugserver",
withLinux = false,
withApple = false,
withJs = false,
)

kotlin {
sourceSets {
findByName("commonMain")?.apply {
kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")

dependencies {
implementation(project(":apollo-normalized-cache"))

// apollo-execution is not published: we bundle it into the aar artifact
compileOnly(project(":apollo-execution"))
api(project(":apollo-ast"))
}
}

findByName("androidMain")?.apply {
dependencies {
implementation(libs.androidx.startup.runtime)
}
}
}
}

val shadow = configurations.create("shadow") {
isCanBeConsumed = false
isCanBeResolved = true
}

dependencies {
add("kspCommonMainMetadata", project(":apollo-ksp"))
add("kspCommonMainMetadata", apollo.apolloKspProcessor(file("src/androidMain/resources/schema.graphqls"), "apolloDebugServer", "com.apollographql.apollo3.debugserver.internal.graphql"))
add(shadow.name, project(":apollo-execution")) {
isTransitive = false
}
}

configurations.getByName("compileOnly").extendsFrom(shadow)

android {
compileSdk = libs.versions.android.sdkversion.compile.get().toInt()
namespace = "com.apollographql.apollo3.debugserver"

defaultConfig {
minSdk = libs.versions.android.sdkversion.min.get().toInt()
}
}

// KMP ksp configuration inspired by https://medium.com/@actiwerks/setting-up-kotlin-multiplatform-with-ksp-7f598b1681bf
tasks.withType<KotlinCompile>().configureEach {
dependsOn("kspCommonMainKotlinMetadata")
}

tasks.configureEach {
if (name.endsWith("sourcesJar", ignoreCase = true)) {
dependsOn("kspCommonMainKotlinMetadata")
}
}

// apollo-execution is not published: we bundle it into the aar artifact
val jarApolloExecution = tasks.register<Jar>("jarApolloExecution") {
archiveBaseName.set("apollo-execution")
from(provider {
shadow.files.map { zipTree(it) }
})
}

tasks.withType<BundleAar>().configureEach {
from(jarApolloExecution) {
into("libs")
}
}
3 changes: 3 additions & 0 deletions libraries/apollo-debug-server/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=apollo-debug-server
POM_NAME=Apollo Kotlin Debug Server
POM_DESCRIPTION=Apollo Kotlin debug server, used by tools to inspect running applications using Apollo.
14 changes: 14 additions & 0 deletions libraries/apollo-debug-server/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- This entry makes ApolloDebugServerInitializer discoverable. -->
<meta-data
android:name="com.apollographql.apollo3.debugserver.internal.initializer.ApolloDebugServerInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.apollographql.apollo3.debugserver.internal.graphql

import com.apollographql.apollo3.debugserver.internal.server.Server
import okio.buffer
import okio.source
import kotlin.reflect.KClass

internal actual fun getExecutableSchema(): String = Server::class.java.classLoader!!
.getResourceAsStream("schema.graphqls")!!
.source()
.buffer()
.readUtf8()
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.apollographql.apollo3.debugserver.internal.initializer

import android.content.Context
import androidx.startup.Initializer

internal class ApolloDebugServerInitializer : Initializer<Unit> {
override fun create(context: Context) {
packageName = context.packageName
}

override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()

companion object {
lateinit var packageName: String
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.apollographql.apollo3.debugserver.internal.server

import android.net.LocalServerSocket
import android.net.LocalSocket
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.debugserver.internal.graphql.GraphQL
import com.apollographql.apollo3.debugserver.internal.initializer.ApolloDebugServerInitializer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.PrintStream
import java.util.concurrent.Executors

internal actual fun createServer(
apolloClients: Map<ApolloClient, String>,
): Server = AndroidServer(apolloClients)

private class AndroidServer(
apolloClients: Map<ApolloClient, String>,
) : Server {
companion object {
private const val SOCKET_NAME_PREFIX = "apollo_debug_"
}

private var localServerSocket: LocalServerSocket? = null
private val dispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)

private val graphQL = GraphQL(apolloClients)

override fun start() {
if (localServerSocket != null) error("Already started")
val localServerSocket = LocalServerSocket("$SOCKET_NAME_PREFIX${ApolloDebugServerInitializer.packageName}")
this.localServerSocket = localServerSocket
coroutineScope.launch {
while (true) {
val clientSocket = try {
localServerSocket.accept()
} catch (_: Exception) {
// Server socket has been closed (stop() was called)
break
}
launch { handleClient(clientSocket) }
}
}
}

private fun handleClient(clientSocket: LocalSocket) {
try {
val bufferedReader = clientSocket.inputStream.bufferedReader()
val printWriter = PrintStream(clientSocket.outputStream.buffered(), true)
val httpRequest = readHttpRequest(bufferedReader)
if (httpRequest.method == "OPTIONS") {
printWriter.print("HTTP/1.1 204 No Content\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: *\r\nAccess-Control-Allow-Headers: *\r\n\r\n")
return
}
printWriter.print("HTTP/1.1 200 OK\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: application/json\r\n\r\n")
printWriter.print(graphQL.executeGraphQL(httpRequest.body ?: ""))
} catch (e: CancellationException) {
// Expected when the server is closed
throw e
} catch (_: Exception) {
// I/O error or otherwise: ignore
} finally {
runCatching { clientSocket.close() }
}
}

private class HttpRequest(
val method: String,
val path: String,
val headers: List<Pair<String, String>>,
val body: String?,
)

private fun readHttpRequest(bufferedReader: BufferedReader): HttpRequest {
val (method, path) = bufferedReader.readLine().split(" ")
val headers = mutableListOf<Pair<String, String>>()
while (true) {
val line = bufferedReader.readLine()
if (line.isEmpty()) break
val (key, value) = line.split(": ")
headers.add(key to value)
}
val contentLength = headers.firstOrNull { it.first.equals("Content-Length", ignoreCase = true) }?.second?.toLongOrNull() ?: 0
val body = if (contentLength <= 0) {
null
} else {
val buffer = CharArray(contentLength.toInt())
bufferedReader.read(buffer)
String(buffer)
}
return HttpRequest(method, path, headers, body)
}

override fun stop() {
runCatching { localServerSocket?.close() }
coroutineScope.cancel()
dispatcher.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
type Query {
apolloClients: [ApolloClient!]!
apolloClient(id: ID!): ApolloClient
}

type ApolloClient {
id: ID!
displayName: String!
normalizedCaches: [NormalizedCache!]!
normalizedCache(id: ID!): NormalizedCache
}

type NormalizedCache {
id: ID!
displayName: String!
recordCount: Int!
records: [Record!]!
}

type Record {
key: String!
size: Int!
fields: Fields!
}

scalar Fields
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.apollographql.apollo3.debugserver

import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.debugserver.internal.server.Server
import com.apollographql.apollo3.debugserver.internal.server.createServer

object ApolloDebugServer {
private val apolloClients = mutableMapOf<ApolloClient, String>()
private var server: Server? = null

fun registerApolloClient(apolloClient: ApolloClient, id: String = "client") {
if (apolloClients.containsKey(apolloClient)) error("Client '$apolloClient' already registered")
if (apolloClients.containsValue(id)) error("Name '$id' already registered")
apolloClients[apolloClient] = id
startOrStopServer()
}

fun unregisterApolloClient(apolloClient: ApolloClient) {
apolloClients.remove(apolloClient)
startOrStopServer()
}

private fun startOrStopServer() {
if (apolloClients.isEmpty()) {
server?.stop()
} else {
if (server == null) {
server = createServer(apolloClients).apply {
start()
}
}
}
}
}
Loading

0 comments on commit ce77936

Please sign in to comment.