Download progress in terminal

This commit is contained in:
Alexandre Archambault 2015-11-29 20:22:25 +01:00
parent adef5a2438
commit ae66d22024
5 changed files with 184 additions and 54 deletions

View File

@ -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 =>

View File

@ -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)

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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