diff --git a/.fury/config b/.fury/config index 3003bc537..b0704379d 100644 --- a/.fury/config +++ b/.fury/config @@ -7,5 +7,5 @@ # # For more information, please visit https://propensive.com/fury/ # -layerRef QmafRvj9EPQ2iskiiJcmupwRdZeyTTGCUq4NosJP9hYEAq +layerRef QmYV5YS9HvjTpWid9KL76HDntKwmtbRT4qdzMCgXMGcbq5 # vim: set noai ts=12 sw=12: diff --git a/.fury/layers.db b/.fury/layers.db index 6cb472ca2..67cee43dc 100644 Binary files a/.fury/layers.db and b/.fury/layers.db differ diff --git a/src/build/build.scala b/src/build/build.scala index 9ab6c2d45..bed0ec467 100644 --- a/src/build/build.scala +++ b/src/build/build.scala @@ -652,7 +652,7 @@ case class LayerCli(cli: Cli)(implicit log: Log) { ttl <- Try(call(ExpiryArg).toOption.getOrElse(if(public) 30 else 7)) _ <- layer.verifyConf(false, conf, Pointer.Root, quiet = raw, force) - ref <- if(!public) Layer.store(layer) + ref <- if(!public) layer.ref else for { token <- ManagedConfig().token.ascribe(NotAuthenticated()).orElse(ConfigCli(cli).doAuth) ref <- Layer.share(ManagedConfig().service, layer, token, ttl) @@ -667,7 +667,7 @@ case class LayerCli(cli: Cli)(implicit log: Log) { layer <- Layer.retrieve(conf) call <- cli.call() _ <- layer.verifyConf(true, conf, Pointer.Root, quiet = false, force = false) - ref <- Layer.store(layer) + ref <- layer.ref _ <- ~log.info(msg"Writing layer database to ${layout.layerDb.relativizeTo(layout.baseDir)}") _ <- Layer.writeDb(layer, layout) gitDir <- ~GitDir(layout) @@ -723,7 +723,7 @@ case class LayerCli(cli: Cli)(implicit log: Log) { layer <- Layer.get(conf.layerRef, conf.published) previous <- layer.previous.ascribe(CannotUndo()) layer <- Layer.get(previous, None) - layerRef <- Layer.store(layer) + layerRef <- layer.ref _ <- ~log.info(msg"Reverted to layer $layerRef") _ <- Layer.saveFuryConf(conf.copy(layerRef = layerRef), layout) } yield log.await() @@ -812,7 +812,6 @@ case class LayerCli(cli: Cli)(implicit log: Log) { published <- imported.remote.ascribe(ImportHasNoRemote()) (newPub, newRef) <- getNewLayer(imported.layerRef, published, version, pointer / importId) newLayer <- Layer.get(newRef, Some(newPub)) - //_ <- ~log.info(msg"newLayer = ${artifact.layerRef}/$newPub") newLayer <- if(recursive) ~updateAll(newLayer, pointer / importId, newLayer.imports.map(_.id).to[List], recursive, None) else ~newLayer diff --git a/src/core/hierarchy.scala b/src/core/hierarchy.scala index 53bdce9ad..6dd31974d 100644 --- a/src/core/hierarchy.scala +++ b/src/core/hierarchy.scala @@ -41,7 +41,7 @@ case class Hierarchy(layer: Layer, path: Pointer, children: Map[ImportId, Hierar def update(pointer: Pointer, newLayer: Layer)(implicit log: Log): Try[Hierarchy] = if(pointer.isEmpty) Success(copy(layer = newLayer)) else children.get(pointer.head).ascribe(CantResolveLayer(pointer)).flatMap { hierarchy => - hierarchy.update(pointer.tail, newLayer).flatMap { h => h.layerRef.map { ref => + hierarchy.update(pointer.tail, newLayer).flatMap { h => h.layer.ref.map { ref => copy( children = children.updated(pointer.head, h), layer = Layer(_.imports(pointer.head).layerRef)(layer) = ref @@ -58,17 +58,16 @@ case class Hierarchy(layer: Layer, path: Pointer, children: Map[ImportId, Hierar def save(pointer: Pointer, layout: Layout)(implicit log: Log): Try[LayerRef] = children.values.to[List].traverse(_.save(pointer, layout)).flatMap { _ => - layerRef.flatMap { ref => + layer.ref.flatMap { ref => if(path.isEmpty) Layer.saveFuryConf(FuryConf(ref, pointer), layout).map(ref.waive) else Success(ref) } } - lazy val layerRef: Try[LayerRef] = Layer.store(layer)(Log()) - def focus(pointer: Pointer): Try[Focus] = layerRef >> (Focus(_, pointer, None)) + def focus(pointer: Pointer): Try[Focus] = layer.ref >> (Focus(_, pointer, None)) def focus(pointer: Pointer, projectId: ProjectId): Try[Focus] = - layerRef >> (Focus(_, pointer, Some((projectId, None)))) + layer.ref >> (Focus(_, pointer, Some((projectId, None)))) def focus(pointer: Pointer, projectId: ProjectId, moduleId: ModuleId): Try[Focus] = - layerRef >> (Focus(_, pointer, Some((projectId, Some(moduleId))))) + layer.ref >> (Focus(_, pointer, Some((projectId, Some(moduleId))))) } diff --git a/src/core/layer.scala b/src/core/layer.scala index ea20c1601..93fd74997 100644 --- a/src/core/layer.scala +++ b/src/core/layer.scala @@ -41,6 +41,8 @@ case class Layer(version: Int, mainRepo: Option[RepoId] = None, previous: Option[LayerRef] = None) { layer => + lazy val ref: Try[LayerRef] = Layer.store(this)(Log()) + def apply(id: ProjectId) = projects.findBy(id) def moduleRefs: SortedSet[ModuleRef] = projects.flatMap(_.moduleRefs) def mainProject: Try[Option[Project]] = main.map(projects.findBy(_)).to[List].sequence.map(_.headOption) @@ -379,6 +381,7 @@ object Layer extends Lens.Partial[Layer] { def saveFuryConf(conf: FuryConf, layout: Layout)(implicit log: Log): Try[FuryConf] = for { confStr <- ~Ogdl.serialize(Ogdl(conf)) + _ <- ~Trigger.notify(layout.baseDir, System.currentTimeMillis()) _ <- layout.confFile.writeSync(confComments+confStr+vimModeline) } yield conf diff --git a/src/core/source.scala b/src/core/source.scala index 82cf50164..d1e11778e 100644 --- a/src/core/source.scala +++ b/src/core/source.scala @@ -74,10 +74,11 @@ sealed abstract class Source extends Key(msg"source") { def dir: Path def glob: Glob def repoIdentifier: RepoId + def local: Boolean def base(snapshots: Snapshots, layout: Layout): Try[Path] def dir(snapshots: Snapshots, layout: Layout): Try[Path] = base(snapshots, layout).map(dir in _) - + def files(snapshots: Snapshots, layout: Layout): Try[Stream[Path]] = dir(snapshots, layout).map { dir => glob(dir, dir.walkTree) } @@ -103,6 +104,7 @@ sealed abstract class Source extends Key(msg"source") { case class RepoSource(repoId: RepoId, dir: Path, glob: Glob) extends Source { def key: String = str"${repoId}:${dir.value}//$glob" def completion: String = str"${repoId}:${dir.value}" + def local: Boolean = false def repoIdentifier: RepoId = repoId def hash(layer: Layer): Try[Digest] = layer.repos.findBy(repoId).map((dir, _).digest[Md5]) def base(snapshots: Snapshots, layout: Layout): Try[Path] = @@ -111,6 +113,7 @@ case class RepoSource(repoId: RepoId, dir: Path, glob: Glob) extends Source { case class LocalSource(dir: Path, glob: Glob) extends Source { def key: String = str"${dir.value}//$glob" + def local: Boolean = true def completion: String = dir.value def hash(layer: Layer): Try[Digest] = Success((-1, dir).digest[Md5]) def repoIdentifier: RepoId = RepoId("local") diff --git a/src/core/structure.scala b/src/core/structure.scala index 238b3167f..c340c0e23 100644 --- a/src/core/structure.scala +++ b/src/core/structure.scala @@ -26,6 +26,14 @@ sealed trait MenuStructure { def show: Boolean def shortcut: Char def needsLayer: Boolean + + def apply(path: List[String]): Option[MenuStructure] = path match { + case Nil => Some(this) + case "" :: tail => apply(tail) + case head :: tail => submenu(head).flatMap(_(tail)) + } + + def submenu(item: String): Option[MenuStructure] } case class Action( @@ -35,7 +43,9 @@ case class Action( show: Boolean = true, shortcut: Char = '\u0000', needsLayer: Boolean = true) - extends MenuStructure + extends MenuStructure { + def submenu(item: String): Option[MenuStructure] = None +} case class Menu( command: Symbol, @@ -47,6 +57,9 @@ case class Menu( )(val items: MenuStructure*) extends MenuStructure { + + def submenu(item: String): Option[MenuStructure] = items.find(_.command.name == item) + def apply(cli: Cli, ctx: Cli, reentrant: Boolean = false): Try[ExitStatus] = { val hasLayer: Boolean = cli.layout.map(_.confFile.exists).getOrElse(false) if(cli.args.args == Seq("interrupt")) { diff --git a/src/core/trigger.scala b/src/core/trigger.scala new file mode 100644 index 000000000..0952e2e63 --- /dev/null +++ b/src/core/trigger.scala @@ -0,0 +1,50 @@ +/* + + Fury, version 0.33.0. Copyright 2018-20 Jon Pretty, Propensive OÜ. + + The primary distribution site is: https://propensive.com/ + + 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. + +*/ +package fury.core + +import fury.io._, fury.text._ + +import scala.concurrent._ +import scala.collection.mutable.HashMap + +import scala.util._ + +object Trigger { + + private case class Waiting(timestamp: Long, promises: Set[Promise[Long]]) + + private val waiting: HashMap[Path, Waiting] = HashMap() + + def listen(path: Path): Promise[Long] = Trigger.synchronized { + val promise = Promise[Long]() + Log().info(msg"Listening for updates to $path") + waiting(path) = waiting.get(path).fold(Waiting(0L, Set(promise))) { wait => + wait.copy(promises = wait.promises + promise) + } + promise + } + + def notify(path: Path, timestamp: Long): Unit = Trigger.synchronized { + Log().info(msg"Notifying ${waiting.get(path).fold(0)(_.promises.size)} listeners to $path") + waiting.get(path).foreach(_.promises.foreach(_.complete(Success(timestamp)))) + waiting(path) = Waiting(System.currentTimeMillis, Set()) + } + + def shutdown(): Unit = Trigger.synchronized { + waiting.values.foreach(_.promises.foreach(_.complete(Success(0L)))) + } +} diff --git a/src/core/uniqueness.scala b/src/core/uniqueness.scala index f2f31cb82..1945f4bc1 100644 --- a/src/core/uniqueness.scala +++ b/src/core/uniqueness.scala @@ -21,6 +21,7 @@ sealed trait Uniqueness[Ref, Origin] { def allOrigins: Set[Origin] def one: Option[Origin] def any: Option[Origin] + def some: Origin = any.orElse(one).get } object Uniqueness { diff --git a/src/frontend/main.scala b/src/frontend/main.scala index 802dc7ea2..5db8161a9 100644 --- a/src/frontend/main.scala +++ b/src/frontend/main.scala @@ -51,6 +51,8 @@ object FuryServer { def main(args: Array[String]): Unit = { def exit(code: Int) = { + Rest.shutdown() + Trigger.shutdown() Lifecycle.shutdown() System.exit(code) } @@ -73,6 +75,7 @@ object FuryServer { env: Environment) : Unit = exit { + Rest.start(env) val pid = Pid(args.head.toInt) implicit val log: Log = Log.log(pid) diff --git a/src/frontend/menu.scala b/src/frontend/menu.scala index 49121a44f..749e301d1 100644 --- a/src/frontend/menu.scala +++ b/src/frontend/menu.scala @@ -135,8 +135,11 @@ object FuryMenu { Action('list, msg"list sources for the module", SourceCli(_).list, shortcut = 'l') ), - Action('stop, msg"gracefully shut down the Fury server", ((_: Cli) => Lifecycle.shutdown()), - needsLayer = false), + Action('stop, msg"gracefully shut down the Fury server", { (_: Cli) => + Rest.shutdown() + Trigger.shutdown() + Lifecycle.shutdown() + }, needsLayer = false), Menu('repo, msg"manage source repositories for the layer", 'list, shortcut = 'r')( Action('add, msg"add a source repository to the layer", RepoCli(_).add, shortcut = 'a'), diff --git a/src/frontend/rest.scala b/src/frontend/rest.scala new file mode 100644 index 000000000..7d55e7bd7 --- /dev/null +++ b/src/frontend/rest.scala @@ -0,0 +1,186 @@ +/* + + Fury, version 0.33.0. Copyright 2018-20 Jon Pretty, Propensive OÜ. + + The primary distribution site is: https://propensive.com/ + + 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. + +*/ +package fury + +import fury.text._, fury.core._, fury.io._, fury.model._ + +import antiphony._ +import euphemism._ +import guillotine._ +import mercator._ + +import scala.util._ +import fury.core.Uniqueness.Ambiguous +import scala.concurrent._ + +object Rest { + + private val port = 6325 + + implicit val log: Log = Log() + + private var Server: Option[LiveHttpServer] = None + + def start(env: Environment): Unit = Rest.synchronized { + if(Server.isEmpty) { + log.info(msg"Starting HTTP server on port $port") + Server = new HttpServer({ request => + val json: Try[Json] = request.path match { + case "/universe" => for { + path <- request.params.get("path").ascribe(MissingParam(Args.PathArg)) + layout <- Try(Layout(Path(env.variables("HOME")), Path(path), env, Path(path))) + conf <- Layer.readFuryConf(layout) + layer <- Layer.get(conf.layerRef, None) + hierarchy <- layer.hierarchy(Pointer.Root) + universe <- hierarchy.universe + apiUniverse <- Api.Universe(universe, layout) + } yield Json(apiUniverse) + + case "/layer" => for { + path <- request.params.get("path").ascribe(MissingParam(Args.PathArg)) + layout <- Try(Layout(Path(env.variables("HOME")), Path(path), env, Path(path))) + conf <- Layer.readFuryConf(layout) + layer <- Layer.get(conf.layerRef, None) + hierarchy <- layer.hierarchy(Pointer.Root) + universe <- hierarchy.universe + apiLayer <- Api.Layer(universe, layout, layer) + } yield Json(apiLayer) + + case "/hierarchy" => for { + path <- request.params.get("path").ascribe(MissingParam(Args.PathArg)) + layout <- Try(Layout(Path(env.variables("HOME")), Path(path), env, Path(path))) + conf <- Layer.readFuryConf(layout) + layer <- Layer.get(conf.layerRef, None) + hierarchy <- layer.hierarchy(Pointer.Root) + apiHierarchy <- Api.Hierarchy(hierarchy) + } yield Json(apiHierarchy) + + case "/wait" => for { + path <- request.params.get("path").ascribe(MissingParam(Args.PathArg)) + layout <- Try(Layout(Path(env.variables("HOME")), Path(path), env, Path(path))) + timestamp <- Try(Await.result(Trigger.listen(layout.baseDir).future, duration.Duration.Inf)) + } yield Json(Api.Update(timestamp)) + } + + Response(Json(Api.Envelope(request.path, json))) + }).bind(port).to[Option] + if(Server.isEmpty) log.warn(msg"Failed to start HTTP server on port $port") + log.info(msg"Started HTTP server") + } + } + + def shutdown(): Unit = { + log.info(msg"Shutting down HTTP server on port $port") + Server.map(_.shutdown()) + log.info(msg"Shutdown complete") + } + + object Api { + case class Envelope(request: String, result: Option[Json], error: Option[String]) + case class Project(id: String, modules: List[Module], description: String) + case class Repo(id: String, commit: String, remote: String, refSpec: String) + case class RepoSet(commit: String, ids: List[String]) + case class ProjectRef(id: String, projects: List[Project]) + case class Import(id: String, ref: String, remotes: List[PublishedLayer]) + case class Hierarchy(id: String, layer: String, children: Option[List[Hierarchy]]) + case class Update(timestamp: Long) + + case class Module( + id: String, + sources: List[Source], + dependencies: List[String], + binaries: List[Binary], + kind: String + ) + + case class Source(id: String, path: Option[String], editable: Boolean) + case class Binary(id: String, group: String, artifact: String, version: String) + case class Layer(id: String, repos: List[Repo], projects: List[Project]) + case class Universe(projects: List[ProjectRef], repos: List[RepoSet], imports: List[Import]) + + object Envelope { + def apply[T](request: String, result: Try[Json]): Envelope = + Envelope(request, result.map(Json(_)).toOption, + result.failed.toOption.map { + case e: Exception => "An unexpected error occurred" + }) + } + + object Project { + def apply(project: fury.core.Project, universe: fury.core.Universe, layout: Layout): Project = + Project( + project.id.key, + project.modules.to[List].map(Module(_, project, universe, layout)), + project.description) + } + + object Module { + def apply(module: fury.core.Module, + project: fury.core.Project, + universe: fury.core.Universe, + layout: Layout): Module = + Module( + id = module.id.key, + sources = module.sources.to[List].map { s => + val dir = for { + checkout <- universe.checkout(module.ref(project), layout) + dir <- s.dir(checkout, layout) + } yield dir.value + Source(s.key, dir.toOption, s.local) + }, + dependencies = module.dependencies.to[List].map(_.key), + binaries = module.binaries.to[List].map { b => Binary(b.id.key, b.group, b.artifact, b.version) }, + module.kind.name.name + ) + + } + + object Universe { + def apply(universe: fury.core.Universe, layout: Layout): Try[Universe] = for { + projects <- universe.projects.to[List].traverse { case (ProjectId(id), project) => + universe.projects.to[List].traverse { case (projectId, elements) => for { + layer <- universe.hierarchy(elements.some) + project <- layer.projects.findBy(projectId) + } yield Project(project, universe, layout) }.map(ProjectRef(id, _)) + } + } yield Universe( + projects, + universe.repoSets.to[List].map { case (RepoSetId(id), repos) => + RepoSet(id, repos.to[List].map(_.repoId.key)) + }, + universe.imports.to[List].map { case (ShortLayerRef(id), LayerProvenance(ref, imports)) => + Import(id, ref.key, imports.to[Set].flatMap(_._2.remote.toSet).to[List]) + } + ) + } + + object Hierarchy { + def apply(hierarchy: fury.core.Hierarchy): Try[Hierarchy] = for { + ref <- hierarchy.layer.ref + children <- hierarchy.children.values.to[List].traverse(Hierarchy(_)) + } yield Hierarchy(hierarchy.path.lastOption.map(_.key).getOrElse("/"), ref.key, + if(children.isEmpty) None else Some(children)) + } + + object Layer { + def apply(universe: fury.core.Universe, layout: Layout, layer: fury.core.Layer): Try[Layer] = for { + ref <- layer.ref + } yield Layer(ref.key, layer.repos.to[List].map { r => Repo(r.id.key, r.commit.key, r.remote.ref, + r.branch.id) }, layer.projects.to[List].map(Project(_, universe, layout))) + } + } +} \ No newline at end of file diff --git a/src/model/ids.scala b/src/model/ids.scala index d595328cf..a2c24364f 100644 --- a/src/model/ids.scala +++ b/src/model/ids.scala @@ -134,6 +134,8 @@ case class Pointer(path: String) { def /(importId: ImportId): Pointer = Pointer(if(isEmpty) str"/${importId.key}" else s"$path/${importId.key}") def tail: Pointer = Pointer(parts.tail.map(_.key).mkString("/", "/", "")) def init: Pointer = Pointer(parts.init.map(_.key).mkString("/", "/", "")) + def lastOption: Option[ImportId] = if(isEmpty) None else Some(last) + def headOption: Option[ImportId] = if(isEmpty) None else Some(head) def head: ImportId = parts.head def last: ImportId = parts.last def isEmpty: Boolean = parts.length == 0