Skip to content

Commit

Permalink
Update Simple Library Server (#1952)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeusgd authored and iamrecursion committed Aug 18, 2021
1 parent 2b894ad commit e936148
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 38 deletions.
7 changes: 7 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
- Added the ability to specify cell ranges for reading XLS and XSLX spreadsheets
([#1954](https://github.com/enso-org/enso/pull/1954)).

## Tooling

- Updated the Simple Library Server to make it more robust; updated the edition
configuration with a proper URL to the Enso Library Repository, making it
possible for new libraries to be downloaded from it
([#1952](https://github.com/enso-org/enso/pull/1952)).

# Enso 0.2.24 (2021-08-13)

## Interpreter/Runtime
Expand Down
4 changes: 3 additions & 1 deletion docs/libraries/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,6 @@ Then you can use the Enso CLI to upload the project:
enso publish-library --upload-url <URL> <path to project root>
```

See `enso publish-library --help` for more information.
The `--upload-url` is optional, if not provided, the library will be uploaded to
the main Enso library repository. See `enso publish-library --help` for more
information.
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ object Constants {
*/
val uploadIntroducedVersion: SemVer =
SemVer(0, 2, 17, Some("SNAPSHOT"))

/** The upload URL associated with the main Enso library repository. */
val defaultUploadUrl = "https://publish.libraries.release.enso.org/"
}
Original file line number Diff line number Diff line change
Expand Up @@ -368,12 +368,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
val settings = runner
.uploadLibrary(
path,
uploadUrl.getOrElse {
throw new IllegalArgumentException(
"The default repository is currently not defined. " +
"You need to explicitly specify the `--upload-url`."
)
},
uploadUrl.getOrElse(Constants.defaultUploadUrl),
authToken.orElse(LauncherEnvironment.getEnvVar("ENSO_AUTH_TOKEN")),
cliOptions.hideProgress,
logLevel,
Expand Down
24 changes: 16 additions & 8 deletions engine/runner/src/main/scala/org/enso/runner/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import org.apache.commons.cli.{Option => CliOption, _}
import org.enso.editions.DefaultEdition
import org.enso.languageserver.boot
import org.enso.languageserver.boot.LanguageServerConfig
import org.enso.libraryupload.LibraryUploader.UploadFailedError
import org.enso.loggingservice.LogLevel
import org.enso.pkg.{Contact, PackageManager, Template}
import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext}
import org.enso.version.VersionDescription
import org.graalvm.polyglot.PolyglotException

import java.io.File
import java.nio.file.Path
import java.util.UUID

import scala.Console.err
import scala.jdk.CollectionConverters._
import scala.util.Try
Expand Down Expand Up @@ -700,13 +701,20 @@ object Main {
exitFail()
}

ProjectUploader.uploadProject(
projectRoot = projectRoot,
uploadUrl = line.getOptionValue(UPLOAD_OPTION),
authToken = Option(line.getOptionValue(AUTH_TOKEN)),
showProgress = !line.hasOption(HIDE_PROGRESS)
)
exitSuccess()
try {
ProjectUploader.uploadProject(
projectRoot = projectRoot,
uploadUrl = line.getOptionValue(UPLOAD_OPTION),
authToken = Option(line.getOptionValue(AUTH_TOKEN)),
showProgress = !line.hasOption(HIDE_PROGRESS)
)
exitSuccess()
} catch {
case UploadFailedError(_) =>
// We catch this error to avoid printing an unnecessary stack trace.
// The error itself is already logged.
exitFail()
}
}

if (line.hasOption(RUN_OPTION)) {
Expand Down
33 changes: 23 additions & 10 deletions lib/scala/cli/src/main/scala/org/enso/cli/task/MappedTask.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@ import scala.util.Try
* Used internally by [[TaskProgress.flatMap]].
*/
private class MappedTask[A, B](source: TaskProgress[A], f: A => Try[B])
extends TaskProgress[B] {
override def addProgressListener(
listener: ProgressListener[B]
): Unit =
source.addProgressListener(new ProgressListener[A] {
override def progressUpdate(done: Long, total: Option[Long]): Unit =
listener.progressUpdate(done, total)
extends TaskProgress[B] { self =>

override def done(result: Try[A]): Unit =
listener.done(result.flatMap(f))
})
var listeners: List[ProgressListener[B]] = Nil
var savedResult: Option[Try[B]] = None

source.addProgressListener(new ProgressListener[A] {
override def progressUpdate(done: Long, total: Option[Long]): Unit =
listeners.foreach(_.progressUpdate(done, total))

override def done(result: Try[A]): Unit = self.synchronized {
val mapped = result.flatMap(f)
savedResult = Some(mapped)
listeners.foreach(_.done(mapped))
}
})

override def addProgressListener(listener: ProgressListener[B]): Unit =
self.synchronized {
listeners ::= listener
savedResult match {
case Some(saved) => listener.done(saved)
case None =>
}
}

override def unit: ProgressUnit = source.unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.enso.cli.task

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

import scala.util.{Success, Try}

class MappedTaskSpec extends AnyWordSpec with Matchers {
"TaskProgress.map" should {
"run only once even with multiple listeners" in {
var runs = 0
val task1 = new TaskProgressImplementation[String]()
val task2 = task1.map { str =>
runs += 1
str + "bar"
}

val emptyListener = new ProgressListener[String] {
override def progressUpdate(done: Long, total: Option[Long]): Unit = ()
override def done(result: Try[String]): Unit = ()
}
task2.addProgressListener(emptyListener)
task2.addProgressListener(emptyListener)

task1.setComplete(Success("foo"))

task2.addProgressListener(emptyListener)
var answer: Option[Try[String]] = None
task2.addProgressListener(new ProgressListener[String] {
override def progressUpdate(done: Long, total: Option[Long]): Unit = ()
override def done(result: Try[String]): Unit = {
answer = Some(result)
}
})

answer shouldEqual Some(Success("foobar"))
runs shouldEqual 1
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ object LibraryUploader {
}
}

/** Indicates that the library upload has failed. */
case class UploadFailedError(message: String)
extends RuntimeException(message)

/** Creates an URL for the upload, including information identifying the
* library version.
*/
Expand Down Expand Up @@ -222,11 +226,7 @@ object LibraryUploader {
val errorMessage =
s"Upload failed: $message (Status code: ${response.statusCode})."
logger.error(errorMessage)
Failure(
new RuntimeException(
errorMessage
)
)
Failure(UploadFailedError(errorMessage))
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion project/Editions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ object Editions {
*/
val contribLibraries: Seq[ContribLibrary] = Seq()

/** The URL to the main library repository. */
val mainLibraryRepositoryUrl = "https://libraries.release.enso.org/libraries"

private val editionsRoot = file("distribution") / "editions"
private val extension = ".yaml"

Expand Down Expand Up @@ -68,7 +71,7 @@ object Editions {
s"""engine-version: $ensoVersion
|repositories:
| - name: main
| url: n/a # Library repository is still a work in progress.
| url: $mainLibraryRepositoryUrl
|libraries:
|${librariesConfigs.mkString("\n")}
|""".stripMargin
Expand Down
54 changes: 47 additions & 7 deletions tools/simple-library-server/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node
const express = require("express");
const crypto = require("crypto");
const path = require("path");
const os = require("os");
const fs = require("fs");
Expand Down Expand Up @@ -63,13 +64,34 @@ if (argv.upload == "disabled") {
console.log("WARNING: Uploads are enabled without any authentication.");
}
}

app.get("/health", function (req, res) {
res.status(200).send("OK");
});

app.use(express.static(argv.root));

let port = argv.port;
if (process.env.PORT) {
port = process.env.PORT;
console.log(
`Overriding the port to ${port} set by the PORT environment variable.`
);
}
console.log(
`Serving the repository located under ${argv.root} on port ${argv.port}.`
`Serving the repository located under ${argv.root} on port ${port}.`
);

app.listen(argv.port);
const server = app.listen(port);

function handleShutdown() {
console.log("Received a signal - shutting down.");
server.close(() => {
console.log("Server terminated.");
});
}
process.on("SIGTERM", handleShutdown);
process.on("SIGINT", handleShutdown);

/// Specifies if a particular file can be compressed in transfer, if supported.
function shouldCompress(req, res) {
Expand Down Expand Up @@ -121,7 +143,21 @@ async function handleUpload(req, res) {
}
}

const libraryPath = path.join(libraryRoot, namespace, name, version);
const libraryBasePath = path.join(libraryRoot, namespace, name);
const libraryPath = path.join(libraryBasePath, version);

/** Finds a name for a temporary directory to move the files to,
so that the upload can then be committed atomically by renaming
a single directory. */
function findRandomTemporaryDirectory() {
const randomName = crypto.randomBytes(32).toString("hex");
const temporaryPath = path.join(libraryBasePath, randomName);
if (fs.existsSync(temporaryPath)) {
return findRandomTemporaryDirectory();
}

return temporaryPath;
}

if (fs.existsSync(libraryPath)) {
return fail(
Expand All @@ -132,11 +168,14 @@ async function handleUpload(req, res) {
);
}

await fsPromises.mkdir(libraryPath, { recursive: true });
const temporaryPath = findRandomTemporaryDirectory();
await fsPromises.mkdir(libraryBasePath, { recursive: true });
await fsPromises.mkdir(temporaryPath, { recursive: true });

console.log(`Uploading library [${namespace}.${name}:${version}].`);
try {
await putFiles(libraryPath, req.files);
await putFiles(temporaryPath, req.files);
await fsPromises.rename(temporaryPath, libraryPath);
} catch (error) {
console.log(`Upload failed: [${error}].`);
console.error(error.stack);
Expand All @@ -154,7 +193,7 @@ function isVersionValid(version) {

/// Checks if the namespace/username is valid.
function isNamespaceValid(namespace) {
return /^[a-z][a-z0-9]*$/.test(namespace) && namespace.length >= 3;
return /^[A-Za-z][a-z0-9]*$/.test(namespace) && namespace.length >= 3;
}

/** Checks if the library name is valid.
Expand Down Expand Up @@ -194,6 +233,7 @@ async function putFiles(directory, files) {
const file = files[i];
const filename = file.originalname;
const destination = path.join(directory, filename);
await fsPromises.rename(file.path, destination);
await fsPromises.copyFile(file.path, destination);
await fsPromises.unlink(file.path);
}
}
3 changes: 3 additions & 0 deletions tools/simple-library-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
"multer": "^1.4.2",
"semver": "^7.3.5",
"yargs": "^17.0.1"
},
"engines": {
"node": ">=14.17.2"
}
}

0 comments on commit e936148

Please sign in to comment.