Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote View #1

Merged
merged 14 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
run: mkdir -p http4s-server/jvm/target http4s-server/js/target json-info/js/target json-info/jvm/target unidoc/target json-info/native/target sfx-ui/target project/target
run: mkdir -p http4s-server/jvm/target http4s-server/js/target json-info/js/target json-info/jvm/target unidoc/target json-info/native/target remote-view/jvm/target sfx-ui/target remote-view/js/target remote-view/native/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
run: tar cf targets.tar http4s-server/jvm/target http4s-server/js/target json-info/js/target json-info/jvm/target unidoc/target json-info/native/target sfx-ui/target project/target
run: tar cf targets.tar http4s-server/jvm/target http4s-server/js/target json-info/js/target json-info/jvm/target unidoc/target json-info/native/target remote-view/jvm/target sfx-ui/target remote-view/js/target remote-view/native/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
Expand Down
16 changes: 15 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ inThisBuild(List(
//tlSitePublishBranch := Some(mainBranch),
))

lazy val root = tlCrossRootProject.aggregate(jsonInfo, sfxUi/*, http4sServer*/, unidocs)
lazy val root = tlCrossRootProject.aggregate(remoteView, jsonInfo, sfxUi/*, http4sServer*/, unidocs)

lazy val commonSettings = Seq(
headerLicenseStyle := HeaderLicenseStyle.SpdxSyntax,
Expand Down Expand Up @@ -117,3 +117,17 @@ lazy val unidocs = project
name := "parsley-debug-view-docs",
ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(jsonInfo.jvm, sfxUi/*, http4sServer*/),
)

lazy val remoteView = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Full)
.in(file("remote-view"))
.settings(
commonSettings,
name := "parsley-debug-remote",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "core" % "3.10.2",
"com.lihaoyi" %% "upickle" % "4.1.0"
)
)

128 changes: 128 additions & 0 deletions remote-view/shared/src/main/scala/parsley/debug/RemoteView.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2023 Parsley Debug View Contributors <https://github.com/j-mie6/parsley-debug-views/graphs/contributors>
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package parsley.debug

import parsley.debug.internal.DebugTreeSerialiser

import sttp.client3.*

// import scala.util.Try
import scala.concurrent.duration.*
import sttp.model.Uri
import scala.util.Try
import scala.util.Failure
import scala.util.Success

/** The RemoteView HTTP module allows the parsley debug tree to be passed off to a server through a specified port on
* local host (by default) or to a specified IP address. This enables all of the debug tree parsing, serving and
* graphics to be done in a separate process, and served reactively to a client.
*
* This module is part of the Debugging Interactively in parsLey Library (DILL) project,
* (https://github.com/j-mie6/parsley-debug-app).
*
* RemoteView uses the STTP library to create HTTP requests to a specified IP address, over a specified port.
* The request is formatted using the upickle JSON formatting library, it is being used over other
* libraries like circe for its improved speed over large data structures.
*/
sealed trait RemoteView extends DebugView.Reusable {
protected val port: Int
protected val address: String

// Printing helpers
private [debug] final val TextToRed: String = "\u001b[31m"
private [debug] final val TextToNormal: String = "\u001b[0m"

// Request Timeouts
private [debug] final val ConnectionTimeout: FiniteDuration = 30.second
private [debug] final val ResponseTimeout: FiniteDuration = 10.second

/**
* Send the debug tree and input to the port and address specified in the
* object construction.
*
* @param input The input source.
* @param tree The debug tree.
*/
override private [debug] def render(input: => String, tree: => DebugTree): Unit = {
// Endpoint for post request
val endPoint: Uri = uri"http://$address:$port/api/remote"
// JSON formatted payload for post request
val payload: String = DebugTreeSerialiser.toJSON(input, tree)

// Send POST
println("Sending Debug Tree to Server")

val backend = TryHttpURLConnectionBackend(
options = SttpBackendOptions.connectionTimeout(ConnectionTimeout)
)

val response: Try[Response[Either[String,String]]] = basicRequest
.readTimeout(ResponseTimeout)
.header("User-Agent", "remoteView")
.contentType("application/json")
.body(payload)
.post(endPoint)
.send(backend)

response match {
case Failure(exception) => println(s"${TextToRed}Remote View request failed! Please validate address ($address) and port number ($port).${TextToNormal}\n\tError : ${exception.toString()}")
j-mie6 marked this conversation as resolved.
Show resolved Hide resolved
case Success(value) => value.body match {
j-mie6 marked this conversation as resolved.
Show resolved Hide resolved
// Left indicates the request is successful, but the response code was not 2xx.
case Left(errorMessage) => println(s"${TextToRed}Request Failed with message : $errorMessage, and status code : ${value.code}${TextToNormal}")
j-mie6 marked this conversation as resolved.
Show resolved Hide resolved
// Right indicates a successful request with 2xx response code.
case Right(body) => println(s"Request successful with message : $body")
}
}
}
}

