/* 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) }