diff --git a/ivy/ComponentManager.scala b/ivy/ComponentManager.scala index e420ffe22..483a0bb31 100644 --- a/ivy/ComponentManager.scala +++ b/ivy/ComponentManager.scala @@ -1,6 +1,7 @@ package xsbt -import java.io.File +import java.io.{File,FileOutputStream} +import ComponentManager.lock /** A component manager provides access to the pieces of xsbt that are distributed as components. * There are two types of components. The first type is compiled subproject jars with their dependencies. @@ -14,33 +15,73 @@ import java.io.File class ComponentManager(provider: xsbti.ComponentProvider, val log: IvyLogger) extends NotNull { /** Get all of the files for component 'id', throwing an exception if no files exist for the component. */ - def files(id: String): Iterable[File] = + def files(id: String)(ifMissing: IfMissing): Iterable[File] = { - val existing = provider.component(id) - val fs = if(existing.isEmpty) { update(id); provider.component(id) } else existing - if(!fs.isEmpty) fs else invalid("Could not find required component '" + id + "'") + def fromGlobal = + lockGlobalCache { + try { update(id); getOrElse(createAndCache) } + catch { case e: NotInCache => createAndCache } + } + def getOrElse(orElse: => Iterable[File]) = + { + val existing = provider.component(id) + if(existing.isEmpty) orElse else existing + } + def notFound = invalid("Could not find required component '" + id + "'") + def createAndCache = + ifMissing match { + case IfMissing.Fail => notFound + case d: IfMissing.Define => + d() + if(d.cache) cache(id) + getOrElse(notFound) + } + + lockLocalCache { getOrElse(fromGlobal) } } + private def lockLocalCache[T](action: => T): T = lock("local cache", provider.lockFile, log) ( action ) + private def lockGlobalCache[T](action: => T): T = lock("global cache", IvyCache.lockFile, log)( action ) /** Get the file for component 'id', throwing an exception if no files or multiple files exist for the component. */ - def file(id: String): File = - files(id).toList match { + def file(id: String)(ifMissing: IfMissing): File = + files(id)(ifMissing).toList match { case x :: Nil => x case xs => invalid("Expected single file for component '" + id + "', found: " + xs.mkString(", ")) } private def invalid(msg: String) = throw new InvalidComponent(msg) private def invalid(e: NotInCache) = throw new InvalidComponent(e.getMessage, e) - def define(id: String, files: Iterable[File]) = provider.defineComponent(id, files.toSeq.toArray) + def define(id: String, files: Iterable[File]) = lockLocalCache { provider.defineComponent(id, files.toSeq.toArray) } /** Retrieve the file for component 'id' from the local repository. */ - def update(id: String): Unit = - try { IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) ) } - catch { case e: NotInCache => invalid(e) } + private def update(id: String): Unit = IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) ) - def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, xsbti.Versions.Sbt) + private def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, xsbti.Versions.Sbt) /** Install the files for component 'id' to the local repository. This is usually used after writing files to the directory returned by 'location'. */ - def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id), log) - def clearCache(id: String): Unit = IvyCache.clearCachedJar(sbtModuleID(id), log) + def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id)(IfMissing.Fail), log) + def clearCache(id: String): Unit = lockGlobalCache { IvyCache.clearCachedJar(sbtModuleID(id), log) } } class InvalidComponent(msg: String, cause: Throwable) extends RuntimeException(msg, cause) { def this(msg: String) = this(msg, null) +} +sealed trait IfMissing extends NotNull +object IfMissing +{ + object Fail extends IfMissing + final class Define(val cache: Boolean, define: => Unit) extends IfMissing { def apply() = define } +} +object ComponentManager +{ + def lock[T](label: String, file: File, log: IvyLogger)(action: => T): T = + { + synchronized { + val channel = new FileOutputStream(file).getChannel + try { + val freeLock = channel.tryLock + val lock = if(freeLock eq null) { log.info("Waiting for " + label + " to be available..."); channel.lock } else freeLock + try { action } + finally { lock.release() } + } + finally { channel.close() } + } + } } \ No newline at end of file diff --git a/ivy/IvyCache.scala b/ivy/IvyCache.scala index 9e9c4d9e4..74c27e3e8 100644 --- a/ivy/IvyCache.scala +++ b/ivy/IvyCache.scala @@ -27,6 +27,7 @@ private object NotInCache /** Provides methods for working at the level of a single jar file with the default Ivy cache.*/ object IvyCache { + def lockFile = new File(System.getProperty("temp.dir"), "sbt.cache.lock") /** Caches the given 'file' with the given ID. It may be retrieved or cleared using this ID.*/ def cacheJar(moduleID: ModuleID, file: File, log: IvyLogger) { diff --git a/ivy/src/test/scala/ComponentManagerTest.scala b/ivy/src/test/scala/ComponentManagerTest.scala index 6928ab98a..90613ce1e 100644 --- a/ivy/src/test/scala/ComponentManagerTest.scala +++ b/ivy/src/test/scala/ComponentManagerTest.scala @@ -4,47 +4,48 @@ import java.io.File import org.specs._ import FileUtilities.{createDirectory, delete, touch, withTemporaryDirectory} import org.apache.ivy.util.ChecksumHelper +import IfMissing.Fail object ComponentManagerTest extends Specification { val TestID = "manager-test" "Component manager" should { "throw an exception if 'file' is called for a non-existing component" in { - withManager { _.file(TestID) must throwA[InvalidComponent] } + withManager { _.file(TestID)(Fail) must throwA[InvalidComponent] } } "throw an exception if 'file' is called for an empty component" in { withManager { manager => manager.define(TestID, Nil) - ( manager.file(TestID) ) must throwA[InvalidComponent] + ( manager.file(TestID)(Fail) ) must throwA[InvalidComponent] } } "return the file for a single-file component" in { withManager { manager => val hash = defineFile(manager, TestID, "a") - checksum(manager.file(TestID)) must beEqualTo(hash) + checksum(manager.file(TestID)(Fail)) must beEqualTo(hash) } } "throw an exception if 'file' is called for multi-file component" in { withManager { manager => defineFiles(manager, TestID, "a", "b") - ( manager.file(TestID) ) must throwA[InvalidComponent] + ( manager.file(TestID)(Fail) ) must throwA[InvalidComponent] } } "return the files for a multi-file component" in { withManager { manager => val hashes = defineFiles(manager, TestID, "a", "b") - checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes) + checksum(manager.files(TestID)(Fail)) must haveTheSameElementsAs(hashes) } } "return the files for a single-file component" in { withManager { manager => val hashes = defineFiles(manager, TestID, "a") - checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes) + checksum(manager.files(TestID)(Fail)) must haveTheSameElementsAs(hashes) } } "throw an exception if 'files' is called for a non-existing component" in { - withManager { _.files(TestID) must throwA[InvalidComponent] } + withManager { _.files(TestID)(Fail) must throwA[InvalidComponent] } } "properly cache a file and then retrieve it to an unresolved component" in { @@ -54,7 +55,7 @@ object ComponentManagerTest extends Specification { definingManager.cache(TestID) withManager { usingManager => - checksum(usingManager.file(TestID)) must beEqualTo(hash) + checksum(usingManager.file(TestID)(Fail)) must beEqualTo(hash) } } finally { definingManager.clearCache(TestID) }