object RemoteView extends DebugView.Reusable with RemoteView {
// Default port uses HTTP port and local host
override protected val port: Int = 80
override protected val address: String = "127.0.0.1"

private final val MinimalIpLength: Int = "0.0.0.0".length
private final val MaximalIpLength: Int = "255.255.255.255".length

private final val MaxUserPort: Integer = 0xFFFF

/** Do some basic validations for a given IP address. */
private def checkIp(address: String): Boolean = {
val addrLenValid: Boolean = address.length >= MinimalIpLength && address.length <= MaximalIpLength
val addrDotValid: Boolean = address.count(_ == '.') == 3

// Check that every number is a number
val numberStrings: Array[String] = address.split('.')
val addrNumValid: Boolean = numberStrings.forall((number: String) => number.length > 0 && {
number.toIntOption match {
case None => false
case Some(number) => number >= 0x0 && number <= 0xFF
}
}
)

addrLenValid && addrDotValid && addrNumValid
}

/** Create a new instance of [[RemoteView]] with a given custom port. */
def apply(userPort: Integer = port, userAddress: String = address): RemoteView = new RemoteView {
require(userPort <= MaxUserPort, s"Remote View port invalid : $userPort > $MaxUserPort")
require(checkIp(userAddress), s"Remote View address invalid : $userAddress")

override implicit val port = userPort
j-mie6 marked this conversation as resolved.
Show resolved Hide resolved
override implicit val address = userAddress
}
}

/** Helper object for connecting to the DILL backend. */
object DillRemoteView extends DebugView.Reusable with RemoteView {
// Default endpoint for DILL backend is port 0x444C ("DL") on localhost
override protected val port: Int = 0x444C
j-mie6 marked this conversation as resolved.
Show resolved Hide resolved
override protected val address: String = "127.0.0.1"

/** Create a new instance of [[RemoteView]] with default ports for the DILL backend server. */
def apply(userAddress: String = address): RemoteView = RemoteView(port, userAddress)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2023 Parsley Debug View Contributors <https://github.com/j-mie6/parsley-debug-views/graphs/contributors>
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package parsley.debug.internal

import parsley.debug.DebugTree
import java.io.Writer

import upickle.default.{ReadWriter => RW, macroRW}

/**
* Case class instance of the DebugTree structure.
*
* This will be serialised to JSON structures of the following form.
*
* {
* name : String
* internal : String
* success : Boolean
* number : Long
* input : String
* children : [DebugTree]
* }
*
* @param name (Possibly) User defined name.
* @param internal Internal parser name.
* @param success Did the parser succeed.
* @param number The unique child number of this node.
* @param input The input string passed to the parser.
* @param children An array of child nodes.
*/
private case class SerialisableDebugTree(name: String, internal: String, success: Boolean, number: Long, input: String, children: List[SerialisableDebugTree])

private object SerialisableDebugTree {
implicit val rw: RW[SerialisableDebugTree] = macroRW
}

private case class SerialisablePayload(input: String, tree: SerialisableDebugTree)

private object SerialisablePayload {
implicit val rw: RW[SerialisablePayload] = macroRW
}

/**
* The Debug Tree Serialiser contains methods for transforming the parsley.debug.DebugTree to a
* JSON stream.
*/
object DebugTreeSerialiser {

private def convertDebugTree(tree: DebugTree): SerialisableDebugTree = {
val children: List[SerialisableDebugTree] = tree.nodeChildren.map(convertDebugTree(_))
SerialisableDebugTree(tree.parserName, tree.internalName, tree.parseResults.exists(_.success), tree.childNumber.getOrElse(0), tree.fullInput, children)
}

/**
* Write a DebugTree to a writer stream as JSON.
*
* @param file A valid writer object.
* @param tree The DebugTree.
*/
def writeJSON(file: Writer, input: String, tree: DebugTree): Unit = {
val treeRoot: SerialisableDebugTree = this.convertDebugTree(tree)
upickle.default.writeTo(SerialisablePayload(input, treeRoot), file)
}

/**
* Transform the DebugTree to a JSON string.
*
* @param tree The DebugTree
* @return JSON formatted String
*/
def toJSON(input: String, tree: DebugTree): String = {
val treeRoot: SerialisableDebugTree = this.convertDebugTree(tree)
upickle.default.write(SerialisablePayload(input, treeRoot))
}
}
Loading