Skip to content

Commit 0a9b9b7

Browse files
feat(database-ui): call sub tasks within db studio command
1 parent e27ced1 commit 0a9b9b7

File tree

1 file changed

+76
-16
lines changed

1 file changed

+76
-16
lines changed

packages/cli/src/main/kotlin/elide/tool/cli/cmd/db/DbStudioCommand.kt

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.micronaut.core.annotation.ReflectiveAccess
1818
import picocli.CommandLine.Command
1919
import picocli.CommandLine.Option
2020
import picocli.CommandLine.Parameters
21+
import java.net.Socket
2122
import java.nio.file.Files
2223
import java.nio.file.Path
2324
import java.nio.file.StandardCopyOption
@@ -30,12 +31,18 @@ import kotlin.io.path.listDirectoryEntries
3031
import kotlin.io.path.name
3132
import kotlin.io.path.readText
3233
import kotlin.io.path.writeText
34+
import kotlinx.coroutines.async
35+
import kotlinx.coroutines.awaitAll
36+
import kotlinx.coroutines.coroutineScope
37+
import kotlinx.coroutines.delay
3338
import kotlinx.serialization.Serializable
3439
import kotlinx.serialization.json.Json
3540
import elide.tool.cli.AbstractSubcommand
3641
import elide.tool.cli.CommandContext
3742
import elide.tool.cli.CommandResult
3843
import elide.tool.cli.ToolState
44+
import elide.tool.exec.SubprocessRunner.runTask
45+
import elide.tool.exec.SubprocessRunner.stringToTask
3946

4047
@Command(
4148
name = "db",
@@ -74,8 +81,26 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
7481
private const val STUDIO_OUTPUT_DIR = ".dev/db-studio"
7582
private const val STUDIO_INDEX_FILE = "index.ts"
7683
private val SQLITE_EXTENSIONS = setOf(".db", ".sqlite", ".sqlite3", ".db3")
84+
private const val MAX_WAIT_SECONDS = 60
85+
private const val PORT_CHECK_INTERVAL_MS = 1000L
7786
}
7887

88+
private suspend fun waitForServerReady(port: Int, name: String, maxWait: Int = MAX_WAIT_SECONDS): Boolean {
89+
var waited = 0
90+
while (waited < maxWait) {
91+
if (isPortListening(port)) {
92+
return true
93+
}
94+
delay(PORT_CHECK_INTERVAL_MS)
95+
waited++
96+
}
97+
return false
98+
}
99+
100+
private fun isPortListening(port: Int): Boolean = runCatching {
101+
Socket("localhost", port).use { true }
102+
}.getOrDefault(false)
103+
79104
// Extension function for SQLite detection
80105
private fun Path.isSqliteDatabase(): Boolean =
81106
isRegularFile() && SQLITE_EXTENSIONS.any { name.endsWith(it, ignoreCase = true) }
@@ -286,24 +311,59 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
286311
configFile.writeText(configContent)
287312

288313
output {
289-
appendLine("Database Studio files generated in: ${outputDir.toAbsolutePath()}")
290-
appendLine()
291-
appendLine("To start the Database Studio:")
292-
appendLine()
293-
appendLine(" Terminal 1 (API Server on port $apiPort):")
294-
appendLine(" cd ${apiDir.toAbsolutePath()}")
295-
appendLine(" elide serve")
296-
appendLine()
297-
appendLine(" Terminal 2 (UI Server on port $port):")
298-
appendLine(" cd ${uiDir.toAbsolutePath()}")
299-
appendLine(" elide serve")
300-
appendLine()
301-
appendLine("Then open: http://localhost:$port")
302-
appendLine()
303-
appendLine("Note: Port configuration is read from elide.pkl in each directory")
314+
appendLine("Starting Database Studio...")
304315
appendLine()
305316
}
306317

307-
return CommandResult.success()
318+
// Start both servers concurrently and wait for them
319+
return try {
320+
coroutineScope {
321+
// Start API server task
322+
val apiTask = async {
323+
runTask(stringToTask(
324+
"elide run $STUDIO_INDEX_FILE",
325+
shell = elide.tooling.runner.ProcessRunner.ProcessShell.None,
326+
workingDirectory = apiDir
327+
))
328+
}
329+
330+
// Start UI server task
331+
val uiTask = async {
332+
runTask(stringToTask(
333+
"elide serve $STUDIO_OUTPUT_DIR/ui",
334+
shell = elide.tooling.runner.ProcessRunner.ProcessShell.None
335+
))
336+
}
337+
338+
// Wait for API server to be ready
339+
if (!waitForServerReady(apiPort, "API Server")) {
340+
return@coroutineScope CommandResult.err(message = "API Server didn't start within $MAX_WAIT_SECONDS seconds")
341+
}
342+
343+
// Wait for UI server to be ready
344+
if (!waitForServerReady(port, "UI Server")) {
345+
return@coroutineScope CommandResult.err(message = "UI Server didn't start within $MAX_WAIT_SECONDS seconds")
346+
}
347+
348+
output {
349+
appendLine()
350+
appendLine("✓ Database Studio is running!")
351+
appendLine()
352+
appendLine(" UI: http://localhost:$port")
353+
appendLine(" API: http://localhost:$apiPort")
354+
appendLine()
355+
appendLine("Press Ctrl+C to stop all servers")
356+
appendLine()
357+
}
358+
359+
// Wait for both servers to complete (they run until interrupted)
360+
awaitAll(apiTask.await().asDeferred(), uiTask.await().asDeferred())
361+
362+
CommandResult.success()
363+
}
364+
} catch (e: Exception) {
365+
// Servers were interrupted or failed, clean up gracefully
366+
CommandResult.success()
367+
}
308368
}
309369
}

0 commit comments

Comments
 (0)