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

Commit 94f2af7

Browse files
mrkocotSpace Team
authored andcommitted
[feature] implement experimental Bazel JVM target debugging through BSP
Merge-request: BAZEL-MR-563 Merged-by: Marcin Kocot <[email protected]>
1 parent 2ad9404 commit 94f2af7

File tree

7 files changed

+170
-25
lines changed

7 files changed

+170
-25
lines changed

logger/src/main/kotlin/org/jetbrains/bsp/bazel/logger/BspClientTestNotifier.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,6 @@ import ch.epfl.scala.bsp4j.TestTask
1515

1616
class BspClientTestNotifier {
1717
private lateinit var bspClient: BuildClient
18-
private var originId: String? = null
19-
20-
fun withOriginId(originId: String?): BspClientTestNotifier {
21-
val bspClientTestNotifier = BspClientTestNotifier()
22-
bspClientTestNotifier.originId = originId
23-
bspClientTestNotifier.bspClient = bspClient
24-
return bspClientTestNotifier
25-
}
2618

2719
/**
2820
* Notifies the client about starting a single test or a test suite

server/src/main/kotlin/org/jetbrains/bsp/bazel/server/BazelBspServer.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class BazelBspServer(
5656
serverContainer.projectProvider,
5757
bazelRunner,
5858
workspaceContextProvider,
59+
bspClientLogger,
5960
bspClientTestNotifier,
6061
bspState,
6162
)

server/src/main/kotlin/org/jetbrains/bsp/bazel/server/bsp/BspServerApi.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@
4949
import ch.epfl.scala.bsp4j.WorkspaceBuildTargetsResult;
5050
import java.util.concurrent.CompletableFuture;
5151
import java.util.function.Supplier;
52-
52+
import org.jetbrains.annotations.NotNull;
5353
import org.jetbrains.bsp.bazel.server.sync.BazelBuildServer;
5454
import org.jetbrains.bsp.bazel.server.sync.ExecuteService;
5555
import org.jetbrains.bsp.bazel.server.sync.ProjectSyncService;
56+
import org.jetbrains.bsp.bazel.server.sync.RunWithDebugParams;
5657
import org.jetbrains.bsp.bazel.server.sync.WorkspaceDirectoriesResult;
5758
import org.jetbrains.bsp.bazel.server.sync.WorkspaceLibrariesResult;
5859

@@ -164,6 +165,12 @@ public CompletableFuture<RunResult> buildTargetRun(RunParams params) {
164165
return runner.handleRequest("buildTargetRun", executeService::run, params);
165166
}
166167

168+
@NotNull
169+
@Override
170+
public CompletableFuture<RunResult> buildTargetRunWithDebug(@NotNull RunWithDebugParams params) {
171+
return runner.handleRequest("buildTargetRunWithDebug", executeService::runWithDebug, params);
172+
}
173+
167174
@Override
168175
public CompletableFuture<CleanCacheResult> buildTargetCleanCache(CleanCacheParams params) {
169176
return runner.handleRequest("buildTargetCleanCache", executeService::clean, params);
@@ -242,6 +249,7 @@ public CompletableFuture<JvmTestEnvironmentResult> buildTargetJvmTestEnvironment
242249
"jvmTestEnvironment", projectSyncService::jvmTestEnvironment, params);
243250
}
244251

252+
@NotNull
245253
@Override
246254
public CompletableFuture<WorkspaceLibrariesResult> workspaceLibraries() {
247255
return runner.handleRequest("libraries", projectSyncService::workspaceBuildLibraries);

server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync/BazelBuildServer.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.jetbrains.bsp.bazel.server.sync
22

33
import ch.epfl.scala.bsp4j.BuildTargetIdentifier
4+
import ch.epfl.scala.bsp4j.RunParams
5+
import ch.epfl.scala.bsp4j.RunResult
46
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
57
import java.util.concurrent.CompletableFuture
68

@@ -24,10 +26,25 @@ data class WorkspaceDirectoriesResult(
2426
val excludedDirectories: List<DirectoryItem>,
2527
)
2628

29+
data class RemoteDebugData(
30+
val debugType: String,
31+
val port: Int,
32+
)
33+
34+
data class RunWithDebugParams(
35+
val originId: String,
36+
val runParams: RunParams,
37+
val debug: RemoteDebugData?,
38+
)
39+
40+
2741
interface BazelBuildServer {
2842
@JsonRequest("workspace/libraries")
2943
fun workspaceLibraries(): CompletableFuture<WorkspaceLibrariesResult>
3044

3145
@JsonRequest("workspace/directories")
3246
fun workspaceDirectories(): CompletableFuture<WorkspaceDirectoriesResult>
47+
48+
@JsonRequest("buildTarget/runWithDebug")
49+
fun buildTargetRunWithDebug(params: RunWithDebugParams): CompletableFuture<RunResult>
3350
}

server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync/BspMappings.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ object BspMappings {
2828

2929
fun toBspUri(uri: BuildTargetIdentifier): String = uri.uri
3030

31+
fun toBspUri(module: Module): String = toBspUri(toBspId(module))
32+
3133
fun getModules(project: Project, targets: List<BuildTargetIdentifier>): Set<Module> =
3234
toLabels(targets).mapNotNull(project::findModule).toSet()
3335

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.jetbrains.bsp.bazel.server.sync
2+
3+
import ch.epfl.scala.bsp4j.RunResult
4+
import ch.epfl.scala.bsp4j.StatusCode
5+
import org.eclipse.lsp4j.jsonrpc.CancelChecker
6+
import org.jetbrains.bsp.bazel.bazelrunner.BazelRunner
7+
import org.jetbrains.bsp.bazel.bazelrunner.params.BazelFlag
8+
import org.jetbrains.bsp.bazel.server.sync.model.Language
9+
import org.jetbrains.bsp.bazel.server.sync.model.Module
10+
11+
class DebugRunner(
12+
private val bazelRunner: BazelRunner,
13+
private val errorMessageSender: (message: String, originId: String) -> Unit,
14+
) {
15+
fun runWithDebug(cancelChecker: CancelChecker, params: RunWithDebugParams, moduleToRun: Module): RunResult {
16+
val uri = BspMappings.toBspUri(moduleToRun)
17+
val requestedDebugType = DebugType.fromDebugData(params.debug)
18+
val debugArguments = generateRunArguments(requestedDebugType)
19+
val requestIsValid = verifyDebugRequest(requestedDebugType, moduleToRun, params.originId)
20+
return if (requestIsValid) {
21+
runBazel(cancelChecker, params, uri, debugArguments)
22+
} else {
23+
RunResult(StatusCode.ERROR).apply { this.originId = params.originId }
24+
}
25+
}
26+
27+
private fun generateRunArguments(debugType: DebugType?): List<String> =
28+
when (debugType) {
29+
is DebugType.JDWP -> listOf(jdwpArgument(debugType.port))
30+
else -> emptyList()
31+
}
32+
33+
/**
34+
* @return `true` if the run request is a valid debug request, or is not a debug request at all
35+
*/
36+
private fun verifyDebugRequest (
37+
debugType: DebugType?,
38+
moduleToRun: Module,
39+
originId: String,
40+
): Boolean =
41+
when (debugType) {
42+
null -> true // not a debug request, nothing to check
43+
is DebugType.JDWP -> {
44+
if (!moduleToRun.isJavaOrKotlin()) {
45+
errorMessageSender("JDWP debugging is only available for Java and Kotlin targets", originId)
46+
false
47+
} else true
48+
}
49+
is DebugType.UNKNOWN -> {
50+
errorMessageSender("\"${debugType.name}\" is not a supported debugging type", originId)
51+
false
52+
}
53+
}
54+
55+
// TODO [#BAZEL-721] - quite a naive predicate, but otherwise we'll need to have rule type info in Module instance
56+
private fun Module.isJavaOrKotlin() = languages.contains(Language.JAVA) || languages.contains(Language.KOTLIN)
57+
58+
/**
59+
* If `debugArguments` is empty, run task will be executed normally without any debugging options
60+
*/
61+
private fun runBazel(
62+
cancelChecker: CancelChecker,
63+
params: RunWithDebugParams,
64+
bspUri: String,
65+
debugArguments: List<String>,
66+
): RunResult {
67+
val bazelProcessResult =
68+
bazelRunner.commandBuilder()
69+
.run()
70+
.withArguments(debugArguments)
71+
.withArgument(bspUri)
72+
.withArguments(params.runParams.arguments)
73+
.withFlag(BazelFlag.color(true))
74+
.executeBazelCommand(params.originId)
75+
.waitAndGetResult(cancelChecker)
76+
return RunResult(bazelProcessResult.statusCode).apply { originId = params.originId }
77+
}
78+
79+
private fun jdwpArgument(port: Int): String =
80+
// all used options are defined in https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/conninv.html#Invocation
81+
"--jvmopt=\"-agentlib:jdwp=" +
82+
"transport=dt_socket," +
83+
"server=n," +
84+
"suspend=y," +
85+
"address=localhost:$port," +
86+
"\""
87+
}
88+
89+
private sealed interface DebugType {
90+
data class UNKNOWN(val name: String) : DebugType // debug type unknown
91+
data class JDWP(val port: Int) : DebugType // used for Java and Kotlin
92+
93+
companion object {
94+
fun fromDebugData(params: RemoteDebugData?): DebugType? =
95+
when (params?.debugType?.lowercase()) {
96+
null -> null
97+
"jdwp" -> JDWP(params.port)
98+
else -> UNKNOWN(params.debugType)
99+
}
100+
}
101+
}

