improved global logging and 'last' command

This commit is contained in:
Mark Harrah 2011-07-24 17:36:42 -04:00
parent f654b03618
commit 30baf74169
8 changed files with 108 additions and 51 deletions

View File

@ -11,7 +11,8 @@ import Path._
object CommandSupport
{
def logger(s: State) = s get Keys.logged getOrElse ConsoleLogger()
def logger(s: State) = globalLogging(s).full
def globalLogging(s: State) = s get Keys.globalLogging.key getOrElse error("Global logging misconfigured")
// slightly better fallback in case of older launcher
def bootDirectory(state: State): File =

View File

@ -5,7 +5,7 @@ package sbt
import java.io.File
import Project.{ScopedKey, Setting}
import Keys.{streams, Streams, TaskStreams}
import Keys.{globalLogging, streams, Streams, TaskStreams}
import Keys.{dummyState, dummyStreamsManager, streamsManager, taskDefinitionKey}
import Scope.{GlobalScope, ThisScope}
import scala.Console.{RED, RESET}
@ -41,7 +41,6 @@ object EvaluateTask
def logIncResult(result: Result[_], streams: Streams) = result match { case Inc(i) => logIncomplete(i, streams); case _ => () }
def logIncomplete(result: Incomplete, streams: Streams)
{
val log = streams(ScopedKey(GlobalScope, Keys.logged)).log
val all = Incomplete linearize result
val keyed = for(Incomplete(Some(key: Project.ScopedKey[_]), _, msg, _, ex) <- all) yield (key, msg, ex)
val un = all.filter { i => i.node.isEmpty || i.message.isEmpty }
@ -51,8 +50,9 @@ object EvaluateTask
for( (key, msg, ex) <- keyed if(msg.isDefined || ex.isDefined) )
{
val msgString = (msg.toList ++ ex.toList.map(ErrorHandling.reducedToString)).mkString("\n\t")
val log = getStreams(key, streams).log
val keyString = if(log.ansiCodesSupported) RED + key.key.label + RESET else key.key.label
getStreams(key, streams).log.error(Scope.display(key.scope, keyString) + ": " + msgString)
log.error(Scope.display(key.scope, keyString) + ": " + msgString)
}
}
def getStreams(key: ScopedKey[_], streams: Streams): TaskStreams =

View File

@ -46,7 +46,7 @@ object Keys
val onUnload = SettingKey[State => State]("on-unload", "Transformation to apply to the build state when the build is unloaded.")
// Command keys
val logged = AttributeKey[Logger]("log", "Provides a Logger for commands.")
val globalLogging = SettingKey[GlobalLogging]("global-logging", "Provides a global Logger, including command logging.")
val historyPath = SettingKey[Option[File]]("history", "The location where command line history is persisted.")
val shellPrompt = SettingKey[State => String]("shell-prompt", "The function that constructs the command prompt from the current build state.")
val analysis = AttributeKey[inc.Analysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.")

View File

@ -13,7 +13,7 @@ package sbt
import inc.{FileValueCache, Locate}
import Project.{inScope, ScopedKey, ScopeLocal, Setting}
import Keys.{appConfiguration, baseDirectory, configuration, streams, Streams, thisProject, thisProjectRef}
import Keys.{isDummy, parseResult, resolvedScoped, taskDefinitionKey}
import Keys.{globalLogging, isDummy, parseResult, resolvedScoped, taskDefinitionKey}
import tools.nsc.reporters.ConsoleReporter
import Build.{analyzed, data}
import Scope.{GlobalScope, ThisScope}
@ -47,10 +47,13 @@ object Load
val compilers = Compiler.compilers(ClasspathOptions.boot)(state.configuration, log)
val evalPluginDef = EvaluateTask.evalPluginDef(log) _
val delegates = defaultDelegates
val injectGlobal: Seq[Project.Setting[_]] = ((appConfiguration in GlobalScope) :== state.configuration) +: EvaluateTask.injectSettings
val inject = InjectSettings(injectGlobal, Nil, const(Nil))
val inject = InjectSettings(injectGlobal(state), Nil, const(Nil))
new LoadBuildConfiguration(stagingDirectory, classpath, loader, compilers, evalPluginDef, definesClass, delegates, EvaluateTask.injectStreams, inject, None, log)
}
def injectGlobal(state: State): Seq[Project.Setting[_]] =
(appConfiguration in GlobalScope :== state.configuration) +:
(globalLogging in GlobalScope := CommandSupport.globalLogging(state)) +:
EvaluateTask.injectSettings
def defaultWithGlobal(state: State, base: File, rawConfig: LoadBuildConfiguration, globalBase: File, log: Logger): LoadBuildConfiguration =
{
val withGlobal = loadGlobal(state, base, defaultGlobalPlugins(globalBase), rawConfig)

View File

@ -4,10 +4,12 @@
package sbt
import java.io.PrintWriter
import java.io.File
import LogManager._
import std.Transform
import Project.ScopedKey
import Keys.{logLevel, logManager, persistLogLevel, persistTraceLevel, traceLevel}
import Scope.GlobalScope
import Keys.{logLevel, logManager, persistLogLevel, persistTraceLevel, state, traceLevel}
object LogManager
{
@ -20,12 +22,13 @@ object LogManager
def defaults(extra: ScopedKey[_] => Seq[AbstractLogger]): LogManager = withLoggers(extra = extra)
def defaultScreen: AbstractLogger = ConsoleLogger()
def defaultBacked(useColor: Boolean): PrintWriter => AbstractLogger =
def defaultBacked(useColor: Boolean = ConsoleLogger.formatEnabled): PrintWriter => ConsoleLogger =
to => ConsoleLogger(ConsoleLogger.printWriterOut(to), useColor = useColor) // TODO: should probably filter ANSI codes when useColor=false
def withScreenLogger(mk: => AbstractLogger): LogManager = withLoggers(mk)
def withLoggers(screen: => AbstractLogger = defaultScreen, backed: PrintWriter => AbstractLogger = defaultBacked(ConsoleLogger.formatEnabled), extra: ScopedKey[_] => Seq[AbstractLogger] = _ => Nil): LogManager =
def withLoggers(screen: => AbstractLogger = defaultScreen, backed: PrintWriter => AbstractLogger = defaultBacked(), extra: ScopedKey[_] => Seq[AbstractLogger] = _ => Nil): LogManager =
new LogManager {
def apply(data: Settings[Scope], task: ScopedKey[_], to: PrintWriter): Logger =
defaultLogger(data, task, screen, backed(to), extra(task).toList)
@ -39,7 +42,12 @@ object LogManager
val backingLevel = getOr(persistLogLevel.key, Level.Debug)
val screenTrace = getOr(traceLevel.key, -1)
val backingTrace = getOr(persistTraceLevel.key, Int.MaxValue)
val extraBacked = data.get(Scope.GlobalScope, Keys.globalLogging.key).map(_.backed).toList
multiLogger( new MultiLoggerConfig(console, backed, extraBacked ::: extra, screenLevel, backingLevel, screenTrace, backingTrace) )
}
def multiLogger(config: MultiLoggerConfig): Logger =
{
import config._
val multi = new MultiLogger(console :: backed :: extra)
// sets multi to the most verbose for clients that inspect the current level
multi setLevel Level.unionAll(backingLevel :: screenLevel :: extra.map(_.getLevel))
@ -50,8 +58,19 @@ object LogManager
backed setTrace backingTrace
multi: Logger
}
def globalDefault(writer: PrintWriter, file: File): GlobalLogging =
{
val backed = defaultBacked()(writer)
val full = multiLogger(defaultMultiConfig( backed ) )
GlobalLogging(full, backed, file)
}
def defaultMultiConfig(backing: AbstractLogger): MultiLoggerConfig =
new MultiLoggerConfig(defaultScreen, backing, Nil, Level.Info, Level.Debug, -1, Int.MaxValue)
}
final case class MultiLoggerConfig(console: AbstractLogger, backed: AbstractLogger, extra: List[AbstractLogger], screenLevel: Level.Value, backingLevel: Level.Value, screenTrace: Int, backingTrace: Int)
trait LogManager
{
def apply(data: Settings[Scope], task: ScopedKey[_], writer: PrintWriter): Logger
}
}
final case class GlobalLogging(full: Logger, backed: ConsoleLogger, backing: File)

View File

@ -10,7 +10,7 @@ package sbt
import Types.idFun
import Command.applyEffect
import Keys.{analysis,historyPath,logged,shellPrompt}
import Keys.{analysis,historyPath,globalLogging,shellPrompt}
import scala.annotation.tailrec
import scala.collection.JavaConversions._
import Function.tupled
@ -30,7 +30,7 @@ final class xMain extends xsbti.AppMain
val initialCommandDefs = Seq(initialize, defaults)
val commands = DefaultsCommand +: InitCommand +: (DefaultBootCommands ++ configuration.arguments.map(_.trim))
val state = State( configuration, initialCommandDefs, Set.empty, None, commands, initialAttributes, None )
MainLoop.run(state)
MainLoop.runLogged(state)
}
}
final class ScriptMain extends xsbti.AppMain
@ -40,7 +40,7 @@ final class ScriptMain extends xsbti.AppMain
import BuiltinCommands.{initialAttributes, ScriptCommands}
val commands = Script.Name +: configuration.arguments.map(_.trim)
val state = State( configuration, ScriptCommands, Set.empty, None, commands, initialAttributes, None )
MainLoop.run(state)
MainLoop.runLogged(state)
}
}
final class ConsoleMain extends xsbti.AppMain
@ -50,12 +50,32 @@ final class ConsoleMain extends xsbti.AppMain
import BuiltinCommands.{initialAttributes, ConsoleCommands}
val commands = IvyConsole.Name +: configuration.arguments.map(_.trim)
val state = State( configuration, ConsoleCommands, Set.empty, None, commands, initialAttributes, None )
MainLoop.run(state)
MainLoop.runLogged(state)
}
}
object MainLoop
{
@tailrec final def run(state: State): xsbti.MainResult =
def runLogged(state: State): xsbti.MainResult =
{
val logFile = File.createTempFile("sbt", ".log")
try {
val result = runLogged(state, logFile)
logFile.delete() // only delete when exiting normally
result
}
catch {
case e: xsbti.FullReload => throw e
case e => System.err.println("sbt appears to be exiting abnormally.\n The log file for this session is at " + logFile); throw e
}
}
def runLogged(state: State, backing: File): xsbti.MainResult =
Using.fileWriter()(backing) { writer =>
val out = new java.io.PrintWriter(writer)
val loggedState = state.put(globalLogging.key, LogManager.globalDefault(out, backing))
try { run(loggedState) } finally { out.close() }
}
@tailrec def run(state: State): xsbti.MainResult =
state.result match
{
case None => run(next(state))
@ -75,7 +95,7 @@ object MainLoop
import CommandSupport._
object BuiltinCommands
{
def initialAttributes = AttributeMap.empty.put(logged, ConsoleLogger())
def initialAttributes = AttributeMap.empty
def ConsoleCommands: Seq[Command] = Seq(ignore, exit, IvyConsole.command, act, nop)
def ScriptCommands: Seq[Command] = Seq(ignore, exit, Script.command, act, nop)
@ -176,7 +196,9 @@ object BuiltinCommands
val reader = new FullReader(history, s.combinedParser)
val line = reader.readLine(prompt)
line match {
case Some(line) => s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands)
case Some(line) =>
if(!line.trim.isEmpty) CommandSupport.globalLogging(s).backed.out.println(Output.DefaultTail + line)
s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands)
case None => s
}
}
@ -308,10 +330,14 @@ object BuiltinCommands
logger(s).info(detailString)
s
}
def lastGrep = Command(LastGrepCommand, lastGrepBrief, lastGrepDetailed)(lastGrepParser) { case (s,(pattern,sk)) =>
val (str, ref) = extractLast(s)
Output.lastGrep(sk, str, pattern, ref)
s
def lastGrep = Command(LastGrepCommand, lastGrepBrief, lastGrepDetailed)(lastGrepParser) {
case (s, (pattern,Some(sk))) =>
val (str, ref) = extractLast(s)
Output.lastGrep(sk, str, pattern)
s
case (s, (pattern, None)) =>
Output.lastGrep(CommandSupport.globalLogging(s).backing, pattern)
s
}
def extractLast(s: State) = {
val ext = Project.extract(s)
@ -321,10 +347,14 @@ object BuiltinCommands
val spacedKeyParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.scopedKeyParser(s))
val optSpacedKeyParser = (s: State) => spacedKeyParser(s).?
def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "<pattern>")) ~ optSpacedKeyParser(s))
def last = Command(LastCommand, lastBrief, lastDetailed)(optSpacedKeyParser) { (s,sk) =>
val (str, ref) = extractLast(s)
Output.last(sk, str, ref)
s
def last = Command(LastCommand, lastBrief, lastDetailed)(optSpacedKeyParser) {
case (s,Some(sk)) =>
val (str, ref) = extractLast(s)
Output.last(sk, str)
s
case (s, None) =>
Output.last( CommandSupport.globalLogging(s).backing )
s
}
def autoImports(extracted: Extracted): EvalImports = new EvalImports(imports(extracted), "<auto-imports>")

View File

@ -4,8 +4,10 @@
package sbt
import java.util.regex.Pattern
import java.io.File
import Keys.{Streams, TaskStreams}
import Project.ScopedKey
import annotation.tailrec
object Output
{
@ -22,17 +24,27 @@ object Output
else
None
}
final val DefaultTail = "> "
def last(key: ScopedKey[_], mgr: Streams): Unit = printLines(lastLines(key, mgr))
def last(file: File, tailDelim: String = DefaultTail): Unit = printLines(tailLines(file, tailDelim))
def lastGrep(key: ScopedKey[_], mgr: Streams, patternString: String): Unit =
lastGrep(lastLines(key, mgr), patternString )
def lastGrep(file: File, patternString: String, tailDelim: String = DefaultTail): Unit =
lastGrep( tailLines(file, tailDelim), patternString)
def lastGrep(lines: Seq[String], patternString: String): Unit =
printLines(lines flatMap showMatches(Pattern compile patternString))
def last(key: Option[ScopedKey[_]], mgr: Streams, ref: ScopeAxis[ProjectRef]): Unit =
printLines(lastLines(key, mgr, ref))
def printLines(lines: Seq[String]) = lines foreach println
def lastGrep(key: Option[ScopedKey[_]], mgr: Streams, patternString: String, ref: ScopeAxis[ProjectRef])
{
val pattern = Pattern.compile(patternString)
printLines(lastLines(key, mgr, ref).flatMap(showMatches(pattern)) )
}
def lastLines(key: Option[ScopedKey[_]], mgr: Streams, ref: ScopeAxis[ProjectRef]): Seq[String] =
lastLines(key getOrElse Project.globalLoggerKey(ref), mgr)
def lastLines(key: ScopedKey[_], mgr: Streams): Seq[String] =
mgr.use(key) { s => IO.readLines(s.readText( Project.fillTaskAxis(key) )) }
def lastLines(key: ScopedKey[_], mgr: Streams): Seq[String] = mgr.use(key) { s => IO.readLines(s.readText( Project.fillTaskAxis(key) )) }
def tailLines(file: File, tailDelim: String): Seq[String] = headLines(IO.readLines(file).reverse, tailDelim).reverse
@tailrec def headLines(lines: Seq[String], tailDelim: String): Seq[String] =
if(lines.isEmpty)
lines
else
{
val (first, tail) = lines.span { line => ! (line startsWith tailDelim) }
if(first.isEmpty) headLines(tail drop 1, tailDelim) else first
}
}

