From 0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 9 Oct 2019 17:10:43 -0400 Subject: Migrate build to mill --- client/src/Main.scala | 62 +++++++++++ client/src/http/ArrayUtils.scala | 28 +++++ client/src/http/CurlBackend.scala | 215 ++++++++++++++++++++++++++++++++++++++ client/src/http/curl.scala | 82 +++++++++++++++ client/src/http/package.scala | 3 + client/src/main/scala/Main.scala | 62 ----------- 6 files changed, 390 insertions(+), 62 deletions(-) create mode 100644 client/src/Main.scala create mode 100644 client/src/http/ArrayUtils.scala create mode 100644 client/src/http/CurlBackend.scala create mode 100644 client/src/http/curl.scala create mode 100644 client/src/http/package.scala delete mode 100644 client/src/main/scala/Main.scala (limited to 'client') diff --git a/client/src/Main.scala b/client/src/Main.scala new file mode 100644 index 0000000..4b11fe0 --- /dev/null +++ b/client/src/Main.scala @@ -0,0 +1,62 @@ +package triad + +import ApiProtocol._ +import http.Request +import commando._ +import spray.json._ + +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.util.control.NonFatal + +object Main { + + val command: Command = cmd("triad")( + opt("server", 's', "url" -> true), + opt("verbose", 'v') + ).sub( + cmd("message")( + opt("author", param = "name" -> true), + pos("content") + ).run { args => + val server = + args.get("server").map(_.head).getOrElse("http://localhost:9090") + val author = args.get("author").map(_.head).getOrElse(sys.env("USER")) + val content = args("content").head + val verbose = args.get("verbose").map(_ => true).getOrElse(false) + + val message = Message(content, author).toJson.compactPrint + + val req = Request("POST", + s"$server/messages", + Map("Content-type" -> "application/json"), + message.getBytes("utf-8")) + + if (verbose) { + System.err.println(req.url) + System.err.println(message) + } + + try { + Await.result(http.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) + } + }, + cmd("completion")().run { _ => + System.out.println(command.completion) + } + ) + + def main(args: Array[String]): Unit = commando.parse(args, command) + +} diff --git a/client/src/http/ArrayUtils.scala b/client/src/http/ArrayUtils.scala new file mode 100644 index 0000000..00c1067 --- /dev/null +++ b/client/src/http/ArrayUtils.scala @@ -0,0 +1,28 @@ +package triad +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/client/src/http/CurlBackend.scala b/client/src/http/CurlBackend.scala new file mode 100644 index 0000000..4dc8577 --- /dev/null +++ b/client/src/http/CurlBackend.scala @@ -0,0 +1,215 @@ +package triad +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()) + +} + +trait CurlBackend extends Backend { + def send(req: Request): Future[Response] = Zone { implicit z => + Future.fromTry(CurlBackend.request(req)) + } +} diff --git a/client/src/http/curl.scala b/client/src/http/curl.scala new file mode 100644 index 0000000..1eda584 --- /dev/null +++ b/client/src/http/curl.scala @@ -0,0 +1,82 @@ +package triad +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 + +} diff --git a/client/src/http/package.scala b/client/src/http/package.scala new file mode 100644 index 0000000..63b5405 --- /dev/null +++ b/client/src/http/package.scala @@ -0,0 +1,3 @@ +package triad + +package object http extends CurlBackend diff --git a/client/src/main/scala/Main.scala b/client/src/main/scala/Main.scala deleted file mode 100644 index 4b11fe0..0000000 --- a/client/src/main/scala/Main.scala +++ /dev/null @@ -1,62 +0,0 @@ -package triad - -import ApiProtocol._ -import http.Request -import commando._ -import spray.json._ - -import scala.concurrent._ -import scala.concurrent.duration._ -import scala.util.control.NonFatal - -object Main { - - val command: Command = cmd("triad")( - opt("server", 's', "url" -> true), - opt("verbose", 'v') - ).sub( - cmd("message")( - opt("author", param = "name" -> true), - pos("content") - ).run { args => - val server = - args.get("server").map(_.head).getOrElse("http://localhost:9090") - val author = args.get("author").map(_.head).getOrElse(sys.env("USER")) - val content = args("content").head - val verbose = args.get("verbose").map(_ => true).getOrElse(false) - - val message = Message(content, author).toJson.compactPrint - - val req = Request("POST", - s"$server/messages", - Map("Content-type" -> "application/json"), - message.getBytes("utf-8")) - - if (verbose) { - System.err.println(req.url) - System.err.println(message) - } - - try { - Await.result(http.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) - } - }, - cmd("completion")().run { _ => - System.out.println(command.completion) - } - ) - - def main(args: Array[String]): Unit = commando.parse(args, command) - -} -- cgit v1.2.3