Skip to content
This repository was archived by the owner on Oct 18, 2024. It is now read-only.

Commit 1168ffd

Browse files
committed
fix(tooling-api): add cancellation support for project initialization
1 parent b371fb7 commit 1168ffd

File tree

5 files changed

+78
-9
lines changed

5 files changed

+78
-9
lines changed

subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/ToolingApiServerImpl.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Fai
4646
import com.itsaky.androidide.tooling.impl.internal.ProjectImpl
4747
import com.itsaky.androidide.tooling.impl.sync.ModelBuilderException
4848
import com.itsaky.androidide.tooling.impl.sync.RootModelBuilder
49+
import com.itsaky.androidide.tooling.impl.sync.RootProjectModelBuilderParams
4950
import com.itsaky.androidide.tooling.impl.util.StopWatch
5051
import com.itsaky.androidide.utils.ILogger
5152
import org.gradle.tooling.BuildCancelledException
@@ -61,6 +62,8 @@ import org.gradle.tooling.internal.consumer.DefaultGradleConnector
6162
import java.io.File
6263
import java.util.concurrent.CompletableFuture
6364
import java.util.concurrent.CompletionException
65+
import java.util.concurrent.locks.ReentrantLock
66+
import kotlin.concurrent.withLock
6467
import kotlin.system.exitProcess
6568

6669
/**
@@ -75,9 +78,14 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
7578
private var connector: GradleConnector? = null
7679
private var connection: ProjectConnection? = null
7780
private var lastInitParams: InitializeProjectParams? = null
78-
private var buildCancellationToken: CancellationTokenSource? = null
81+
private var _buildCancellationToken: CancellationTokenSource? = null
7982
private val log = ILogger.newInstance(javaClass.simpleName)
8083

84+
private val cancellationTokenAccessLock = ReentrantLock(/* fair = */ true)
85+
private var buildCancellationToken: CancellationTokenSource?
86+
get() = cancellationTokenAccessLock.withLock { _buildCancellationToken }
87+
set(value) = cancellationTokenAccessLock.withLock { _buildCancellationToken = value }
88+
8189
/**
8290
* Whether the project has been initialized or not.
8391
*/
@@ -162,8 +170,14 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
162170

163171
stopWatch.lapFromLast("Project connection established")
164172

