mirror of https://github.com/sbt/sbt.git
Download progress in terminal
This commit is contained in:
parent
adef5a2438
commit
ae66d22024
|
|
@ -162,6 +162,7 @@ lazy val cli = project
|
|||
name := "coursier-cli",
|
||||
libraryDependencies ++= Seq(
|
||||
"com.github.alexarchambault" %% "case-app" % "1.0.0-SNAPSHOT",
|
||||
"com.lihaoyi" %% "ammonite-terminal" % "0.5.0",
|
||||
"ch.qos.logback" % "logback-classic" % "1.1.3"
|
||||
),
|
||||
resourceGenerators in Compile += packageBin.in(bootstrap).in(Compile).map { jar =>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ case class CommonOptions(
|
|||
@Recurse
|
||||
cacheOptions: CacheOptions
|
||||
) {
|
||||
val verbose0 = verbose.length + (if (quiet) 1 else 0)
|
||||
val verbose0 = verbose.length - (if (quiet) 1 else 0)
|
||||
}
|
||||
|
||||
object CacheOptions {
|
||||
|
|
@ -70,7 +70,7 @@ case class Fetch(
|
|||
|
||||
val files0 = helper.fetch(main = true, sources = false, javadoc = false)
|
||||
|
||||
Console.out.println(
|
||||
println(
|
||||
files0
|
||||
.map(_.toString)
|
||||
.mkString("\n")
|
||||
|
|
@ -115,7 +115,7 @@ case class Launch(
|
|||
|
||||
val mainClass =
|
||||
if (mainClasses.isEmpty) {
|
||||
Console.err.println(s"No main class found. Specify one with -M or --main.")
|
||||
Helper.errPrintln("No main class found. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
} else if (mainClasses.size == 1) {
|
||||
val (_, mainClass) = mainClasses.head
|
||||
|
|
@ -135,8 +135,7 @@ case class Launch(
|
|||
} yield mainClass
|
||||
|
||||
mainClassOpt.getOrElse {
|
||||
println(mainClasses)
|
||||
Console.err.println(s"Cannot find default main class. Specify one with -M or --main.")
|
||||
Helper.errPrintln(s"Cannot find default main class. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
}
|
||||
}
|
||||
|
|
@ -147,18 +146,20 @@ case class Launch(
|
|||
val cls =
|
||||
try cl.loadClass(mainClass0)
|
||||
catch { case e: ClassNotFoundException =>
|
||||
println(s"Error: class $mainClass0 not found")
|
||||
Helper.errPrintln(s"Error: class $mainClass0 not found")
|
||||
sys.exit(255)
|
||||
}
|
||||
val method =
|
||||
try cls.getMethod("main", classOf[Array[String]])
|
||||
catch { case e: NoSuchMethodError =>
|
||||
println(s"Error: method main not found in $mainClass0")
|
||||
Helper.errPrintln(s"Error: method main not found in $mainClass0")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
if (common.verbose0 >= 1)
|
||||
println(s"Calling $mainClass0 ${extraArgs.mkString(" ")}")
|
||||
Helper.errPrintln(s"Launching $mainClass0 ${extraArgs.mkString(" ")}")
|
||||
else if (common.verbose0 == 0)
|
||||
Helper.errPrintln(s"Launching")
|
||||
|
||||
Thread.currentThread().setContextClassLoader(cl)
|
||||
method.invoke(null, extraArgs.toArray)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.io.File
|
||||
import java.io.{ OutputStreamWriter, File }
|
||||
import java.util.UUID
|
||||
|
||||
import scalaz.{ \/-, -\/ }
|
||||
|
|
@ -28,31 +28,6 @@ object Helper {
|
|||
|
||||
def errPrintln(s: String) = Console.err.println(s)
|
||||
|
||||
def defaultLogger: Files.Logger =
|
||||
new Files.Logger {
|
||||
def foundLocally(url: String, f: File) = {}
|
||||
def downloadingArtifact(url: String) =
|
||||
errPrintln(s"Downloading $url")
|
||||
def downloadedArtifact(url: String, success: Boolean) = {}
|
||||
def downloadLength(url: String, length: Long) = {}
|
||||
def downloadProgress(url: String, downloaded: Long) = {}
|
||||
}
|
||||
|
||||
def verboseLogger: Files.Logger =
|
||||
new Files.Logger {
|
||||
def foundLocally(url: String, f: File) =
|
||||
errPrintln(s"Found $url locally (${fileRepr(f)})")
|
||||
def downloadingArtifact(url: String) =
|
||||
errPrintln(s"Downloading $url")
|
||||
def downloadedArtifact(url: String, success: Boolean) =
|
||||
errPrintln(
|
||||
if (success) s"Downloaded $url"
|
||||
else s"Failed: $url"
|
||||
)
|
||||
def downloadLength(url: String, length: Long) = {}
|
||||
def downloadProgress(url: String, downloaded: Long) = {}
|
||||
}
|
||||
|
||||
def mainClasses(cl: ClassLoader): Map[(String, String), String] = {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
|
|
@ -79,15 +54,6 @@ class Helper(
|
|||
import common._
|
||||
import Helper.errPrintln
|
||||
|
||||
|
||||
val logger =
|
||||
if (verbose0 < 0)
|
||||
None
|
||||
else if (verbose0 == 0)
|
||||
Some(Helper.defaultLogger)
|
||||
else
|
||||
Some(Helper.verboseLogger)
|
||||
|
||||
implicit val cachePolicy =
|
||||
if (offline)
|
||||
CachePolicy.LocalOnly
|
||||
|
|
@ -198,9 +164,15 @@ class Helper(
|
|||
filter = Some(dep => keepOptional || !dep.optional)
|
||||
)
|
||||
|
||||
val logger =
|
||||
if (verbose0 >= 0)
|
||||
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
||||
else
|
||||
None
|
||||
logger.foreach(_.init())
|
||||
val fetchQuiet = coursier.Fetch(repositories, files.fetch(logger = logger))
|
||||
val fetch0 =
|
||||
if (verbose0 == 0) fetchQuiet
|
||||
if (verbose0 <= 0) fetchQuiet
|
||||
else {
|
||||
modVers: Seq[(Module, String)] =>
|
||||
val print = Task{
|
||||
|
|
@ -218,6 +190,8 @@ class Helper(
|
|||
.run(fetch0, maxIterations)
|
||||
.run
|
||||
|
||||
logger.foreach(_.stop())
|
||||
|
||||
if (!res.isDone) {
|
||||
errPrintln(s"Maximum number of iteration reached!")
|
||||
sys.exit(1)
|
||||
|
|
@ -254,7 +228,7 @@ class Helper(
|
|||
.toList
|
||||
.sortBy(repr)
|
||||
|
||||
if (verbose0 >= 0) {
|
||||
if (verbose0 >= 1) {
|
||||
println("")
|
||||
println(
|
||||
trDeps
|
||||
|
|
@ -278,8 +252,8 @@ class Helper(
|
|||
}
|
||||
|
||||
def fetch(main: Boolean, sources: Boolean, javadoc: Boolean): Seq[File] = {
|
||||
println("")
|
||||
|
||||
if (verbose0 >= 0)
|
||||
errPrintln("Fetching artifacts")
|
||||
val artifacts0 = res.artifacts
|
||||
val main0 = main || (!sources && !javadoc)
|
||||
val artifacts = artifacts0.flatMap{ artifact =>
|
||||
|
|
@ -294,9 +268,15 @@ class Helper(
|
|||
l
|
||||
}
|
||||
|
||||
val logger =
|
||||
if (verbose0 >= 0)
|
||||
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
||||
else
|
||||
None
|
||||
logger.foreach(_.init())
|
||||
val tasks = artifacts.map(artifact => files.file(artifact, logger = logger).run.map(artifact.->))
|
||||
def printTask = Task{
|
||||
if (verbose0 >= 0 && artifacts.nonEmpty)
|
||||
def printTask = Task {
|
||||
if (verbose0 >= 1 && artifacts.nonEmpty)
|
||||
println(s"Found ${artifacts.length} artifacts")
|
||||
}
|
||||
val task = printTask.flatMap(_ => Task.gatherUnordered(tasks))
|
||||
|
|
@ -305,6 +285,8 @@ class Helper(
|
|||
val errors = results.collect{case (artifact, -\/(err)) => artifact -> err }
|
||||
val files0 = results.collect{case (artifact, \/-(f)) => f }
|
||||
|
||||
logger.foreach(_.stop())
|
||||
|
||||
if (errors.nonEmpty) {
|
||||
println(s"${errors.size} error(s):")
|
||||
for ((artifact, error) <- errors) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
package coursier.cli
|
||||
|
||||
import java.io.Writer
|
||||
import java.util.concurrent._
|
||||
|
||||
import ammonite.terminal.{ TTY, Ansi }
|
||||
|
||||
import coursier.Files.Logger
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
class TermDisplay(out: Writer) extends Logger {
|
||||
|
||||
private val ansi = new Ansi(out)
|
||||
private var width = 80
|
||||
private val refreshInterval = 1000 / 60
|
||||
private val lock = new AnyRef
|
||||
private val t = new Thread("TermDisplay") {
|
||||
override def run() = lock.synchronized {
|
||||
val baseExtraWidth = width / 5
|
||||
@tailrec def helper(lineCount: Int): Unit =
|
||||
Option(q.poll(100L, TimeUnit.MILLISECONDS)) match {
|
||||
case None => helper(lineCount)
|
||||
case Some(Left(())) => // poison pill
|
||||
case Some(Right(())) =>
|
||||
// update display
|
||||
|
||||
for (_ <- 0 until lineCount) {
|
||||
ansi.up(1)
|
||||
ansi.clearLine(2)
|
||||
}
|
||||
|
||||
val downloads0 = downloads.synchronized {
|
||||
downloads
|
||||
.toVector
|
||||
.map { url => url -> infos.get(url) }
|
||||
.sortBy { case (_, info) => - info.pct.sum }
|
||||
}
|
||||
|
||||
for ((url, info) <- downloads0) {
|
||||
assert(info != null, s"Incoherent state ($url)")
|
||||
val pctOpt = info.pct.map(100.0 * _)
|
||||
val extra = s"(${pctOpt.map(pct => f"$pct%.2f %%, ").mkString}${info.downloaded}${info.length.map(" / " + _).mkString})"
|
||||
|
||||
val total = url.length + 1 + extra.length
|
||||
val (url0, extra0) =
|
||||
if (total >= width) { // or > ? If equal, does it go down 2 lines?
|
||||
val overflow = total - width + 1
|
||||
|
||||
val extra0 =
|
||||
if (extra.length > baseExtraWidth)
|
||||
extra.take((baseExtraWidth max (extra.length - overflow)) - 1) + "…"
|
||||
else
|
||||
extra
|
||||
|
||||
val total0 = url.length + 1 + extra0.length
|
||||
val overflow0 = total0 - width + 1
|
||||
|
||||
val url0 =
|
||||
if (total0 >= width)
|
||||
url.take(((width - baseExtraWidth - 1) max (url.length - overflow0)) - 1) + "…"
|
||||
else
|
||||
url
|
||||
|
||||
(url0, extra0)
|
||||
} else
|
||||
(url, extra)
|
||||
|
||||
out.write(s"$url0 $extra0\n")
|
||||
}
|
||||
|
||||
out.flush()
|
||||
Thread.sleep(refreshInterval)
|
||||
helper(downloads0.length)
|
||||
}
|
||||
|
||||
helper(0)
|
||||
}
|
||||
}
|
||||
|
||||
t.setDaemon(true)
|
||||
|
||||
def init(): Unit = {
|
||||
width = TTY.consoleDim("cols")
|
||||
ansi.clearLine(2)
|
||||
t.start()
|
||||
}
|
||||
|
||||
def stop(): Unit = {
|
||||
q.put(Left(()))
|
||||
lock.synchronized(())
|
||||
}
|
||||
|
||||
private case class Info(downloaded: Long, length: Option[Long]) {
|
||||
def pct: Option[Double] = length.map(downloaded.toDouble / _)
|
||||
}
|
||||
|
||||
private val downloads = new ArrayBuffer[String]
|
||||
private val infos = new ConcurrentHashMap[String, Info]
|
||||
|
||||
private val q = new LinkedBlockingDeque[Either[Unit, Unit]]
|
||||
def update(): Unit = {
|
||||
if (q.size() == 0)
|
||||
q.put(Right(()))
|
||||
}
|
||||
|
||||
override def downloadingArtifact(url: String): Unit = {
|
||||
assert(!infos.containsKey(url))
|
||||
val prev = infos.putIfAbsent(url, Info(0L, None))
|
||||
assert(prev == null)
|
||||
|
||||
downloads.synchronized {
|
||||
downloads.append(url)
|
||||
}
|
||||
|
||||
update()
|
||||
}
|
||||
override def downloadLength(url: String, length: Long): Unit = {
|
||||
val info = infos.get(url)
|
||||
assert(info != null)
|
||||
val newInfo = info.copy(length = Some(length))
|
||||
infos.put(url, newInfo)
|
||||
|
||||
update()
|
||||
}
|
||||
override def downloadProgress(url: String, downloaded: Long): Unit = {
|
||||
val info = infos.get(url)
|
||||
assert(info != null)
|
||||
val newInfo = info.copy(downloaded = downloaded)
|
||||
infos.put(url, newInfo)
|
||||
|
||||
update()
|
||||
}
|
||||
override def downloadedArtifact(url: String, success: Boolean): Unit = {
|
||||
downloads.synchronized {
|
||||
downloads -= url
|
||||
}
|
||||
|
||||
val info = infos.remove(url)
|
||||
assert(info != null)
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -269,11 +269,11 @@ object Files {
|
|||
val defaultConcurrentDownloadCount = 6
|
||||
|
||||
trait Logger {
|
||||
def foundLocally(url: String, f: File): Unit
|
||||
def downloadingArtifact(url: String): Unit
|
||||
def downloadLength(url: String, length: Long): Unit
|
||||
def downloadProgress(url: String, downloaded: Long): Unit
|
||||
def downloadedArtifact(url: String, success: Boolean): Unit
|
||||
def foundLocally(url: String, f: File): Unit = {}
|
||||
def downloadingArtifact(url: String): Unit = {}
|
||||
def downloadLength(url: String, length: Long): Unit = {}
|
||||
def downloadProgress(url: String, downloaded: Long): Unit = {}
|
||||
def downloadedArtifact(url: String, success: Boolean): Unit = {}
|
||||
}
|
||||
|
||||
var bufferSize = 1024*1024
|
||||
|
|
|
|||
Loading…
Reference in New Issue