Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Update Simple Library Server #1952

Merged
merged 13 commits into from
Aug 18, 2021
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"
}
}