173+
this.buildCancellationToken = GradleConnector.newCancellationTokenSource()
174+
165175
val project = try {
166-
val impl = RootModelBuilder(params).build(connection) as? ProjectImpl?
176+
val modelBuilderParams = RootProjectModelBuilderParams(
177+
connection,
178+
this.buildCancellationToken!!.token()
179+
)
180+
val impl = RootModelBuilder(params).build(modelBuilderParams) as? ProjectImpl?
167181
?: throw ModelBuilderException("Failed to build project model")
168182
impl
169183
} catch (err: Throwable) {
@@ -309,6 +323,7 @@ internal class ToolingApiServerImpl(private val project: ProjectImpl) :
309323

310324
try {
311325
this.buildCancellationToken!!.cancel()
326+
this.buildCancellationToken = null
312327
} catch (e: Exception) {
313328
val failureReason = CANCELLATION_ERROR
314329
failureReason.message = "${failureReason.message}: ${e.message}"

subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/RootModelBuilder.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import com.itsaky.androidide.tooling.impl.Main.finalizeLauncher
2929
import com.itsaky.androidide.tooling.impl.internal.ProjectImpl
3030
import com.itsaky.androidide.utils.ILogger
3131
import org.gradle.tooling.ConfigurableLauncher
32-
import org.gradle.tooling.ProjectConnection
3332
import org.gradle.tooling.model.idea.IdeaProject
3433
import java.io.Serializable
3534

@@ -39,16 +38,18 @@ import java.io.Serializable
3938
* @author Akash Yadav
4039
*/
4140
class RootModelBuilder(initializationParams: InitializeProjectParams) :
42-
AbstractModelBuilder<ProjectConnection, IProject>(initializationParams), Serializable {
41+
AbstractModelBuilder<RootProjectModelBuilderParams, IProject>(initializationParams), Serializable {
4342

4443
private val serialVersionUID = 1L
4544

46-
override fun build(param: ProjectConnection): IProject {
45+
override fun build(param: RootProjectModelBuilderParams): IProject {
46+
47+
val (projectConnection, cancellationToken) = param
4748

4849
// do not reference the 'initializationParams' field in the
4950
val initializationParams = initializationParams
5051

51-
val executor = param.action { controller ->
52+
val executor = projectConnection.action { controller ->
5253
val ideaProject = controller.getModelAndLog(IdeaProject::class.java)
5354

5455
val ideaModules = ideaProject.modules
@@ -106,6 +107,10 @@ class RootModelBuilder(initializationParams: InitializeProjectParams) :
106107
finalizeLauncher(executor)
107108
applyAndroidModelBuilderProps(executor)
108109

110+
if (cancellationToken != null) {
111+
executor.withCancellationToken(cancellationToken)
112+
}
113+
109114
val logger = ILogger.newInstance("ProjectReader")
110115
logger.warn("Starting build. See build output for more details...")
111116

subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/types.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,22 @@ package com.itsaky.androidide.tooling.impl.sync
1919

2020
import com.android.builder.model.v2.models.Versions
2121
import org.gradle.tooling.BuildController
22+
import org.gradle.tooling.CancellationToken
23+
import org.gradle.tooling.ProjectConnection
2224
import org.gradle.tooling.model.idea.IdeaModule
2325
import org.gradle.tooling.model.idea.IdeaProject
2426

27+
/**
28+
* Parameters for the root project model builder.
29+
*
30+
* @property projectConnection The project connection
31+
* @property cancellationToken The cancellation token.
32+
*/
33+
data class RootProjectModelBuilderParams(
34+
val projectConnection: ProjectConnection,
35+
val cancellationToken: CancellationToken?
36+
)
37+
2538
/**
2639
* Parameters for building model for an Android project.
2740
*

subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,25 @@ class MultiModuleAndroidProjectTest {
141141
)
142142
}
143143

144+
@Test
145+
fun `test project initialization cancellation`() {
146+
// launch project initialization
147+
val (server, _, initFuture) = ToolingApiTestLauncher().launchServerAsync()
148+
Thread.sleep(1000L)
149+
150+
// cancel initialization request
151+
val cancellationResult = server.cancelCurrentBuild().get()
152+
println("Cancellation result: $cancellationResult")
153+
assertThat(cancellationResult).isNotNull()
154+
assertThat(cancellationResult.wasEnqueued).isTrue()
155+
assertThat(cancellationResult.failureReason).isNull()
156+
157+
// verify that the initialization failed with reason BUILD_CANCELLED
158+
val initResult = initFuture.get()
159+
assertThat(initResult!!.isSuccessful).isFalse()
160+
assertThat(initResult.failure).isEqualTo(TaskExecutionResult.Failure.BUILD_CANCELLED)
161+
}
162+
144163
private fun doAssertions(project: IProject, server: IToolingApiServer) {
145164
assertThat(project).isNotNull()
146165

testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/ToolingApiTestLauncher.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class ToolingApiTestLauncher {
6666
"jdk.compiler" to "com.sun.tools.javac.util")
6767

6868
@JvmOverloads
69-
fun launchServer(
69+
fun launchServerAsync(
7070
projectDir: Path = FileProvider.testProjectRoot(),
7171
client: MultiVersionTestClient = MultiVersionTestClient(),
7272
initParams: InitializeProjectParams = InitializeProjectParams(
@@ -76,7 +76,7 @@ class ToolingApiTestLauncher {
7676
log: ILogger = ILogger.newInstance("BuildOutputLogger"),
7777
sysProps: Map<String, String> = emptyMap(),
7878
sysEnvs: Map<String, String> = emptyMap()
79-
): Triple<IToolingApiServer, IProject, InitializeResult?> {
79+
): Triple<IToolingApiServer, IProject, CompletableFuture<InitializeResult>> {
8080
val cmdLine = createProcessCmd(
8181
FileProvider.implModule()
8282
.resolve("build/libs/tooling-api-all.jar").pathString,
@@ -101,10 +101,27 @@ class ToolingApiTestLauncher {
101101

102102
val server = launcher.remoteProxy as IToolingApiServer
103103
val project = launcher.remoteProxy as IProject
104-
val result = server.initialize(initParams).get()
104+
val result = server.initialize(initParams)
105105
return Triple(server, project, result)
106106
}
107107

108+
@JvmOverloads
109+
fun launchServer(
110+
projectDir: Path = FileProvider.testProjectRoot(),
111+
client: MultiVersionTestClient = MultiVersionTestClient(),
112+
initParams: InitializeProjectParams = InitializeProjectParams(
113+
projectDir.pathString,
114+
client.gradleDistParams
115+
),
116+
log: ILogger = ILogger.newInstance("BuildOutputLogger"),
117+
sysProps: Map<String, String> = emptyMap(),
118+
sysEnvs: Map<String, String> = emptyMap()
119+
): Triple<IToolingApiServer, IProject, InitializeResult?> {
120+
return launchServerAsync(projectDir, client, initParams, log, sysProps, sysEnvs).let { result ->
121+
Triple(result.first, result.second, result.third.get())
122+
}
123+
}
124+
108125
private fun createProcessCmd(
109126
jar: String,
110127
sysProps: Map<String, String> = emptyMap()

0 commit comments

Comments
 (0)