View File

@ -6,7 +6,7 @@ package sbt
import java.io.File
import java.net.URI
import Project._
import Keys.{appConfiguration, stateBuildStructure, commands, configuration, historyPath, logged, projectCommand, sessionSettings, shellPrompt, streams, thisProject, thisProjectRef, watch}
import Keys.{appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, streams, thisProject, thisProjectRef, watch}
import Scope.{GlobalScope,ThisScope}
import Load.BuildStructure
import CommandSupport.logger
@ -151,11 +151,10 @@ object Project extends Init[Scope] with ProjectExtra
def getHooks(data: Settings[Scope]): (State => State, State => State) = (getHook(Keys.onLoad, data), getHook(Keys.onUnload, data))
def current(state: State): ProjectRef = session(state).current
def updateCurrent(s0: State): State =
def updateCurrent(s: State): State =
{
val structure = Project.structure(s0)
val ref = Project.current(s0)
val s = installGlobalLogger(s0, structure, ref)
val structure = Project.structure(s)
val ref = Project.current(s)
val project = Load.getProject(structure.units, ref.build, ref.project)
val label = Keys.name in ref get structure.data getOrElse ref.project
logger(s).info("Set current project to " + label + " (in build " + ref.build +")")
@ -314,13 +313,6 @@ object Project extends Init[Scope] with ProjectExtra
val extracted = Project.extract(state)
EvaluateTask.evaluateTask(extracted.structure, taskKey, state, extracted.currentRef, checkCycles, maxWorkers)
}
def globalLoggerKey(ref: ScopeAxis[ResolvedReference]) = fillTaskAxis(ScopedKey(GlobalScope.copy(project = ref), streams.key))
def installGlobalLogger(s: State, structure: BuildStructure, ref: ProjectRef): State =
{
val str = structure.streams(globalLoggerKey(Select(ref)))
str.open()
s.put(logged, str.log).addExitHook { str.close() }
}
// this is here instead of Scoped so that it is considered without need for import (because of Project.Initialize)
implicit def richInitializeTask[T](init: Initialize[Task[T]]): Scoped.RichInitializeTask[T] = new Scoped.RichInitializeTask(init)
}