server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync/ExecuteService.kt

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import ch.epfl.scala.bsp4j.TestParams
1313
import ch.epfl.scala.bsp4j.TestResult
1414
import ch.epfl.scala.bsp4j.TestStatus
1515
import ch.epfl.scala.bsp4j.TextDocumentIdentifier
16-
import io.grpc.Server
1716
import org.eclipse.lsp4j.jsonrpc.CancelChecker
1817
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
1918
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError
2019
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode
2120
import org.jetbrains.bsp.bazel.bazelrunner.BazelProcessResult
2221
import org.jetbrains.bsp.bazel.bazelrunner.BazelRunner
2322
import org.jetbrains.bsp.bazel.bazelrunner.params.BazelFlag
23+
import org.jetbrains.bsp.bazel.logger.BspClientLogger
2424
import org.jetbrains.bsp.bazel.logger.BspClientTestNotifier
2525
import org.jetbrains.bsp.bazel.server.bep.BepServer
2626
import org.jetbrains.bsp.bazel.server.bsp.managers.BazelBspCompilationManager
@@ -37,12 +37,17 @@ class ExecuteService(
3737
private val projectProvider: ProjectProvider,
3838
private val bazelRunner: BazelRunner,
3939
private val workspaceContextProvider: WorkspaceContextProvider,
40+
private val bspClientLogger: BspClientLogger,
4041
private val bspClientTestNotifier: BspClientTestNotifier,
4142
private val hasAnyProblems: Map<String, Set<TextDocumentIdentifier>>
4243
) {
43-
private fun <T> withBepServer(body : (BepReader) -> T) :T {
44+
private val debugRunner = DebugRunner(bazelRunner) { message, originId ->
45+
bspClientLogger.withOriginId(originId).error(message)
46+
}
47+
48+
private fun <T> withBepServer(body : (BepReader) -> T): T {
4449
val server = BepServer.newBepServer(compilationManager.client, compilationManager.workspaceRoot, hasAnyProblems, Optional.empty())
45-
val bepReader = BepReader(server);
50+
val bepReader = BepReader(server)
4651
return body(bepReader)
4752
}
4853

@@ -95,16 +100,7 @@ class ExecuteService(
95100

96101
fun run(cancelChecker: CancelChecker, params: RunParams): RunResult {
97102
val targets = selectTargets(cancelChecker, listOf(params.target))
98-
if (targets.isEmpty()) {
99-
throw ResponseErrorException(
100-
ResponseError(
101-
ResponseErrorCode.InvalidRequest,
102-
"No supported target found for " + params.target.uri,
103-
null
104-
)
105-
)
106-
}
107-
val bspId = targets.single()
103+
val bspId = targets.singleOrResponseError(params.target)
108104
val result = build(cancelChecker, targets, params.originId)
109105
if (result.isNotSuccess) {
110106
return RunResult(result.statusCode)
@@ -120,6 +116,18 @@ class ExecuteService(
120116
return RunResult(bazelProcessResult.statusCode).apply { originId = originId }
121117
}
122118

119+
fun runWithDebug(cancelChecker: CancelChecker, params: RunWithDebugParams): RunResult {
120+
val modules = selectModules(cancelChecker, listOf(params.runParams.target))
121+
val singleModule = modules.singleOrResponseError(params.runParams.target)
122+
val bspId = toBspId(singleModule)
123+
val result = build(cancelChecker, listOf(bspId), params.originId)
124+
if (result.isNotSuccess) {
125+
return RunResult(result.statusCode)
126+
}
127+
return debugRunner.runWithDebug(cancelChecker, params, singleModule)
128+
}
129+
130+
@Suppress("UNUSED_PARAMETER") // params is used by BspRequestsRunner.handleRequest
123131
fun clean(cancelChecker: CancelChecker, params: CleanCacheParams?): CleanCacheResult {
124132
withBepServer { bepReader ->
125133
bazelRunner.commandBuilder().clean()
@@ -137,11 +145,13 @@ class ExecuteService(
137145
).processResult
138146
}
139147

140-
private fun selectTargets(cancelChecker: CancelChecker, targets: List<BuildTargetIdentifier>): List<BuildTargetIdentifier> {
148+
private fun selectTargets(cancelChecker: CancelChecker, targets: List<BuildTargetIdentifier>): List<BuildTargetIdentifier> =
149+
selectModules(cancelChecker, targets).map { toBspId(it) }
150+
151+
private fun selectModules(cancelChecker: CancelChecker, targets: List<BuildTargetIdentifier>): List<Module> {
141152
val project = projectProvider.get(cancelChecker)
142153
val modules = BspMappings.getModules(project, targets)
143-
val modulesToBuild = modules.filter { isBuildable(it) }
144-
return modulesToBuild.map(::toBspId)
154+
return modules.filter { isBuildable(it) }
145155
}
146156

147157
private fun isBuildable(m: Module): Boolean =
@@ -153,3 +163,17 @@ class ExecuteService(
153163
workspaceContextProvider.currentWorkspaceContext().buildManualTargets.value)
154164

155165
}
166+
167+
private fun <T> List<T>.singleOrResponseError(
168+
requestedTarget: BuildTargetIdentifier,
169+
): T =
170+
when {
171+
this.isEmpty() -> throwResponseError("No supported target found for ${requestedTarget.uri}")
172+
this.size == 1 -> this.single()
173+
else -> throwResponseError("More than one supported target found for ${requestedTarget.uri}")
174+
}
175+
176+
private fun throwResponseError(message: String, data: Any? = null): Nothing =
177+
throw ResponseErrorException(
178+
ResponseError(ResponseErrorCode.InvalidRequest, message, data)
179+
)

0 commit comments

Comments
 (0)