mirror of
https://github.com/sbt/sbt.git
synced 2026-10-06 18:14:04 +02:00
107 lines
3.5 KiB
Scala
107 lines
3.5 KiB
Scala
/* sbt -- Simple Build Tool
|
|
* Copyright 2009, 2010 Mark Harrah
|
|
*/
|
|
package xsbt.boot
|
|
|
|
import java.io.{File, FileOutputStream, IOException}
|
|
import java.nio.channels.FileChannel
|
|
import java.util.concurrent.Callable
|
|
import scala.collection.immutable.List
|
|
import scala.annotation.tailrec
|
|
|
|
object GetLocks
|
|
{
|
|
/** Searches for Locks in parent class loaders before returning Locks from this class loader.
|
|
* Normal class loading doesn't work because the launcher class loader hides xsbt classes.*/
|
|
def find: xsbti.GlobalLock =
|
|
Loaders(getClass.getClassLoader.getParent).flatMap(tryGet).headOption.getOrElse(Locks)
|
|
private[this] def tryGet(loader: ClassLoader): List[xsbti.GlobalLock] =
|
|
try { getLocks0(loader) :: Nil } catch { case e: ClassNotFoundException => Nil }
|
|
private[this] def getLocks0(loader: ClassLoader) =
|
|
Class.forName("xsbt.boot.Locks$", true, loader).getField("MODULE$").get(null).asInstanceOf[xsbti.GlobalLock]
|
|
}
|
|
|
|
// gets a file lock by first getting a JVM-wide lock.
|
|
object Locks extends xsbti.GlobalLock
|
|
{
|
|
private[this] val locks = new Cache[File, Unit, GlobalLock]( (f, _) => new GlobalLock(f))
|
|
def apply[T](file: File, action: Callable[T]): T = if(file eq null) action.call else apply0(file, action)
|
|
private[this] def apply0[T](file: File, action: Callable[T]): T =
|
|
{
|
|
val lock =
|
|
synchronized
|
|
{
|
|
file.getParentFile.mkdirs()
|
|
file.createNewFile()
|
|
locks(file.getCanonicalFile, ())
|
|
}
|
|
lock.withLock(action)
|
|
}
|
|
|
|
private[this] class GlobalLock(file: File)
|
|
{
|
|
private[this] var fileLocked = false
|
|
def withLock[T](run: Callable[T]): T =
|
|
synchronized
|
|
{
|
|
if(fileLocked)
|
|
run.call
|
|
else
|
|
{
|
|
fileLocked = true
|
|
try { ignoringDeadlockAvoided(run) }
|
|
finally { fileLocked = false }
|
|
}
|
|
}
|
|
|
|
// https://github.com/sbt/sbt/issues/650
|
|
// This approach means a real deadlock won't be detected
|
|
@tailrec private[this] def ignoringDeadlockAvoided[T](run: Callable[T]): T =
|
|
{
|
|
val result =
|
|
try { Some(withFileLock(run)) }
|
|
catch { case i: IOException if isDeadlockAvoided(i) =>
|
|
// there should be a timeout to the deadlock avoidance, so this is just a backup
|
|
Thread.sleep(200)
|
|
None
|
|
}
|
|
result match { // workaround for no tailrec optimization in the above try/catch
|
|
case Some(t) => t
|
|
case None => ignoringDeadlockAvoided(run)
|
|
}
|
|
}
|
|
|
|
// The actual message is not specified by FileChannel.lock, so this may need to be adjusted for different JVMs
|
|
private[this] def isDeadlockAvoided(i: IOException): Boolean =
|
|
i.getMessage == "Resource deadlock avoided"
|
|
|
|
private[this] def withFileLock[T](run: Callable[T]): T =
|
|
{
|
|
def withChannelRetries(retries: Int)(channel: FileChannel): T =
|
|
try { withChannel(channel) }
|
|
catch { case i: InternalLockNPE =>
|
|
if(retries > 0) withChannelRetries(retries - 1)(channel) else throw i
|
|
}
|
|
|
|
def withChannel(channel: FileChannel) =
|
|
{
|
|
val freeLock = try { channel.tryLock } catch { case e: NullPointerException => throw new InternalLockNPE(e) }
|
|
if(freeLock eq null)
|
|
{
|
|
System.out.println("Waiting for lock on " + file + " to be available...");
|
|
val lock = try { channel.lock } catch { case e: NullPointerException => throw new InternalLockNPE(e) }
|
|
try { run.call }
|
|
finally { lock.release() }
|
|
}
|
|
else
|
|
{
|
|
try { run.call }
|
|
finally { freeLock.release() }
|
|
}
|
|
}
|
|
Using(new FileOutputStream(file).getChannel)(withChannelRetries(5))
|
|
}
|
|
}
|
|
private[this] final class InternalLockNPE(cause: Exception) extends RuntimeException(cause)
|
|
}
|