diff --git a/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt b/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt index 5a6947ed0e..9e0642867a 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt @@ -19,6 +19,8 @@ import com.google.android.material.textview.MaterialTextView import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.activities.editor.HelpActivity import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.api.ILanguageServerRegistry +import com.itsaky.androidide.lsp.java.JavaLanguageServer import com.itsaky.androidide.lsp.java.debug.JdwpOptions import com.itsaky.androidide.projects.IProjectManager import com.itsaky.androidide.projects.isPluginProject @@ -44,10 +46,10 @@ class DebugAction( context: Context, override val order: Int, ) : AbstractRunAction( - context = context, - labelRes = R.string.action_start_debugger, - iconRes = R.drawable.ic_db_startdebugger, - ) { + context = context, + labelRes = R.string.action_start_debugger, + iconRes = R.drawable.ic_db_startdebugger, +) { override val id = ID override fun retrieveTooltipTag(isReadOnlyContext: Boolean) = TooltipTag.EDITOR_TOOLBAR_DEBUG @@ -80,6 +82,16 @@ class DebugAction( return false } + val javaLsp = ILanguageServerRegistry.default + .getServer(JavaLanguageServer.SERVER_ID) + if (javaLsp?.debugAdapter?.isReady != true + ) { + withContext(Dispatchers.Main.immediate) { + showDebuggerNotReadyMessage(activity) + } + return false + } + if (!canShowPairingNotification(activity)) { withContext(Dispatchers.Main.immediate) { showNotificationPermissionDialog(activity) @@ -190,7 +202,7 @@ class DebugAction( val nm = context.getSystemService(NotificationManager::class.java) val channel = nm.getNotificationChannel(AdbPairingService.NOTIFICATION_CHANNEL) return nm.areNotificationsEnabled() && - (channel == null || channel.importance != NotificationManager.IMPORTANCE_NONE) + (channel == null || channel.importance != NotificationManager.IMPORTANCE_NONE) } private fun showNotificationPermissionDialog(context: Context): AlertDialog? = @@ -215,4 +227,14 @@ class DebugAction( }.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }.show() + + private fun showDebuggerNotReadyMessage(context: Context) = + DialogUtils + .newMaterialDialogBuilder(context) + .setMessage( + context.getString(R.string.debugger_not_ready) + System.lineSeparator() + .repeat(2) + context.getString(R.string.debugger_error_suggestion_network_restriction) + ) + .setPositiveButton(android.R.string.ok, null) + .show() } diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt index 017ca90126..5853a547e9 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt @@ -55,6 +55,7 @@ import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.idetooltips.TooltipTag import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.IDELanguageClientImpl +import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult import com.itsaky.androidide.lsp.java.utils.CancelChecker import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.builder.BuildService @@ -95,13 +96,16 @@ import com.itsaky.androidide.viewmodel.BuildViewModel import io.github.rosemoe.sora.text.ICUUtils import io.github.rosemoe.sora.util.IntPair import io.sentry.Sentry +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.adfa.constants.CONTENT_KEY import org.koin.android.ext.android.inject +import org.slf4j.LoggerFactory import java.io.File import java.io.FileNotFoundException +import java.net.SocketException import java.nio.file.NoSuchFileException import java.util.concurrent.CompletableFuture import java.util.regex.Pattern @@ -170,6 +174,8 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() { private val buildServiceConnection = GradleBuildServiceConnnection() companion object { + private val logger = LoggerFactory.getLogger(ProjectHandlerActivity::class.java) + const val STATE_KEY_FROM_SAVED_INSTANACE = "ide.editor.isFromSavedInstance" const val STATE_KEY_SHOULD_INITIALIZE = "ide.editor.isInitializing" } @@ -449,7 +455,9 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() { log.error("Gradle build service doesn't exist or the IDE is not allowed to access it.") } - initLspClient() + lifecycleScope.launch { + initLspClient() + } } fun initializeProject(forceSync: Boolean = false) { @@ -956,12 +964,67 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() { startActivity(intent) } - private fun initLspClient() { + private suspend fun initLspClient() { if (!IDELanguageClientImpl.isInitialized()) { IDELanguageClientImpl.initialize(this as EditorHandlerActivity) } + connectClient(IDELanguageClientImpl.getInstance()) - connectDebugClient(debuggerViewModel.debugClient) + + val results = try { + connectDebugClient(debuggerViewModel.debugClient).values + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + + Sentry.captureException(e) + logger.error("Unable to connect LSP servers with debug client", e) + listOf(DebugClientConnectionResult.Failure(cause = e)) + } + + if (results.any { it is DebugClientConnectionResult.Failure }) { + // one or more debug adapters failed to initialize + val message = buildString { + results.filterIsInstance().forEach { result -> + val msg = result.contextRes?.let(::getString) + ?: result.context + ?: (result.cause as? SocketException?).let { err -> + val msg = err?.message ?: "" + when { + msg.contains("EPERM") -> getString(string.debugger_error_errno_eperm) + msg.contains("ECONNREFUSED") -> getString(string.debugger_error_errno_econnrefused) + else -> null + } + } + ?: (result.cause as? ErrnoException? ?: result.cause?.cause as? ErrnoException?)?.let { err -> + when (err.errno) { + OsConstants.EPERM -> getString(string.debugger_error_errno_eperm) + OsConstants.ECONNREFUSED -> getString(string.debugger_error_errno_econnrefused) + else -> getString(R.string.debugger_error_errno, err.errno) + } + } + ?: getString(R.string.debugger_error_debugger_startup_failure) + + append(msg) + append(System.lineSeparator()) + } + + if (isNotBlank()) { + append(System.lineSeparator()) + } + + append(getString(R.string.debugger_error_suggestion_network_restriction)) + } + + withContext(Dispatchers.Main) { + newMaterialDialogBuilder(this@ProjectHandlerActivity) + .setTitle(R.string.debugger_error_network_access_error) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } } open fun getProgressSheet(msg: Int): ProgressSheet? { diff --git a/app/src/main/java/com/itsaky/androidide/handlers/LspHandler.kt b/app/src/main/java/com/itsaky/androidide/handlers/LspHandler.kt index 327502c61d..3a988a0642 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/LspHandler.kt +++ b/app/src/main/java/com/itsaky/androidide/handlers/LspHandler.kt @@ -31,28 +31,28 @@ import com.itsaky.androidide.utils.FeatureFlags */ object LspHandler { - fun registerLanguageServers() { - ILanguageServerRegistry.getDefault().apply { - getServer(JavaLanguageServer.SERVER_ID) ?: register(JavaLanguageServer()) - if (FeatureFlags.isExperimentsEnabled) { - getServer(KotlinLanguageServer.SERVER_ID) ?: register(KotlinLanguageServer()) - } - getServer(XMLLanguageServer.SERVER_ID) ?: register(XMLLanguageServer()) - } - } - - fun connectClient(client: ILanguageClient) { - ILanguageServerRegistry.getDefault().connectClient(client) - } + fun registerLanguageServers() { + ILanguageServerRegistry.default.apply { + getServer(JavaLanguageServer.SERVER_ID) ?: register(JavaLanguageServer()) + if (FeatureFlags.isExperimentsEnabled) { + getServer(KotlinLanguageServer.SERVER_ID) ?: register(KotlinLanguageServer()) + } + getServer(XMLLanguageServer.SERVER_ID) ?: register(XMLLanguageServer()) + } + } - fun connectDebugClient(client: IDebugClient) { - ILanguageServerRegistry.getDefault().connectDebugClient(client) - } + fun connectClient(client: ILanguageClient) { + ILanguageServerRegistry.default.connectClient(client) + } - fun destroyLanguageServers(isConfigurationChange: Boolean) { - if (isConfigurationChange) { - return - } - ILanguageServerRegistry.getDefault().destroy() - } + @Throws(Throwable::class) + suspend fun connectDebugClient(client: IDebugClient) = + ILanguageServerRegistry.default.connectDebugClient(client) + + fun destroyLanguageServers(isConfigurationChange: Boolean) { + if (isConfigurationChange) { + return + } + ILanguageServerRegistry.default.destroy() + } } diff --git a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt index fb661b77ac..f33540f1ad 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt @@ -549,7 +549,7 @@ class CodeEditorView( else -> return null } - return ILanguageServerRegistry.getDefault().getServer(serverID) + return ILanguageServerRegistry.default.getServer(serverID) } private fun configureEditorIfNeeded() { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/JavaLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/JavaLanguage.kt index 834c449575..3c3e29dc76 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/JavaLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/JavaLanguage.kt @@ -47,7 +47,7 @@ class JavaLanguage(context: Context) : } override val languageServer: ILanguageServer? - get() = ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID) + get() = ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) override fun checkIsCompletionChar(c: Char): Boolean { return MyCharacter.isJavaIdentifierPart(c) || c == '.' diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt index 3616ed0607..69adc8575c 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt @@ -20,7 +20,6 @@ package com.itsaky.androidide.editor.language.treesitter import android.content.Context import com.itsaky.androidide.editor.language.newline.TSBracketsHandler import com.itsaky.androidide.editor.language.newline.TSCStyleBracketsHandler -import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage.Factory import com.itsaky.androidide.editor.language.utils.CommonSymbolPairs import com.itsaky.androidide.lsp.api.ILanguageServer import com.itsaky.androidide.lsp.api.ILanguageServerRegistry @@ -46,7 +45,7 @@ open class KotlinLanguage(context: Context) : } override val languageServer: ILanguageServer? - get() = ILanguageServerRegistry.getDefault().getServer(KotlinLanguageServer.SERVER_ID) + get() = ILanguageServerRegistry.default.getServer(KotlinLanguageServer.SERVER_ID) override fun checkIsCompletionChar(c: Char): Boolean { return MyCharacter.isJavaIdentifierPart(c) || c == '.' diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/XMLLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/XMLLanguage.kt index 3bf099d57b..14faa6febf 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/XMLLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/XMLLanguage.kt @@ -35,7 +35,7 @@ class XMLLanguage(context: Context) : TreeSitterLanguage(context, lang = TSLanguageXml.getInstance(), langType = TS_TYPE) { override val languageServer: ILanguageServer? - get() = ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID) + get() = ILanguageServerRegistry.default.getServer(XMLLanguageServer.SERVER_ID) companion object { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt index 9d12324f45..116ffb0009 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt @@ -309,12 +309,12 @@ open class EditorActionsMenu(val editor: IDEEditor) : data.put(com.itsaky.androidide.models.Range::class.java, editor.cursorLSPRange) data.put( JavaLanguageServer::class.java, - ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID) + ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) as? JavaLanguageServer? ) data.put( XMLLanguageServer::class.java, - ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID) + ILanguageServerRegistry.default.getServer(XMLLanguageServer.SERVER_ID) as? XMLLanguageServer? ) data.put(TextTarget::class.java, IdeEditorAdapter(this.editor)) diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java deleted file mode 100644 index 57fdd1223d..0000000000 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE 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. - * - * AndroidIDE 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 AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.lsp.api; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent; -import com.itsaky.androidide.lsp.debug.IDebugClient; -import com.itsaky.androidide.projects.api.Workspace; -import com.itsaky.androidide.utils.ILogger; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -/** - * Thread-safe implementation of {@link ILanguageServerRegistry}. - * - * @author Akash Yadav - */ -public class DefaultLanguageServerRegistry extends ILanguageServerRegistry { - - private final Map mRegister = new HashMap<>(); - private final ReadWriteLock lock = new ReentrantReadWriteLock(); - - @Override - public void connectClient(@NonNull final ILanguageClient client) { - Objects.requireNonNull(client); - lock.readLock().lock(); - try { - for (final var server : mRegister.values()) { - server.connectClient(client); - } - } finally { - lock.readLock().unlock(); - } - } - - @Override - public void connectDebugClient(@NonNull final IDebugClient client) { - Objects.requireNonNull(client); - lock.readLock().lock(); - try { - for (final var server : mRegister.values()) { - server.connectDebugClient(client); - } - } finally { - lock.readLock().unlock(); - } - } - - @Override - public void destroy() { - EventBus.getDefault().unregister(this); - lock.readLock().lock(); - try { - for (var server : mRegister.values()) { - server.shutdown(); - } - } finally { - lock.readLock().unlock(); - } - - lock.writeLock().lock(); - try { - mRegister.clear(); - } finally { - lock.writeLock().unlock(); - } - } - - @Nullable - @Override - public ILanguageServer getServer(@NonNull final String serverId) { - lock.readLock().lock(); - try { - return mRegister.get(serverId); - } finally { - lock.readLock().unlock(); - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - @SuppressWarnings("unused") - public void onProjectInitialized(ProjectInitializedEvent event) { - final var project = event.get(Workspace.class); - if (project == null) { - return; - } - - ILogger.ROOT.debug("Dispatching ProjectInitializedEvent to language servers..."); - for (final var server : mRegister.values()) { - server.setupWithProject(project); - } - } - - @Override - public void register(@NonNull final ILanguageServer server) { - if (!EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().register(this); - } - lock.writeLock().lock(); - try { - final var old = mRegister.put(server.getServerId(), server); - if (old != null) { - mRegister.put(old.getServerId(), old); - } - } finally { - lock.writeLock().unlock(); - } - } - - @Override - public void unregister(@NonNull final String serverId) { - lock.writeLock().lock(); - try { - final var registered = mRegister.remove(serverId); - if (registered == null) { - throw new IllegalStateException("No server found for the given server ID"); - } - } finally { - lock.writeLock().unlock(); - } - } -} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.kt new file mode 100644 index 0000000000..33b729a3fc --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/DefaultLanguageServerRegistry.kt @@ -0,0 +1,155 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE 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. + * + * AndroidIDE 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 AndroidIDE. If not, see . + */ +package com.itsaky.androidide.lsp.api + +import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent +import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult +import com.itsaky.androidide.lsp.debug.IDebugClient +import com.itsaky.androidide.projects.api.Workspace +import kotlinx.coroutines.CancellationException +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.Objects +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.withLock + +/** + * Thread-safe implementation of [ILanguageServerRegistry]. + * + * @author Akash Yadav + */ +class DefaultLanguageServerRegistry : ILanguageServerRegistry() { + private val mRegister = HashMap() + private val lock: ReadWriteLock = ReentrantReadWriteLock() + + override fun connectClient(client: ILanguageClient) { + Objects.requireNonNull(client) + lock.readLock().lock() + try { + for (server in mRegister.values) { + server.connectClient(client) + } + } finally { + lock.readLock().unlock() + } + } + + @Throws(Throwable::class) + override suspend fun connectDebugClient(client: IDebugClient): Map { + Objects.requireNonNull(client) + val servers = lock.readLock().withLock { + mRegister.values.toList() + } + + return buildMap { + for (server in servers) { + try { + this[server.serverId] = server.connectDebugClient(client) + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + + sLogger.error( + "Unable to connect LSP server '{}' to debug client", + server.serverId, + e + ) + + this[server.serverId] = DebugClientConnectionResult.Failure(cause = e) + } + } + } + } + + override fun destroy() { + EventBus.getDefault().unregister(this) + val servers = lock.readLock().withLock { mRegister.values.toList() } + for (server in servers) { + try { + server.shutdown() + } catch (e: Exception) { + sLogger.error("Unable to shut down LSP server {}", server.serverId, e) + } + } + + lock.writeLock().withLock { + mRegister.clear() + } + } + + override fun getServer(serverId: String): ILanguageServer? { + lock.readLock().lock() + try { + return mRegister[serverId] + } finally { + lock.readLock().unlock() + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + @Suppress("unused") + fun onProjectInitialized(event: ProjectInitializedEvent) { + val project = event.get(Workspace::class.java) ?: return + + sLogger.debug("Dispatching ProjectInitializedEvent to language servers...") + val servers = lock.readLock().withLock { mRegister.values.toList() } + for (server in servers) { + server.setupWithProject(project) + } + } + + override fun register(server: ILanguageServer) { + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + + lock.writeLock().lock() + try { + val old = mRegister.putIfAbsent(server.serverId, server) + if (old != null) { + sLogger.warn("Attempt to re-register LSP server with ID '{}'", server.serverId) + } + } finally { + lock.writeLock().unlock() + } + } + + override fun unregister(serverId: String) { + val registered = lock.writeLock().withLock { + mRegister.remove(serverId) + } + + checkNotNull(registered) { "No server found for the given server ID" } + + try { + registered.shutdown() + } catch (e: Exception) { + sLogger.error("Unable to shut down server {}", registered.serverId, e) + throw e + } + } + + companion object { + private val sLogger: Logger = + LoggerFactory.getLogger(DefaultLanguageServerRegistry::class.java) + } +} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt index 071100bf22..fdcbeff632 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServer.kt @@ -16,6 +16,7 @@ */ package com.itsaky.androidide.lsp.api +import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult import com.itsaky.androidide.lsp.debug.IDebugAdapter import com.itsaky.androidide.lsp.debug.IDebugClient import com.itsaky.androidide.lsp.models.CodeFormatResult @@ -41,7 +42,8 @@ import java.nio.file.Path * @author Akash Yadav */ interface ILanguageServer { - val serverId: String? + + val serverId: String /** * Get the instance of the language client connected to this server. @@ -78,10 +80,11 @@ interface ILanguageServer { * * @param client The debugger client. */ - fun connectDebugClient(client: IDebugClient) = Unit + suspend fun connectDebugClient(client: IDebugClient): DebugClientConnectionResult = + debugAdapter?.connectDebugClient(client) ?: DebugClientConnectionResult.Success /** - * Apply settings to the language server. Its up to the language server how it applies these + * Apply settings to the language server. It's up to the language server how it applies these * settings to the language service providers. * * @param settings The new settings to use. Pass `null` to use default settings. diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServerRegistry.java b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServerRegistry.java deleted file mode 100644 index fd9398dea1..0000000000 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServerRegistry.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE 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. - * - * AndroidIDE 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 AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.lsp.api; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.itsaky.androidide.lsp.debug.IDebugClient; - -/** - * A language server registry which keeps track of registered language servers. - * - * @author Akash Yadav - */ -public abstract class ILanguageServerRegistry { - - private static ILanguageServerRegistry sRegistry = null; - - public static ILanguageServerRegistry getDefault() { - if (sRegistry == null) { - sRegistry = new DefaultLanguageServerRegistry(); - } - - return sRegistry; - } - - /** - * Register the language server. - * - * @param server The server to register. - */ - public abstract void register(@NonNull ILanguageServer server); - - /** Connects client to all the registered {@link ILanguageServer}s. */ - public abstract void connectClient(@NonNull ILanguageClient client); - - /** - * Connects debug client to all the registered {@link ILanguageServer}s. - * @param client The debug client to register. - */ - public abstract void connectDebugClient(@NonNull IDebugClient client); - - /** - * Unregister the given server. If any server is registered with the given server ID, a shutdown - * request will be sent to that server. - * - * @param serverId The ID of the server to unregister. - */ - public abstract void unregister(@NonNull String serverId); - - /** Calls {@link #unregister(String)} for all the registered language servers. */ - public abstract void destroy(); - - /** - * Get the {@link ILanguageServer} registered with the given server ID. - * - * @param serverId The ID of the language server. - * @return The {@link ILanguageServer} instance. Or null if no server is registered - * with the provided ID. - */ - @Nullable - public abstract ILanguageServer getServer(@NonNull String serverId); -} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServerRegistry.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServerRegistry.kt new file mode 100644 index 0000000000..acd4c84323 --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/api/ILanguageServerRegistry.kt @@ -0,0 +1,80 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE 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. + * + * AndroidIDE 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 AndroidIDE. If not, see . + */ +package com.itsaky.androidide.lsp.api + +import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult +import com.itsaky.androidide.lsp.debug.IDebugClient + +/** + * A language server registry which keeps track of registered language servers. + * + * @author Akash Yadav + */ +abstract class ILanguageServerRegistry { + /** + * Register the language server. + * + * @param server The server to register. + */ + abstract fun register(server: ILanguageServer) + + /** + * Connects client to all the registered [ILanguageServer]s. + */ + abstract fun connectClient(client: ILanguageClient) + + /** + * Connects debug client to all the registered [ILanguageServer]s. + * + * @param client The debug client to register. + * @return A map of server IDs to their corresponding [DebugClientConnectionResult]. + */ + @Throws(Throwable::class) + abstract suspend fun connectDebugClient(client: IDebugClient): Map + + /** + * Unregister the given server. If any server is registered with the given server ID, a shutdown + * request will be sent to that server. + * + * @param serverId The ID of the server to unregister. + */ + abstract fun unregister(serverId: String) + + /** + * Calls [.unregister] for all the registered language servers. + */ + abstract fun destroy() + + /** + * Get the [ILanguageServer] registered with the given server ID. + * + * @param serverId The ID of the language server. + * @return The [ILanguageServer] instance. Or `null` if no server is registered + * with the provided ID. + */ + abstract fun getServer(serverId: String): ILanguageServer? + + companion object { + + /** + * The default implementation of [ILanguageServerRegistry]. + */ + val default: ILanguageServerRegistry by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + DefaultLanguageServerRegistry() + } + } +} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/DebugClientConnectionResult.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/DebugClientConnectionResult.kt new file mode 100644 index 0000000000..39bb5aec26 --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/DebugClientConnectionResult.kt @@ -0,0 +1,25 @@ +package com.itsaky.androidide.lsp.debug + +import androidx.annotation.StringRes + +/** + * Result of connecting a [IDebugAdapter] to a [IDebugClient]. + */ +sealed class DebugClientConnectionResult { + + /** + * The connection was successful. + */ + data object Success: DebugClientConnectionResult() + + /** + * The connection failed. + * + * @property context Additional context about the error. + */ + data class Failure( + val context: String? = null, + @field:StringRes val contextRes: Int? = null, + val cause: Throwable? = null, + ): DebugClientConnectionResult() +} \ No newline at end of file diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/IDebugAdapter.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/IDebugAdapter.kt index 73b0af6a5c..9870313fa4 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/IDebugAdapter.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/debug/IDebugAdapter.kt @@ -15,12 +15,18 @@ import com.itsaky.androidide.lsp.debug.model.ThreadListResponse * @author Akash Yadav */ interface IDebugAdapter { + + /** + * Whether the debug adapter is ready for a debug session. + */ + val isReady: Boolean + /** * Connect the debug adapter to the given client. * * @param client The client to connect to. */ - fun connectDebugClient(client: IDebugClient) + suspend fun connectDebugClient(client: IDebugClient): DebugClientConnectionResult /** * Get the remote clients connected to this debug adapter. diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt index c5f096f702..bf0e04417d 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt @@ -27,6 +27,7 @@ import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clea import com.itsaky.androidide.lsp.api.ILanguageClient import com.itsaky.androidide.lsp.api.ILanguageServer import com.itsaky.androidide.lsp.api.IServerSettings +import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult import com.itsaky.androidide.lsp.debug.IDebugAdapter import com.itsaky.androidide.lsp.debug.IDebugClient import com.itsaky.androidide.lsp.internal.model.CachedCompletion @@ -134,13 +135,14 @@ class JavaLanguageServer : ILanguageServer { this.client = client } - override fun connectDebugClient(client: IDebugClient) { + override suspend fun connectDebugClient(client: IDebugClient): DebugClientConnectionResult { if (JdwpOptions.JDWP_ENABLED) { log.info("Connecting to debug client: {}", client) - this.debugAdapter.connectDebugClient(client) - } else { - log.info("Not connecting to debug client. JDWP disabled.") + return this.debugAdapter.connectDebugClient(client) } + + log.info("Not connecting to debug client. JDWP disabled.") + return DebugClientConnectionResult.Success } override fun applySettings(settings: IServerSettings?) { diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt index 68e9481434..fd9c7647b8 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt @@ -99,7 +99,7 @@ abstract class BaseJavaCodeAction : EditorActionItem { } protected fun ActionData.requireLanguageServer(): JavaLanguageServer { - return ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID) + return ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) as JavaLanguageServer } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt index 896af9b34f..d8510d1f4e 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt @@ -4,6 +4,7 @@ import android.system.ErrnoException import android.system.OsConstants import androidx.annotation.WorkerThread import com.itsaky.androidide.lsp.api.ILanguageServerRegistry +import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult import com.itsaky.androidide.lsp.debug.IDebugAdapter import com.itsaky.androidide.lsp.debug.IDebugClient import com.itsaky.androidide.lsp.debug.RemoteClient @@ -41,6 +42,7 @@ import com.sun.jdi.event.VMDisconnectEvent import com.sun.jdi.request.EventRequest import com.sun.jdi.request.StepRequest import com.sun.tools.jdi.SocketListeningConnector +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -72,6 +74,9 @@ internal class JavaDebugAdapter : "Listener state is not initialized" } + override val isReady: Boolean + get() = _listenerState?.isListening == true && listenerThread?.run { isAlive && !isInterrupted } == true + companion object { private val logger = LoggerFactory.getLogger(JavaDebugAdapter::class.java) @@ -88,7 +93,7 @@ internal class JavaDebugAdapter : * Get the current instance of the [JavaDebugAdapter]. */ fun currentInstance(): JavaDebugAdapter? { - val lsp = ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID) + val lsp = ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) return ((lsp as? JavaLanguageServer?)?.debugAdapter as? JavaDebugAdapter?) } @@ -117,7 +122,7 @@ internal class JavaDebugAdapter : fun evalContext() = connVm().evalContext - override fun connectDebugClient(client: IDebugClient) { + override suspend fun connectDebugClient(client: IDebugClient): DebugClientConnectionResult { val listeningConnectors = vmm.listeningConnectors() listeningConnectors.forEach { conn -> logger.info("Listening connector: {}", conn.javaClass.canonicalName) @@ -127,7 +132,7 @@ internal class JavaDebugAdapter : vmm.listeningConnectors().filterIsInstance().firstOrNull() if (connector == null) { logger.error("No listening connectors found, or the connector is not a SocketListeningConnector") - return + return DebugClientConnectionResult.Failure() } val args = connector.defaultArguments() @@ -140,18 +145,38 @@ internal class JavaDebugAdapter : args.map { (_, value) -> "$value" }.joinToString(), ) - this._listenerState = + _listenerState?.invalidate() + _listenerState = ListenerState( client = client, connector = connector, args = args, ) - this.listenerThread = + val failure = withContext(Dispatchers.IO) { + try { + logger.debug("startListening") + listenerState.startListening() + null + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + logger.error("Failed to listen for incoming JDWP connections", e) + return@withContext DebugClientConnectionResult.Failure(cause = e) + } + } + + if (failure != null) { + return failure + } + + listenerThread = JDWPListenerThread( _listenerState!!, this::onConnectedToVm, ).also { thread -> thread.start() } + return DebugClientConnectionResult.Success } @WorkerThread @@ -559,7 +584,7 @@ internal class JavaDebugAdapter : override fun close() { logger.debug("close") try { - _listenerState?.stopListening() + _listenerState?.invalidate() listenerThread?.interrupt() } catch (err: Throwable) { logger.error("Unable to stop VM connection listener", err) @@ -604,8 +629,9 @@ internal class JDWPListenerThread( override fun run() { logger.debug("run::start") - if (!listenerState.isListening) { - logger.debug("startListening") + if (!listenerState.isListening && !listenerState.isInvalidated) { + logger.warn("Listener should've been listening at this point, but it's not. " + + "Trying to start listening...") listenerState.startListening() } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt index 5f3afccdc2..0793247bcc 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/ListenerState.kt @@ -5,15 +5,24 @@ import com.sun.jdi.VirtualMachine import com.sun.jdi.connect.Connector import com.sun.tools.jdi.SocketListeningConnector import com.sun.tools.jdi.isListening +import java.util.concurrent.atomic.AtomicBoolean internal data class ListenerState( val client: IDebugClient, val connector: SocketListeningConnector, val args: Map, ) { + private val invalidated = AtomicBoolean(false) + val isListening: Boolean get() = connector.isListening(args) + /** + * Whether this listener state has been invalidated. + */ + val isInvalidated: Boolean + get() = invalidated.get() + /** * Start listening for connections from VMs. * @@ -32,4 +41,15 @@ internal data class ListenerState( * @return The connected VM. */ fun accept(): VirtualMachine = connector.accept(args) + + /** + * Invalidate this listener state. + */ + fun invalidate() { + if (isListening) { + stopListening() + } + + invalidated.set(true) + } } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt index 8a3fe59fbf..b499ba5126 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt @@ -73,7 +73,7 @@ object AdvancedEditProvider { } val client = - ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID)?.client ?: return + ILanguageServerRegistry.default.getServer(XMLLanguageServer.SERVER_ID)?.client ?: return val start = event.changeRange.start.requireIndex() val (endLine, endCol, end) = event.changeRange.end diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index ca8b98ecd8..712bb0537a 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -767,6 +767,19 @@ Set Invalid value for this variable The value of variable \'%1$s\' cannot be updated. + Network access error + Unable to listen for JDWP connections. Operation failed with error %1$d. + Code On the Go is not allowed to open or bind a network socket on the local network. + Network access for Code On the Go is blocked by the system. The app is not allowed to use the network or the service is unavailable. + Unable to start Code On the Go debugger. + The debugger is not ready for a debug session. + Please ensure Code On the Go has permissions to access the local network. This can be checked by + going to the "App info" screen of Code On the Go in your device\'s settings, and checking if + network access is allowed under \"Data usage\" (exact name might differ between devices). + + If network access is allowed and the issue still persists, please file a bug report with the + logs attached. + No activity to handle action %1$s