From 30baf741697f76d232a5513286a321e66ff70480 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Sun, 24 Jul 2011 17:36:42 -0400 Subject: [PATCH] improved global logging and 'last' command --- main/CommandSupport.scala | 3 +- main/EvaluateTask.scala | 6 ++-- main/Keys.scala | 2 +- main/Load.scala | 9 ++++-- main/LogManager.scala | 29 +++++++++++++++---- main/Main.scala | 60 +++++++++++++++++++++++++++++---------- main/Output.scala | 34 +++++++++++++++------- main/Project.scala | 16 +++-------- 8 files changed, 108 insertions(+), 51 deletions(-) diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala index 1be8a743e..142b88dc7 100644 --- a/main/CommandSupport.scala +++ b/main/CommandSupport.scala @@ -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 = diff --git a/main/EvaluateTask.scala b/main/EvaluateTask.scala index a6e183874..87ca2e3ca 100644 --- a/main/EvaluateTask.scala +++ b/main/EvaluateTask.scala @@ -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 = diff --git a/main/Keys.scala b/main/Keys.scala index 464b3c5fe..b615e8b7e 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -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.") diff --git a/main/Load.scala b/main/Load.scala index d32beb3c1..753dda16d 100644 --- a/main/Load.scala +++ b/main/Load.scala @@ -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) diff --git a/main/LogManager.scala b/main/LogManager.scala index 9be6760fa..5ac124bc2 100644 --- a/main/LogManager.scala +++ b/main/LogManager.scala @@ -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 -} \ No newline at end of file +} +final case class GlobalLogging(full: Logger, backed: ConsoleLogger, backing: File) \ No newline at end of file diff --git a/main/Main.scala b/main/Main.scala index 77142c631..c38d064f7 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -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, "")) ~ 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), "") diff --git a/main/Output.scala b/main/Output.scala index 1e7804189..cc4624e7f 100644 --- a/main/Output.scala +++ b/main/Output.scala @@ -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 + } } diff --git a/main/Project.scala b/main/Project.scala index dddb2ad5c..33d50786f 100644 --- a/main/Project.scala +++ b/main/Project.scala @@ -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) }