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 @@ -4,8 +4,8 @@ import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.duration.Duration
import scala.concurrent.duration.DurationInt
import scala.language.postfixOps
import scala.util.Properties

import scala.meta.internal.metals.Cancelable
Expand Down Expand Up @@ -132,6 +132,7 @@ object ShellRunner {
processErr: String => Unit = scribe.error(_),
propagateError: Boolean = false,
maybeJavaHome: Option[String] = None,
timeout: Duration = 10.seconds,
)(implicit ec: ExecutionContext): Option[String] = {

val sbOut = new StringBuilder()
Expand All @@ -149,7 +150,7 @@ object ShellRunner {
propagateError,
)

val exit = Await.result(ps.complete, 10 second)
val exit = Await.result(ps.complete, timeout)

if (exit == 0) {
Some(sbOut.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -817,8 +817,10 @@ case class DebugUnresolvedTestClassParams(
case class DebugUnresolvedAttachRemoteParams(
hostName: String,
port: Int,
buildTarget: String,
)
@Nullable buildTarget: String = null,
) {
def buildTargetOpt: Option[String] = Option(buildTarget)
}

case class DebugDiscoveryParams(
@Nullable path: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1083,17 +1083,23 @@ class WorkspaceLspService(
.liftToLspError
.asJavaObject
case ServerCommands.StartAttach(params) if params.hostName != null =>
onFirstSatifying(service =>
onFirstSatifying { service =>
Future.successful(
service.findBuildTargetByDisplayName(params.buildTarget)
params.buildTargetOpt match {
case None =>
Option(service.focusedDocumentBuildTarget.get)
.flatMap(service.buildTargets.info(_))
case Some(buildTarget) =>
service.findBuildTargetByDisplayName(buildTarget)
}
)
)(
}(
_.isDefined,
(service, someTarget) =>
service.createDebugSession(someTarget.get.getId()),
() =>
failedRequest(
s"Could not find '${params.buildTarget}' build target"
s"Could not find '${Option(params.buildTarget).getOrElse("")}' build target"
),
).asJavaObject
case ServerCommands.DiscoverAndRun(params) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ final class Debugger(server: RemoteServer)(implicit ec: ExecutionContext) {
server.launch(arguments.asJava).asScala.ignoreValue
}

def attach(port: Int): Future[Unit] = {
val arguments = Map[String, AnyRef](
"hostName" -> "localhost",
"port" -> Integer.valueOf(port),
)
server.attach(arguments.asJava).asScala.ignoreValue
}

def configurationDone: Future[Unit] = {
server
.configurationDone(new ConfigurationDoneArguments)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ private[debug] final class RemoteServer(
sendRequest("evaluate", args)
}

override def attach(
args: java.util.Map[String, AnyRef]
): CompletableFuture[Void] = {
sendRequest("attach", args)
}

override def completions(
args: CompletionsArguments
): CompletableFuture[CompletionsResponse] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ final class TestDebugger(
ifNotFailed(debugger.launch(debug = true))
}

def attach(port: Int): Future[Unit] = {
Debug.printEnclosing()
ifNotFailed(debugger.attach(port))
}

def launch(debug: Boolean): Future[Unit] = {
Debug.printEnclosing()
ifNotFailed(debugger.launch(debug))
Expand Down
96 changes: 96 additions & 0 deletions tests/unit/src/test/scala/tests/DebugProtocolSuite.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package tests

import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.util.Collections.emptyList
import java.util.Collections.singletonList

import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Random

import scala.meta.internal.builds.ShellRunner
import scala.meta.internal.metals.DebugDiscoveryParams
import scala.meta.internal.metals.DebugSession
import scala.meta.internal.metals.DebugUnresolvedAttachRemoteParams
import scala.meta.internal.metals.DebugUnresolvedMainClassParams
import scala.meta.internal.metals.DebugUnresolvedTestClassParams
import scala.meta.internal.metals.JdkSources
import scala.meta.internal.metals.JsonParser._
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.ServerCommands.StartAttach
import scala.meta.internal.metals.debug.DiscoveryFailures._
import scala.meta.internal.metals.debug.Stoppage
import scala.meta.internal.metals.debug.TestDebugger

import ch.epfl.scala.bsp4j.DebugSessionParamsDataKind
import ch.epfl.scala.bsp4j.ScalaMainClass
Expand Down Expand Up @@ -66,6 +78,90 @@ class DebugProtocolSuite
} yield assertNoDiff(output, "FooBarFoo")
}

test("attach") {
val port = 5566
def runningMain() = Future {
val classpathJar = workspace.resolve(".metals/.tmp").list.head.toString()
ShellRunner.runSync(
List(
JdkSources.defaultJavaHome(None).head.resolve("bin/java").toString,
"-Dproperty=Foo",
s"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=$port",
"-cp",
classpathJar,
"a.AttachingMain",
"Bar",
),
workspace,
true,
Map("HELLO" -> "Foo"),
propagateError = true,
timeout = 60.seconds,
)
}

for {
_ <- initialize(
s"""/metals.json
|{
| "a": {}
|}
|/a/src/main/scala/a/Main.scala
|package a
|object AttachingMain {
| def main(args: Array[String]) = {
| val foo = sys.props.getOrElse("property", "")
| val bar = args(0)
| val env = sys.env.get("HELLO")
| print(foo + bar)
| env.foreach(print)
| System.exit(0)
| }
|}
|""".stripMargin
)
_ <- server.headServer.buildServerPromise.future
_ <- server.didOpen("a/src/main/scala/a/Main.scala")
_ <- server.didSave("a/src/main/scala/a/Main.scala")
// creates a classpath jar that we can use
_ <- server
.executeCommand(
ServerCommands.DiscoverMainClasses,
new DebugDiscoveryParams(
null,
"run",
"a.AttachingMain",
),
)
runMain = runningMain()
debugSession <- server.executeCommand(
StartAttach,
DebugUnresolvedAttachRemoteParams("localhost", port),
)
debugger = debugSession match {
case DebugSession(_, uri) =>
scribe.info(s"Starting debug session for $uri")
TestDebugger(
URI.create(uri),
Stoppage.Handler.Continue,
requestOtherThreadStackTrace = false,
)
case _ => throw new RuntimeException("Debug session not found")
}
_ <- debugger.initialize
_ <- debugger.attach(port)
_ <- debugger.configurationDone
output <- runMain
_ <- debugger.disconnect
_ <- debugger.shutdown
_ <- debugger.allOutput
} yield assertNoDiff(
output.getOrElse(""),
s"""|Listening for transport dt_socket at address: $port
|FooBarFoo""".stripMargin,
)
}

test("broken-workspace") {
cleanWorkspace()

Expand Down
Loading