概要/日記
Kotlin Multiplatform (for Javascript)と firebase-kotlin-SDK を使用し Node.js アプリを作る。
サンプルアプリはDB上の要求リストがあれば即座に応じてOSコマンドを実行しその結果を格納する。
これの続き:
前提
- Ubuntu 22.04 on Windwos 11 wsl2
- Firebaseプロジェクト・Firestoreが有効であること
- 認証無し。実験用
Node.js用プロジェクト構成
KtNodeSvr/build.gradle.kts
plugins {
kotlin("multiplatform") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
}
repositories {
mavenCentral()
}
kotlin {
js {
nodejs { }
binaries.executable()
}
sourceSets {
jsMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation("dev.gitlive:firebase-firestore:1.13.0") // https://mvnrepository.com/artifact/dev.gitlive/firebase-firestore
implementation(npm("child_process", "1.0.2"))
implementation(project(":shared"))
}
}
}
settings.gradle.kts
rootProject.name = "KtMpApp"
include(":KtNodeSvr")
include(":KtHTMLFirestoreApp")
include(":shared")
KtNodeSvr/src/jsMain/kotlin/KtNodeSvr.kt
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseOptions
import dev.gitlive.firebase.firestore.*
import dev.gitlive.firebase.initialize
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
external val process: dynamic
val options = FirebaseOptions(
apiKey = process.env.APPKEY as String,
projectId = "xxxxxx",
databaseUrl = "https://xxxxxxxxxxxxx.firebaseio.com",
applicationId = "1:xxxxxxxxxxx:web:xxxxxxxxxxxxxxxxxxxx",
)
val app = Firebase.initialize(Unit, options)
val db = Firebase.firestore(app).apply {
settings = firestoreSettings(settings) { cacheSettings = memoryCacheSettings { } }
}
external fun require(module: String): dynamic
suspend fun main() = runCatching {
val args = (process.argv as Array<String>).drop(2)
val (tg, pw) = args
val refRqs = db.collection("fireshell").document(tg).collection("requests")
refRqs.orderBy("time", Direction.ASCENDING).where { "isComplete" notEqualTo true }.limit(10).snapshots.collect {
it.documents.forEach { ds ->
val req = ds.data<Request>()
runCatching {
println("Run: ${req.cmd}")
val res = spawn(req.cmd)
ds.reference.set<Request>(req.copy(isComplete = true, result = res))
}.onFailure {
ds.reference.set<Request>(req.copy(isComplete = true, exception = it.message))
}
}
}
}.onFailure { println(it.stackTraceToString()) }.getOrElse { }
suspend fun spawn(cmdLine: String) = suspendCoroutine { cont ->
var r = 0
val child_process = require("child_process")
val (cmd, args) = cmdLine.split(" ").let { it.first() to it.drop(1).toTypedArray() }
val ls = child_process.spawn(cmd, args)
val stdout = mutableListOf<String>()
val stderr = mutableListOf<String>()
ls.stdout.on("data") { data -> stdout.add("$data") }
ls.stderr.on("data", { data -> stderr.add("$data") })
ls.on("close") { c -> if (r++ == 0) cont.resume(SpawnResult(c, stdout.joinToString(), stderr.joinToString())) }
ls.on("error") { err -> if (r++ == 0) cont.resumeWithException(Exception("Error: spawn($cmdLine):${err}")) }
}
shared/src/jsMain/kotlin/DataType.kt
import dev.gitlive.firebase.firestore.Timestamp
import dev.gitlive.firebase.firestore.Timestamp.Companion.now
import kotlinx.serialization.Serializable
@Serializable
data class SpawnResult(
val exitCode: Int,
val stdout: String,
val stderr: String,
)
@Serializable
data class Request(
val cmd: String,
val time: Timestamp = now(),
val isComplete: Boolean = false,
val result: SpawnResult? = null,
val exception: String? = null,
)
Firestore側の設定
Firestore → ルール
service cloud.firestore {
match /databases/{db}/documents/fireshell/{document=**} {
allow read, write:
// if request.resource.data.password == resource.data.password;
if true;
}
}
このルールは指定コレクションについて無制限のアクセスを許す。テスト目的でのみ使用すること。
(APIKEYはクライアントアプリに組み込まれるため秘匿されない)
Firestore→インデクス
コレクションID: requests
フィールド:
1. time=Accending(昇順)
2. isComplete=Accending(昇順)
クエリのスコープ: コレクション
複合インデクスは複数条件での列挙に必須
ビルド/実行
sh gradlew kotlinNpmInstall
sh gradlew :KtNodeSvr:jsDevelopmentExecutableCompileSync
export APPKEY=<Firebase-App-Key>
node build/js/packages/KtMpApp-KtNodeSvr/kotlin/KtMpApp-KtNodeSvr.js default password