aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@inpher.io>2019-11-26 12:57:50 -0500
committerJakob Odersky <jakob@inpher.io>2019-11-26 13:40:36 -0500
commit62b5a6a24a8824f996ad82b7bf0b0e5f1210326a (patch)
treeb32dd1fd4e3b5e3ae3e449ff7c051daa31e28a28
parentf38dd59d93f56213a8400841c7ebb0a7202144a7 (diff)
downloadscala-tutorial-62b5a6a24a8824f996ad82b7bf0b0e5f1210326a.tar.gz
scala-tutorial-62b5a6a24a8824f996ad82b7bf0b0e5f1210326a.tar.bz2
scala-tutorial-62b5a6a24a8824f996ad82b7bf0b0e5f1210326a.zip
Add CLI based on scalanative
-rw-r--r--build.sc7
-rw-r--r--cli/src/Main.scala66
-rw-r--r--cli/src/http/ArrayUtils.scala27
-rw-r--r--cli/src/http/CurlBackend.scala227
-rw-r--r--cli/src/http/Request.scala8
-rw-r--r--cli/src/http/Response.scala7
-rw-r--r--cli/src/http/curl.scala85
7 files changed, 426 insertions, 1 deletions
diff --git a/build.sc b/build.sc
index 3799ef2..3912908 100644
--- a/build.sc
+++ b/build.sc
@@ -1,4 +1,4 @@
-import mill._, scalalib._, scalajslib._
+import mill._, scalalib._, scalajslib._, scalanativelib._
trait Shared extends ScalaModule {
def sharedSources = T.sources(build.millSourcePath / "shared")
@@ -46,3 +46,8 @@ object webapp extends ScalaJSModule with Shared {
}
}
+
+object cli extends ScalaNativeModule {
+ def scalaVersion = "2.11.12" // scala native does not support newer versions yet
+ def scalaNativeVersion = "0.3.8"
+}
diff --git a/cli/src/Main.scala b/cli/src/Main.scala
new file mode 100644
index 0000000..e7cb5aa
--- /dev/null
+++ b/cli/src/Main.scala
@@ -0,0 +1,66 @@
+
+import java.time.Instant
+import scala.collection.mutable
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.util.control.NonFatal
+
+object Main extends App {
+
+ val usage = """Usage: cli [--author=<author>] content"""
+
+ def error(message: String) = {
+ System.err.println(message)
+ System.err.println(usage)
+ sys.exit(1)
+ }
+
+ // process arguments
+ var author: Option[String] = None
+ val content = new mutable.StringBuilder
+
+ val it = args.iterator
+ while(it.hasNext) {
+ val arg = it.next()
+ if (arg == "--author") {
+ if (it.hasNext) {
+ author = Some(it.next())
+ } else {
+ error("expected author")
+ }
+ } else if (arg.startsWith("--")) {
+ error("invalid option")
+ } else {
+ content ++= arg
+ }
+ }
+
+ // we could use the shared message model if we used a serialization library
+ // that also supports scala native, such as https://github.com/jodersky/spray-json/
+ //
+ // val message = Message(author, Instant.now.getEpochSecond, content)
+
+ val message = s"""{"message":{
+ "author": "${author.getOrElse(sys.env("USER"))}",
+ "date" : ${Instant.now().toEpochMilli / 1000},
+ "content": "$content"
+ }}"""
+ val req = http.Request("POST", "http://localhost:8080", body = message.getBytes)
+
+ try {
+ Await.result(http.CurlBackend.send(req), 10.seconds) match {
+ case resp if 200 <= resp.statusCode && resp.statusCode <= 300 =>
+ sys.exit(0)
+ case resp =>
+ System.err.println(
+ s"Bad response code while posting message ${resp.statusCode}."
+ )
+ sys.exit(1)
+ }
+ } catch {
+ case NonFatal(e) =>
+ System.err.println(e.getMessage)
+ sys.exit(1)
+ }
+
+}
diff --git a/cli/src/http/ArrayUtils.scala b/cli/src/http/ArrayUtils.scala
new file mode 100644
index 0000000..80944d4
--- /dev/null
+++ b/cli/src/http/ArrayUtils.scala
@@ -0,0 +1,27 @@
+package http
+
+import scala.scalanative.native._
+
+object ArrayUtils {
+
+ def toBuffer(array: Array[Byte])(implicit z: Zone): Ptr[Byte] = {
+ val buffer = z.alloc(array.size)
+ var i = 0
+ while (i < array.size) {
+ buffer(i) = array(i)
+ i += 1
+ }
+ buffer
+ }
+
+ def toArray(buffer: Ptr[Byte], size: CSize): Array[Byte] = {
+ val array = new Array[Byte](size.toInt)
+ var i = 0
+ while (i < array.size) {
+ array(i) = buffer(i)
+ i += 1
+ }
+ array
+ }
+
+}
diff --git a/cli/src/http/CurlBackend.scala b/cli/src/http/CurlBackend.scala
new file mode 100644
index 0000000..472d750
--- /dev/null
+++ b/cli/src/http/CurlBackend.scala
@@ -0,0 +1,227 @@
+package http
+
+import curl._
+import curlh._
+
+import scala.collection.{Map, mutable}
+import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.Future
+import scala.scalanative.native._
+import scala.util.{Failure, Success, Try}
+
+object CurlBackend {
+
+ type Chunk = CStruct4[Ptr[CStruct0], Ptr[CStruct0], CSize, Ptr[Byte]]
+ implicit class ChunksOps(val self: Ptr[Chunk]) extends AnyVal {
+ @inline def prev: Ptr[Chunk] = (!self._1).cast[Ptr[Chunk]]
+ @inline def prev_=(other: Ptr[Chunk]): Unit =
+ !(self._1) = other.cast[Ptr[CStruct0]]
+ @inline def next: Ptr[Chunk] = (!self._2).cast[Ptr[Chunk]]
+ @inline def next_=(other: Ptr[Chunk]): Unit =
+ !(self._2) = other.cast[Ptr[CStruct0]]
+ @inline def size: CSize = !(self._3)
+ @inline def size_=(value: CSize): Unit = !(self._3) = value
+ @inline def buffer: Ptr[Byte] = !(self._4)
+ @inline def buffer_=(value: Ptr[Byte]): Unit = !(self._4) = value
+ }
+
+ object Chunk {
+
+ @inline def NullPtr[T] = 0.cast[Ptr[T]]
+
+ def allocHead() = allocAppend(0, NullPtr[Chunk])
+
+ def allocAppend(
+ size: CSize,
+ head: Ptr[Chunk] = NullPtr[Chunk]
+ ): Ptr[Chunk] = {
+ val chunk: Ptr[Chunk] = stdlib.malloc(sizeof[Chunk]).cast[Ptr[Chunk]]
+ if (chunk == NullPtr[Chunk]) return NullPtr[Chunk]
+
+ chunk.buffer = stdlib.malloc(size)
+
+ if (chunk.buffer == NullPtr[Chunk] && size != 0) {
+ stdlib.free(chunk.cast[Ptr[Byte]])
+ return NullPtr[Chunk]
+ }
+ chunk.size = size
+
+ if (head == NullPtr[Chunk]) { // this will be the head
+ chunk.next = chunk
+ chunk.prev = chunk
+ } else {
+ val last = head.prev
+ last.next = chunk
+ chunk.prev = last
+ head.prev = chunk
+ chunk.next = head
+ }
+ chunk
+ }
+
+ def freeAll(head: Ptr[Chunk]): Unit = {
+ var chunk: Ptr[Chunk] = head
+ do {
+ val next = chunk.next
+ stdlib.free(chunk.buffer)
+ stdlib.free(chunk.cast[Ptr[Byte]])
+ chunk = next
+ } while (chunk != head)
+ }
+
+ def toArray(head: Ptr[Chunk]): Array[Byte] = {
+ val buffer = new ArrayBuffer[Byte]()
+ var chunk = head
+ do {
+ val next = chunk.next
+ var i = 0L
+ while (i < next.size) {
+ buffer += next.buffer(i)
+ i += 1
+ }
+ chunk = next
+ } while (chunk != head)
+ buffer.toArray
+ }
+
+ def traverse(head: Ptr[Chunk])(fct: Array[Byte] => Unit) = {
+ var chunk = head
+ do {
+ val next = chunk.next
+ val buffer = new ArrayBuffer[Byte]()
+ var i = 0L
+ while (i < next.size) {
+ buffer += next.buffer(i)
+ i += 1
+ }
+ chunk = next
+ fct(buffer.toArray)
+ } while (chunk != head)
+ }
+
+ }
+
+ private def receive(
+ data: Ptr[Byte],
+ size: CSize,
+ nmemb: CSize,
+ userdata: Ptr[Byte]
+ ): CSize = {
+ val head = userdata.cast[Ptr[Chunk]]
+ val length = size * nmemb
+ val chunk = Chunk.allocAppend(length, head)
+ string.memcpy(chunk.buffer, data, chunk.size)
+ chunk.size
+ }
+ private val receivePtr: WriteFunction = CFunctionPtr.fromFunction4(receive)
+
+ private def chain[A](success: A)(calls: (() => A)*) = {
+ var result: A = success
+ for (c <- calls if result == success) {
+ result = c()
+ }
+ result
+ }
+
+ private def request(request: Request)(implicit z: Zone): Try[Response] = {
+ val curl: CURL = curl_easy_init()
+ if (curl != null) {
+ val errorBuffer = stackalloc[Byte](CURL_ERROR_SIZE)
+ !errorBuffer = 0
+ val requestHeaders = stackalloc[curl_slist](1)
+ !requestHeaders = 0.cast[curl_slist]
+
+ val responseChunks = Chunk.allocHead()
+ val responseHeaderChunks = Chunk.allocHead()
+
+ val curlResult = chain(CURLcode.CURL_OK)(
+ () =>
+ curl_easy_setopt(curl, CURLoption.CURLOPT_ERRORBUFFER, errorBuffer),
+ () =>
+ curl_easy_setopt(
+ curl,
+ CURLoption.CURLOPT_CUSTOMREQUEST,
+ toCString(request.method)
+ ),
+ () =>
+ curl_easy_setopt(
+ curl,
+ CURLoption.CURLOPT_URL,
+ toCString(request.url)
+ ),
+ () => {
+ val buffer = ArrayUtils.toBuffer(request.body)
+ curl_easy_setopt(curl, CURLoption.CURLOPT_POSTFIELDS, buffer)
+ curl_easy_setopt(
+ curl,
+ CURLoption.CURLOPT_POSTFIELDSIZE,
+ request.body.size
+ )
+ },
+ () => {
+ for ((k, v) <- request.headers) {
+ !requestHeaders =
+ curl_slist_append(!requestHeaders, toCString(s"$k:$v"))
+ }
+ curl_easy_setopt(curl, CURLoption.CURLOPT_HTTPHEADER, !requestHeaders)
+ },
+ () =>
+ curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEFUNCTION, receivePtr),
+ () =>
+ curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEDATA, responseChunks),
+ () =>
+ curl_easy_setopt(
+ curl,
+ CURLoption.CURLOPT_HEADERDATA,
+ responseHeaderChunks
+ ),
+ () => curl_easy_perform(curl)
+ )
+
+ val result = curlResult match {
+ case CURLcode.CURL_OK =>
+ val responseCode: Ptr[Long] = stackalloc[Long](1)
+ curl_easy_getinfo(curl, CURLINFO.CURLINFO_RESPONSE_CODE, responseCode)
+
+ val responseHeaders = mutable.HashMap.empty[String, String]
+ Chunk.traverse(responseHeaderChunks) { headerChunk =>
+ val line = new String(headerChunk, "utf-8").trim
+ if (line.contains(":")) {
+ val parts = line.split(":", 2)
+ responseHeaders += parts(0) -> parts(1)
+ }
+ }
+
+ Success(
+ Response(
+ statusCode = (!responseCode).toInt,
+ headers = responseHeaders.toMap,
+ body = Chunk.toArray(responseChunks)
+ )
+ )
+
+ case code =>
+ val message = curl_easy_strerror(curl, code)
+ Failure(
+ new RuntimeException(
+ s"${fromCString(errorBuffer)} (curl exit status $code)"
+ )
+ )
+ }
+ Chunk.freeAll(responseChunks)
+ Chunk.freeAll(responseHeaderChunks)
+ curl_slist_free_all(!requestHeaders)
+ curl_easy_cleanup(curl)
+ result
+ } else {
+ Failure(new RuntimeException(s"curl failed to initialize"))
+ }
+ }
+
+ def curlVersion = fromCString(curl_version())
+
+ def send(req: Request): Future[Response] = Zone { implicit z =>
+ Future.fromTry(CurlBackend.request(req))
+ }
+
+}
diff --git a/cli/src/http/Request.scala b/cli/src/http/Request.scala
new file mode 100644
index 0000000..c1544f8
--- /dev/null
+++ b/cli/src/http/Request.scala
@@ -0,0 +1,8 @@
+package http
+
+case class Request(
+ method: String,
+ url: String,
+ headers: Map[String, String] = Map.empty,
+ body: Array[Byte] = Array.empty
+)
diff --git a/cli/src/http/Response.scala b/cli/src/http/Response.scala
new file mode 100644
index 0000000..d271daa
--- /dev/null
+++ b/cli/src/http/Response.scala
@@ -0,0 +1,7 @@
+package http
+
+case class Response(
+ statusCode: Int,
+ headers: Map[String, String],
+ body: Array[Byte]
+)
diff --git a/cli/src/http/curl.scala b/cli/src/http/curl.scala
new file mode 100644
index 0000000..9107353
--- /dev/null
+++ b/cli/src/http/curl.scala
@@ -0,0 +1,85 @@
+package http
+
+import scala.scalanative.native._
+
+object curlh {
+ final val CURL_ERROR_SIZE = 256
+
+ type CURL = Ptr[CStruct0]
+
+ /*
+ * #define CURLOPTTYPE_LONG 0
+ * #define CURLOPTTYPE_OBJECTPOINT 10000
+ * #define CURLOPTTYPE_STRINGPOINT 10000
+ * #define CURLOPTTYPE_FUNCTIONPOINT 20000
+ * #define CURLOPTTYPE_OFF_T 30000
+ */
+
+ type CURLcode = CInt
+ object CURLcode {
+ final val CURL_OK: CInt = 0
+ }
+
+ type CURLoption = CInt
+ object CURLoption {
+ final val CURLOPT_VERBOSE: CInt = 41
+ final val CURLOPT_POSTFIELDSIZE: CInt = 60
+ final val CURLOPT_WRITEDATA: CInt = 10001
+ final val CURLOPT_URL: CInt = 10002
+ final val CURLOPT_ERRORBUFFER: CInt = 10010
+ final val CURLOPT_POSTFIELDS: CInt = 10015
+ final val CURLOPT_HTTPHEADER: CInt = 10023
+ final val CURLOPT_HEADERDATA: CInt = 10029
+ final val CURLOPT_CUSTOMREQUEST: CInt = 10036
+ final val CURLOPT_WRITEFUNCTION: CInt = 20011
+ }
+
+ type CURLINFO = CInt
+ object CURLINFO {
+ final val CURLINFO_RESPONSE_CODE = 0x200002
+ }
+
+ type WriteFunction = CFunctionPtr4[
+ Ptr[Byte], // data
+ CSize, // size
+ CSize, // nmemb
+ Ptr[Byte], // userdata
+ CSize // return
+ ]
+
+ type curl_slist = Ptr[CStruct0]
+
+}
+
+@link("curl")
+@extern
+object curl {
+ import curlh._
+
+ def curl_easy_init(): CURL = extern
+
+ def curl_easy_setopt(
+ curl: CURL,
+ option: CURLoption,
+ parameter: CVararg*
+ ): CURLcode = extern
+
+ def curl_easy_perform(curl: CURL): CURLcode = extern
+
+ def curl_easy_getinfo(
+ curl: CURL,
+ option: CURLINFO,
+ parameter: CVararg*
+ ): CURLcode = extern
+
+ def curl_easy_cleanup(curl: CURL): Unit = extern
+
+ def curl_easy_strerror(curl: CURL, code: CURLcode): CString = extern
+
+ def curl_slist_append(head: curl_slist, string: CString): curl_slist = extern
+
+ def curl_slist_free_all(head: curl_slist): Unit = extern
+
+ def curl_version(): CString = extern
+
+}