From fbba780f00d3ad58aa6d7ef16a07ff3e2c78162f Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Mon, 23 Feb 2026 17:37:04 +0000 Subject: [PATCH] Revert "Vendor in-process NGINX Unit and simplify build (#181)" This reverts commit 88b1511a93f22a20e77a772d087265df8f299ae4. --- .github/workflows/build.yml | 81 +- .mill-version | 1 + .scalafmt.conf | 5 + README.md | 66 +- build.mill => build.mill.scala | 196 +- integration/test/src/BaseTests.scala | 5 - integration/test/src/UndertowCaskTests.scala | 38 +- integration/test/src/WebsocketTests.scala | 36 +- integration/test/src/utils.scala | 35 +- .../src/snunit/tests/HelloWorld.scala | 62 +- mill | 288 ++- sbt-plugin/.scalafmt.conf | 1 + sbt-plugin/build.sbt | 56 + sbt-plugin/project/build.properties | 1 + sbt-plugin/project/plugins.sbt | 3 + sbt-plugin/project/versions.mill.scala | 1 + .../src/main/scala/DockerClangPlugin.scala | 48 + sbt-plugin/src/main/scala/SNUnitPlugin.scala | 43 + .../sbt-test/snunit-plugin/simple/build.sbt | 15 + .../simple/project/build.properties | 1 + .../snunit-plugin/simple/project/plugins.sbt | 5 + .../simple/src/main/scala/Main.scala | 10 + .../src/sbt-test/snunit-plugin/simple/test | 2 + .../src/snunit/CEAsyncServerBuilder.scala | 13 +- .../src/mill/snunit/RunModule.scala | 9 + .../src/snunit/plugin/SNUnit.scala | 171 ++ .../test/resources/simple/src/Main.scala | 10 + .../snunit/plugin/SNUnitMillPluginTests.scala | 47 + .../SNUnitGenericServerInterpreter.scala | 6 +- .../src/io/undertow/Undertow.scala | 10 +- .../undertow/server/util/HeaderValues.scala | 35 +- .../resources/scala-native/snunit/README.md | 21 - .../scala-native/snunit/nxt_auto_config.h | 24 - .../resources/scala-native/snunit/nxt_unit.h | 209 --- .../scala-native/snunit/nxt_unit_embed.c | 1602 ----------------- .../scala-native/snunit/nxt_unit_field.h | 39 - .../scala-native/snunit/nxt_unit_request.h | 59 - .../scala-native/snunit/nxt_unit_response.h | 34 - .../scala-native/snunit/nxt_unit_sptr.h | 41 - .../scala-native/snunit/nxt_unit_typedefs.h | 36 - .../scala-native/snunit/nxt_unit_websocket.h | 33 - .../scala-native/snunit/nxt_version.h | 24 - .../snunit/nxt_websocket_header.h | 86 - snunit/src/snunit/SyncServerBuilder.scala | 13 +- snunit/src/snunit/unsafe/unsafe.scala | 3 +- .../snunit/unsafe/BytesCoversionTests.scala | 4 +- unitd.mill.scala | 60 + versions.mill.scala | 23 + 48 files changed, 1068 insertions(+), 2543 deletions(-) create mode 100644 .mill-version rename build.mill => build.mill.scala (51%) create mode 120000 sbt-plugin/.scalafmt.conf create mode 100644 sbt-plugin/build.sbt create mode 100644 sbt-plugin/project/build.properties create mode 100644 sbt-plugin/project/plugins.sbt create mode 120000 sbt-plugin/project/versions.mill.scala create mode 100644 sbt-plugin/src/main/scala/DockerClangPlugin.scala create mode 100644 sbt-plugin/src/main/scala/SNUnitPlugin.scala create mode 100644 sbt-plugin/src/sbt-test/snunit-plugin/simple/build.sbt create mode 100644 sbt-plugin/src/sbt-test/snunit-plugin/simple/project/build.properties create mode 100644 sbt-plugin/src/sbt-test/snunit-plugin/simple/project/plugins.sbt create mode 100644 sbt-plugin/src/sbt-test/snunit-plugin/simple/src/main/scala/Main.scala create mode 100644 sbt-plugin/src/sbt-test/snunit-plugin/simple/test create mode 100644 snunit-mill-plugin/src/mill/snunit/RunModule.scala create mode 100644 snunit-mill-plugin/src/snunit/plugin/SNUnit.scala create mode 100644 snunit-mill-plugin/test/resources/simple/src/Main.scala create mode 100644 snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala delete mode 100644 snunit/resources/scala-native/snunit/README.md delete mode 100644 snunit/resources/scala-native/snunit/nxt_auto_config.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_embed.c delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_field.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_request.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_response.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_sptr.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_typedefs.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_unit_websocket.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_version.h delete mode 100644 snunit/resources/scala-native/snunit/nxt_websocket_header.h create mode 100644 unitd.mill.scala create mode 100644 versions.mill.scala diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 318391cd..e546b6e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,15 +6,24 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 - - uses: coursier/cache-action@v7 + - uses: actions/checkout@v3 + - uses: coursier/cache-action@v6 + - name: out folder cache + uses: actions/cache@v3 + with: + path: ./out + key: out-folder-cache - name: Install Dependencies run: | + curl -sL https://nginx.org/keys/nginx_signing.key | sudo apt-key add - + echo "deb https://packages.nginx.org/unit/ubuntu/ noble unit" | sudo tee -a /etc/apt/sources.list.d/unit.list + echo "deb-src https://packages.nginx.org/unit/ubuntu/ noble unit" | sudo tee -a /etc/apt/sources.list.d/unit.list sudo apt-get update - sudo apt-get install -y libidn2-dev + unit_version="1.34.*" + sudo apt-get install -y libuv1-dev libidn2-dev unit=$unit_version unit-dev=$unit_version - name: Download dependencies run: | - ./mill __.prepareOffline + __.resolvedMvnDeps + ./mill __.prepareOffline - name: Check formatting run: | ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources @@ -28,12 +37,26 @@ jobs: - name: Run Unit Tests run: ./mill snunit.test - name: Run Integration Tests - run: ./mill integration.test + run: | + sudo systemctl stop unit.service + ./mill integration.test + - name: Start NGINX Unit daemon + run: sudo systemctl start unit.service + # TODO: Enable again + # - name: Run Mill Plugin Tests + # run: ./mill -i snunit-mill-plugin.__.test + # - name: Build and Test Sbt plugin + # run: | + # ( + # cd sbt-plugin + # sudo systemctl start unit.service + # sbt scalafmtCheckAll scalafmtSbtCheck scripted + # ) check-binary-compatibility: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Check Binary Compatibility @@ -44,29 +67,35 @@ jobs: needs: build runs-on: ubuntu-24.04 env: - SONATYPE_PGP_PRIVATE_KEY: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY }} - SONATYPE_PGP_PRIVATE_KEY_PASSWORD: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY_PASSWORD }} + PGP_PASSPHRASE: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY_PASSWORD }} + PGP_SECRET: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY }} SONATYPE_USER: ${{ secrets.SONATYPE_USER }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} LANG: "en_US.UTF-8" LC_MESSAGES: "en_US.UTF-8" LC_ALL: "en_US.UTF-8" steps: - - uses: actions/checkout@v5 - - uses: coursier/cache-action@v7 - - name: Publish to Maven Central - run: | - if [[ $(git tag --points-at HEAD) != '' ]]; then - echo $SONATYPE_PGP_PRIVATE_KEY | base64 --decode > gpg_key - gpg --import --no-tty --batch --yes gpg_key - rm gpg_key - ./mill mill.scalalib.SonatypeCentralPublishModule/publishAll \ - --username "$SONATYPE_USER" \ - --password "$SONATYPE_PASSWORD" \ - --gpgArgs "--passphrase=$SONATYPE_PGP_PRIVATE_KEY_PASSWORD,--no-tty,--pinentry-mode,loopback,--batch,--yes,-a,-b" \ - --publishArtifacts __.publishArtifacts \ - --readTimeout 36000 \ - --awaitTimeout 36000 \ - --connectTimeout 36000 \ - --shouldRelease true - fi + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: coursier/cache-action@v6 + - name: Compilation cache + uses: actions/cache@v2 + with: + path: ./out + key: out + - name: Publish plugin shared locally + run: ./mill snunit-plugins-shared.__.publishLocal + - run: ./mill io.kipp.mill.ci.release.ReleaseModule/publishAll + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + # TODO: Support again Sbt plugin once Mill plugin is stable + # - name: Publish Sbt Plugin + # run: | + # if [[ $(git tag --points-at HEAD) != '' ]]; then + # cd sbt-plugin + # sbt publishSigned sonatypeBundleRelease + # fi diff --git a/.mill-version b/.mill-version new file mode 100644 index 00000000..dfa546bb --- /dev/null +++ b/.mill-version @@ -0,0 +1 @@ +0.12.14 \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index 0bb9ecb8..23df56e7 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,8 @@ version = "3.8.3" maxColumn = 120 runner.dialect = scala3 +fileOverride { + "glob:**/*.mill.scala" { + runner.dialect = scala213 + } +} diff --git a/README.md b/README.md index 087fce6c..ff00542b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,71 @@ def run = SNUnit is a Scala Native library to write HTTP server applications on top of [NGINX Unit](https://unit.nginx.org/). It allows you to write both synchronous -and asynchronous web servers with great performance. +and asynchronous web servers with automatic restart on crashes, automatic +load balancing of multiple processes, great performance and all the nice +[NGINX Unit features](http://unit.nginx.org/#key-features). + +## Running your app + +Once built your SNUnit binary, you need to deploy it to the `unitd` server. + +You need to run `unitd` in a terminal with: + +```bash +unitd --no-daemon --log /dev/stdout --control unix:control.sock +``` + +This will run `unitd` with a UNIX socket file named control.sock in your current directory. + +Then, you need to create a json file with your configuration: + +```json +{ + "listeners": { + "*:8081": { + "pass": "applications/myapp" + } + }, + "applications": { + "myapp": { + "type": "external", + "executable": "snunit/binary/path" + } + } +} +``` + +Where `executable` is the binary path which can be absolute or relative +to the `unitd` working directory. + +This configuration passes all requests sent to the port `8081` to the application `myapp`. + +To know more about configuring NGINX Unit, refer to [its documentation](http://unit.nginx.org/configuration). + +To deploy the setting you can use curl: + +```bash +curl -X PUT --unix-socket control.sock -d @config.json localhost/config +``` + +If everything went right, you should see this response: + +```json +{ + "success": "Reconfiguration done." +} +``` + +In case of problems, you will get a 4xx response like this: + +```json +{ + "error": "Invalid configuration.", + "detail": "Required parameter \"executable\" is missing." +} +``` + +Further information can be found in `unitd` logs in the running terminal. ## Sync and async support diff --git a/build.mill b/build.mill.scala similarity index 51% rename from build.mill rename to build.mill.scala index 004f6c84..42df6ab2 100644 --- a/build.mill +++ b/build.mill.scala @@ -1,35 +1,20 @@ -//| mill-version: 1.1.1 -//| mvnDeps: -//| - com.goyeau::mill-scalafix::0.6.0 -//| - com.lihaoyi::mill-contrib-buildinfo:$MILL_VERSION -//| - com.github.lolgab::mill-mima::0.2.0 - package build -import mill.*, mill.scalalib.*, mill.scalanativelib.*, mill.scalanativelib.api.* -import mill.scalalib.publish.* +import $ivy.`com.goyeau::mill-scalafix::0.3.1` +import $ivy.`io.chris-kipp::mill-ci-release::0.1.9` +import $ivy.`com.github.lolgab::mill-crossplatform::0.2.3` +import $ivy.`com.lihaoyi::mill-contrib-buildinfo:` +import $ivy.`com.github.lolgab::mill-mima::0.1.1` + +import mill._, mill.scalalib._, mill.scalanativelib._, mill.scalanativelib.api._ +import mill.scalalib.publish._ import mill.util.Jvm import com.goyeau.mill.scalafix.ScalafixModule +import io.kipp.mill.ci.release.CiReleaseModule import mill.contrib.buildinfo.BuildInfo -import com.github.lolgab.mill.mima.* - -object Versions { - val scalaNative = "0.5.10" - val upickle = "4.4.2" - val undertow = "2.3.22.Final" - val scala3 = "3.3.7" - val tapir = "1.13.6" - val cask = "0.10.2" - val catsEffect = "3.7.0-RC1" - val http4s023 = "0.23.26" - val http4s1 = "1.0.0-M41" - val utest = "0.9.5" - val osLib = "0.11.7" - val sttp = "3.11.0" - val pprint = "0.9.6" - val castor = "0.3.0" - val scalaJavaTime = "2.6.0" -} +import com.github.lolgab.mill.mima._ +import com.github.lolgab.mill.crossplatform._ +import versions.Versions val scalaVersions = Seq(Versions.scala3) @@ -40,10 +25,12 @@ val http4sAndScalaVersions = for { http4sV <- http4sVersions } yield (scalaV, http4sV) -val osLib = mvn"com.lihaoyi::os-lib:${Versions.osLib}" -val upickle = mvn"com.lihaoyi::upickle::${Versions.upickle}" -val undertow = mvn"io.undertow:undertow-core:${Versions.undertow}" -val utest = mvn"com.lihaoyi::utest::${Versions.utest}" +val osLib = ivy"com.lihaoyi::os-lib:${Versions.osLib}" +val upickle = ivy"com.lihaoyi::upickle::${Versions.upickle}" +val undertow = ivy"io.undertow:undertow-core:${Versions.undertow}" +val utest = ivy"com.lihaoyi::utest::${Versions.utest}" + +val testServerPort = 8081 object Common { trait Shared extends ScalaModule with ScalafixModule { @@ -56,15 +43,37 @@ object Common { def scalaVersion = Versions.scala3 } trait Native extends Jvm with ScalaNativeModule { - def scalaNativeVersion = Versions.scalaNative + def scalaNativeVersion = versions.Versions.scalaNative + + def baseTestConfig(binary: os.Path) = { + val appName = "app" + ujson.Obj( + "applications" -> ujson.Obj( + appName -> ujson.Obj( + "type" -> "external", + "executable" -> binary.toString + ) + ), + "listeners" -> ujson.Obj( + s"*:$testServerPort" -> ujson.Obj( + "pass" -> s"applications/$appName" + ) + ) + ) + } + + def deployTestApp() = Task.Command { + val binary = nativeLink() + unitd.runBackground(baseTestConfig(binary)) + } + } } -trait Publish extends PublishModule with Mima { - def publishVersion = util.VcsVersion.calcVcsState(Task.log).format() +trait Publish extends CiReleaseModule with Mima { def pomSettings = PomSettings( - description = "Scala Native server based on NGINX Unit", + description = "Scala Native server using NGINX Unit", organization = "com.github.lolgab", url = "https://github.com/lolgab/snunit", licenses = Seq(License.`Apache-2.0`), @@ -79,38 +88,35 @@ trait Publish extends PublishModule with Mima { } object snunit extends Common.Native with Publish { object test extends ScalaNativeTests with TestModule.Utest { - def mvnDeps = super.mvnDeps() ++ Seq(utest) + def ivyDeps = super.ivyDeps() ++ Agg(utest) } } // object `snunit-async-cats-effect` extends Common.Native with Publish { // def moduleDeps = Seq(snunit) -// def mvnDeps = +// def ivyDeps = // Task { -// super.mvnDeps() ++ Seq( -// mvn"org.typelevel::cats-effect::${Versions.catsEffect}" +// super.ivyDeps() ++ Agg( +// ivy"org.typelevel::cats-effect::${Versions.catsEffect}" // ) // } // } object `snunit-undertow` extends Common.Native with Publish { def moduleDeps = Seq(snunit) - def mvnDeps = super.mvnDeps() ++ Seq(undertow) + def ivyDeps = super.ivyDeps() ++ Agg(undertow) // Remove class and tasty files override def jar = Task { - val res = Task.dest / "out.jar" Jvm.createJar( - jar = res, - inputPaths = localClasspath().map(_.path).filter(os.exists), - manifest = manifest(), - fileFilter = (_, file) => + localClasspath().map(_.path).filter(os.exists), + manifest(), + (_, file) => file.ext match { case "class" | "tasty" => false case _ => true } ) - PathRef(res) } // Sometimes it gives problems compiling since our internal API is different // than the java API in the class files. @@ -119,15 +125,15 @@ object `snunit-undertow` extends Common.Native with Publish { object `snunit-tapir` extends Common.Native with Publish { def moduleDeps = Seq(snunit) - def mvnDeps = super.mvnDeps() ++ Seq(mvn"com.softwaremill.sttp.tapir::tapir-server::${Versions.tapir}") + def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.softwaremill.sttp.tapir::tapir-server::${Versions.tapir}") } // object `snunit-tapir-cats-effect` extends Common.Native with Publish { // def moduleDeps = Seq( // `snunit-tapir`, // `snunit-async-cats-effect` // ) -// def mvnDeps = super.mvnDeps() ++ Seq( -// mvn"com.softwaremill.sttp.tapir::tapir-cats-effect::${Versions.tapir}" +// def ivyDeps = super.ivyDeps() ++ Agg( +// ivy"com.softwaremill.sttp.tapir::tapir-cats-effect::${Versions.tapir}" // ) // } @@ -143,18 +149,18 @@ object `snunit-tapir` extends Common.Native with Publish { // case s"1.$_" => "1" // } // def artifactName = s"snunit-http4s$http4sBinaryVersion" -// def mvnDeps = super.mvnDeps() ++ Seq( -// mvn"org.http4s::http4s-server::$http4sVersion" +// def ivyDeps = super.ivyDeps() ++ Agg( +// ivy"org.http4s::http4s-server::$http4sVersion" // ) // def sources = T.sources { -// super.sources() ++ Seq(PathRef(millSourcePath / s"http4s-$http4sBinaryVersion" / "src")) +// super.sources() ++ Agg(PathRef(millSourcePath / s"http4s-$http4sBinaryVersion" / "src")) // } // } def caskSources = Task { val dest = Task.dest os.proc("git", "clone", "--branch", Versions.cask, "--depth", "1", "https://github.com/com-lihaoyi/cask", dest).call() - os.proc("git", "apply", mill.api.BuildCtx.workspaceRoot / "cask.patch").call(cwd = dest / "cask") + os.proc("git", "apply", T.workspace / "cask.patch").call(cwd = dest / "cask") PathRef(dest) } def castorSources = Task { @@ -170,10 +176,10 @@ object `snunit-cask` extends Common.Native with Publish { Seq(cask / "src", cask / "util" / "src", cask / "src-3", castor / "src", castor / "src-js-native").map(PathRef(_)) } def moduleDeps = Seq(`snunit-undertow`) - def mvnDeps = super.mvnDeps() ++ Seq( + def ivyDeps = super.ivyDeps() ++ Agg( upickle, - mvn"io.github.cquiroz::scala-java-time::${Versions.scalaJavaTime}", - mvn"com.lihaoyi::pprint::${Versions.pprint}" + ivy"io.github.cquiroz::scala-java-time::${Versions.scalaJavaTime}", + ivy"com.lihaoyi::pprint::${Versions.pprint}" ) } @@ -188,21 +194,21 @@ object integration extends ScalaModule { object `multiple-handlers` extends Common.Native { def moduleDeps = Seq(snunit) } - object `undertow-helloworld` extends Module { - object native extends Common.Native, PlatformScalaModule { + object `undertow-helloworld` extends CrossPlatform { + object native extends CrossPlatformScalaModule with Common.Native { def moduleDeps = Seq(`snunit-undertow`) } - object jvm extends Common.Jvm, PlatformScalaModule { - def mvnDeps = super.mvnDeps() ++ Seq(undertow) + object jvm extends CrossPlatformScalaModule with Common.Jvm { + def ivyDeps = super.ivyDeps() ++ Agg(undertow) } } - object `cask-helloworld` extends Module { - object jvm extends Common.Jvm, PlatformScalaModule { - def mvnDeps = super.mvnDeps() ++ Seq( - mvn"com.lihaoyi::cask:${Versions.cask}" + object `cask-helloworld` extends CrossPlatform { + object jvm extends CrossPlatformScalaModule with Common.Jvm { + def ivyDeps = super.ivyDeps() ++ Agg( + ivy"com.lihaoyi::cask:${Versions.cask}" ) } - object native extends Common.Native, PlatformScalaModule { + object native extends CrossPlatformScalaModule with Common.Native { def moduleDeps = Seq(`snunit-cask`) } } @@ -218,8 +224,8 @@ object integration extends ScalaModule { // def moduleDeps = Seq( // `snunit-http4s`(http4sVersion) // ) - // def mvnDeps = super.mvnDeps() ++ Seq( - // mvn"org.http4s::http4s-dsl::$http4sVersion" + // def ivyDeps = super.ivyDeps() ++ Agg( + // ivy"org.http4s::http4s-dsl::$http4sVersion" // ) // } // object `http4s-app` extends Common.Native { @@ -227,8 +233,8 @@ object integration extends ScalaModule { // def moduleDeps = Seq( // `snunit-http4s`(http4sVersion) // ) - // def mvnDeps = super.mvnDeps() ++ Seq( - // mvn"org.http4s::http4s-dsl::$http4sVersion" + // def ivyDeps = super.ivyDeps() ++ Agg( + // ivy"org.http4s::http4s-dsl::$http4sVersion" // ) // } // object `tapir-helloworld-cats-effect` extends Common.Native { @@ -239,18 +245,58 @@ object integration extends ScalaModule { } def scalaVersion = Versions.scala3 object test extends ScalaTests with TestModule.Utest with BuildInfo { - override def resources = super.resources() - def testParallelism = false def buildInfoMembers = Seq( + BuildInfo.Value("port", testServerPort.toString), BuildInfo.Value("scalaVersions", scalaVersions.mkString(":")), - BuildInfo.Value("http4sVersions", http4sVersions.mkString(":")) + BuildInfo.Value("http4sVersions", http4sVersions.mkString(":")), + BuildInfo.Value("unitControl", unitd.control.toString) ) def buildInfoPackageName = "snunit.test" - def mvnDeps = - Seq( + def ivyDeps = + Agg( utest, osLib, - mvn"com.softwaremill.sttp.client3::core:${Versions.sttp}" + ivy"com.softwaremill.sttp.client3::core:${Versions.sttp}" ) } } + +// TODO: Update to latest Mill API +// object `snunit-mill-plugin` extends Common.Shared with Publish with BuildInfo { +// def buildInfoMembers = Seq( +// BuildInfo.Value("snunitVersion", publishVersion()) +// ) +// def buildInfoPackageName = "snunit.plugin.internal" +// def artifactName = s"mill-snunit_mill${Versions.mill011.split('.').take(2).mkString(".")}" +// def scalaVersion = mill.main.BuildInfo.scalaVersion +// def compileIvyDeps = super.compileIvyDeps() ++ Agg( +// ivy"com.lihaoyi::mill-scalanativelib:${mill.main.BuildInfo.millVersion}" +// ) + +// object test extends ScalaTests with TestModule.Utest with BuildInfo { +// def ivyDeps = Agg( +// ivy"com.lihaoyi::mill-testkit:${mill.main.BuildInfo.millVersion}", +// ivy"com.lihaoyi::mill-scalanativelib:${mill.main.BuildInfo.millVersion}" +// ) +// def forkEnv = Map("MILL_EXECUTABLE_PATH" -> millExecutable.assembly().path.toString) +// def buildInfoMembers = Seq( +// BuildInfo.Value("scalaNativeVersion", versions.Versions.scalaNative), +// BuildInfo.Value("scalaVersion", versions.Versions.scala3) +// ) +// def buildInfoPackageName = "snunit.plugin" +// object millExecutable extends JavaModule { +// def ivyDeps = Agg( +// ivy"com.lihaoyi:mill-dist:${mill.main.BuildInfo.millVersion}" +// ) +// def mainClass = Some("mill.runner.client.MillClientMain") +// def resources = Task { +// // make sure snunit is published +// snunit.publishLocal()() + +// val p = Task.dest / "mill/local-test-overrides" / s"com.lihaoyi-${`snunit-mill-plugin`.artifactId()}" +// os.write(p, `snunit-mill-plugin`.localClasspath().map(_.path).mkString("\n"), createFolders = true) +// Seq(PathRef(Task.dest)) +// } +// } +// } +// } diff --git a/integration/test/src/BaseTests.scala b/integration/test/src/BaseTests.scala index c7d95d87..6eada065 100644 --- a/integration/test/src/BaseTests.scala +++ b/integration/test/src/BaseTests.scala @@ -32,11 +32,6 @@ object BaseTests extends TestSuite { val expectedResult = "" assert(result == expectedResult) } - locally { - val result = request.post(uri"$baseUrl/echo").body("hello").text() - val expectedResult = "hello" - assert(result == expectedResult) - } locally { val responseHeaders = request .get(uri"$baseUrl/headers") diff --git a/integration/test/src/UndertowCaskTests.scala b/integration/test/src/UndertowCaskTests.scala index bb55b369..baf3a86d 100644 --- a/integration/test/src/UndertowCaskTests.scala +++ b/integration/test/src/UndertowCaskTests.scala @@ -6,29 +6,33 @@ object UndertowCaskTests extends TestSuite { val tests = Tests { test("undertow-helloworld") { withDeployedExampleMultiplatform("undertow-helloworld") { - val result = request.get(baseUrl).text() - val expectedResult = "Hello World" - assert(result == expectedResult) + runOnAllPlatforms { baseUrl => + val result = request.get(baseUrl).text() + val expectedResult = "Hello World" + assert(result == expectedResult) + } } } test("cask-helloworld") { withDeployedExampleMultiplatform("cask-helloworld") { - locally { - val result = request.get(baseUrl).text() - val expectedResult = "Hello World!" - assert(result == expectedResult) - } + runOnAllPlatforms { baseUrl => + locally { + val result = request.get(baseUrl).text() + val expectedResult = "Hello World!" + assert(result == expectedResult) + } - locally { - val result = request.get(uri"$baseUrl/hello?name=Lorenzo").text() - val expectedResult = "Hello Lorenzo!" - assert(result == expectedResult) - } + locally { + val result = request.get(uri"$baseUrl/hello?name=Lorenzo").text() + val expectedResult = "Hello Lorenzo!" + assert(result == expectedResult) + } - locally { - val result = request.post(uri"$baseUrl/do-thing").body("hello").text() - val expectedResult = "olleh" - assert(result == expectedResult) + locally { + val result = request.post(uri"$baseUrl/do-thing").body("hello").text() + val expectedResult = "olleh" + assert(result == expectedResult) + } } } } diff --git a/integration/test/src/WebsocketTests.scala b/integration/test/src/WebsocketTests.scala index ebb7e5b9..5eaa6846 100644 --- a/integration/test/src/WebsocketTests.scala +++ b/integration/test/src/WebsocketTests.scala @@ -6,27 +6,25 @@ import sttp.client3._ object WebsocketTests extends TestSuite { val tests = Tests { test("hello-world") { - Console.err.println("!!! IGNORED !!!") - if false then - withDeployedExample("websocket-echo") { - for - response <- request - .get(websocketBaseUrl) - .websocket() + withDeployedExample("websocket-echo") { + for + response <- request + .get(websocketBaseUrl) + .websocket() - websocket = response.body + websocket = response.body - _ <- websocket.send(Frame.Ping(Array.emptyByteArray)) - _ <- websocket.send(Frame.Text("Hello", false, None)) - _ <- websocket.send(Frame.Text("World", false, None)) - case Frame.Pong(_) <- websocket.receive() - case Frame.Text(firstFrame, _, _) <- websocket.receive() - case Frame.Text(secondFrame, _, _) <- websocket.receive() - _ <- websocket.close() - yield - firstFrame ==> "Hello" - secondFrame ==> "World" - } + _ <- websocket.send(Frame.Ping(Array.emptyByteArray)) + _ <- websocket.send(Frame.Text("Hello", false, None)) + _ <- websocket.send(Frame.Text("World", false, None)) + case Frame.Pong(_) <- websocket.receive() + case Frame.Text(firstFrame, _, _) <- websocket.receive() + case Frame.Text(secondFrame, _, _) <- websocket.receive() + _ <- websocket.close() + yield + firstFrame ==> "Hello" + secondFrame ==> "World" + } } } } diff --git a/integration/test/src/utils.scala b/integration/test/src/utils.scala index 40ef2444..4f78303d 100644 --- a/integration/test/src/utils.scala +++ b/integration/test/src/utils.scala @@ -22,13 +22,8 @@ private def runMillCommand(command: String) = os ) def withDeployedExample[T](projectName: String, crossSuffix: String = "")(f: => T): T = { - val Vector(s"\"$_:$_:$_:$nativeBinary\"") = - runMillCommand(s"integration.tests.$projectName$crossSuffix.nativeLink").out.lines(): @unchecked - val workspace = os.Path(sys.env("MILL_WORKSPACE_ROOT")) - val process2 = os.proc(nativeBinary).spawn(cwd = workspace) - Thread.sleep(1000) - try { f } - finally { process2.close() } + runMillCommand(s"integration.tests.$projectName$crossSuffix.deployTestApp") + f } def withDeployedExampleHttp4s(projectName: String)(f: => Unit) = { BuildInfo.http4sVersions.split(':').foreach { versions => @@ -37,27 +32,23 @@ def withDeployedExampleHttp4s(projectName: String)(f: => Unit) = { } def withDeployedExampleMultiplatform(projectName: String)(f: => Unit) = { val projectPrefix = s"integration.tests.$projectName" - - // Run test against JVM version - val Vector(s"\"$_:$_:$_:$path\"") = runMillCommand(s"$projectPrefix.jvm.launcher").out.lines(): @unchecked - val process1 = os.proc(path).spawn() + runMillCommand(s"$projectPrefix.native.deployTestApp") + val result = runMillCommand(s"$projectPrefix.jvm.launcher").out.lines().head + val s""""$_:$_:$_:$path"""" = result: @unchecked + val process = os.proc(path).spawn() Thread.sleep(1000) try { f } - finally { process1.close() } - - val Vector(s"\"$_:$_:$_:$nativeBinary\"") = - runMillCommand(s"$projectPrefix.native.nativeLink").out.lines(): @unchecked - val process2 = os.proc(nativeBinary).spawn() - Thread.sleep(1000) - try { f } - finally { process2.close() } - + finally { process.close() } } private val futureBackend = HttpClientFutureBackend() -val baseUrl = uri"http://localhost:8080" -val websocketBaseUrl = uri"ws://localhost:8080" +val baseUrl = uri"http://localhost:${BuildInfo.port}" +val websocketBaseUrl = uri"ws://localhost:${BuildInfo.port}" + +def runOnAllPlatforms(f: Uri => Unit) = { + Seq(uri"http://localhost:8080", baseUrl).foreach(f) +} def request = quickRequest diff --git a/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala b/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala index 5af5b19b..f929a422 100644 --- a/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala +++ b/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala @@ -5,45 +5,41 @@ import snunit.* object MyHandler extends RequestHandler { val array = "Hello world!\n".getBytes def handleRequest(req: Request): Unit = { - (req.method, req.path) match - case Method.GET -> "/array" => - req.send( - statusCode = StatusCode.OK, - content = array, - headers = Headers("Content-Type" -> "text/plain") - ) - case Method.GET -> "/headers" => - req.send( - statusCode = StatusCode.OK, - content = "Request headers", - headers = req.headers - ) - case Method.GET -> path => - val content = - if (path.startsWith("/path")) req.path - else if (path.startsWith("/version")) req.version - else if (path.startsWith("/target")) req.target - else if (path.startsWith("/query")) req.query - else if (path == "/empty") "" - else "Hello world!\n" - req.send( - statusCode = StatusCode.OK, - content = content, - headers = Headers("Content-Type" -> "text/plain") - ) - case Method.POST -> "/echo" => - req.send( - statusCode = StatusCode.OK, - content = req.contentRaw(), - headers = Headers("Content-Type" -> "text/plain") - ) - + req.method match { + case Method.GET => + val path = req.path + if (path == "/array") + req.send( + statusCode = StatusCode.OK, + content = array, + headers = Headers("Content-Type" -> "text/plain") + ) + else if (path == "/headers") + req.send( + statusCode = StatusCode.OK, + content = "Request headers", + headers = req.headers + ) + else + val content = + if (path.startsWith("/path")) req.path + else if (path.startsWith("/version")) req.version + else if (path.startsWith("/target")) req.target + else if (path.startsWith("/query")) req.query + else if (path == "/empty") "" + else "Hello world!\n" + req.send( + statusCode = StatusCode.OK, + content = content, + headers = Headers("Content-Type" -> "text/plain") + ) case _ => req.send( statusCode = StatusCode.NotFound, content = s"Not found\n", headers = Headers("Content-Type" -> "text/plain") ) + } } } diff --git a/mill b/mill index 678a3eb8..4a0cb640 100755 --- a/mill +++ b/mill @@ -1,15 +1,64 @@ #!/usr/bin/env sh +# This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. +# +# This script determines the Mill version to use by trying these sources +# - env-variable `MILL_VERSION` +# - local file `.mill-version` +# - local file `.config/mill-version` +# - `mill-version` from YAML fronmatter of current buildfile +# - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) +# - env-variable `DEFAULT_MILL_VERSION` +# +# If a version has the suffix '-native' a native binary will be used. +# If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. +# If no such suffix is found, the script will pick a default based on version and platform. +# +# Once a version was determined, it tries to use either +# - a system-installed mill, if found and it's version matches +# - an already downloaded version under ~/.cache/mill/download +# +# If no working mill version was found on the system, +# this script downloads a binary file from Maven Central or Github Pages (this is version dependent) +# into a cache location (~/.cache/mill/download). +# +# Mill Project URL: https://github.com/com-lihaoyi/mill +# Script Version: 1.0.0-M1-21-7b6fae-DIRTY892b63e8 +# +# If you want to improve this script, please also contribute your changes back! +# This script was generated from: dist/scripts/src/mill.sh +# +# Licensed under the Apache License, Version 2.0 + set -e -if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.0-10-fec938"; fi +if [ "$1" = "--setup-completions" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then + DEFAULT_MILL_VERSION="0.12.10" +fi -if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi -if [ -z "$MILL_MAIN_CLI" ] ; then MILL_MAIN_CLI="${0}"; fi +if [ -z "${GITHUB_RELEASE_CDN}" ] ; then + GITHUB_RELEASE_CDN="" +fi + MILL_REPO_URL="https://github.com/com-lihaoyi/mill" +if [ -z "${CURL_CMD}" ] ; then + CURL_CMD=curl +fi + +# Explicit commandline argument takes precedence over all other methods +if [ "$1" = "--mill-version" ] ; then + echo "The --mill-version option is no longer supported." 1>&2 +fi + MILL_BUILD_SCRIPT="" if [ -f "build.mill" ] ; then @@ -20,77 +69,83 @@ elif [ -f "build.sc" ] ; then MILL_BUILD_SCRIPT="build.sc" fi -# `s/.*://`: -# This is a greedy match that removes everything from the beginning of the line up to (and including) the last -# colon (:). This effectively isolates the value part of the declaration. -# -# `s/#.*//`: -# This removes any comments at the end of the line. -# -# `s/['\"]//g`: -# This removes all single and double quotes from the string, wherever they appear (g is for "global"). -# -# `s/^[[:space:]]*//; s/[[:space:]]*$//`: -# These two expressions trim any leading or trailing whitespace ([[:space:]] matches spaces and tabs). -TRIM_VALUE_SED="s/.*://; s/#.*//; s/['\"]//g; s/^[[:space:]]*//; s/[[:space:]]*$//" +# Please note, that if a MILL_VERSION is already set in the environment, +# We reuse it's value and skip searching for a value. +# If not already set, read .mill-version file if [ -z "${MILL_VERSION}" ] ; then if [ -f ".mill-version" ] ; then MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" elif [ -f ".config/mill-version" ] ; then MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" - elif [ -f "build.mill.yaml" ] ; then - MILL_VERSION="$(grep -E "mill-version:" "build.mill.yaml" | sed -E "$TRIM_VALUE_SED")" elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then - MILL_VERSION="$(grep -E "//\|.*mill-version" "${MILL_BUILD_SCRIPT}" | sed -E "$TRIM_VALUE_SED")" + MILL_VERSION="$(cat ${MILL_BUILD_SCRIPT} | grep '//[|] *mill-version: *' | sed 's;//| *mill-version: *;;')" fi fi -if [ -z "${MILL_VERSION}" ] ; then MILL_VERSION="${DEFAULT_MILL_VERSION}"; fi - MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" -if [ -z "${MILL_FINAL_DOWNLOAD_FOLDER}" ] ; then MILL_FINAL_DOWNLOAD_FOLDER="${MILL_USER_CACHE_DIR}/download"; fi +if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then + MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" +fi -MILL_NATIVE_SUFFIX="-native" -MILL_JVM_SUFFIX="-jvm" -FULL_MILL_VERSION=$MILL_VERSION -ARTIFACT_SUFFIX="" +# If not already set, try to fetch newest from Github +if [ -z "${MILL_VERSION}" ] ; then + # TODO: try to load latest version from release page + echo "No mill version specified." 1>&2 + echo "You should provide a version via a '//| mill-version: ' comment or a '.mill-version' file." 1>&2 + + mkdir -p "${MILL_DOWNLOAD_PATH}" + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( + # we might be on OSX or BSD which don't have -d option for touch + # but probably a -A [-][[hh]mm]SS + touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) || ( + # in case we still failed, we retry the first touch command with the intention + # to show the (previously suppressed) error message + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) -# Check if GLIBC version is at least the required version -# Returns 0 (true) if GLIBC >= required version, 1 (false) otherwise -check_glibc_version() { - required_version="2.39" - required_major=$(echo "$required_version" | cut -d. -f1) - required_minor=$(echo "$required_version" | cut -d. -f2) - # Get GLIBC version from ldd --version (first line contains version like "ldd (GNU libc) 2.31") - glibc_version=$(ldd --version 2>/dev/null | head -n 1 | grep -oE '[0-9]+\.[0-9]+$' || echo "") - if [ -z "$glibc_version" ]; then - # If we can't determine GLIBC version, assume it's too old - return 1 + # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 + # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then + if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then + # we know a current latest version + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) fi - glibc_major=$(echo "$glibc_version" | cut -d. -f1) - glibc_minor=$(echo "$glibc_version" | cut -d. -f2) - if [ "$glibc_major" -gt "$required_major" ]; then - return 0 - elif [ "$glibc_major" -eq "$required_major" ] && [ "$glibc_minor" -ge "$required_minor" ]; then - return 0 + + if [ -z "${MILL_VERSION}" ] ; then + # we don't know a current latest version + echo "Retrieving latest mill version ..." 1>&2 + LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # Last resort + MILL_VERSION="${DEFAULT_MILL_VERSION}" + echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 else - return 1 + echo "Using mill version ${MILL_VERSION}" 1>&2 fi -} +fi +MILL_NATIVE_SUFFIX="-native" +MILL_JVM_SUFFIX="-jvm" +FULL_MILL_VERSION=$MILL_VERSION +ARTIFACT_SUFFIX="" set_artifact_suffix(){ if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then - # Native binaries require new enough GLIBC; fall back to JVM launcher if older - if ! check_glibc_version; then - return + if [ "$(uname -m)" = "aarch64" ]; then + ARTIFACT_SUFFIX="-native-linux-aarch64" + else + ARTIFACT_SUFFIX="-native-linux-amd64" fi - if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64" - else ARTIFACT_SUFFIX="-native-linux-amd64"; fi elif [ "$(uname)" = "Darwin" ]; then - if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64" - else ARTIFACT_SUFFIX="-native-mac-amd64"; fi + if [ "$(uname -m)" = "arm64" ]; then + ARTIFACT_SUFFIX="-native-mac-aarch64" + else + ARTIFACT_SUFFIX="-native-mac-amd64" + fi else echo "This native mill launcher supports only Linux and macOS." 1>&2 exit 1 @@ -127,77 +182,146 @@ case "$MILL_VERSION" in ;; esac -MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX" +MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" + +try_to_use_system_mill() { + if [ "$(uname)" != "Linux" ]; then + return 0 + fi + + MILL_IN_PATH="$(command -v mill || true)" + + if [ -z "${MILL_IN_PATH}" ]; then + return 0 + fi + + SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") + if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then + # MILL_IN_PATH is (very likely) a shell script and not the mill + # executable, ignore it. + return 0 + fi + + SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") + SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") + SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") + + if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then + mkdir -p "${MILL_USER_CACHE_DIR}" + fi + + SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" + if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then + parseSystemMillInfo() { + LINE_NUMBER="${1}" + # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the + # variable definition in that line in two halves and return + # the value, and finally remove the quotes. + sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ + cut -d= -f2 |\ + sed 's/"\(.*\)"/\1/' + } + + CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) + CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) + CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) + CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) + + if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ + && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ + && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then + if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${SYSTEM_MILL_PATH}" + return 0 + else + return 0 + fi + fi + fi + + SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') + + cat < "${SYSTEM_MILL_INFO_FILE}" +CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" +CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" +CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" +CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" +EOF + + if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${SYSTEM_MILL_PATH}" + fi +} +try_to_use_system_mill # If not already downloaded, download it if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then case $MILL_VERSION in 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) - MILL_DOWNLOAD_SUFFIX="" - MILL_DOWNLOAD_FROM_MAVEN=0 + DOWNLOAD_SUFFIX="" + DOWNLOAD_FROM_MAVEN=0 ;; 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) - MILL_DOWNLOAD_SUFFIX="-assembly" - MILL_DOWNLOAD_FROM_MAVEN=0 + DOWNLOAD_SUFFIX="-assembly" + DOWNLOAD_FROM_MAVEN=0 ;; *) - MILL_DOWNLOAD_SUFFIX="-assembly" - MILL_DOWNLOAD_FROM_MAVEN=1 + DOWNLOAD_SUFFIX="-assembly" + DOWNLOAD_FROM_MAVEN=1 ;; esac case $MILL_VERSION in 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) - MILL_DOWNLOAD_EXT="jar" + DOWNLOAD_EXT="jar" ;; 0.12.* ) - MILL_DOWNLOAD_EXT="exe" + DOWNLOAD_EXT="exe" ;; 0.* ) - MILL_DOWNLOAD_EXT="jar" + DOWNLOAD_EXT="jar" ;; *) - MILL_DOWNLOAD_EXT="exe" + DOWNLOAD_EXT="exe" ;; esac - MILL_TEMP_DOWNLOAD_FILE="${MILL_OUTPUT_DIR:-out}/mill-temp-download" - mkdir -p "$(dirname "${MILL_TEMP_DOWNLOAD_FILE}")" - - if [ "$MILL_DOWNLOAD_FROM_MAVEN" = "1" ] ; then - MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${MILL_DOWNLOAD_EXT}" + DOWNLOAD_FILE=$(mktemp mill.XXXXXX) + if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then + DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${DOWNLOAD_EXT}" else MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') - MILL_DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${MILL_DOWNLOAD_SUFFIX}" + DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" unset MILL_VERSION_TAG fi - if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then - echo $MILL_DOWNLOAD_URL + echo $DOWNLOAD_URL echo $MILL exit 0 fi + # TODO: handle command not found + echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 + ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" + chmod +x "${DOWNLOAD_FILE}" + mkdir -p "${MILL_DOWNLOAD_PATH}" + mv "${DOWNLOAD_FILE}" "${MILL}" - echo "Downloading mill ${MILL_VERSION} from ${MILL_DOWNLOAD_URL} ..." 1>&2 - curl -f -L -o "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL_DOWNLOAD_URL}" - - chmod +x "${MILL_TEMP_DOWNLOAD_FILE}" - - mkdir -p "${MILL_FINAL_DOWNLOAD_FOLDER}" - mv "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL}" + unset DOWNLOAD_FILE + unset DOWNLOAD_SUFFIX +fi - unset MILL_TEMP_DOWNLOAD_FILE - unset MILL_DOWNLOAD_SUFFIX +if [ -z "$MILL_MAIN_CLI" ] ; then + MILL_MAIN_CLI="${0}" fi MILL_FIRST_ARG="" -if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--help" ] ; then +if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then # Need to preserve the first position of those listed options MILL_FIRST_ARG=$1 shift fi -unset MILL_FINAL_DOWNLOAD_FOLDER +unset MILL_DOWNLOAD_PATH unset MILL_OLD_DOWNLOAD_PATH unset OLD_MILL unset MILL_VERSION diff --git a/sbt-plugin/.scalafmt.conf b/sbt-plugin/.scalafmt.conf new file mode 120000 index 00000000..4cb05e83 --- /dev/null +++ b/sbt-plugin/.scalafmt.conf @@ -0,0 +1 @@ +../.scalafmt.conf \ No newline at end of file diff --git a/sbt-plugin/build.sbt b/sbt-plugin/build.sbt new file mode 100644 index 00000000..7f060727 --- /dev/null +++ b/sbt-plugin/build.sbt @@ -0,0 +1,56 @@ +import xerial.sbt.Sonatype._ +import sjsonnew._ +import sjsonnew.BasicJsonProtocol._ +import sjsonnew.shaded.scalajson.ast.unsafe._ +import sjsonnew.support.scalajson.unsafe._ +import scala.sys.process._ + +val snunitVersion = Def.setting { + val snunitDir = baseDirectory.value / ".." + val versionString = + Process( + Seq("./mill", "--ticker", "false", "show", s"snunit-plugins-shared[${build.Versions.scala212}].publishVersion"), + cwd = snunitDir + ).!! + val JString(version) = Parser.parseFromString(versionString).get + version +} + +lazy val snunitSbtPlugin = project + .in(file(".")) + .settings( + name := "sbt-snunit", + version := snunitVersion.value, + sbtPlugin := true, + scalaVersion := build.Versions.scala212, + organization := "com.github.lolgab", + addSbtPlugin("org.scala-native" % "sbt-scala-native" % build.Versions.scalaNative), + libraryDependencies ++= Seq( + "com.github.lolgab" %% "snunit-plugins-shared" % snunitVersion.value + ), + publishTo := sonatypePublishToBundle.value, + publishMavenStyle := true, + sonatypeProfileName := "com.github.lolgab", + licenses := Seq("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")), + sonatypeProjectHosting := Some(GitHubHosting("lolgab", "snunit", "lorenzolespaul@gmail.com")), + developers := List( + Developer( + id = "lolgab", + name = "Lorenzo Gabriele", + email = "lorenzolespaul@gmail.com", + url = url("https://github.com/lolgab") + ) + ), + credentials += Credentials( + "Sonatype Nexus Repository Manager", + "oss.sonatype.org", + sys.env.getOrElse("SONATYPE_USERNAME", "username"), + sys.env.getOrElse("SONATYPE_PASSWORD", "password") + ), + scriptedLaunchOpts := { + scriptedLaunchOpts.value ++ + Seq("-Xmx1024M", "-Dplugin.version=" + version.value) + }, + scriptedBufferLog := false + ) + .enablePlugins(SbtPlugin) diff --git a/sbt-plugin/project/build.properties b/sbt-plugin/project/build.properties new file mode 100644 index 00000000..ee4c672c --- /dev/null +++ b/sbt-plugin/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.1 diff --git a/sbt-plugin/project/plugins.sbt b/sbt-plugin/project/plugins.sbt new file mode 100644 index 00000000..032365c3 --- /dev/null +++ b/sbt-plugin/project/plugins.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.13") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") diff --git a/sbt-plugin/project/versions.mill.scala b/sbt-plugin/project/versions.mill.scala new file mode 120000 index 00000000..837b2ffe --- /dev/null +++ b/sbt-plugin/project/versions.mill.scala @@ -0,0 +1 @@ +../../versions.mill.scala \ No newline at end of file diff --git a/sbt-plugin/src/main/scala/DockerClangPlugin.scala b/sbt-plugin/src/main/scala/DockerClangPlugin.scala new file mode 100644 index 00000000..9762374e --- /dev/null +++ b/sbt-plugin/src/main/scala/DockerClangPlugin.scala @@ -0,0 +1,48 @@ +package snunit.plugin + +import sbt._ +import Keys._ +import sbt.util.CacheImplicits._ +import scala.scalanative.sbtplugin.ScalaNativePlugin +import sjsonnew.{:*:, LList, LNil} +import java.nio.file.Paths + +private[plugin] object SjsonnewImplicits { + implicit val ScriptIso = LList.iso( + { (s: ClangScripts.Script) => ("path", s.path) :*: ("content", s.content) :*: LNil }, + { in: String :*: String :*: LNil => ClangScripts.Script(in._1, in._2) } + ) + + implicit val ScriptsIso = LList.iso( + { s: ClangScripts.Scripts => ("clang", s.clang) :*: ("clangpp", s.clangpp) :*: LNil }, + { in: ClangScripts.Script :*: ClangScripts.Script :*: LNil => ClangScripts.Scripts(in._1, in._2) } + ) +} + +object DockerClangPlugin extends AutoPlugin { + import SjsonnewImplicits._ + + override def requires = ScalaNativePlugin + + object autoImport { + val createDockerClangScripts = taskKey[ClangScripts.Scripts]("create docker clang scripts") + } + + import autoImport._ + + override lazy val projectSettings: Seq[Setting[_]] = Seq( + createDockerClangScripts := { + val dest = target.value + val cached = Cache.cached[Unit, ClangScripts.Scripts](dest)((_: Unit) => + ClangScripts.createAndWriteClangScripts(dest = dest.toString(), pwd = ".") + ) + cached.apply(()) + }, + ScalaNativePlugin.autoImport.nativeConfig := { + val scripts = createDockerClangScripts.value + ScalaNativePlugin.autoImport.nativeConfig.value + .withClang(Paths.get(scripts.clang.path)) + .withClangPP(Paths.get(scripts.clangpp.path)) + } + ) +} diff --git a/sbt-plugin/src/main/scala/SNUnitPlugin.scala b/sbt-plugin/src/main/scala/SNUnitPlugin.scala new file mode 100644 index 00000000..274cb886 --- /dev/null +++ b/sbt-plugin/src/main/scala/SNUnitPlugin.scala @@ -0,0 +1,43 @@ +package snunit.plugin + +import sbt._ +import Keys._ +import sbt.util.CacheImplicits._ +import scala.scalanative.sbtplugin.ScalaNativePlugin +import sjsonnew.{:*:, LList, LNil} + +object SNUnitPlugin extends AutoPlugin { + override def trigger = allRequirements + override def requires = ScalaNativePlugin + + private val shared = taskKey[SNUnitPluginShared]("") + object autoImport { + val snunitPort = settingKey[Int]("Port where the SNUnit app runs") + val snunitCurlCommand = settingKey[Seq[String]]("curl command to use") + val snunitVersion: String = snunit.plugin.internal.BuildInfo.snunitVersion + + val deployToNGINXUnit = taskKey[Unit]("Deploy app to NGINX Unit") + } + + import autoImport._ + import ScalaNativePlugin.autoImport._ + + override def projectSettings = super.projectSettings ++ Seq( + shared := { + val str = streams.value + val logger = new Logger { + def info(s: String): Unit = str.log.info(s) + def warn(s: String): Unit = str.log.warn(s) + def error(s: String): Unit = str.log.error(s) + } + new SNUnitPluginShared(BuildTool.Sbt, logger, snunitCurlCommand.value) + }, + snunitCurlCommand := Seq("curl"), + snunitPort := 8080, + deployToNGINXUnit := { + val port = snunitPort.value + val executable = (Compile / nativeLink).value + shared.value.deployToNGINXUnit(executable.toString, port) + } + ) +} diff --git a/sbt-plugin/src/sbt-test/snunit-plugin/simple/build.sbt b/sbt-plugin/src/sbt-test/snunit-plugin/simple/build.sbt new file mode 100644 index 00000000..633691a5 --- /dev/null +++ b/sbt-plugin/src/sbt-test/snunit-plugin/simple/build.sbt @@ -0,0 +1,15 @@ +enablePlugins(ScalaNativePlugin) +enablePlugins(SNUnitPlugin) + +scalaVersion := "3.4.2" +snunitPort := 8085 +snunitCurlCommand := Seq("sudo", "curl") +libraryDependencies += "com.github.lolgab" %%% "snunit" % snunitVersion + +lazy val doCallToServer = taskKey[Unit]("test that server works") +doCallToServer := { + import sys.process._ + val port = snunitPort.value + val response = s"curl -sL http://127.0.0.1:$port".!! + require(response == "Hello world\n") +} diff --git a/sbt-plugin/src/sbt-test/snunit-plugin/simple/project/build.properties b/sbt-plugin/src/sbt-test/snunit-plugin/simple/project/build.properties new file mode 100644 index 00000000..ee4c672c --- /dev/null +++ b/sbt-plugin/src/sbt-test/snunit-plugin/simple/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.1 diff --git a/sbt-plugin/src/sbt-test/snunit-plugin/simple/project/plugins.sbt b/sbt-plugin/src/sbt-test/snunit-plugin/simple/project/plugins.sbt new file mode 100644 index 00000000..3b848608 --- /dev/null +++ b/sbt-plugin/src/sbt-test/snunit-plugin/simple/project/plugins.sbt @@ -0,0 +1,5 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.github.lolgab" % "sbt-snunit" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} diff --git a/sbt-plugin/src/sbt-test/snunit-plugin/simple/src/main/scala/Main.scala b/sbt-plugin/src/sbt-test/snunit-plugin/simple/src/main/scala/Main.scala new file mode 100644 index 00000000..b22e787a --- /dev/null +++ b/sbt-plugin/src/sbt-test/snunit-plugin/simple/src/main/scala/Main.scala @@ -0,0 +1,10 @@ +import snunit.* + +object HelloWorld { + def main(args: Array[String]): Unit = { + SyncServerBuilder + .setRequestHandler(_.send(StatusCode.OK, "Hello world", Headers.empty)) + .build() + .listen() + } +} diff --git a/sbt-plugin/src/sbt-test/snunit-plugin/simple/test b/sbt-plugin/src/sbt-test/snunit-plugin/simple/test new file mode 100644 index 00000000..50c4e273 --- /dev/null +++ b/sbt-plugin/src/sbt-test/snunit-plugin/simple/test @@ -0,0 +1,2 @@ +> deployToNGINXUnit +> doCallToServer diff --git a/snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala b/snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala index fc5ab19d..cb979a32 100644 --- a/snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala +++ b/snunit-async-cats-effect/src/snunit/CEAsyncServerBuilder.scala @@ -22,9 +22,6 @@ private[snunit] object CEAsyncServerBuilder { private val initArray: Array[Byte] = new Array[Byte](sizeof[nxt_unit_init_t].toInt) private val init: nxt_unit_init_t_* = initArray.at(0).asInstanceOf[nxt_unit_init_t_*] - private var host: String = "0.0.0.0" - private var port: Int = 8080 - private var dispatcher: Dispatcher[IO] = dispatcher def setDispatcher[F[_]: LiftIO](dispatcher: Dispatcher[F]): this.type = this.dispatcher = new Dispatcher[IO] { @@ -51,20 +48,12 @@ private[snunit] object CEAsyncServerBuilder { ServerBuilder.setWebsocketHandler(websocketHandler) this } - def setHost(host: String): this.type = { - this.host = host - this - } - def setPort(port: Int): this.type = { - this.port = port - this - } def build: IO[Unit] = IO { ServerBuilder.setBaseHandlers(init) init.callbacks.add_port = CEAsyncServerBuilder.add_port init.callbacks.remove_port = CEAsyncServerBuilder.remove_port init.callbacks.quit = CEAsyncServerBuilder.quit - Zone { nxt_unit_init(init, toCString(host), port) } + nxt_unit_init(init) }.flatMap(ctx => if (ctx.isNull) IO.raiseError(new Exception("Failed to create Unit object")) else IO.unit diff --git a/snunit-mill-plugin/src/mill/snunit/RunModule.scala b/snunit-mill-plugin/src/mill/snunit/RunModule.scala new file mode 100644 index 00000000..d3d81c97 --- /dev/null +++ b/snunit-mill-plugin/src/mill/snunit/RunModule.scala @@ -0,0 +1,9 @@ +package mill.snunitinternal + +object RunModule { + + def backgroundSetup(dest: os.Path): (os.Path, os.Path, String) = { + mill.scalalib.RunModule.backgroundSetup(dest) + } + +} diff --git a/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala b/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala new file mode 100644 index 00000000..5295df86 --- /dev/null +++ b/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala @@ -0,0 +1,171 @@ +package snunit.plugin + +import mill._ +import mill.scalanativelib._ +import upickle.default._ + +trait SNUnit extends ScalaNativeModule { + def snunitVersion: String = snunit.plugin.internal.BuildInfo.snunitVersion + def snunitNGINXUnitVersion: Target[String] = Task { "1.34.1" } + def snunitNGINXUnitUser: Target[Option[String]] = Task.Input { T.env.get("USER") } + def snunitNGINXUnitGroup: Target[Option[String]] = Task.Input { T.env.get("GROUP") } + def snunitNGINXUnitSources = Task { + val dir = s"unit-${snunitNGINXUnitVersion()}" + val file = s"$dir.tar.gz" + os.write(Task.dest / file, requests.get.stream(s"https://sources.nginx.org/unit/$file")) + val unitDir = Task.dest / dir + os.proc("tar", "xzf", Task.dest / file).call(cwd = Task.dest) + PathRef(Task.dest / dir) + } + + def snunitNGINXUnitBinary = Task { + val platform = System.getProperty("os.name", "unknown").toLowerCase() + + val openSslParams = + if (platform.contains("mac")) { + List("--cc-opt=-I/opt/homebrew/opt/openssl@3/include", "--ld-opt=-L/opt/homebrew/opt/openssl@3/lib") + } else Nil + + val unitDir = snunitNGINXUnitSources().path + + os.proc( + "./configure", + "--logdir=./logdir", + "--log=/dev/stdout", + snunitNGINXUnitUser().map(user => s"--user=$user"), + snunitNGINXUnitGroup().map(group => s"--group=$group"), + "--runstatedir=./runstatedir", + "--pid=unit.pid", + "--control=unix:control.sock", + "--modulesdir=./modulesdir", + "--statedir=./statedir", + "--tmpdir=/tmp", + // "--otel", TODO: Support otel + "--openssl", + openSslParams + ).call(cwd = unitDir, stdout = os.Inherit) + os.proc("make", "build/sbin/unitd", "build/lib/libunit.a").call(cwd = unitDir, stdout = os.Inherit) + val unitd = Task.dest / "unitd" + val libunit = Task.dest / "libunit.a" + os.copy(unitDir / "build/sbin/unitd", unitd) + os.copy(unitDir / "build/lib/libunit.a", libunit) + SNUnit.NGINXUnitInstallation(unitd = PathRef(unitd), libunit = PathRef(libunit)) + } + + /** Port where SNUnit app runs + */ + def snunitPort: Target[Int] = Task { 8080 } + + def snunitNGINXUnitConfig: Target[String] = + s"""{ + | "listeners": { + | "*:${snunitPort()}": { + | "pass": "applications/app" + | } + | }, + | "applications": { + | "app": { + | "type": "external", + | "executable": "${nativeLink()}" + | } + | } + |}""".stripMargin + + def snunitNGINXUnitWorkdir = Task { + Task.dest + } + + private def runImpl = Task.Anon { + val wd = snunitNGINXUnitWorkdir() + snunitKillNGINXUnit().apply() + val statedir = wd / "statedir" + os.makeDir.all(statedir) + val nginxUnit = snunitNGINXUnitBinary().unitd.path + val nginxUnitConfig = snunitNGINXUnitConfig() + os.write.over(statedir / "conf.json", nginxUnitConfig) + (wd, nginxUnit) + } + + /** Run app on NGINX Unit + */ + override def run(args: Task[Args] = Task.Anon(Args())): Command[Unit] = Task.Command { + val (wd, nginxUnit) = runImpl() + os.proc(nginxUnit, "--no-daemon").call(wd, stdout = os.Inherit) + + () + } + + override def runBackground(args: String*): Command[Unit] = Task.Command { + val (procUuidPath, procLockfile, procUuid) = _root_.mill.snunitinternal.RunModule.backgroundSetup(Task.dest) + + val (wd, nginxUnit) = runImpl() + + mill.util.Jvm.runSubprocess( + mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", + classPath = mill.scalalib.ZincWorkerModule.backgroundWrapperClasspath().map(_.path).toSeq, + jvmArgs = Nil, + envArgs = forkEnv(), + mainArgs = Seq( + procUuidPath.toString, + procLockfile.toString, + procUuid, + "500", + "", + nginxUnit.toString, + "--no-daemon" + ) ++ args, + workingDir = wd, + background = true, + useCpPassingJar = false, + runBackgroundLogToConsole = true, + javaHome = mill.scalalib.ZincWorkerModule.javaHome().map(_.path) + ) + + () + } + + def snunitKillNGINXUnit(): Command[Unit] = Task.Command { + val pidFile = snunitNGINXUnitWorkdir() / "unit.pid" + + if (os.exists(pidFile)) { + os.proc("kill", os.read(snunitNGINXUnitWorkdir() / "unit.pid").trim).call(stdout = os.Inherit) + } + + () + } + + // def buildDocker(): Command[Unit] = T.command { + // // TODO + // } + + override def nativeLinkingOptions: Target[Seq[String]] = Task { + val unitBinary = snunitNGINXUnitBinary() + super.nativeLinkingOptions() ++ Seq( + unitBinary.libunit.path.toString + ) + } +} + +object SNUnit { + case class NGINXUnitInstallation(unitd: mill.api.PathRef, libunit: mill.api.PathRef) + object NGINXUnitInstallation { + implicit val rw: ReadWriter[NGINXUnitInstallation] = macroRW + } + + // TODO: Support programmatic config + case class NGINXUnitConfig( + listeners: NGINXUnitConfig.Listeners, + applications: NGINXUnitConfig.Applications + ) + object NGINXUnitConfig { + case class Listeners() + object Listeners { + implicit val rw: ReadWriter[Listeners] = macroRW + } + case class Applications() + object Applications { + implicit val rw: ReadWriter[Applications] = macroRW + } + implicit val rw: ReadWriter[NGINXUnitConfig] = macroRW + } +} diff --git a/snunit-mill-plugin/test/resources/simple/src/Main.scala b/snunit-mill-plugin/test/resources/simple/src/Main.scala new file mode 100644 index 00000000..ee2c45e0 --- /dev/null +++ b/snunit-mill-plugin/test/resources/simple/src/Main.scala @@ -0,0 +1,10 @@ +import snunit.* + +object HelloWorld { + def main(args: Array[String]): Unit = { + SyncServerBuilder + .setRequestHandler(_.send(StatusCode.OK, "TEST SNUnit Mill Plugin", Headers.empty)) + .build() + .listen() + } +} diff --git a/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala b/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala new file mode 100644 index 00000000..db08a388 --- /dev/null +++ b/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala @@ -0,0 +1,47 @@ +package snunit.plugin + +import mill._ +import mill.scalalib._ +import mill.testkit.{TestBaseModule, UnitTester} +import utest._ +import java.util.concurrent.atomic.AtomicBoolean + +object SNUnitMillPluginTests extends TestSuite { + def tests: Tests = Tests { + test("simple") { + val port = 45354 + object build extends TestBaseModule with SNUnit { + def scalaVersion = BuildInfo.scalaVersion + def scalaNativeVersion = BuildInfo.scalaNativeVersion + override def snunitPort = port + override def ivyDeps = Task { super.ivyDeps() ++ Agg(ivy"com.github.lolgab::snunit::$snunitVersion") } + } + + val resourceFolder = os.Path(sys.env("MILL_TEST_RESOURCE_DIR").split(";").head) + + UnitTester(build, resourceFolder / "simple").scoped { eval => + val ended = new AtomicBoolean(false) + scala.concurrent.ExecutionContext.global.execute { () => + eval(build.run()) + ended.set(true) + } + var started = false + while (!started && !ended.get()) { + try { + val response = requests.get(s"http://127.0.0.1:$port").text() + started = true + assert(response == "TEST SNUnit Mill Plugin") + } catch { + case (_: java.net.ConnectException) | _: requests.UnknownHostException => + println("waiting for server to start...") + Thread.sleep(5000) + } + } + if (ended.get()) { + sys.error("NGINX Unit failed to run") + } + eval(build.snunitKillNGINXUnit()) + } + } + } +} diff --git a/snunit-tapir/src/snunit/tapir/SNUnitGenericServerInterpreter.scala b/snunit-tapir/src/snunit/tapir/SNUnitGenericServerInterpreter.scala index d951fadf..14c9c54d 100644 --- a/snunit-tapir/src/snunit/tapir/SNUnitGenericServerInterpreter.scala +++ b/snunit-tapir/src/snunit/tapir/SNUnitGenericServerInterpreter.scala @@ -96,9 +96,7 @@ private[tapir] trait SNUnitGenericServerInterpreter { private val deleteFile: TapirFile => Wrapper[Unit] = _ => monadError.unit(()) implicit val bodyListener: BodyListener[Wrapper, Array[Byte]] = new BodyListener[Wrapper, Array[Byte]] { - def onComplete(body: Array[Byte])(cb: Try[Unit] => Wrapper[Unit]): Wrapper[Array[Byte]] = { - cb(Success(())).map(_ => body) - } + def onComplete(body: Array[Byte])(cb: Try[Unit] => Wrapper[Unit]): Wrapper[Array[Byte]] = ??? } private class SNUnitServerRequest(req: snunit.Request) extends ServerRequest { @@ -147,7 +145,7 @@ private[tapir] trait SNUnitGenericServerInterpreter { wrapSideEffect( req.send(snunit.StatusCode.NotFound, Array.emptyByteArray, snunit.Headers.empty) ) - case RequestResult.Response(response, _) => + case RequestResult.Response(response) => val body = response.body.getOrElse(Array.emptyByteArray) val headers = snunit.Headers(response.headers, _.name, _.value) wrapSideEffect( diff --git a/snunit-undertow/src/io/undertow/Undertow.scala b/snunit-undertow/src/io/undertow/Undertow.scala index ba230bc7..808461d8 100644 --- a/snunit-undertow/src/io/undertow/Undertow.scala +++ b/snunit-undertow/src/io/undertow/Undertow.scala @@ -9,8 +9,6 @@ class Undertow private (builder: Undertow.Builder) { def start(): Unit = { SyncServerBuilder .setRequestHandler(req => handler.handleRequest(new HttpServerExchange(req))) - .setHost(builder.host) - .setPort(builder.port) .build() .listen() } @@ -20,14 +18,8 @@ object Undertow { final class Builder private[Undertow] () { private[Undertow] var handler: HttpHandler = null - private[Undertow] var host: String = null - private[Undertow] var port: Int = -1 - - def addHttpListener(port: Int, host: String): Builder = - this.host = host - this.port = port - this + def addHttpListener(port: Int, host: String): Builder = this def setHandler(handler: HttpHandler): Builder = { this.handler = handler this diff --git a/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala b/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala index 04854705..67b8350b 100644 --- a/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala +++ b/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala @@ -7,8 +7,28 @@ final class HeaderValues private[undertow] (key: String, value: String) with java.util.Deque[String] with java.util.List[String] { def getHeaderName(): String = key + def addFirst(x$1: String): Unit = ??? + def addLast(x$1: String): Unit = ??? def descendingIterator(): java.util.Iterator[String] = ??? def element(): String = ??? + def getFirst(): String = ??? + def getLast(): String = ??? + def offer(x$1: String): Boolean = ??? + def offerFirst(x$1: String): Boolean = ??? + def offerLast(x$1: String): Boolean = ??? + def peek(): String = ??? + def peekFirst(): String = ??? + def peekLast(): String = ??? + def poll(): String = ??? + def pollFirst(): String = ??? + def pollLast(): String = ??? + def pop(): String = ??? + def push(x$1: String): Unit = ??? + def remove(): String = ??? + def removeFirst(): String = ??? + def removeFirstOccurrence(x$1: Object): Boolean = ??? + def removeLast(): String = ??? + def removeLastOccurrence(x$1: Object): Boolean = ??? // Members declared in java.util.List def add(x$1: Int, x$2: String): Unit = ??? @@ -23,19 +43,4 @@ final class HeaderValues private[undertow] (key: String, value: String) def set(x$1: Int, x$2: String): String = ??? def size(): Int = ??? def subList(x$1: Int, x$2: Int): java.util.List[String] = ??? - def offer(x$0: String): Boolean = ??? - def offerFirst(x$0: String): Boolean = ??? - def offerLast(x$0: String): Boolean = ??? - def peek(): String = ??? - def peekFirst(): String = ??? - def peekLast(): String = ??? - def poll(): String = ??? - def pollFirst(): String = ??? - def pollLast(): String = ??? - def pop(): String = ??? - def push(x$0: String): Unit = ??? - def remove(): String = ??? - def removeFirstOccurrence(x$0: Object): Boolean = ??? - def removeLastOccurrence(x$0: Object): Boolean = ??? - override def reversed(): HeaderValues = ??? } diff --git a/snunit/resources/scala-native/snunit/README.md b/snunit/resources/scala-native/snunit/README.md deleted file mode 100644 index 24e60751..00000000 --- a/snunit/resources/scala-native/snunit/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Vendored in-process NGINX Unit (embed mode) - -This directory contains a **minimal in-process** implementation of the NGINX Unit application API (`nxt_unit_*`). It allows snunit to use the **same Scala API** (`unsafe.scala`, `Request`, `SyncServer`, etc.) while serving HTTP **directly in the application process**, without running the Unit daemon (`unitd`) or linking against `libunit.a`. - -## Contents - -- **nxt_unit_embed.c** – Standalone implementation of the `nxt_unit_*` API backed by a minimal HTTP/1 server (listen, accept, parse request, call your `request_handler`, send response). Provides the same symbols as `libunit` so your existing `@link("unit")` Scala code works unchanged. -- **Unit app headers** – Struct definitions and declarations that match the official Unit API so the embed implementation and Scala Native use the same layout: - - `nxt_unit_typedefs.h`, `nxt_unit_sptr.h`, `nxt_unit_field.h`, `nxt_unit_request.h`, `nxt_unit_response.h`, `nxt_unit.h` -- **nxt_auto_config.h**, **nxt_version.h** – Minimal stubs for the embed build (no Unit `configure`). - -## Build - -Scala Native compiles all `.c` (and `.cpp`) files under `src/main/resources/scala-native` (or `resources/scala-native` for the snunit library) and links them into the final binary. No extra build step is needed. - -When using this vendored embed implementation: - -1. **Do not** link against the external `libunit.a` (do not add `-lunit`). The symbols are provided by `nxt_unit_embed.c`. If you use the snunit Mill plugin, it adds `-lunit` for the “run with unitd” workflow; for in-process serving, **do not** use that plugin’s `nativeLinkingOptions` (or use a run target that does not link `libunit` and does not start `unitd`). -2. **Run the binary directly**: build your app with snunit as a dependency, then run the native binary (e.g. `./out` or `mill run` without the Unit plugin). The process will listen on the host/port passed to `nxt_unit_init(init, host, port)` (e.g. via `SyncServerBuilder.setHost` / `setPort` or `CEAsyncServerBuilder.setHost` / `setPort`). - -The previous `unit_embed.c` / `unit_embed.h` (which depended on the full Unit core) have been removed; `nxt_unit_embed.c` is the single self-contained implementation and provides the full `nxt_unit_*` API. diff --git a/snunit/resources/scala-native/snunit/nxt_auto_config.h b/snunit/resources/scala-native/snunit/nxt_auto_config.h deleted file mode 100644 index 0639ef57..00000000 --- a/snunit/resources/scala-native/snunit/nxt_auto_config.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Stub for vendored embed build (no Unit configure). - */ -#ifndef _NXT_AUTO_CONFIG_H_INCLUDED_ -#define _NXT_AUTO_CONFIG_H_INCLUDED_ - -#define NXT_DEBUG 0 - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit.h b/snunit/resources/scala-native/snunit/nxt_unit.h deleted file mode 100644 index 145785dc..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit.h +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef _NXT_UNIT_H_INCLUDED_ -#define _NXT_UNIT_H_INCLUDED_ - -#include -#include -#include -#include - -#include "nxt_auto_config.h" -#include "nxt_version.h" -#include "nxt_unit_typedefs.h" -#include "nxt_unit_request.h" -#include "nxt_unit_response.h" - -enum { - NXT_UNIT_OK = 0, - NXT_UNIT_ERROR = 1, - NXT_UNIT_AGAIN = 2, - NXT_UNIT_CANCELLED = 3, -}; - -enum { - NXT_UNIT_LOG_ALERT = 0, - NXT_UNIT_LOG_ERR = 1, - NXT_UNIT_LOG_WARN = 2, - NXT_UNIT_LOG_NOTICE = 3, - NXT_UNIT_LOG_INFO = 4, - NXT_UNIT_LOG_DEBUG = 5, -}; - -#define NXT_UNIT_INIT_ENV "NXT_UNIT_INIT" -#define NXT_UNIT_SHARED_PORT_ID ((uint16_t) 0xFFFFu) - -struct nxt_unit_s { void *data; }; - -struct nxt_unit_ctx_s { - void *data; - nxt_unit_t *unit; -}; - -struct nxt_unit_port_id_s { - pid_t pid; - uint32_t hash; - uint16_t id; -}; - -struct nxt_unit_port_s { - nxt_unit_port_id_t id; - int in_fd; - int out_fd; - void *data; -}; - -struct nxt_unit_buf_s { - char *start; - char *free; - char *end; -}; - -struct nxt_unit_request_info_s { - nxt_unit_t *unit; - nxt_unit_ctx_t *ctx; - nxt_unit_port_t *response_port; - nxt_unit_request_t *request; - nxt_unit_buf_t *request_buf; - nxt_unit_response_t *response; - nxt_unit_buf_t *response_buf; - uint32_t response_max_fields; - nxt_unit_buf_t *content_buf; - uint64_t content_length; - int content_fd; - void *data; -}; - -struct nxt_unit_callbacks_s { - void (*request_handler)(nxt_unit_request_info_t *req); - void (*data_handler)(nxt_unit_request_info_t *req); - void (*websocket_handler)(nxt_unit_websocket_frame_t *ws); - void (*close_handler)(nxt_unit_request_info_t *req); - int (*add_port)(nxt_unit_ctx_t *, nxt_unit_port_t *port); - void (*remove_port)(nxt_unit_t *, nxt_unit_ctx_t *, nxt_unit_port_t *port); - void (*remove_pid)(nxt_unit_t *, pid_t pid); - void (*quit)(nxt_unit_ctx_t *); - void (*shm_ack_handler)(nxt_unit_ctx_t *); - ssize_t (*port_send)(nxt_unit_ctx_t *, nxt_unit_port_t *port, - const void *buf, size_t buf_size, - const void *oob, size_t oob_size); - ssize_t (*port_recv)(nxt_unit_ctx_t *, nxt_unit_port_t *port, - void *buf, size_t buf_size, void *oob, size_t *oob_size); - int (*ready_handler)(nxt_unit_ctx_t *); -}; - -struct nxt_unit_init_s { - void *data; - void *ctx_data; - int max_pending_requests; - uint32_t request_data_size; - uint32_t shm_limit; - uint32_t request_limit; - nxt_unit_callbacks_t callbacks; - nxt_unit_port_t ready_port; - uint32_t ready_stream; - nxt_unit_port_t router_port; - nxt_unit_port_t read_port; - int shared_port_fd; - int shared_queue_fd; - int log_fd; -}; - -typedef ssize_t (*nxt_unit_read_func_t)(nxt_unit_read_info_t *read_info, - void *dst, size_t size); - -struct nxt_unit_read_info_s { - nxt_unit_read_func_t read; - int eof; - uint32_t buf_size; - void *data; -}; - -nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port); -int nxt_unit_run(nxt_unit_ctx_t *ctx); -int nxt_unit_run_ctx(nxt_unit_ctx_t *ctx); -int nxt_unit_run_shared(nxt_unit_ctx_t *ctx); -nxt_unit_request_info_t *nxt_unit_dequeue_request(nxt_unit_ctx_t *ctx); -int nxt_unit_run_once(nxt_unit_ctx_t *ctx); -int nxt_unit_process_port_msg(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port); -void nxt_unit_done(nxt_unit_ctx_t *ctx); -nxt_unit_ctx_t *nxt_unit_ctx_alloc(nxt_unit_ctx_t *, void *); -void nxt_unit_port_id_init(nxt_unit_port_id_t *port_id, pid_t pid, uint16_t id); -uint16_t nxt_unit_field_hash(const char *name, size_t name_length); -void nxt_unit_split_host(char *host_start, uint32_t host_length, - char **name, uint32_t *name_length, char **port, uint32_t *port_length); -void nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req); - -int nxt_unit_response_init(nxt_unit_request_info_t *req, - uint16_t status, uint32_t max_fields_count, uint32_t max_fields_size); -int nxt_unit_response_realloc(nxt_unit_request_info_t *req, - uint32_t max_fields_count, uint32_t max_fields_size); -int nxt_unit_response_is_init(nxt_unit_request_info_t *req); -int nxt_unit_response_add_field(nxt_unit_request_info_t *req, - const char *name, uint8_t name_length, - const char *value, uint32_t value_length); -int nxt_unit_response_add_content(nxt_unit_request_info_t *req, - const void *src, uint32_t size); -int nxt_unit_response_send(nxt_unit_request_info_t *req); -int nxt_unit_response_is_sent(nxt_unit_request_info_t *req); -nxt_unit_buf_t *nxt_unit_response_buf_alloc(nxt_unit_request_info_t *req, - uint32_t size); -int nxt_unit_request_is_websocket_handshake(nxt_unit_request_info_t *req); -int nxt_unit_response_upgrade(nxt_unit_request_info_t *req); -int nxt_unit_response_is_websocket(nxt_unit_request_info_t *req); -nxt_unit_request_info_t *nxt_unit_get_request_info_from_data(void *data); -int nxt_unit_buf_send(nxt_unit_buf_t *buf); -void nxt_unit_buf_free(nxt_unit_buf_t *buf); -nxt_unit_buf_t *nxt_unit_buf_next(nxt_unit_buf_t *buf); -uint32_t nxt_unit_buf_max(void); -uint32_t nxt_unit_buf_min(void); -int nxt_unit_response_write(nxt_unit_request_info_t *req, const void *start, - size_t size); -ssize_t nxt_unit_response_write_nb(nxt_unit_request_info_t *req, - const void *start, size_t size, size_t min_size); -int nxt_unit_response_write_cb(nxt_unit_request_info_t *req, - nxt_unit_read_info_t *read_info); -ssize_t nxt_unit_request_read(nxt_unit_request_info_t *req, void *dst, - size_t size); -ssize_t nxt_unit_request_readline_size(nxt_unit_request_info_t *req, - size_t max_size); -void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc); - -int nxt_unit_websocket_send(nxt_unit_request_info_t *req, uint8_t opcode, - uint8_t last, const void *start, size_t size); -int nxt_unit_websocket_sendv(nxt_unit_request_info_t *req, uint8_t opcode, - uint8_t last, const struct iovec *iov, int iovcnt); -ssize_t nxt_unit_websocket_read(nxt_unit_websocket_frame_t *ws, void *dst, - size_t size); -int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws); -void nxt_unit_websocket_done(nxt_unit_websocket_frame_t *ws); - -void *nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size); -void nxt_unit_free(nxt_unit_ctx_t *ctx, void *p); - -#if defined __has_attribute && __has_attribute(format) -#define NXT_ATTR_FORMAT __attribute__((format(printf, 3, 4))) -#else -#define NXT_ATTR_FORMAT -#endif - -void nxt_unit_log(nxt_unit_ctx_t *ctx, int level, const char *fmt, ...) NXT_ATTR_FORMAT; -void nxt_unit_req_log(nxt_unit_request_info_t *req, int level, - const char *fmt, ...) NXT_ATTR_FORMAT; - -#endif /* _NXT_UNIT_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c deleted file mode 100644 index 86e1f298..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ /dev/null @@ -1,1602 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Standalone implementation of the nxt_unit_* API backed by an in-process - * HTTP/1 server. Same API as libunit so snunit's Scala API works unchanged. - * Listen address/port: passed to nxt_unit_init(init, host, port). - */ - -#define _POSIX_C_SOURCE 200809L - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef SCALANATIVE_MULTITHREADING_ENABLED -#include -#endif -#include - -/* Prefer epoll (Linux) and kqueue (BSD/macOS) over poll for efficiency. */ -#if defined(__linux__) -#define EMB_USE_EPOLL 1 -#include -#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) -#define EMB_USE_KQUEUE 1 -#include -/* One write event per EV_ENABLE; avoids spin when combining apply+wait in one kevent() (unit uses EV_DISPATCH on macOS). */ -#ifdef EV_DISPATCH -#define EMB_KQ_WRITE_ONESHOT EV_DISPATCH -#else -#define EMB_KQ_WRITE_ONESHOT EV_ONESHOT -#endif -#else -#define EMB_USE_POLL 1 -#include -#endif - -#include "nxt_unit.h" -#include "nxt_unit_websocket.h" - -/* Minimal SHA-1 for WebSocket accept key (RFC 6455). Public domain. */ -#define SHA1_ROTL32(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) -static void sha1_block(uint32_t *h, const unsigned char *block) { - uint32_t w[80], a, b, c, d, e, t, f, k; - size_t j; - for (j = 0; j < 16; j++) - w[j] = (uint32_t) block[j*4] << 24 | (uint32_t) block[j*4+1] << 16 | (uint32_t) block[j*4+2] << 8 | block[j*4+3]; - for (j = 16; j < 80; j++) - w[j] = SHA1_ROTL32(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); - a = h[0]; b = h[1]; c = h[2]; d = h[3]; e = h[4]; - for (j = 0; j < 80; j++) { - if (j < 20) { f = (b & c) | ((~b) & d); k = 0x5A827999U; } - else if (j < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1U; } - else if (j < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDCU; } - else { f = b ^ c ^ d; k = 0xCA62C1D6U; } - t = SHA1_ROTL32(a, 5) + f + e + k + w[j]; - e = d; d = c; c = SHA1_ROTL32(b, 30); b = a; a = t; - } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; -} -static void sha1_hash(const unsigned char *data, size_t len, unsigned char *out) { - uint32_t h[5] = { 0x67452301U, 0xEFCDAB89U, 0x98BADCFEU, 0x10325476U, 0xC3D2E1F0U }; - unsigned char block[128]; - size_t i, rem; - uint64_t bitlen = (uint64_t) len * 8; - for (i = 0; i + 64 <= len; i += 64) - sha1_block(h, data + i); - rem = len - i; - memcpy(block, data + i, rem); - block[rem] = 0x80; - memset(block + rem + 1, 0, 64 - rem - 1); - if (rem >= 56) { - sha1_block(h, block); - memset(block, 0, 56); - } - block[56] = (unsigned char)(bitlen >> 56); block[57] = (unsigned char)(bitlen >> 48); - block[58] = (unsigned char)(bitlen >> 40); block[59] = (unsigned char)(bitlen >> 32); - block[60] = (unsigned char)(bitlen >> 24); block[61] = (unsigned char)(bitlen >> 16); - block[62] = (unsigned char)(bitlen >> 8); block[63] = (unsigned char)bitlen; - sha1_block(h, block); - out[0] = (unsigned char)(h[0] >> 24); out[1] = (unsigned char)(h[0] >> 16); out[2] = (unsigned char)(h[0] >> 8); out[3] = (unsigned char)h[0]; - out[4] = (unsigned char)(h[1] >> 24); out[5] = (unsigned char)(h[1] >> 16); out[6] = (unsigned char)(h[1] >> 8); out[7] = (unsigned char)h[1]; - out[8] = (unsigned char)(h[2] >> 24); out[9] = (unsigned char)(h[2] >> 16); out[10] = (unsigned char)(h[2] >> 8); out[11] = (unsigned char)h[2]; - out[12] = (unsigned char)(h[3] >> 24); out[13] = (unsigned char)(h[3] >> 16); out[14] = (unsigned char)(h[3] >> 8); out[15] = (unsigned char)h[3]; - out[16] = (unsigned char)(h[4] >> 24); out[17] = (unsigned char)(h[4] >> 16); out[18] = (unsigned char)(h[4] >> 8); out[19] = (unsigned char)h[4]; -} - -static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -static void base64_encode(const unsigned char *in, size_t inlen, char *out, size_t outcap) { - size_t i, j = 0; - for (i = 0; i + 3 <= inlen && j + 4 <= outcap; i += 3, j += 4) { - out[j] = b64[in[i] >> 2]; - out[j+1] = b64[((in[i] & 3) << 4) | (in[i+1] >> 4)]; - out[j+2] = b64[((in[i+1] & 15) << 2) | (in[i+2] >> 6)]; - out[j+3] = b64[in[i+2] & 63]; - } - if (i < inlen && j + 4 <= outcap) { - out[j] = b64[in[i] >> 2]; - if (inlen - i == 1) { out[j+1] = b64[(in[i] & 3) << 4]; out[j+2] = '='; out[j+3] = '='; } - else { out[j+1] = b64[((in[i] & 3) << 4) | (in[i+1] >> 4)]; out[j+2] = b64[(in[i+1] & 15) << 2]; out[j+3] = '='; } - } - out[j < outcap ? j : outcap - 1] = '\0'; -} - -/* --- Embed context --- */ -typedef struct { - nxt_unit_t unit; - nxt_unit_ctx_t ctx; - nxt_unit_init_t *init; - int listen_fd; - int port; - char listen_addr[64]; - int quit; - int ev_fd; /* epoll fd (Linux) or kqueue fd (BSD/macOS); -1 when not running */ -#if EMB_USE_KQUEUE - struct kevent *kq_changes; /* batched changes; applied in main loop kevent() */ - int kq_nchanges; - int kq_mchanges; -#endif -} embed_ctx_t; - -/* Per-connection state */ -#define RECV_BUF_SIZE 65536 -#define SEND_BUF_SIZE 65536 -/* Cap read/write syscalls per wakeup to rebalance kevent vs read/write and improve fairness. */ -#define EMB_DRAIN_MAX_READ 4 -#define EMB_DRAIN_MAX_WRITE 4 -#define REQ_POOL_SIZE 32768 -/* Reserve space for response (response struct + fields + header name/value strings). Request parsing must not use this. */ -#define RESPONSE_POOL_RESERVE 8192 -#define REQ_POOL_MAX (REQ_POOL_SIZE - RESPONSE_POOL_RESERVE) - -typedef struct conn conn_t; -struct conn { - conn_t *next; - int fd; - char recv_buf[RECV_BUF_SIZE]; - size_t recv_len; - size_t recv_parsed; - size_t headers_end; /* End of headers when request was parsed (before body reads) */ - char send_buf[SEND_BUF_SIZE]; - size_t send_len; - int request_ready; - int response_sent; - embed_ctx_t *emb; -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* Sync when handler runs on another thread (e.g. BlockingHandler): main thread waits until handler calls response_send or request_done. */ - pthread_mutex_t dispatch_mutex; - pthread_cond_t dispatch_cond; - int handler_done; -#endif - /* Request/response structs point into req_pool */ - nxt_unit_request_info_t *req_info; - nxt_unit_request_t *request; - nxt_unit_response_t *response; - nxt_unit_buf_t request_buf; - nxt_unit_buf_t response_buf; - nxt_unit_buf_t content_buf; - char req_pool[REQ_POOL_SIZE]; - size_t req_pool_used; - int is_websocket; - /* WebSocket frame state (when is_websocket) */ - nxt_websocket_header_t ws_header; - nxt_unit_websocket_frame_t ws_frame; - size_t ws_payload_off; /* offset in recv_buf */ - size_t ws_payload_len; - size_t ws_frame_size; /* total frame bytes for websocket_done */ - uint8_t ws_mask[4]; -}; - -static embed_ctx_t *global_emb; - -static conn_t *conn_new(int fd, embed_ctx_t *emb); -static void conn_free(conn_t *c); -static int conn_parse_request(conn_t *c); -static int conn_dispatch_request(conn_t *c); -static int conn_send_response(conn_t *c); -static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t **p, conn_t *c); -static uint16_t field_hash(const char *name, size_t len); - -#if EMB_USE_EPOLL -static int embed_ev_add_listen(embed_ctx_t *emb); -static int embed_ev_add_conn(embed_ctx_t *emb, conn_t *c); -static void embed_ev_remove_conn(embed_ctx_t *emb, conn_t *c); -static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want); -static void embed_ev_rearm_conn(embed_ctx_t *emb, conn_t *c); -#elif EMB_USE_KQUEUE -/* Append one change to kq_changes; flush if full (like unit's nxt_kqueue_get_kevent). */ -static struct kevent *embed_kq_change(embed_ctx_t *emb); -static void embed_kq_flush(embed_ctx_t *emb); -static int embed_ev_add_listen(embed_ctx_t *emb); -static int embed_ev_add_conn(embed_ctx_t *emb, conn_t *c); -static void embed_ev_remove_conn(embed_ctx_t *emb, conn_t *c); -static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want); -#endif - -#if EMB_USE_EPOLL -/* Listen fd: level-triggered (like kqueue listen fd without EV_CLEAR). */ -static int embed_ev_add_listen(embed_ctx_t *emb) { - struct epoll_event ev; - memset(&ev, 0, sizeof(ev)); - ev.events = EPOLLIN; - ev.data.ptr = NULL; /* listen fd identified by ptr == NULL */ - return epoll_ctl(emb->ev_fd, EPOLL_CTL_ADD, emb->listen_fd, &ev); -} -/* Connection fd: edge-triggered + oneshot (align with kqueue EV_CLEAR + EMB_KQ_WRITE_ONESHOT). */ -static int embed_ev_add_conn(embed_ctx_t *emb, conn_t *c) { - struct epoll_event ev; - memset(&ev, 0, sizeof(ev)); - ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; - if (c->send_len > 0) ev.events |= EPOLLOUT; - ev.data.ptr = c; - return epoll_ctl(emb->ev_fd, EPOLL_CTL_ADD, c->fd, &ev); -} -static void embed_ev_remove_conn(embed_ctx_t *emb, conn_t *c) { - (void) emb; - epoll_ctl(emb->ev_fd, EPOLL_CTL_DEL, c->fd, NULL); -} -static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want) { - struct epoll_event ev; - memset(&ev, 0, sizeof(ev)); - ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT | (want ? EPOLLOUT : 0); - ev.data.ptr = c; - epoll_ctl(emb->ev_fd, EPOLL_CTL_MOD, c->fd, &ev); -} -/* Re-arm after processing (EPOLLONESHOT disables fd after one event; match kqueue one-event-per-enable). */ -static void embed_ev_rearm_conn(embed_ctx_t *emb, conn_t *c) { - struct epoll_event ev; - memset(&ev, 0, sizeof(ev)); - ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; - if (c->send_len > 0) ev.events |= EPOLLOUT; - ev.data.ptr = c; - epoll_ctl(emb->ev_fd, EPOLL_CTL_MOD, c->fd, &ev); -} -#elif EMB_USE_KQUEUE -static struct kevent *embed_kq_change(embed_ctx_t *emb) { - struct kevent *kev; - if (emb->kq_nchanges >= emb->kq_mchanges) { - (void) kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, NULL, 0, NULL); - emb->kq_nchanges = 0; - } - kev = &emb->kq_changes[emb->kq_nchanges]; - emb->kq_nchanges++; - return kev; -} -static int embed_ev_add_listen(embed_ctx_t *emb) { - struct kevent *kev = embed_kq_change(emb); - EV_SET(kev, emb->listen_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL); - return 0; -} -/* EV_CLEAR: reset after retrieval. EMB_KQ_WRITE_ONESHOT: one write event per enable so single kevent(apply+wait) doesn't spin. */ -static int embed_ev_add_conn(embed_ctx_t *emb, conn_t *c) { - struct kevent *kev; - kev = embed_kq_change(emb); - EV_SET(kev, c->fd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, c); - kev = embed_kq_change(emb); - EV_SET(kev, c->fd, EVFILT_WRITE, EV_ADD | (c->send_len > 0 ? EV_ENABLE : EV_DISABLE) | EV_CLEAR | EMB_KQ_WRITE_ONESHOT, 0, 0, c); - return 0; -} -static void embed_ev_remove_conn(embed_ctx_t *emb, conn_t *c) { - struct kevent *kev; - kev = embed_kq_change(emb); - EV_SET(kev, c->fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); - kev = embed_kq_change(emb); - EV_SET(kev, c->fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); -} -/* Apply pending kq_changes before closing fd; otherwise kevent() gets EBADF. */ -static void embed_kq_flush(embed_ctx_t *emb) { - if (emb->kq_nchanges > 0) { - (void) kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, NULL, 0, NULL); - emb->kq_nchanges = 0; - } -} -static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want) { - struct kevent *kev = embed_kq_change(emb); - if (want) - EV_SET(kev, c->fd, EVFILT_WRITE, EV_ADD | EV_ENABLE | EV_CLEAR | EMB_KQ_WRITE_ONESHOT, 0, 0, c); - else - EV_SET(kev, c->fd, EVFILT_WRITE, EV_DISABLE, 0, 0, c); -} -#endif - -/* --- nxt_unit_init --- */ -nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) { - embed_ctx_t *emb; - int fd, opt = 1; - struct sockaddr_in sa; - - if (init == NULL) { - fprintf(stderr, "nxt_unit_embed: init is NULL\n"); - fflush(stderr); - return NULL; - } - if (host == NULL || host[0] == '\0') { - fprintf(stderr, "nxt_unit_embed: host is NULL or empty\n"); - fflush(stderr); - return NULL; - } - - emb = (embed_ctx_t *) calloc(1, sizeof(embed_ctx_t)); - if (emb == NULL) { - fprintf(stderr, "nxt_unit_embed: calloc failed\n"); - fflush(stderr); - return NULL; - } - emb->listen_fd = -1; - emb->ev_fd = -1; - - emb->unit.data = init->data; - emb->ctx.data = init->ctx_data; - emb->ctx.unit = &emb->unit; - emb->init = init; - - strncpy(emb->listen_addr, host, sizeof(emb->listen_addr) - 1); - emb->listen_addr[sizeof(emb->listen_addr) - 1] = '\0'; - emb->port = (port > 0) ? port : 8080; - - fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) { - fprintf(stderr, "nxt_unit_embed: socket() failed: %s\n", strerror(errno)); - fflush(stderr); - free(emb); - return NULL; - } - setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - memset(&sa, 0, sizeof(sa)); - sa.sin_family = AF_INET; - sa.sin_port = htons((uint16_t) emb->port); - if (strcmp(emb->listen_addr, "0.0.0.0") == 0) - sa.sin_addr.s_addr = INADDR_ANY; - else - inet_pton(AF_INET, emb->listen_addr, &sa.sin_addr); - if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) { - fprintf(stderr, "nxt_unit_embed: bind(%s:%d) failed: %s\n", - emb->listen_addr, emb->port, strerror(errno)); - fflush(stderr); - close(fd); - free(emb); - return NULL; - } - if (listen(fd, 128) != 0) { - fprintf(stderr, "nxt_unit_embed: listen() failed: %s\n", strerror(errno)); - fflush(stderr); - close(fd); - free(emb); - return NULL; - } - if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) { - fprintf(stderr, "nxt_unit_embed: fcntl(O_NONBLOCK) failed: %s\n", strerror(errno)); - fflush(stderr); - close(fd); - free(emb); - return NULL; - } - emb->listen_fd = fd; - global_emb = emb; - return &emb->ctx; -} - -/* --- nxt_unit_run --- */ -/* Returns 1 if conn was removed (caller must not advance p), 0 otherwise. */ -static int run_loop_process_conn(embed_ctx_t *emb, conn_t **p, conn_t *c, int can_read, int can_write, int err_or_hup) { - int n; - (void) emb; - if (err_or_hup) { - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } - /* Drain send buffer: up to EMB_DRAIN_MAX_WRITE write() per wakeup. */ - if (can_write && c->send_len > 0) { - int nw = 0; - for (;;) { - if (nw >= EMB_DRAIN_MAX_WRITE) - break; - n = (int) write(c->fd, c->send_buf, c->send_len); - nw++; - if (n > 0) { - memmove(c->send_buf, c->send_buf + (size_t) n, c->send_len - (size_t) n); - c->send_len -= (size_t) n; - if (c->send_len == 0) { -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_want_write(emb, c, 0); -#endif - break; - } - continue; - } - if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) - break; - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } - } - /* Drain recv: up to EMB_DRAIN_MAX_READ read() per wakeup. */ - if (can_read) { - int nr = 0; - for (;;) { - if (nr >= EMB_DRAIN_MAX_READ) - break; - { - size_t space = RECV_BUF_SIZE - c->recv_len; - if (space == 0) - break; - n = (int) read(c->fd, c->recv_buf + c->recv_len, space); - nr++; - if (n > 0) { - c->recv_len += (size_t) n; - continue; - } - if (n == 0) { - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } - if (errno == EAGAIN || errno == EWOULDBLOCK) - break; - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } - } - if (c->recv_len == 0) { - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } - if (c->is_websocket) { - if (conn_parse_websocket_frames(emb, p, c) != 0) { - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - if (c->send_len > 0) embed_ev_want_write(emb, c, 1); -#endif - } else { - if (conn_parse_request(c) != 0) { - *p = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - embed_ev_remove_conn(emb, c); -#if EMB_USE_KQUEUE - embed_kq_flush(emb); -#endif -#endif - conn_free(c); - return 1; - } - /* If we have req_info but not yet request_ready, we may be waiting for the full body (Content-Length) */ - if (c->req_info != NULL && !c->request_ready && c->request != NULL - && c->recv_len >= c->recv_parsed + (size_t) c->request->content_length) { - c->request_buf.end = c->recv_buf + c->recv_len; /* so app can read full body */ - c->request_ready = 1; - } - if (c->request_ready) { - c->response_sent = 0; /* allow send for this request (keep-alive: next request on same conn) */ - conn_dispatch_request(c); - conn_send_response(c); -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - if (c->send_len > 0) embed_ev_want_write(emb, c, 1); -#endif - c->request_ready = 0; - c->response_sent = 1; - /* Calculate consumed: end of headers + body length. - * recv_parsed may have been advanced by nxt_unit_request_read during handler, - * but we need to consume the entire request including the body, even if the app didn't read it. */ - size_t consumed; - if (c->request != NULL && c->request->content_length > 0) { - /* Consume full request: headers_end (stored when parsed) + body length */ - consumed = c->headers_end + (size_t) c->request->content_length; - if (consumed > c->recv_len) consumed = c->recv_len; - /* Also ensure we consume at least what was read (if body was partially read) */ - if (consumed < c->recv_parsed) consumed = c->recv_parsed; - } else { - /* No body, consumed is just what was parsed (end of headers) */ - consumed = c->recv_parsed; - } - if (consumed > 0 && consumed < c->recv_len) { - memmove(c->recv_buf, c->recv_buf + consumed, c->recv_len - consumed); - c->recv_len -= consumed; - } else if (consumed >= c->recv_len) { - c->recv_len = 0; - } - c->recv_parsed = 0; - c->headers_end = 0; - c->req_pool_used = 0; - if (!c->is_websocket) { - c->req_info = NULL; - c->request = NULL; - c->response = NULL; - } - } - } - } - return 0; -} - -int nxt_unit_run(nxt_unit_ctx_t *ctx) { - embed_ctx_t *emb; - conn_t *conns = NULL, *c, **p; - int n, new_fd, opt = 1; - struct sockaddr_in peer; - socklen_t peer_len; - - if (ctx == NULL) return NXT_UNIT_ERROR; - emb = (embed_ctx_t *) ((char *) ctx - offsetof(embed_ctx_t, ctx)); - emb->quit = 0; - -#if EMB_USE_EPOLL - { -#define EMB_EPOLL_MAXEV 256 - struct epoll_event events[EMB_EPOLL_MAXEV]; - int nevents, i; - - emb->ev_fd = epoll_create(1); - if (emb->ev_fd < 0) return NXT_UNIT_ERROR; - if (embed_ev_add_listen(emb) != 0) { - close(emb->ev_fd); - emb->ev_fd = -1; - return NXT_UNIT_ERROR; - } - while (!emb->quit) { - /* -1: block until an event. No spinning (0) or long wait (1000ms). */ - nevents = epoll_wait(emb->ev_fd, events, EMB_EPOLL_MAXEV, -1); - if (nevents < 0) { if (errno == EINTR) continue; break; } - if (nevents == 0) continue; - - for (i = 0; i < nevents; i++) { - int rev = events[i].events; - if (events[i].data.ptr == NULL) { - /* listen fd */ - peer_len = sizeof(peer); - new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (new_fd >= 0) { - fcntl(new_fd, F_SETFL, O_NONBLOCK); - setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); - c = conn_new(new_fd, emb); - if (c != NULL) { - c->next = conns; - conns = c; - if (embed_ev_add_conn(emb, c) != 0) { - conns = c->next; - conn_free(c); - } - } else - close(new_fd); - } - continue; - } - c = (conn_t *) events[i].data.ptr; - p = &conns; - while (*p != NULL && *p != c) p = &(*p)->next; - if (*p == NULL) continue; - if (!run_loop_process_conn(emb, p, c, - (rev & EPOLLIN) != 0, - (rev & EPOLLOUT) != 0, - (rev & (EPOLLERR | EPOLLHUP)) != 0)) - embed_ev_rearm_conn(emb, c); - } - } - close(emb->ev_fd); - emb->ev_fd = -1; - } -#elif EMB_USE_KQUEUE - { -#define EMB_KQUEUE_MAXEV 256 -#define EMB_KQUEUE_MAXCHANGES 512 - struct kevent events[EMB_KQUEUE_MAXEV]; - int nevents, i; - - emb->ev_fd = kqueue(); - if (emb->ev_fd < 0) return NXT_UNIT_ERROR; - emb->kq_mchanges = EMB_KQUEUE_MAXCHANGES; - emb->kq_changes = (struct kevent *) malloc((size_t) emb->kq_mchanges * sizeof(struct kevent)); - if (emb->kq_changes == NULL) { - close(emb->ev_fd); - emb->ev_fd = -1; - return NXT_UNIT_ERROR; - } - emb->kq_nchanges = 0; - if (embed_ev_add_listen(emb) != 0) { - free(emb->kq_changes); - close(emb->ev_fd); - emb->ev_fd = -1; - return NXT_UNIT_ERROR; - } - /* Single kevent(apply+wait) for low latency. EV_DISPATCH/EV_ONESHOT on write gives one event per enable so we don't spin. */ - while (!emb->quit) { - nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, events, EMB_KQUEUE_MAXEV, NULL); - emb->kq_nchanges = 0; - if (nevents < 0) { if (errno == EINTR) continue; break; } - if (nevents == 0) continue; - - for (i = 0; i < nevents; i++) { - int filter = events[i].filter; - int flags = events[i].flags; - int err_or_hup = (flags & EV_ERROR) != 0 || (flags & EV_EOF) != 0; - if (events[i].udata == NULL) { - /* listen fd */ - if (filter == EVFILT_READ && !err_or_hup) { - peer_len = sizeof(peer); - new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (new_fd >= 0) { - fcntl(new_fd, F_SETFL, O_NONBLOCK); - setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); - c = conn_new(new_fd, emb); - if (c != NULL) { - c->next = conns; - conns = c; - if (embed_ev_add_conn(emb, c) != 0) { - conns = c->next; - conn_free(c); - } - } else - close(new_fd); - } - } - continue; - } - c = (conn_t *) events[i].udata; - p = &conns; - while (*p != NULL && *p != c) p = &(*p)->next; - if (*p == NULL) continue; - if (filter == EVFILT_READ) - run_loop_process_conn(emb, p, c, 1, 0, err_or_hup); - else if (filter == EVFILT_WRITE) - run_loop_process_conn(emb, p, c, 0, 1, err_or_hup); - } - } - free(emb->kq_changes); - emb->kq_changes = NULL; - close(emb->ev_fd); - emb->ev_fd = -1; - } -#else - { - struct pollfd *pfds; - int nfds, cap, i; - - cap = 64; - pfds = (struct pollfd *) malloc((size_t) cap * sizeof(struct pollfd)); - if (pfds == NULL) return NXT_UNIT_ERROR; - - while (!emb->quit) { - nfds = 0; - pfds[nfds].fd = emb->listen_fd; - pfds[nfds].events = POLLIN; - nfds++; - for (c = conns; c != NULL; c = c->next) { - if (nfds >= cap) { - cap *= 2; - struct pollfd *np = (struct pollfd *) realloc(pfds, (size_t) cap * sizeof(struct pollfd)); - if (np == NULL) { free(pfds); return NXT_UNIT_ERROR; } - pfds = np; - } - pfds[nfds].fd = c->fd; - pfds[nfds].events = POLLIN; - if (c->send_len > 0) pfds[nfds].events |= POLLOUT; - nfds++; - } - - /* -1: block until an event. No spinning (0) or long wait (1000ms). */ - n = poll(pfds, (nfds_t) nfds, -1); - if (n < 0) { if (errno == EINTR) continue; break; } - if (n == 0) continue; - - if (pfds[0].revents & POLLIN) { - peer_len = sizeof(peer); - new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (new_fd >= 0) { - fcntl(new_fd, F_SETFL, O_NONBLOCK); - setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); - c = conn_new(new_fd, emb); - if (c != NULL) { - c->next = conns; - conns = c; - } else - close(new_fd); - } - } - - p = &conns; - while (*p != NULL) { - c = *p; - for (i = 1; i < nfds && pfds[i].fd != c->fd; i++) ; - if (i >= nfds) { p = &c->next; continue; } - if (!run_loop_process_conn(emb, p, c, - (pfds[i].revents & POLLIN) != 0, - (pfds[i].revents & POLLOUT) != 0, - (pfds[i].revents & (POLLERR | POLLHUP)) != 0)) - p = &(*p)->next; - } - } - free(pfds); - } -#endif - - for (c = conns; c != NULL; ) { - conn_t *next = c->next; -#if EMB_USE_EPOLL || EMB_USE_KQUEUE - if (emb->ev_fd >= 0) embed_ev_remove_conn(emb, c); -#endif - conn_free(c); - c = next; - } - return NXT_UNIT_OK; -} - -int nxt_unit_run_ctx(nxt_unit_ctx_t *ctx) { - return nxt_unit_run(ctx); -} -int nxt_unit_run_shared(nxt_unit_ctx_t *ctx) { - return nxt_unit_run(ctx); -} -nxt_unit_request_info_t *nxt_unit_dequeue_request(nxt_unit_ctx_t *ctx) { - (void) ctx; - return NULL; -} -int nxt_unit_run_once(nxt_unit_ctx_t *ctx) { - (void) ctx; - return NXT_UNIT_AGAIN; -} -int nxt_unit_process_port_msg(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port) { - (void) ctx; - (void) port; - return NXT_UNIT_OK; -} - -void nxt_unit_done(nxt_unit_ctx_t *ctx) { - embed_ctx_t *emb; - if (ctx == NULL) return; - emb = (embed_ctx_t *) ((char *) ctx - offsetof(embed_ctx_t, ctx)); - if (emb->listen_fd >= 0) { - close(emb->listen_fd); - emb->listen_fd = -1; - } - global_emb = NULL; - free(emb); -} - -nxt_unit_ctx_t *nxt_unit_ctx_alloc(nxt_unit_ctx_t *parent, void *data) { - (void) parent; - (void) data; - return NULL; -} -void nxt_unit_port_id_init(nxt_unit_port_id_t *port_id, pid_t pid, uint16_t id) { - port_id->pid = pid; - port_id->hash = 0; - port_id->id = id; -} -uint16_t nxt_unit_field_hash(const char *name, size_t name_length) { - return (uint16_t) field_hash(name, name_length); -} -void nxt_unit_split_host(char *host_start, uint32_t host_length, - char **name, uint32_t *name_length, char **port, uint32_t *port_length) { - (void) host_start; - (void) host_length; - (void) name; - (void) name_length; - (void) port; - (void) port_length; -} -void nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req) { - (void) req; -} - -/* --- Response API --- */ -/* Recover conn from req: (1) conn* is stored immediately before req_info in the pool (works when - * handler runs on a worker thread); (2) current_dispatch_conn is set during dispatch (works when - * handler runs on the same thread). req->data is not used (ABI/layout may differ on Scala Native). */ -static conn_t *current_dispatch_conn; - -static conn_t *req_to_conn(nxt_unit_request_info_t *req) { - conn_t *c; - if (req == NULL) return NULL; - c = *(conn_t **)((char *) req - sizeof(conn_t *)); - if (c != NULL && c->req_info == req) return c; - return current_dispatch_conn; -} - -#define RESPONSE_MIN_FIELDS 32 - -/* Pool allocations for 8-byte-aligned structs (request_info, request, response on Linux). */ -static inline void pool_align_8(conn_t *c) { - uintptr_t p = (uintptr_t)(c->req_pool + c->req_pool_used); - c->req_pool_used += (size_t)((8 - (p % 8)) % 8); -} - -int nxt_unit_response_init(nxt_unit_request_info_t *req, - uint16_t status, uint32_t max_fields_count, uint32_t max_fields_size) { - conn_t *c = req_to_conn(req); - uint32_t alloc_count; - if (c == NULL || c->response != NULL) return NXT_UNIT_ERROR; - alloc_count = max_fields_count < RESPONSE_MIN_FIELDS ? RESPONSE_MIN_FIELDS : max_fields_count; - pool_align_8(c); - c->response = (nxt_unit_response_t *) (c->req_pool + c->req_pool_used); - c->req_pool_used += sizeof(nxt_unit_response_t) + (size_t) alloc_count * sizeof(nxt_unit_field_t); - if (c->req_pool_used > REQ_POOL_SIZE) return NXT_UNIT_ERROR; - c->response->content_length = 0; - c->response->fields_count = 0; - c->response->piggyback_content_length = 0; - c->response->status = status; - req->response = c->response; - req->response_buf = &c->response_buf; - req->response_buf->start = c->send_buf + 512; - req->response_buf->free = req->response_buf->start; - req->response_buf->end = c->send_buf + SEND_BUF_SIZE; - req->response_max_fields = alloc_count; - return NXT_UNIT_OK; -} -int nxt_unit_response_realloc(nxt_unit_request_info_t *req, - uint32_t max_fields_count, uint32_t max_fields_size) { - (void) req; - (void) max_fields_count; - (void) max_fields_size; - return NXT_UNIT_ERROR; -} -int nxt_unit_response_is_init(nxt_unit_request_info_t *req) { - return req_to_conn(req) != NULL && req_to_conn(req)->response != NULL ? 1 : 0; -} -int nxt_unit_response_add_field(nxt_unit_request_info_t *req, - const char *name, uint8_t name_length, - const char *value, uint32_t value_length) { - conn_t *c = req_to_conn(req); - nxt_unit_field_t *f; - char *dst; - if (c == NULL || c->response == NULL) return NXT_UNIT_ERROR; - if (c->response->fields_count >= req->response_max_fields) return NXT_UNIT_ERROR; - if (c->req_pool_used + (size_t) name_length + (size_t) value_length + 32 > REQ_POOL_SIZE) return NXT_UNIT_ERROR; - f = &c->response->fields[c->response->fields_count]; - dst = c->req_pool + c->req_pool_used; - f->hash = (uint16_t) field_hash(name, (size_t) name_length); - f->skip = 0; - f->hopbyhop = 0; - f->name_length = name_length; - f->value_length = value_length; - f->name.offset = (uint32_t) ((uintptr_t) dst - (uintptr_t) &f->name); - memcpy(dst, name, (size_t) name_length); - dst[name_length] = '\0'; - c->req_pool_used += (size_t) name_length + 1; - f->value.offset = (uint32_t) ((uintptr_t) (c->req_pool + c->req_pool_used) - (uintptr_t) &f->value); - memcpy(c->req_pool + c->req_pool_used, value, (size_t) value_length); - *(c->req_pool + c->req_pool_used + (size_t) value_length) = '\0'; - c->req_pool_used += (size_t) value_length + 1; - c->response->fields_count++; - return NXT_UNIT_OK; -} -int nxt_unit_response_add_content(nxt_unit_request_info_t *req, const void *src, uint32_t size) { - conn_t *c = req_to_conn(req); - if (c == NULL || c->response_buf.free + size > c->response_buf.end) return NXT_UNIT_ERROR; - memcpy(c->response_buf.free, src, (size_t) size); - c->response_buf.free += size; - c->response->content_length += size; - return NXT_UNIT_OK; -} -int nxt_unit_response_send(nxt_unit_request_info_t *req) { - conn_t *c = req_to_conn(req); - int r; - if (c == NULL) return NXT_UNIT_ERROR; - r = conn_send_response(c); -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - pthread_mutex_lock(&c->dispatch_mutex); - c->handler_done = 1; - pthread_cond_signal(&c->dispatch_cond); - pthread_mutex_unlock(&c->dispatch_mutex); -#endif - return r; -} -int nxt_unit_response_is_sent(nxt_unit_request_info_t *req) { - conn_t *c = req_to_conn(req); - return c != NULL && c->response_sent ? 1 : 0; -} -nxt_unit_buf_t *nxt_unit_response_buf_alloc(nxt_unit_request_info_t *req, uint32_t size) { - conn_t *c = req_to_conn(req); - if (c == NULL) return NULL; - if (c->req_pool_used + size > REQ_POOL_SIZE) return NULL; - c->response_buf.start = c->req_pool + c->req_pool_used; - c->response_buf.free = c->response_buf.start; - c->response_buf.end = c->response_buf.start + size; - c->req_pool_used += size; - return &c->response_buf; -} -int nxt_unit_request_is_websocket_handshake(nxt_unit_request_info_t *req) { - return req != NULL && req->request != NULL && req->request->websocket_handshake ? 1 : 0; -} -int nxt_unit_response_upgrade(nxt_unit_request_info_t *req) { - conn_t *c; - nxt_unit_request_t *r; - nxt_unit_field_t *f; - char *key_ptr = NULL; - size_t key_len = 0; - unsigned char key_buf[64]; - unsigned char accept_bin[20]; - char accept_b64[32]; - const char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - size_t magic_len = 36; - int i, n; - if (req == NULL || req->request == NULL || !req->request->websocket_handshake) - return NXT_UNIT_ERROR; - c = req_to_conn(req); - if (c == NULL) return NXT_UNIT_ERROR; - r = req->request; - for (i = 0; i < (int) r->fields_count; i++) { - f = &r->fields[i]; - if (f->name_length == 18 && strncasecmp((char *) nxt_unit_sptr_get(&f->name), "Sec-WebSocket-Key", 18) == 0) { - key_ptr = (char *) nxt_unit_sptr_get(&f->value); - key_len = (size_t) f->value_length; - break; - } - } - if (key_ptr == NULL || key_len == 0 || key_len > 64) return NXT_UNIT_ERROR; - memcpy(key_buf, key_ptr, key_len); - memcpy(key_buf + key_len, magic, magic_len); - sha1_hash(key_buf, key_len + magic_len, accept_bin); - base64_encode(accept_bin, 20, accept_b64, sizeof(accept_b64)); - n = snprintf(c->send_buf, SEND_BUF_SIZE, - "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\nSec-WebSocket-Version: 13\r\n\r\n", - accept_b64); - if (n <= 0 || (size_t) n >= SEND_BUF_SIZE) return NXT_UNIT_ERROR; - c->send_len = (size_t) n; - c->response_sent = 1; - c->is_websocket = 1; - return NXT_UNIT_OK; -} -int nxt_unit_response_is_websocket(nxt_unit_request_info_t *req) { - conn_t *c = req_to_conn(req); - return (c != NULL && c->is_websocket) ? 1 : 0; -} - -/* Parse WebSocket frames from recv_buf; dispatch each to websocket_handler. Returns 0 on success, -1 on error/close. */ -static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t **p, conn_t *c) { - size_t payload_len, frame_len, ext_len, i; - uint8_t *buf; - uint64_t payload_len64; - conn_t *prev_conn; - - (void) p; - while (c->recv_parsed + 2 <= c->recv_len) { - buf = (uint8_t *) (c->recv_buf + c->recv_parsed); - memcpy(&c->ws_header, buf, 2); - ext_len = 0; - payload_len64 = (uint64_t) (buf[1] & 0x7F); - if (payload_len64 == 126) { - ext_len = 2; - if (c->recv_parsed + 4 > c->recv_len) return 0; - payload_len64 = (uint64_t) buf[2] << 8 | buf[3]; - } else if (payload_len64 == 127) { - ext_len = 8; - if (c->recv_parsed + 10 > c->recv_len) return 0; - payload_len64 = (uint64_t) buf[2] << 56 | (uint64_t) buf[3] << 48 | (uint64_t) buf[4] << 40 | (uint64_t) buf[5] << 32 - | (uint64_t) buf[6] << 24 | (uint64_t) buf[7] << 16 | (uint64_t) buf[8] << 8 | buf[9]; - } - payload_len = (size_t) payload_len64; - if (payload_len > RECV_BUF_SIZE) return -1; - frame_len = 2 + ext_len + (buf[1] & 0x80 ? 4 : 0) + payload_len; - if (c->recv_parsed + frame_len > c->recv_len) return 0; - c->ws_payload_off = c->recv_parsed + 2 + ext_len + (buf[1] & 0x80 ? 4 : 0); - c->ws_payload_len = payload_len; - if (buf[1] & 0x80) { - memcpy(c->ws_mask, buf + 2 + ext_len, 4); - for (i = 0; i < payload_len; i++) - c->recv_buf[c->ws_payload_off + i] ^= c->ws_mask[i & 3]; - } - c->ws_frame_size = frame_len; - c->content_buf.start = c->recv_buf + c->ws_payload_off; - c->content_buf.free = c->content_buf.start; - c->content_buf.end = c->content_buf.start + payload_len; - c->ws_frame.req = c->req_info; - c->ws_frame.payload_len = payload_len64; - c->ws_frame.header = &c->ws_header; - c->ws_frame.mask = NULL; - c->ws_frame.content_buf = &c->content_buf; - c->ws_frame.content_length = payload_len64; - prev_conn = current_dispatch_conn; - current_dispatch_conn = c; - if (emb->init->callbacks.websocket_handler != NULL) - emb->init->callbacks.websocket_handler(&c->ws_frame); - current_dispatch_conn = prev_conn; - if ((buf[0] & 0x0F) == 8) - return -1; - /* recv_parsed advanced by nxt_unit_websocket_done when handler is done */ - } - if (c->recv_parsed > 0 && c->recv_parsed < c->recv_len) { - memmove(c->recv_buf, c->recv_buf + c->recv_parsed, c->recv_len - c->recv_parsed); - c->recv_len -= c->recv_parsed; - c->recv_parsed = 0; - } else if (c->recv_parsed >= c->recv_len) { - c->recv_len = 0; - c->recv_parsed = 0; - } - return 0; -} - -nxt_unit_request_info_t *nxt_unit_get_request_info_from_data(void *data) { - return (nxt_unit_request_info_t *) data; -} -int nxt_unit_buf_send(nxt_unit_buf_t *buf) { - (void) buf; - return NXT_UNIT_OK; -} -void nxt_unit_buf_free(nxt_unit_buf_t *buf) { - (void) buf; -} -nxt_unit_buf_t *nxt_unit_buf_next(nxt_unit_buf_t *buf) { - (void) buf; - return NULL; -} -uint32_t nxt_unit_buf_max(void) { return 65536; } -uint32_t nxt_unit_buf_min(void) { return 4096; } -int nxt_unit_response_write(nxt_unit_request_info_t *req, const void *start, size_t size) { - conn_t *c = req_to_conn(req); - if (c == NULL || c->response_buf.free + size > c->response_buf.end) return NXT_UNIT_ERROR; - memcpy(c->response_buf.free, start, size); - c->response_buf.free += size; - c->response->content_length += (uint64_t) size; - return NXT_UNIT_OK; -} -ssize_t nxt_unit_response_write_nb(nxt_unit_request_info_t *req, - const void *start, size_t size, size_t min_size) { - (void) min_size; - if (nxt_unit_response_write(req, start, size) != NXT_UNIT_OK) return -1; - return (ssize_t) size; -} -int nxt_unit_response_write_cb(nxt_unit_request_info_t *req, nxt_unit_read_info_t *read_info) { - (void) req; - (void) read_info; - return NXT_UNIT_ERROR; -} -ssize_t nxt_unit_request_read(nxt_unit_request_info_t *req, void *dst, size_t size) { - conn_t *c = req_to_conn(req); - size_t avail; - if (c == NULL) return -1; - avail = c->recv_len - c->recv_parsed; - if (size > avail) size = avail; - memcpy(dst, c->recv_buf + c->recv_parsed, size); - c->recv_parsed += size; - return (ssize_t) size; -} -ssize_t nxt_unit_request_readline_size(nxt_unit_request_info_t *req, size_t max_size) { - (void) req; - (void) max_size; - return -1; -} -void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc) { - conn_t *c = req_to_conn(req); - (void) rc; - if (c != NULL) { -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - pthread_mutex_lock(&c->dispatch_mutex); - c->handler_done = 1; - pthread_cond_signal(&c->dispatch_cond); - pthread_mutex_unlock(&c->dispatch_mutex); -#endif - } -} - -int nxt_unit_websocket_send(nxt_unit_request_info_t *req, uint8_t opcode, - uint8_t last, const void *start, size_t size) { - conn_t *c; - size_t len, header_len, total; - uint8_t *p; - if (req == NULL) return NXT_UNIT_ERROR; - c = req_to_conn(req); - if (c == NULL || !c->is_websocket) return NXT_UNIT_ERROR; - header_len = 2 + (size > 125 ? (size > 65535 ? 8 : 2) : 0); - total = header_len + size; - if (c->send_len + total > SEND_BUF_SIZE) return NXT_UNIT_ERROR; - p = (uint8_t *) (c->send_buf + c->send_len); - p[0] = (uint8_t) ((last ? 0x80 : 0) | (opcode & 0x0F)); - if (size <= 125) { - p[1] = (uint8_t) size; - } else if (size <= 65535) { - p[1] = 126; - p[2] = (uint8_t)(size >> 8); p[3] = (uint8_t)size; - } else { - p[1] = 127; - p[2] = (uint8_t)(size >> 56); p[3] = (uint8_t)(size >> 48); p[4] = (uint8_t)(size >> 40); p[5] = (uint8_t)(size >> 32); - p[6] = (uint8_t)(size >> 24); p[7] = (uint8_t)(size >> 16); p[8] = (uint8_t)(size >> 8); p[9] = (uint8_t)size; - } - if (size > 0 && start != NULL) - memcpy(p + header_len, start, size); - c->send_len += total; - return NXT_UNIT_OK; -} -int nxt_unit_websocket_sendv(nxt_unit_request_info_t *req, uint8_t opcode, - uint8_t last, const struct iovec *iov, int iovcnt) { - size_t total = 0; - int i; - if (req == NULL || iov == NULL) return NXT_UNIT_ERROR; - for (i = 0; i < iovcnt; i++) - total += iov[i].iov_len; - if (total == 0) - return nxt_unit_websocket_send(req, opcode, last, NULL, 0); - if (total > SEND_BUF_SIZE) return NXT_UNIT_ERROR; - { - conn_t *c = req_to_conn(req); - size_t len, header_len, off = 0; - uint8_t *p; - if (c == NULL || !c->is_websocket) return NXT_UNIT_ERROR; - header_len = 2 + (total > 125 ? (total > 65535 ? 8 : 2) : 0); - if (c->send_len + header_len + total > SEND_BUF_SIZE) return NXT_UNIT_ERROR; - p = (uint8_t *) (c->send_buf + c->send_len); - p[0] = (uint8_t) ((last ? 0x80 : 0) | (opcode & 0x0F)); - if (total <= 125) { - p[1] = (uint8_t) total; - } else if (total <= 65535) { - p[1] = 126; - p[2] = (uint8_t)(total >> 8); p[3] = (uint8_t)total; - } else { - p[1] = 127; - p[2] = (uint8_t)(total >> 56); p[3] = (uint8_t)(total >> 48); p[4] = (uint8_t)(total >> 40); p[5] = (uint8_t)(total >> 32); - p[6] = (uint8_t)(total >> 24); p[7] = (uint8_t)(total >> 16); p[8] = (uint8_t)(total >> 8); p[9] = (uint8_t)total; - } - for (i = 0; i < iovcnt && off < total; i++) { - len = iov[i].iov_len; - if (len > total - off) len = total - off; - memcpy(p + header_len + off, iov[i].iov_base, len); - off += len; - } - c->send_len += header_len + total; - } - return NXT_UNIT_OK; -} -ssize_t nxt_unit_websocket_read(nxt_unit_websocket_frame_t *ws, void *dst, size_t size) { - conn_t *c; - size_t avail; - ssize_t res; - if (ws == NULL || dst == NULL) return -1; - c = req_to_conn(ws->req); - if (c == NULL) return -1; - avail = (size_t) (ws->content_buf->end - ws->content_buf->free); - if (size > avail) size = avail; - memcpy(dst, ws->content_buf->free, size); - res = (ssize_t) size; - ws->content_buf->free += size; - ws->content_length -= (uint64_t) res; /* match Unit: remaining payload length */ - return res; -} -int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) { - (void) ws; - return NXT_UNIT_OK; -} -void nxt_unit_websocket_done(nxt_unit_websocket_frame_t *ws) { - conn_t *c; - if (ws == NULL) return; - c = req_to_conn(ws->req); - if (c != NULL) - c->recv_parsed += c->ws_frame_size; -} - -void *nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size) { - (void) ctx; - return malloc(size); -} -void nxt_unit_free(nxt_unit_ctx_t *ctx, void *p) { - (void) ctx; - free(p); -} - -void nxt_unit_log(nxt_unit_ctx_t *ctx, int level, const char *fmt, ...) { - (void) ctx; - (void) level; - (void) fmt; -} -void nxt_unit_req_log(nxt_unit_request_info_t *req, int level, const char *fmt, ...) { - (void) req; - (void) level; - (void) fmt; -} - -/* --- Helpers --- */ -#define nxt_lowcase(c) ((unsigned char) ((c >= 'A' && c <= 'Z') ? c | 0x20 : c)) - -static uint16_t field_hash(const char *name, size_t len) { - /* Match nxt_unit_field_hash from NGINX Unit: lowercase before hashing */ - uint32_t hash = 159406; /* Magic value copied from nxt_http_parse.c */ - size_t i; - for (i = 0; i < len; i++) { - unsigned char ch = nxt_lowcase(name[i]); - hash = (hash << 4) + hash + ch; - } - hash = (hash >> 16) ^ hash; - return (uint16_t) hash; -} - -static conn_t *conn_new(int fd, embed_ctx_t *emb) { - conn_t *c = (conn_t *) calloc(1, sizeof(conn_t)); - if (c == NULL) return NULL; - c->fd = fd; - c->emb = emb; -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - pthread_mutex_init(&c->dispatch_mutex, NULL); - pthread_cond_init(&c->dispatch_cond, NULL); -#endif - return c; -} - -static void conn_free(conn_t *c) { - if (c == NULL) return; - if (c->fd >= 0) close(c->fd); -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - pthread_mutex_destroy(&c->dispatch_mutex); - pthread_cond_destroy(&c->dispatch_cond); -#endif - free(c); -} - -/* Percent-decode path (RFC 3986); target stays raw. Decoded length is at most src_len. */ -static size_t percent_decode_path(const char *src, size_t src_len, char *dst) { - size_t di = 0; - for (size_t si = 0; si < src_len; ) { - if (src[si] == '%' && si + 2 < src_len) { - unsigned int a = (unsigned char) src[si + 1]; - unsigned int b = (unsigned char) src[si + 2]; - int ha = (a >= '0' && a <= '9') ? a - '0' : (a >= 'A' && a <= 'F') ? a - 'A' + 10 : (a >= 'a' && a <= 'f') ? a - 'a' + 10 : -1; - int hb = (b >= '0' && b <= '9') ? b - '0' : (b >= 'A' && b <= 'F') ? b - 'A' + 10 : (b >= 'a' && b <= 'f') ? b - 'a' + 10 : -1; - if (ha >= 0 && hb >= 0) { - dst[di++] = (char) ((ha << 4) | hb); - si += 3; - continue; - } - } - dst[di++] = src[si++]; - } - return di; -} - -/* Minimal HTTP/1 request parser: request line + headers until \r\n\r\n or \n\n */ -static int conn_parse_request(conn_t *c) { - char *p, *end, *line_end, *colon; - size_t line_len, method_len, path_len, i; - size_t headers_end; - nxt_unit_request_t *req; - nxt_unit_request_info_t *req_info; - nxt_unit_field_t *f; - char *pool; - size_t pool_off; - char *host_value = NULL; - size_t host_value_len = 0; - - if (c->req_info != NULL) return 0; - end = c->recv_buf + c->recv_len; - headers_end = 0; - for (p = c->recv_buf + c->recv_parsed; p + 2 <= end; p++) { - if (p[0] == '\r' && p[1] == '\n' && p + 4 <= end && p[2] == '\r' && p[3] == '\n') { - headers_end = (size_t)(p + 4 - c->recv_buf); - break; - } - if (p[0] == '\n' && p[1] == '\n') { - headers_end = (size_t)(p + 2 - c->recv_buf); - break; - } - } - if (headers_end == 0) return 0; - c->recv_parsed = headers_end; - c->headers_end = headers_end; /* Store for calculating consumed later */ - end = c->recv_buf + headers_end; /* parse only up to end of headers */ - - /* Allocate conn* then request_info so req_to_conn can get conn from req (works from any thread) */ - if (c->req_pool_used + 8 + sizeof(conn_t *) + sizeof(nxt_unit_request_info_t) + sizeof(nxt_unit_request_t) + 64 * sizeof(nxt_unit_field_t) + 4096 > REQ_POOL_MAX) - return -1; - pool_align_8(c); - *(conn_t **)(c->req_pool + c->req_pool_used) = c; - c->req_pool_used += sizeof(conn_t *); - req_info = (nxt_unit_request_info_t *) (c->req_pool + c->req_pool_used); - c->req_pool_used += sizeof(nxt_unit_request_info_t); - req = (nxt_unit_request_t *) (c->req_pool + c->req_pool_used); - c->req_pool_used += sizeof(nxt_unit_request_t) + 64 * sizeof(nxt_unit_field_t); - pool_off = c->req_pool_used; - pool = c->req_pool + pool_off; - - req_info->unit = &c->emb->unit; - req_info->ctx = &c->emb->ctx; - req_info->response_port = NULL; - req_info->request = req; - req_info->request_buf = &c->request_buf; - req_info->response = NULL; - req_info->response_buf = NULL; - req_info->response_max_fields = 0; - req_info->content_buf = &c->content_buf; - req_info->content_length = 0; - req_info->content_fd = -1; - req_info->data = c; - - memset(req, 0, sizeof(nxt_unit_request_t)); - req->content_length_field = NXT_UNIT_NONE_FIELD; - req->content_type_field = NXT_UNIT_NONE_FIELD; - req->cookie_field = NXT_UNIT_NONE_FIELD; - req->authorization_field = NXT_UNIT_NONE_FIELD; - - p = c->recv_buf; - line_end = (char *) memchr(p, '\r', (size_t)(end - p)); - if (line_end == NULL) line_end = (char *) memchr(p, '\n', (size_t)(end - p)); - if (line_end == NULL) return -1; - line_len = (size_t)(line_end - p); - if (line_end < end && *line_end == '\r' && line_end + 1 < end && line_end[1] == '\n') - line_end++; /* skip \r for next line start */ - for (i = 0; i < line_len && p[i] != ' '; i++) ; - if (i >= line_len) return -1; - method_len = i; - while (i < line_len && p[i] == ' ') i++; - if (i >= line_len) return -1; - path_len = 0; - while (i + path_len < line_len && p[i + path_len] != ' ' && p[i + path_len] != '?') path_len++; - size_t query_len = 0; - if (i + path_len < line_len && p[i + path_len] == '?') { - size_t q_start = i + path_len + 1; - while (q_start + query_len < line_len && p[q_start + query_len] != ' ') - query_len++; - } - /* target = raw path + ? + query (full request-target for URI parsing); path = percent-decoded; query = query string */ - size_t target_len = path_len + (query_len > 0 ? 1 + query_len : 0); - if (pool_off + method_len + target_len + 1 + path_len * 2 + query_len + 64 + 8 > REQ_POOL_MAX) return -1; - memcpy(pool, p, method_len); - pool[method_len] = '\0'; - req->method_length = (uint8_t) method_len; - req->method.offset = (uint32_t) ((uintptr_t) pool - (uintptr_t) &req->method); - pool += method_len + 1; - pool_off += method_len + 1; - /* target: full request-target (path?query) as received, so URI parsers get path and query */ - memcpy(c->req_pool + pool_off, p + i, path_len); - if (query_len > 0) { - c->req_pool[pool_off + path_len] = '?'; - memcpy(c->req_pool + pool_off + path_len + 1, p + i + path_len + 1, query_len); - c->req_pool[pool_off + path_len + 1 + query_len] = '\0'; - } else { - c->req_pool[pool_off + path_len] = '\0'; - } - req->target_length = (uint32_t) target_len; - req->target.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &req->target); - pool_off += target_len + 1; - pool = c->req_pool + pool_off; - /* path: percent-decoded (RFC 3986, like NGINX Unit) */ - { - size_t path_dec_len = percent_decode_path(p + i, path_len, c->req_pool + pool_off); - c->req_pool[pool_off + path_dec_len] = '\0'; - req->path_length = (uint32_t) path_dec_len; - req->path.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &req->path); - pool += path_dec_len + 1; - pool_off += path_dec_len + 1; - } - /* query: from ? to next space (like NGINX Unit) */ - if (query_len > 0) { - memcpy(c->req_pool + pool_off, p + i + path_len + 1, query_len); - c->req_pool[pool_off + query_len] = '\0'; - req->query_length = (uint32_t) query_len; - req->query.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &req->query); - pool_off += query_len + 1; - } else { - req->query_length = 0; - req->query.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off - 1) - (uintptr_t) &req->query); - } - req->version_length = 8; - req->version.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &req->version); - memcpy(c->req_pool + pool_off, "HTTP/1.1", 8); - c->req_pool[pool_off + 8] = '\0'; - pool_off += 9; - req->remote_length = 0; - req->local_addr_length = 0; - req->local_port_length = 0; - req->tls = 0; - req->websocket_handshake = 0; - req->app_target = 0; - - p = line_end + 1; - if (p < end && p[-1] == '\r' && p < end) p++; /* skip \n after \r */ - req->fields_count = 0; - while (p < end && !(p[0] == '\n' || (p[0] == '\r' && p + 1 < end && p[1] == '\n'))) { - line_end = (char *) memchr(p, '\r', (size_t)(end - p)); - if (line_end == NULL) line_end = (char *) memchr(p, '\n', (size_t)(end - p)); - if (line_end == NULL) return -1; - colon = (char *) memchr(p, ':', (size_t)(line_end - p)); - if (colon != NULL && colon > p) { - size_t name_len = (size_t)(colon - p); - while (name_len > 0 && (p[name_len - 1] == ' ' || p[name_len - 1] == '\t')) name_len--; - colon++; - while (colon < line_end && (*colon == ' ' || *colon == '\t')) colon++; - size_t value_len = (size_t)(line_end - colon); - if (req->fields_count < 64 && pool_off + name_len + value_len + 4 < REQ_POOL_MAX) { - f = &req->fields[req->fields_count]; - f->hash = (uint16_t) field_hash(p, name_len); - f->name_length = (uint8_t) name_len; - f->value_length = (uint32_t) value_len; - f->name.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &f->name); - memcpy(c->req_pool + pool_off, p, name_len); - c->req_pool[pool_off + name_len] = '\0'; - pool_off += name_len + 1; - f->value.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &f->value); - memcpy(c->req_pool + pool_off, colon, value_len); - c->req_pool[pool_off + value_len] = '\0'; - pool_off += value_len + 1; - if (f->hash == NXT_UNIT_HASH_CONTENT_LENGTH) - req->content_length = (uint64_t) strtoull((char *) nxt_unit_sptr_get(&f->value), NULL, 10); - if (f->hash == NXT_UNIT_HASH_HOST && host_value == NULL) { - host_value = (char *) nxt_unit_sptr_get(&f->value); - host_value_len = value_len; - } - req->fields_count++; - } - } - p = line_end + 1; - if (p <= end && line_end < end && *line_end == '\r' && p[0] == '\n') p++; - } - /* Set server_name from Host header (like NGINX Unit) */ - req->server_name.offset = (uint32_t) ((uintptr_t) (c->req_pool + pool_off) - (uintptr_t) &req->server_name); - if (host_value != NULL && host_value_len > 0) { - /* Remove port number if present (e.g., "example.com:8080" -> "example.com") */ - size_t host_len = host_value_len; - for (i = 0; i < host_value_len; i++) { - if (host_value[i] == ':') { - host_len = i; - break; - } - } - if (pool_off + host_len + 1 > REQ_POOL_MAX) return -1; - memcpy(c->req_pool + pool_off, host_value, host_len); - c->req_pool[pool_off + host_len] = '\0'; - req->server_name_length = (uint32_t) host_len; - pool_off += host_len + 1; - } else { - /* Default to localhost if no Host header (like NGINX Unit) */ - if (pool_off + 10 > REQ_POOL_MAX) return -1; - memcpy(c->req_pool + pool_off, "localhost", 9); - c->req_pool[pool_off + 9] = '\0'; - req->server_name_length = 9; - pool_off += 10; - } - /* Detect WebSocket handshake: GET with Upgrade: websocket, Connection: upgrade, Sec-WebSocket-Key, Sec-WebSocket-Version: 13 (match Unit) */ - for (i = 0; i < (int) req->fields_count; i++) { - nxt_unit_field_t *hf = &req->fields[i]; - char *hname = (char *) nxt_unit_sptr_get(&hf->name); - char *hval = (char *) nxt_unit_sptr_get(&hf->value); - size_t nlen = (size_t) hf->name_length; - size_t vlen = (size_t) hf->value_length; - size_t k; - if (nlen == 7 && strncasecmp(hname, "Upgrade", 7) == 0 - && vlen == 9 && strncasecmp(hval, "websocket", 9) == 0) - req->websocket_handshake = (uint8_t) (req->websocket_handshake | 1); - if (nlen == 10 && strncasecmp(hname, "Connection", 10) == 0 && vlen >= 7) { - for (k = 0; k + 7 <= vlen; k++) - if (strncasecmp(hval + k, "upgrade", 7) == 0) { - req->websocket_handshake = (uint8_t) (req->websocket_handshake | 2); - break; - } - } - if (nlen == 18 && strncasecmp(hname, "Sec-WebSocket-Key", 18) == 0 && vlen > 0) - req->websocket_handshake = (uint8_t) (req->websocket_handshake | 4); - if (nlen == 21 && strncasecmp(hname, "Sec-WebSocket-Version", 21) == 0 - && vlen == 2 && (hval[0] == '1' && hval[1] == '3')) - req->websocket_handshake = (uint8_t) (req->websocket_handshake | 8); - } - if (req->websocket_handshake != 15) req->websocket_handshake = 0; /* need all four */ - - c->req_pool_used = pool_off; - c->request_buf.start = c->recv_buf; - c->request_buf.free = c->recv_buf + c->recv_parsed; - c->request_buf.end = c->recv_buf + c->recv_len; - c->req_info = req_info; - c->request = req; - /* Only mark request ready when we have the full body (if Content-Length set) */ - if (c->recv_len >= c->recv_parsed + (size_t) req->content_length) - c->request_ready = 1; - return 0; -} - -static int conn_dispatch_request(conn_t *c) { - conn_t *prev = current_dispatch_conn; - current_dispatch_conn = c; -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - c->handler_done = 0; -#endif - if (c->emb->init->callbacks.request_handler != NULL) - c->emb->init->callbacks.request_handler(c->req_info); -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* If handler runs on another thread (e.g. BlockingHandler), wait until it calls response_send or request_done. */ - pthread_mutex_lock(&c->dispatch_mutex); - while (!c->handler_done) - pthread_cond_wait(&c->dispatch_cond, &c->dispatch_mutex); - pthread_mutex_unlock(&c->dispatch_mutex); -#endif - current_dispatch_conn = prev; - return 0; -} - -static int conn_send_response(conn_t *c) { - nxt_unit_response_t *r; - char status_line[64]; - int n, i; - size_t len, body_len; - - if (c->response_sent) - return NXT_UNIT_OK; - /* Note: We always build headers here because response_sent == 0 means we're starting a new response. - * Reset send_len to 0 to clear any leftover data from a previous response. */ - c->send_len = 0; - if (c->response == NULL) { - snprintf(c->send_buf, sizeof(c->send_buf), - "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); - c->send_len = strlen(c->send_buf); - goto send; - } - r = c->response; - body_len = (size_t)(c->response_buf.free - c->response_buf.start); - n = snprintf(status_line, sizeof(status_line), "HTTP/1.1 %u \r\n", (unsigned) r->status); - if (n <= 0 || (size_t) n >= sizeof(status_line)) return NXT_UNIT_ERROR; - len = 0; - if (len + (size_t) n <= SEND_BUF_SIZE) - memcpy(c->send_buf + len, status_line, (size_t) n); - len += (size_t) n; - /* Add Content-Length so the client knows when the body ends (avoids curl hanging on empty body) */ - if (len + 32 <= SEND_BUF_SIZE) { - n = snprintf(c->send_buf + len, SEND_BUF_SIZE - len, "Content-Length: %zu\r\n", body_len); - if (n > 0) len += (size_t) n; - } - for (i = 0; i < (int) r->fields_count && len < SEND_BUF_SIZE - 4; i++) { - nxt_unit_field_t *f = &r->fields[i]; - char *name = (char *) nxt_unit_sptr_get(&f->name); - char *value = (char *) nxt_unit_sptr_get(&f->value); - n = snprintf(c->send_buf + len, SEND_BUF_SIZE - len, "%.*s: %.*s\r\n", - (int) f->name_length, name, (int) f->value_length, value); - if (n <= 0) break; - len += (size_t) n; - } - if (len + 2 <= SEND_BUF_SIZE) { - c->send_buf[len++] = '\r'; - c->send_buf[len++] = '\n'; - } - if (body_len > 0 && len + body_len <= SEND_BUF_SIZE) { - /* Body may be at response_buf.start (e.g. send_buf+512); append after headers */ - memmove(c->send_buf + len, c->response_buf.start, body_len); - len += body_len; - } - c->send_len = len; -send: - n = (int) write(c->fd, c->send_buf, c->send_len); - if (n > 0) { - memmove(c->send_buf, c->send_buf + (size_t) n, c->send_len - (size_t) n); - c->send_len -= (size_t) n; - } - if (c->send_len == 0) - c->response_sent = 1; - return NXT_UNIT_OK; -} diff --git a/snunit/resources/scala-native/snunit/nxt_unit_field.h b/snunit/resources/scala-native/snunit/nxt_unit_field.h deleted file mode 100644 index 0d81f6b7..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_field.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef _NXT_UNIT_FIELD_H_INCLUDED_ -#define _NXT_UNIT_FIELD_H_INCLUDED_ - -#include -#include "nxt_unit_sptr.h" - -#define NXT_UNIT_HASH_CONTENT_LENGTH 0x1EA0 -#define NXT_UNIT_HASH_CONTENT_TYPE 0x5F7D -#define NXT_UNIT_HASH_COOKIE 0x23F2 -#define NXT_UNIT_HASH_HOST 0x69C8 - -struct nxt_unit_field_s { - uint16_t hash; - uint8_t skip:1; - uint8_t hopbyhop:1; - uint8_t name_length; - uint32_t value_length; - nxt_unit_sptr_t name; - nxt_unit_sptr_t value; -}; - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit_request.h b/snunit/resources/scala-native/snunit/nxt_unit_request.h deleted file mode 100644 index bc434750..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_request.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef _NXT_UNIT_REQUEST_H_INCLUDED_ -#define _NXT_UNIT_REQUEST_H_INCLUDED_ - -#include -#include "nxt_unit_sptr.h" -#include "nxt_unit_field.h" - -#define NXT_UNIT_NONE_FIELD 0xFFFFFFFFU - -struct nxt_unit_request_s { - uint8_t method_length; - uint8_t version_length; - uint8_t remote_length; - uint8_t local_addr_length; - uint8_t local_port_length; - uint8_t tls; - uint8_t websocket_handshake; - uint8_t app_target; - uint32_t server_name_length; - uint32_t target_length; - uint32_t path_length; - uint32_t query_length; - uint32_t fields_count; - uint32_t content_length_field; - uint32_t content_type_field; - uint32_t cookie_field; - uint32_t authorization_field; - uint64_t content_length; - nxt_unit_sptr_t method; - nxt_unit_sptr_t version; - nxt_unit_sptr_t remote; - nxt_unit_sptr_t local_addr; - nxt_unit_sptr_t local_port; - nxt_unit_sptr_t server_name; - nxt_unit_sptr_t target; - nxt_unit_sptr_t path; - nxt_unit_sptr_t query; - nxt_unit_sptr_t preread_content; - nxt_unit_field_t fields[]; -}; - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit_response.h b/snunit/resources/scala-native/snunit/nxt_unit_response.h deleted file mode 100644 index fe3854b1..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_response.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef _NXT_UNIT_RESPONSE_H_INCLUDED_ -#define _NXT_UNIT_RESPONSE_H_INCLUDED_ - -#include -#include "nxt_unit_sptr.h" -#include "nxt_unit_field.h" - -struct nxt_unit_response_s { - uint64_t content_length; - uint32_t fields_count; - uint32_t piggyback_content_length; - uint16_t status; - nxt_unit_sptr_t piggyback_content; - nxt_unit_field_t fields[]; -}; - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit_sptr.h b/snunit/resources/scala-native/snunit/nxt_unit_sptr.h deleted file mode 100644 index a5936c34..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_sptr.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef _NXT_UNIT_SPTR_H_INCLUDED_ -#define _NXT_UNIT_SPTR_H_INCLUDED_ - -#include -#include -#include - -#include "nxt_unit_typedefs.h" - -union nxt_unit_sptr_u { - uint8_t base[1]; - uint32_t offset; -}; - -static inline void nxt_unit_sptr_set(nxt_unit_sptr_t *sptr, void *ptr) { - sptr->offset = (uint32_t) ((uint8_t *) ptr - (uint8_t *) sptr); -} - -/* Offset is from the address of the sptr itself (matches Scala: sptr + !sptr). */ -static inline void *nxt_unit_sptr_get(nxt_unit_sptr_t *sptr) { - return (uint8_t *) sptr + sptr->offset; -} - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h b/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h deleted file mode 100644 index a412a212..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef _NXT_UNIT_TYPEDEFS_H_INCLUDED_ -#define _NXT_UNIT_TYPEDEFS_H_INCLUDED_ - -typedef struct nxt_unit_s nxt_unit_t; -typedef struct nxt_unit_ctx_s nxt_unit_ctx_t; -typedef struct nxt_unit_port_id_s nxt_unit_port_id_t; -typedef struct nxt_unit_port_s nxt_unit_port_t; -typedef struct nxt_unit_buf_s nxt_unit_buf_t; -typedef struct nxt_unit_request_info_s nxt_unit_request_info_t; -typedef struct nxt_unit_callbacks_s nxt_unit_callbacks_t; -typedef struct nxt_unit_init_s nxt_unit_init_t; -typedef union nxt_unit_sptr_u nxt_unit_sptr_t; -typedef struct nxt_unit_field_s nxt_unit_field_t; -typedef struct nxt_unit_request_s nxt_unit_request_t; -typedef struct nxt_unit_response_s nxt_unit_response_t; -typedef struct nxt_unit_read_info_s nxt_unit_read_info_t; -typedef struct nxt_unit_websocket_frame_s nxt_unit_websocket_frame_t; - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit_websocket.h b/snunit/resources/scala-native/snunit/nxt_unit_websocket.h deleted file mode 100644 index 353ee01f..00000000 --- a/snunit/resources/scala-native/snunit/nxt_unit_websocket.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef _NXT_UNIT_WEBSOCKET_H_INCLUDED_ -#define _NXT_UNIT_WEBSOCKET_H_INCLUDED_ - -#include -#include "nxt_unit_typedefs.h" -#include "nxt_websocket_header.h" - -struct nxt_unit_websocket_frame_s { - nxt_unit_request_info_t *req; - uint64_t payload_len; - nxt_websocket_header_t *header; - uint8_t *mask; - nxt_unit_buf_t *content_buf; - uint64_t content_length; -}; - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_version.h b/snunit/resources/scala-native/snunit/nxt_version.h deleted file mode 100644 index b217e8e5..00000000 --- a/snunit/resources/scala-native/snunit/nxt_version.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Stub for vendored embed build. - */ -#ifndef _NXT_VERSION_H_INCLUDED_ -#define _NXT_VERSION_H_INCLUDED_ - -#define NXT_VERSION "embed" - -#endif diff --git a/snunit/resources/scala-native/snunit/nxt_websocket_header.h b/snunit/resources/scala-native/snunit/nxt_websocket_header.h deleted file mode 100644 index 8b3c5d1d..00000000 --- a/snunit/resources/scala-native/snunit/nxt_websocket_header.h +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) NGINX, Inc. - * - * Vendored from NGINX Unit (https://unit.nginx.org/). - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef _NXT_WEBSOCKET_HEADER_H_INCLUDED_ -#define _NXT_WEBSOCKET_HEADER_H_INCLUDED_ - -#include - -#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -#define NXT_HAVE_BIG_ENDIAN 1 -#define NXT_HAVE_LITTLE_ENDIAN 0 -#else -#define NXT_HAVE_BIG_ENDIAN 0 -#define NXT_HAVE_LITTLE_ENDIAN 1 -#endif - -typedef struct { -#if (NXT_HAVE_BIG_ENDIAN) - uint8_t fin:1; - uint8_t rsv1:1; - uint8_t rsv2:1; - uint8_t rsv3:1; - uint8_t opcode:4; - - uint8_t mask:1; - uint8_t payload_len:7; -#endif - -#if (NXT_HAVE_LITTLE_ENDIAN) - uint8_t opcode:4; - uint8_t rsv3:1; - uint8_t rsv2:1; - uint8_t rsv1:1; - uint8_t fin:1; - - uint8_t payload_len:7; - uint8_t mask:1; -#endif - - uint8_t payload_len_[8]; -} nxt_websocket_header_t; - - -enum { - NXT_WEBSOCKET_OP_CONT = 0x00, - NXT_WEBSOCKET_OP_TEXT = 0x01, - NXT_WEBSOCKET_OP_BINARY = 0x02, - NXT_WEBSOCKET_OP_CLOSE = 0x08, - NXT_WEBSOCKET_OP_PING = 0x09, - NXT_WEBSOCKET_OP_PONG = 0x0A, - - NXT_WEBSOCKET_OP_CTRL = 0x08, -}; - - -enum { - NXT_WEBSOCKET_CR_NORMAL = 1000, - NXT_WEBSOCKET_CR_GOING_AWAY = 1001, - NXT_WEBSOCKET_CR_PROTOCOL_ERROR = 1002, - NXT_WEBSOCKET_CR_UNPROCESSABLE_INPUT = 1003, - NXT_WEBSOCKET_CR_RESERVED = 1004, - NXT_WEBSOCKET_CR_NOT_PROVIDED = 1005, - NXT_WEBSOCKET_CR_ABNORMAL = 1006, - NXT_WEBSOCKET_CR_INVALID_DATA = 1007, - NXT_WEBSOCKET_CR_POLICY_VIOLATION = 1008, - NXT_WEBSOCKET_CR_MESSAGE_TOO_BIG = 1009, - NXT_WEBSOCKET_CR_EXTENSION_REQUIRED = 1010, - NXT_WEBSOCKET_CR_INTERNAL_SERVER_ERROR = 1011, - NXT_WEBSOCKET_CR_TLS_HANDSHAKE_FAILED = 1015, -}; - - -#endif /* _NXT_WEBSOCKET_HEADER_H_INCLUDED_ */ diff --git a/snunit/src/snunit/SyncServerBuilder.scala b/snunit/src/snunit/SyncServerBuilder.scala index 8a070d49..c49793b3 100644 --- a/snunit/src/snunit/SyncServerBuilder.scala +++ b/snunit/src/snunit/SyncServerBuilder.scala @@ -9,8 +9,6 @@ object SyncServerBuilder { private val init: nxt_unit_init_t_* = { initArray.at(0).asInstanceOf[nxt_unit_init_t_*] } - private var host: String = "0.0.0.0" - private var port: Int = 8080 def setRequestHandler(requestHandler: RequestHandler): this.type = { ServerBuilder.setRequestHandler(requestHandler) this @@ -19,18 +17,9 @@ object SyncServerBuilder { ServerBuilder.setWebsocketHandler(websocketHandler) this } - def setHost(host: String): this.type = { - this.host = host - this - } - def setPort(port: Int): this.type = { - this.port = port - this - } def build(): SyncServer = { ServerBuilder.setBaseHandlers(init) - val ctx: nxt_unit_ctx_t_* = Zone: - nxt_unit_init(init, toCString(host), port) + val ctx: nxt_unit_ctx_t_* = nxt_unit_init(init) if (ctx.isNull) { throw new Exception("Failed to create Unit object") } diff --git a/snunit/src/snunit/unsafe/unsafe.scala b/snunit/src/snunit/unsafe/unsafe.scala index 78a7750d..4dd1fa21 100644 --- a/snunit/src/snunit/unsafe/unsafe.scala +++ b/snunit/src/snunit/unsafe/unsafe.scala @@ -114,13 +114,14 @@ opaque type nxt_websocket_header_t_* = Ptr[nxt_websocket_header_t] export externs.* +@link("unit") @extern object externs { /* * Initialize Unit application library with necessary callbacks and * ready/reply port parameters, send 'READY' response to master. */ - def nxt_unit_init(init: nxt_unit_init_t_*, host: CString, port: CInt): nxt_unit_ctx_t_* = extern + def nxt_unit_init(init: nxt_unit_init_t_*): nxt_unit_ctx_t_* = extern /* * Main function useful in case when application does not have it's own diff --git a/snunit/test/src/snunit/unsafe/BytesCoversionTests.scala b/snunit/test/src/snunit/unsafe/BytesCoversionTests.scala index 83acdead..566e61c4 100644 --- a/snunit/test/src/snunit/unsafe/BytesCoversionTests.scala +++ b/snunit/test/src/snunit/unsafe/BytesCoversionTests.scala @@ -12,7 +12,7 @@ object foo { buffer.get() ==> 'l' buffer.get() ==> 'l' buffer.get() ==> 'o' - assertThrows[java.nio.BufferUnderflowException] { buffer.get() } + intercept[java.nio.BufferUnderflowException] { buffer.get() } } } @@ -63,7 +63,7 @@ object BytesConversionTests extends TestSuite { buffer.get() ==> 'l' buffer.get() ==> 'l' buffer.get() ==> 'o' - assertThrows[java.nio.BufferUnderflowException] { buffer.get() } + intercept[java.nio.BufferUnderflowException] { buffer.get() } } test("foo") { foo.bar() diff --git a/unitd.mill.scala b/unitd.mill.scala new file mode 100644 index 00000000..1b1c8d06 --- /dev/null +++ b/unitd.mill.scala @@ -0,0 +1,60 @@ +package build + +import java.util.concurrent.atomic.AtomicBoolean + +private val dest = os.home / ".cache" / "snunit" +val control = dest / "control.sock" +private val pid = dest / "unit.pid" + +// Variables are not maintained among invocations in interactive (`-i`) mode. +// Running Mill with `-i` in TestUtils breaks makes it impossible to kill the previous processes. +private var optProc: Option[os.SubProcess] = None +private def closeUnitd(): Unit = { + optProc.foreach { proc => + proc.close() + // Wait for Unit to close itself gracefully + Thread.sleep(100) + optProc = None + } + // We also try to kill the process in the pid file + if (os.exists(pid)) { + os.proc("kill", os.read(pid).trim).call(check = false) + } +} +def runBackground(config: ujson.Obj): Unit = { + closeUnitd() + val statedir = dest / "statedir" + os.makeDir.all(statedir) + os.write.over(statedir / "conf.json", config) + os.remove(control) + val started = new AtomicBoolean(false) + optProc = Some( + os.proc( + "unitd", + "--no-daemon", + "--log", + dest / "log.txt", + "--statedir", + statedir, + "--control", + s"unix:$control", + "--pid", + pid + ).spawn( + stdout = os.Inherit, + stderr = os.ProcessOutput.Readlines(line => { + line match { + case s"$_ unit $_ started" => + Thread.sleep(100) + started.set(true) + case _ => + } + System.err.println(line) + }) + ) + ) + while (!started.get()) { + println("Waiting for unit to start...") + Thread.sleep(100) + } +} diff --git a/versions.mill.scala b/versions.mill.scala new file mode 100644 index 00000000..79a8486e --- /dev/null +++ b/versions.mill.scala @@ -0,0 +1,23 @@ +package build + +// This file is system-linked in sbt-plugin/project/Versions.scala +// It needs to be a valid Scala file so we can't use top level `val`s +object Versions { + val scalaNative = "0.5.7" + val upickle = "4.0.2" + val undertow = "2.3.18.Final" + val scala212 = "2.12.19" + val scala3 = "3.3.4" + val tapir = "1.11.1" + val cask = "0.10.2" + val catsEffect = "3.6-623178c" + val http4s023 = "0.23.26" + val http4s1 = "1.0.0-M41" + val mill011 = "0.11.13" + val utest = "0.8.4" + val osLib = "0.11.3" + val sttp = "3.10.1" + val pprint = "0.9.0" + val castor = "0.3.0" + val scalaJavaTime = "2.6.0" +}