mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(tooling-api): handle failures related to project directory
This commit is contained in:
+23
-5
@@ -50,6 +50,10 @@ import com.itsaky.androidide.tasks.executeWithProgress
|
||||
import com.itsaky.androidide.tooling.api.messages.AndroidInitializationParams
|
||||
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
|
||||
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_DIRECTORY_INACCESSIBLE
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_FOUND
|
||||
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
|
||||
import com.itsaky.androidide.tooling.api.models.mapToSelectedVariants
|
||||
import com.itsaky.androidide.utils.DURATION_INDEFINITE
|
||||
@@ -394,7 +398,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
|
||||
if (result == null || !result.isSuccessful || error != null) {
|
||||
log.error("An error occurred initializing the project with Tooling API", error)
|
||||
ThreadUtils.runOnUiThread {
|
||||
postProjectInit(false)
|
||||
postProjectInit(false, result?.failure)
|
||||
}
|
||||
return@whenCompleteAsync
|
||||
}
|
||||
@@ -464,7 +468,9 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
|
||||
manager.notifyProjectUpdate()
|
||||
updateBuildVariants(manager.androidBuildVariants)
|
||||
|
||||
com.itsaky.androidide.tasks.runOnUiThread { postProjectInit(true) }
|
||||
com.itsaky.androidide.tasks.runOnUiThread {
|
||||
postProjectInit(true, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,11 +479,23 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
|
||||
editorViewModel.isInitializing = true
|
||||
}
|
||||
|
||||
protected open fun postProjectInit(isSuccessful: Boolean) {
|
||||
protected open fun postProjectInit(isSuccessful: Boolean, failure: TaskExecutionResult.Failure?) {
|
||||
val manager = ProjectManagerImpl.getInstance()
|
||||
if (!isSuccessful) {
|
||||
setStatus(getString(string.msg_project_initialization_failed))
|
||||
flashError(string.msg_project_initialization_failed)
|
||||
val initFailed = getString(string.msg_project_initialization_failed)
|
||||
setStatus(initFailed)
|
||||
|
||||
val msg = when(failure) {
|
||||
PROJECT_DIRECTORY_INACCESSIBLE -> string.msg_project_dir_inaccessible
|
||||
PROJECT_NOT_DIRECTORY -> string.msg_file_is_not_dir
|
||||
PROJECT_NOT_FOUND -> string.msg_project_dir_doesnt_exist
|
||||
else -> null
|
||||
}?.let {
|
||||
"$initFailed: ${getString(it)}"
|
||||
}
|
||||
|
||||
flashError(msg ?: initFailed)
|
||||
|
||||
editorViewModel.isInitializing = false
|
||||
manager.projectInitialized = false
|
||||
return
|
||||
|
||||
@@ -622,4 +622,7 @@
|
||||
<string name="idepref_general_localeSelector_title">Language</string>
|
||||
<string name="idepref_general_localeSelector_summary">Select a language for AndroidIDE.</string>
|
||||
<string name="locale_system_default">System Default</string>
|
||||
<string name="msg_project_dir_inaccessible">Project directory is not accessible</string>
|
||||
<string name="msg_file_is_not_dir">Selected file is not a directory</string>
|
||||
<string name="msg_project_dir_doesnt_exist">Project directory does not exist</string>
|
||||
</resources>
|
||||
|
||||
+4
@@ -219,6 +219,10 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
|
||||
return
|
||||
}
|
||||
|
||||
if (builder.isBuildInProgress) {
|
||||
return
|
||||
}
|
||||
|
||||
val tasks = getAndroidModules().flatMap { module ->
|
||||
val variant = module.getSelectedVariant()
|
||||
if (variant == null) {
|
||||
|
||||
+69
-13
@@ -35,6 +35,10 @@ import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Fai
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.BUILD_FAILED
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.CONNECTION_CLOSED
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.CONNECTION_ERROR
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_DIRECTORY_INACCESSIBLE
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_FOUND
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_INITIALIZED
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.UNKNOWN
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.UNSUPPORTED_BUILD_ARGUMENT
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.UNSUPPORTED_CONFIGURATION
|
||||
@@ -57,7 +61,6 @@ import org.gradle.tooling.internal.consumer.DefaultGradleConnector
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CompletionException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
@@ -68,7 +71,6 @@ import kotlin.system.exitProcess
|
||||
internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
IToolingApiServer {
|
||||
|
||||
private val _isInitialized = AtomicBoolean(false)
|
||||
private var client: IToolingApiClient? = null
|
||||
private var connector: GradleConnector? = null
|
||||
private var connection: ProjectConnection? = null
|
||||
@@ -79,9 +81,13 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
/**
|
||||
* Whether the project has been initialized or not.
|
||||
*/
|
||||
var isInitialized: Boolean
|
||||
get() = _isInitialized.get()
|
||||
private set(value) = _isInitialized.set(value)
|
||||
var isInitialized: Boolean = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Whether a build or project synchronization is in progress.
|
||||
*/
|
||||
private var isBuildInProgress: Boolean = false
|
||||
|
||||
/**
|
||||
* Whether the server has a live connection to Gradle.
|
||||
@@ -102,7 +108,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
}
|
||||
|
||||
override fun initialize(params: InitializeProjectParams): CompletableFuture<InitializeResult> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
return runBuild {
|
||||
try {
|
||||
log.debug("Got initialize request", params)
|
||||
|
||||
@@ -112,6 +118,14 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
cancelCurrentBuild().get()
|
||||
}
|
||||
|
||||
val projectDirectory = File(params.directory)
|
||||
val failureReason = validateProjectDirectory(projectDirectory)
|
||||
|
||||
if (failureReason != null) {
|
||||
log.error("Cannot initialize project: $failureReason")
|
||||
return@runBuild InitializeResult(false, failureReason)
|
||||
}
|
||||
|
||||
val stopWatch = StopWatch("Connection to project")
|
||||
val isReinitializing = connector != null && connection != null && params == lastInitParams
|
||||
|
||||
@@ -123,7 +137,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
// or the project is being initialized with different parameters
|
||||
connector?.disconnect()
|
||||
|
||||
connector = GradleConnector.newConnector().forProjectDirectory(File(params.directory))
|
||||
connector = GradleConnector.newConnector().forProjectDirectory(projectDirectory)
|
||||
setupConnectorForGradleInstallation(this.connector!!, params.gradleDistribution)
|
||||
stopWatch.lap("Connector created")
|
||||
}
|
||||
@@ -163,15 +177,24 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
this.isInitialized = true
|
||||
|
||||
notifyBuildSuccess(emptyList())
|
||||
return@supplyAsync InitializeResult(true)
|
||||
return@runBuild InitializeResult(true)
|
||||
} catch (err: Throwable) {
|
||||
log.error("Failed to initialize project", err)
|
||||
notifyBuildFailure(emptyList())
|
||||
return@supplyAsync InitializeResult(false)
|
||||
return@runBuild InitializeResult(false, getTaskFailureType(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateProjectDirectory(
|
||||
projectDirectory: File
|
||||
) = when {
|
||||
!projectDirectory.exists() -> PROJECT_NOT_FOUND
|
||||
!projectDirectory.isDirectory -> PROJECT_NOT_DIRECTORY
|
||||
!projectDirectory.canRead() -> PROJECT_DIRECTORY_INACCESSIBLE
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun isServerInitialized(): CompletableFuture<Boolean> {
|
||||
return CompletableFuture.supplyAsync { isInitialized }
|
||||
}
|
||||
@@ -184,8 +207,21 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
}
|
||||
|
||||
override fun executeTasks(message: TaskExecutionMessage): CompletableFuture<TaskExecutionResult> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
assertProjectInitialized()
|
||||
return runBuild {
|
||||
if (!isServerInitialized().get()) {
|
||||
log.error("Cannot execute tasks: $PROJECT_NOT_INITIALIZED")
|
||||
return@runBuild TaskExecutionResult(false, PROJECT_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
val lastInitParams = this.lastInitParams
|
||||
if (lastInitParams != null) {
|
||||
val projectDirectory = File(lastInitParams.directory)
|
||||
val failureReason = validateProjectDirectory(projectDirectory)
|
||||
if (failureReason != null) {
|
||||
log.error("Cannot execute tasks: $failureReason")
|
||||
return@runBuild TaskExecutionResult(isSuccessful = false, failureReason)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Received request to run tasks.", message)
|
||||
|
||||
@@ -215,10 +251,10 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
builder.run()
|
||||
this.buildCancellationToken = null
|
||||
notifyBuildSuccess(message.tasks)
|
||||
return@supplyAsync TaskExecutionResult.SUCCESS
|
||||
return@runBuild TaskExecutionResult.SUCCESS
|
||||
} catch (error: Throwable) {
|
||||
notifyBuildFailure(message.tasks)
|
||||
return@supplyAsync TaskExecutionResult(false, getTaskFailureType(error), error.message)
|
||||
return@runBuild TaskExecutionResult(false, getTaskFailureType(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,6 +366,26 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
|
||||
else -> UNKNOWN
|
||||
}
|
||||
|
||||
private inline fun <T : Any?> supplyAsync(crossinline action: () -> T): CompletableFuture<T> =
|
||||
CompletableFuture.supplyAsync {
|
||||
action()
|
||||
}
|
||||
|
||||
private inline fun <T : Any?> runBuild(crossinline action: () -> T): CompletableFuture<T> =
|
||||
supplyAsync {
|
||||
if (isBuildInProgress) {
|
||||
log.error("Cannot run build, build is already in prorgess!")
|
||||
throw IllegalStateException("Build is already in progress")
|
||||
}
|
||||
|
||||
isBuildInProgress = true
|
||||
try {
|
||||
action()
|
||||
} finally {
|
||||
isBuildInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertProjectInitialized() {
|
||||
if (!isServerInitialized().get()) {
|
||||
throw CompletionException(IllegalStateException("Project is not initialized!"))
|
||||
|
||||
+32
@@ -28,6 +28,7 @@ import com.itsaky.androidide.tooling.api.IProject
|
||||
import com.itsaky.androidide.tooling.api.IToolingApiServer
|
||||
import com.itsaky.androidide.tooling.api.ProjectType
|
||||
import com.itsaky.androidide.tooling.api.messages.TaskExecutionMessage
|
||||
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
|
||||
import com.itsaky.androidide.tooling.api.models.AndroidProjectMetadata
|
||||
import com.itsaky.androidide.tooling.api.models.JavaModuleExternalDependency
|
||||
import com.itsaky.androidide.tooling.api.models.JavaModuleProjectDependency
|
||||
@@ -37,11 +38,22 @@ import com.itsaky.androidide.tooling.events.ProgressEvent
|
||||
import com.itsaky.androidide.tooling.events.task.TaskStartEvent
|
||||
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
|
||||
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher.MultiVersionTestClient
|
||||
import com.itsaky.androidide.utils.FileProvider
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.setPosixFilePermissions
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
/** @author Akash Yadav */
|
||||
@RunWith(JUnit4::class)
|
||||
@@ -54,6 +66,26 @@ class MultiModuleAndroidProjectTest {
|
||||
doAssertions(project, server)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test non-existing project initialization`() {
|
||||
val (_, _, result) = ToolingApiTestLauncher().launchServer(
|
||||
projectDir = Paths.get("/directory/does/not/exist/"))
|
||||
assertThat(result?.isSuccessful).isFalse()
|
||||
assertThat(result?.failure).isEqualTo(TaskExecutionResult.Failure.PROJECT_NOT_FOUND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test project initialization with file path`() {
|
||||
val path = FileProvider.projectRoot().resolve("build").resolve("should-be-directory.txt")
|
||||
path.deleteIfExists()
|
||||
path.createFile()
|
||||
|
||||
val (_, _, result) = ToolingApiTestLauncher().launchServer(
|
||||
projectDir = path)
|
||||
assertThat(result?.isSuccessful).isFalse()
|
||||
assertThat(result?.failure).isEqualTo(TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test task name should be shown if gradle configuration cache is enabled`() {
|
||||
|
||||
|
||||
+1
-1
@@ -23,4 +23,4 @@ package com.itsaky.androidide.tooling.api.messages.result
|
||||
* @param isSuccessful Whether the project initialization was successful.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class InitializeResult(val isSuccessful: Boolean)
|
||||
class InitializeResult(val isSuccessful: Boolean, val failure: TaskExecutionResult.Failure? = TaskExecutionResult.Failure.UNKNOWN)
|
||||
|
||||
+5
-3
@@ -22,11 +22,10 @@ package com.itsaky.androidide.tooling.api.messages.result
|
||||
*
|
||||
* @param isSuccessful The result of the task execution.
|
||||
* @param failure The type of failure. Non-null only if [isSuccessful] is `false`.
|
||||
* @param failureMessage The message describing the failure.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
data class TaskExecutionResult(val isSuccessful: Boolean, val failure: Failure?, val failureMessage: String?) {
|
||||
data class TaskExecutionResult(val isSuccessful: Boolean, val failure: Failure?) {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -34,11 +33,14 @@ data class TaskExecutionResult(val isSuccessful: Boolean, val failure: Failure?,
|
||||
* Result for a successful build.
|
||||
*/
|
||||
@JvmStatic
|
||||
val SUCCESS = TaskExecutionResult(true, null, null)
|
||||
val SUCCESS = TaskExecutionResult(true, null)
|
||||
}
|
||||
|
||||
enum class Failure {
|
||||
PROJECT_NOT_FOUND,
|
||||
PROJECT_NOT_INITIALIZED,
|
||||
PROJECT_NOT_DIRECTORY,
|
||||
PROJECT_DIRECTORY_INACCESSIBLE,
|
||||
UNKNOWN,
|
||||
UNSUPPORTED_GRADLE_VERSION,
|
||||
UNSUPPORTED_CONFIGURATION,
|
||||
|
||||
Reference in New Issue
Block a user