Skip to content

Commit

Permalink
[x2cpg|jimple2cpg] Added server mode to jimple2cpg after fixing respo…
Browse files Browse the repository at this point in the history
…nse header (#4995)

Turns out the HTTPServer expects an explicit "Connection: close" header which requests
the connection to be closed after the transaction ends. Otherwise, it would wait for a 10sec timeout
for the next thread to become available. In case we only allow for one thread (jimple2cpg) that would mean
additional waiting which renders the whole server approach useless. This in now fixes as we immediately close
the connection after the frontend is finished.
  • Loading branch information
max-leuthaeuser authored Oct 9, 2024
1 parent 8dbccfb commit c69ecf5
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package io.joern.jimple2cpg

import io.joern.jimple2cpg.Frontend.*
import io.joern.x2cpg.{X2CpgConfig, X2CpgMain}
import io.joern.x2cpg.utils.server.FrontendHTTPServer
import scopt.OParser

import java.util.concurrent.ExecutorService

/** Command line configuration parameters
*/
final case class Config(
Expand Down Expand Up @@ -74,8 +77,14 @@ private object Frontend {

/** Entry point for command line CPG creator
*/
object Main extends X2CpgMain(cmdLineParser, new Jimple2Cpg()) {
object Main extends X2CpgMain(cmdLineParser, new Jimple2Cpg()) with FrontendHTTPServer[Config, Jimple2Cpg] {

override protected def newDefaultConfig(): Config = Config()

override protected val executor: ExecutorService = FrontendHTTPServer.singleThreadExecutor()

def run(config: Config, jimple2Cpg: Jimple2Cpg): Unit = {
jimple2Cpg.run(config)
if (config.serverMode) { startup() }
else { jimple2Cpg.run(config) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.joern.jimple2cpg.io

import better.files.File
import io.joern.jimple2cpg.testfixtures.JimpleCode2CpgFixture
import io.joern.jimple2cpg.testfixtures.JimpleCodeToCpgFixture
import io.joern.x2cpg.utils.server.FrontendHTTPClient
import io.shiftleft.codepropertygraph.cpgloading.CpgLoader
import io.shiftleft.semanticcpg.language.*
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

import scala.collection.parallel.CollectionConverters.RangeIsParallelizable
import scala.util.Failure
import scala.util.Success

class Jimple2CpgHTTPServerTests extends JimpleCode2CpgFixture with BeforeAndAfterAll {

private var port: Int = -1

private def newProjectUnderTest(index: Option[Int] = None): File = {
val dir = File.newTemporaryDirectory("jimple2cpgTestsHttpTest")
val file = dir / "main.java"
file.createIfNotExists(createParents = true)
val indexStr = index.map(_.toString).getOrElse("")
file.writeText(s"""
|class Foo {
| static void main$indexStr(int argc, char argv) {
| System.out.println("Hello World!");
| }
|}
|""".stripMargin)
JimpleCodeToCpgFixture.compileJava(dir.path, List(file.toJava))
file.deleteOnExit()
dir.deleteOnExit()
}

override def beforeAll(): Unit = {
// Start server
port = io.joern.jimple2cpg.Main.startup()
}

override def afterAll(): Unit = {
// Stop server
io.joern.jimple2cpg.Main.stop()
}

"Using jimple2cpg in server mode" should {
"build CPGs correctly (single test)" in {
val cpgOutFile = File.newTemporaryFile("jimple2cpg.bin")
cpgOutFile.deleteOnExit()
val projectUnderTest = newProjectUnderTest()
val input = projectUnderTest.path.toAbsolutePath.toString
val output = cpgOutFile.toString
val client = FrontendHTTPClient(port)
val req = client.buildRequest(Array(s"input=$input", s"output=$output"))
client.sendRequest(req) match {
case Failure(exception) => fail(exception.getMessage)
case Success(out) =>
out shouldBe output
val cpg = CpgLoader.load(output)
cpg.method.name.l should contain("main")
cpg.call.code.l should contain("""$stack2.println("Hello World!")""")
}
}

"build CPGs correctly (multi-threaded test)" in {
(0 until 10).par.foreach { index =>
val cpgOutFile = File.newTemporaryFile("jimple2cpg.bin")
cpgOutFile.deleteOnExit()
val projectUnderTest = newProjectUnderTest(Some(index))
val input = projectUnderTest.path.toAbsolutePath.toString
val output = cpgOutFile.toString
val client = FrontendHTTPClient(port)
val req = client.buildRequest(Array(s"input=$input", s"output=$output"))
client.sendRequest(req) match {
case Failure(exception) => fail(exception.getMessage)
case Success(out) =>
out shouldBe output
val cpg = CpgLoader.load(output)
cpg.method.name.l should contain(s"main$index")
cpg.call.code.l should contain("""$stack2.println("Hello World!")""")
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import java.net.http.HttpRequest
import java.net.URI
import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse.BodyHandlers
import scala.util.Success
import scala.util.Failure
import scala.util.Success
import scala.util.Try

/** Represents an HTTP client for interacting with a frontend server.
Expand Down Expand Up @@ -61,5 +61,4 @@ case class FrontendHTTPClient(port: Int) {
case r => Failure(new IOException(s"Sending request failed with code ${r.statusCode()}: ${r.body()}"))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ trait FrontendHTTPServer[T <: X2CpgConfig[T], X <: X2CpgFrontend[T]] { this: X2C
@Context(value = "/run", methods = Array("POST"))
def run(req: server.Request, resp: server.Response): Int = {
resp.getHeaders.add("Content-Type", "text/plain")
resp.getHeaders.add("Connection", "close")

val params = req.getParamsList.asScala
val outputDir = params
Expand Down

0 comments on commit c69ecf5

Please sign in to comment.