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
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class MainActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityMainBinding? = null
private val analyticsManager: IAnalyticsManager by inject()
private var feedbackButtonManager: FeedbackButtonManager? = null
private var webServer: WebServer? = null

private val onBackPressedCallback =
object : OnBackPressedCallback(true) {
Expand Down Expand Up @@ -328,15 +329,19 @@ class MainActivity : EdgeToEdgeIDEActivity() {
try {
val dbFile = Environment.DOC_DB
log.info("Starting WebServer - using database file from: {}", dbFile.absolutePath)
val webServer = WebServer(ServerConfig(databasePath = dbFile.absolutePath))
webServer.start()
val server = WebServer(ServerConfig(databasePath = dbFile.absolutePath))
webServer = server
server.start()
} catch (e: Exception) {
log.error("Failed to start WebServer", e)
} finally {
webServer = null
}
}
}

override fun onDestroy() {
webServer?.stop()
ITemplateProvider.getInstance().release()
super.onDestroy()
_binding = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.zip.ZipException
import java.util.zip.ZipInputStream
import kotlin.coroutines.cancellation.CancellationException
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.math.pow
Expand Down Expand Up @@ -77,6 +78,9 @@ object AssetsInstallationHelper {

if (result.isFailure) {
val e = result.exceptionOrNull()
if (e is CancellationException) {
throw e
}
val msg = e?.message ?: "Failed to install assets"
logger.error("Failed to install assets", e)
onProgress(Progress(msg))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipInputStream
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively

data object BundledAssetsInstaller : BaseAssetsInstaller() {
private val logger = LoggerFactory.getLogger(BundledAssetsInstaller::class.java)
Expand All @@ -38,6 +40,7 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() {
stagingDir: Path,
): Unit = Unit

@OptIn(ExperimentalPathApi::class)
@WorkerThread
override suspend fun doInstall(
context: Context,
Expand All @@ -52,10 +55,14 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() {
ANDROID_SDK_ZIP,
LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME,
-> {
val destDir = destinationDirForArchiveEntry(entryName).toPath()
if (Files.exists(destDir)) {
destDir.deleteRecursively()
}
Files.createDirectories(destDir)
val assetPath = ToolsManager.getCommonAsset("$entryName.br")
assets.open(assetPath).use { assetStream ->
BrotliInputStream(assetStream).use { srcStream ->
val destDir = destinationDirForArchiveEntry(entryName).toPath()
AssetsInstallationHelper.extractZipToDir(srcStream, destDir)
}
}
Expand Down Expand Up @@ -164,8 +171,11 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() {
logger.debug("Extracting plugin artifacts from '{}'", entryName)
val pluginDir = Environment.PLUGIN_API_JAR.parentFile
?: throw IllegalStateException("Plugin API parent directory is null")
pluginDir.mkdirs()
val pluginDirPath = pluginDir.toPath().toAbsolutePath().normalize()
if (Files.exists(pluginDirPath)) {
pluginDirPath.deleteRecursively()
}
Files.createDirectories(pluginDirPath)

val assetPath = ToolsManager.getCommonAsset("$entryName.br")
assets.open(assetPath).use { assetStream ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import java.io.FileNotFoundException
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import java.util.zip.ZipInputStream
import kotlin.system.measureTimeMillis

Expand All @@ -40,6 +42,7 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() {
zipFile = ZipFile(Environment.SPLIT_ASSETS_ZIP)
}

@OptIn(ExperimentalPathApi::class)
@WorkerThread
override suspend fun doInstall(
context: Context,
Expand All @@ -60,6 +63,10 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() {
GRADLE_API_NAME_JAR_ZIP,
-> {
val destDir = destinationDirForArchiveEntry(entry.name).toPath()
if (Files.exists(destDir)) {
destDir.deleteRecursively()
}
Files.createDirectories(destDir)
logger.debug("Extracting '{}' to dir: {}", entry.name, destDir)
AssetsInstallationHelper.extractZipToDir(zipInput, destDir)
logger.debug("Completed extracting '{}' to dir: {}", entry.name, destDir)
Expand Down Expand Up @@ -128,8 +135,11 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() {
logger.debug("Extracting plugin artifacts from '{}'", entry.name)
val pluginDir = Environment.PLUGIN_API_JAR.parentFile
?: throw IllegalStateException("Plugin API parent directory is null")
pluginDir.mkdirs()
val pluginDirPath = pluginDir.toPath().toAbsolutePath().normalize()
if (Files.exists(pluginDirPath)) {
pluginDirPath.deleteRecursively()
}
Files.createDirectories(pluginDirPath)

ZipInputStream(zipInput).use { pluginZip ->
var pluginEntry = pluginZip.nextEntry
Expand Down
53 changes: 42 additions & 11 deletions app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.nio.file.Files
Expand Down Expand Up @@ -74,8 +75,20 @@ FROM LastChange
}
}

/**
* Stops the server by closing the listening socket. Safe to call from any thread.
* Causes [start]'s accept loop to exit. No-op if not started or already stopped.
*/
fun stop() {
if (!::serverSocket.isInitialized) return
try {
serverSocket.close()
} catch (e: Exception) {
log.debug("Error closing server socket: {}", e.message)
}
}

fun start() {
lateinit var clientSocket: Socket
try {
log.debug("Starting WebServer on {}, port {}, debugEnabled={}, debugEnablePath='{}', debugDatabasePath='{}'.",
config.bindName, config.port, debugEnabled, config.debugEnablePath, config.debugDatabasePath)
Expand All @@ -92,24 +105,42 @@ FROM LastChange
// NEW FEATURE: Log database metadata when debug is enabled
if (debugEnabled) logDatabaseLastChanged()

serverSocket = ServerSocket(config.port, 0, java.net.InetAddress.getByName(config.bindName))
serverSocket = ServerSocket().apply { setReuseAddress(true) }
serverSocket.bind(InetSocketAddress(config.bindName, config.port))
log.info("WebServer started successfully.")

while (true) {
var clientSocket: Socket? = null
try {
clientSocket = serverSocket.accept()
if (debugEnabled) log.debug("Returned from socket accept().")
handleClient(clientSocket)
} catch (e: Exception) {
log.error("Error handling client: {}", e.message)
try {
val writer = PrintWriter(clientSocket.getOutputStream(), true)
sendError(writer, 500, "Internal Server Error 1")
clientSocket = serverSocket.accept()
if (debugEnabled) log.debug("Returned from socket accept().")
} catch (e: java.net.SocketException) {
if (e.message?.contains("Closed", ignoreCase = true) == true) {
if (debugEnabled) log.debug("WebServer socket closed, shutting down.")
break
}
log.error("Accept failed: {}", e.message)
continue
}
try {
clientSocket?.let { handleClient(it) }
} catch (e: Exception) {
log.error("Error sending error response: {}", e.message)
if (e is java.net.SocketException && e.message?.contains("Closed", ignoreCase = true) == true) {
if (debugEnabled) log.debug("Client disconnected: {}", e.message)
} else {
log.error("Error handling client: {}", e.message)
clientSocket?.let { socket ->
try {
sendError(PrintWriter(socket.getOutputStream(), true), 500, "Internal Server Error 1")
} catch (e2: Exception) {
log.error("Error sending error response: {}", e2.message)
}
}
}
}
} finally {
clientSocket.close() // TODO: What if the client socket isn't open? How to check? --DS, 22-Jul-2025
clientSocket?.close()
}
}
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.itsaky.androidide.viewmodel.InstallationState.InstallationGranted
import com.itsaky.androidide.viewmodel.InstallationState.InstallationPending
import com.itsaky.androidide.viewmodel.InstallationState.Installing
import io.sentry.Sentry
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -103,6 +104,10 @@ class InstallationViewModel : ViewModel() {
}
}
} catch (e: Exception) {
if (e is CancellationException) {
_state.update { InstallationPending }
throw e
}
Sentry.captureException(e)
log.error("IDE setup installation failed", e)
val errorMsg = e.message ?: context.getString(R.string.unknown_error)
Expand Down