/
State.scala
258 lines (224 loc) · 12.3 KB
/
State.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/* sbt -- Simple Build Tool
* Copyright 2008, 2009, 2010 Mark Harrah
*/
package sbt
import java.io.File
import java.util.concurrent.Callable
/**
* Data structure representing all command execution information.
*
* @param configuration provides access to the launcher environment, including the application configuration, Scala versions, jvm/filesystem wide locking, and the launcher itself
* @param definedCommands the list of command definitions that evaluate command strings. These may be modified to change the available commands.
* @param onFailure the command to execute when another command fails. `onFailure` is cleared before the failure handling command is executed.
* @param remainingCommands the sequence of commands to execute. This sequence may be modified to change the commands to be executed. Typically, the `::` and `:::` methods are used to prepend new commands to run.
* @param exitHooks code to run before sbt exits, usually to ensure resources are cleaned up.
* @param history tracks the recently executed commands
* @param attributes custom command state. It is important to clean up attributes when no longer needed to avoid memory leaks and class loader leaks.
* @param next the next action for the command processor to take. This may be to continue with the next command, adjust global logging, or exit.
*/
final case class State(
configuration: xsbti.AppConfiguration,
definedCommands: Seq[Command],
exitHooks: Set[ExitHook],
onFailure: Option[String],
remainingCommands: Seq[String],
history: State.History,
attributes: AttributeMap,
globalLogging: GlobalLogging,
next: State.Next) extends Identity {
lazy val combinedParser = Command.combine(definedCommands)(this)
}
trait Identity {
override final def hashCode = super.hashCode
override final def equals(a: Any) = super.equals(a)
override final def toString = super.toString
}
/** Convenience methods for State transformations and operations. */
trait StateOps {
def process(f: (String, State) => State): State
/** Schedules `commands` to be run before any remaining commands.*/
def :::(commands: Seq[String]): State
/** Schedules `command` to be run before any remaining commands.*/
def ::(command: String): State
/** Sets the next command processing action to be to continue processing the next command.*/
def continue: State
/**
* Reboots sbt. A reboot restarts execution from the entry point of the launcher.
* A reboot is designed to be as close as possible to actually restarting the JVM without actually doing so.
* Because the JVM is not restarted, JVM exit hooks are not run.
* State.exitHooks should be used instead and those will be run before rebooting.
* If `full` is true, the boot directory is deleted before starting again.
* This command is currently implemented to not return, but may be implemented in the future to only reboot at the next command processing step.
*/
def reboot(full: Boolean): State
/** Sets the next command processing action to do.*/
def setNext(n: State.Next): State
@deprecated("Use setNext", "0.11.0") def setResult(ro: Option[xsbti.MainResult]): State
/**
* Restarts sbt without dropping loaded Scala classes. It is a shallower restart than `reboot`.
* This method takes a snapshot of the remaining commands and will resume executing those commands after reload.
* This means that any commands added to this State will be dropped.
*/
def reload: State
/** Sets the next command processing action to be to rotate the global log and continue executing commands.*/
def clearGlobalLog: State
/** Sets the next command processing action to be to keep the previous log and continue executing commands. */
def keepLastLog: State
/** Sets the next command processing action to be to exit with a zero exit code if `ok` is true and a nonzero exit code if `ok` if false.*/
def exit(ok: Boolean): State
/** Marks the currently executing command as failing. This triggers failure handling by the command processor. See also `State.onFailure`*/
def fail: State
/**
* Marks the currently executing command as failing due to the given exception.
* This displays the error appropriately and triggers failure handling by the command processor.
* Note that this does not throw an exception and returns normally.
* It is only once control is returned to the command processor that failure handling at the command level occurs.
*/
def handleError(t: Throwable): State
/** Registers `newCommands` as available commands. */
def ++(newCommands: Seq[Command]): State
/** Registers `newCommand` as an available command. */
def +(newCommand: Command): State
/** Gets the value associated with `key` from the custom attributes map.*/
def get[T](key: AttributeKey[T]): Option[T]
/** Sets the value associated with `key` in the custom attributes map.*/
def put[T](key: AttributeKey[T], value: T): State
/** Removes the `key` and any associated value from the custom attributes map.*/
def remove(key: AttributeKey[_]): State
/** Sets the value associated with `key` in the custom attributes map by transforming the current value.*/
def update[T](key: AttributeKey[T])(f: Option[T] => T): State
/** Returns true if `key` exists in the custom attributes map, false if it does not exist.*/
def has(key: AttributeKey[_]): Boolean
/** The application base directory, which is not necessarily the current working directory.*/
def baseDir: File
/** The Logger used for general command logging.*/
def log: Logger
/** Evaluates the provided expression with a JVM-wide and machine-wide lock on `file`.*/
def locked[T](file: File)(t: => T): T
/** Runs any defined exitHooks and then clears them.*/
def runExitHooks(): State
/** Registers a new exit hook, which will run when sbt exits or restarts.*/
def addExitHook(f: => Unit): State
/** An advisory flag that is `true` if this application will execute commands based on user input.*/
def interactive: Boolean
/** Changes the advisory `interactive` flag. */
def setInteractive(flag: Boolean): State
/** Get the class loader cache for the application.*/
def classLoaderCache: classpath.ClassLoaderCache
/** Create and register a class loader cache. This should be called once at the application entry-point.*/
def initializeClassLoaderCache: State
}
object State {
/** Indicates where command execution should resume after a failure.*/
val FailureWall = BasicCommandStrings.FailureWall
/** Represents the next action for the command processor.*/
sealed trait Next
/** Indicates that the command processor should process the next command.*/
object Continue extends Next
/** Indicates that the application should exit with the given result.*/
final class Return(val result: xsbti.MainResult) extends Next
/** Indicates that global logging should be rotated.*/
final object ClearGlobalLog extends Next
/** Indicates that the previous log file should be preserved instead of discarded.*/
final object KeepLastLog extends Next
/**
* Provides a list of recently executed commands. The commands are stored as processed instead of as entered by the user.
* @param executed the list of the most recently executed commands, with the most recent command first.
* @param maxSize the maximum number of commands to keep, or 0 to keep an unlimited number.
*/
final class History private[State] (val executed: Seq[String], val maxSize: Int) {
/** Adds `command` as the most recently executed command.*/
def ::(command: String): History =
{
val prependTo = if (maxSize > 0 && executed.size >= maxSize) executed.take(maxSize - 1) else executed
new History(command +: prependTo, maxSize)
}
/** Changes the maximum number of commands kept, adjusting the current history if necessary.*/
def setMaxSize(size: Int): History =
new History(if (size <= 0) executed else executed.take(size), size)
def current: String = executed.head
def previous: Option[String] = executed.drop(1).headOption
}
/** Constructs an empty command History with a default, finite command limit.*/
def newHistory = new History(Vector.empty, complete.HistoryCommands.MaxLines)
def defaultReload(state: State): Reboot =
{
val app = state.configuration.provider
new Reboot(app.scalaProvider.version, state.remainingCommands, app.id, state.configuration.baseDirectory)
}
/** Provides operations and transformations on State. */
implicit def stateOps(s: State): StateOps = new StateOps {
def process(f: (String, State) => State): State =
s.remainingCommands match {
case Seq() => exit(true)
case Seq(x, xs @ _*) =>
log.debug(s"> $x")
f(x, s.copy(remainingCommands = xs, history = x :: s.history))
}
def :::(newCommands: Seq[String]): State = s.copy(remainingCommands = newCommands ++ s.remainingCommands)
def ::(command: String): State = (command :: Nil) ::: this
def ++(newCommands: Seq[Command]): State = s.copy(definedCommands = (s.definedCommands ++ newCommands).distinct)
def +(newCommand: Command): State = this ++ (newCommand :: Nil)
def baseDir: File = s.configuration.baseDirectory
def setNext(n: Next) = s.copy(next = n)
def setResult(ro: Option[xsbti.MainResult]) = ro match { case None => continue; case Some(r) => setNext(new Return(r)) }
def continue = setNext(Continue)
def reboot(full: Boolean) = { runExitHooks(); throw new xsbti.FullReload(s.remainingCommands.toArray, full) }
def reload = runExitHooks().setNext(new Return(defaultReload(s)))
def clearGlobalLog = setNext(ClearGlobalLog)
def keepLastLog = setNext(KeepLastLog)
def exit(ok: Boolean) = runExitHooks().setNext(new Return(Exit(if (ok) 0 else 1)))
def get[T](key: AttributeKey[T]) = s.attributes get key
def put[T](key: AttributeKey[T], value: T) = s.copy(attributes = s.attributes.put(key, value))
def update[T](key: AttributeKey[T])(f: Option[T] => T): State = put(key, f(get(key)))
def has(key: AttributeKey[_]) = s.attributes contains key
def remove(key: AttributeKey[_]) = s.copy(attributes = s.attributes remove key)
def log = s.globalLogging.full
def handleError(t: Throwable): State = handleException(t, s, log)
def fail =
{
import BasicCommandStrings.Compat.{ FailureWall => CompatFailureWall }
val remaining = s.remainingCommands.dropWhile(c => c != FailureWall && c != CompatFailureWall)
if (remaining.isEmpty)
applyOnFailure(s, Nil, exit(ok = false))
else
applyOnFailure(s, remaining, s.copy(remainingCommands = remaining))
}
private[this] def applyOnFailure(s: State, remaining: Seq[String], noHandler: => State): State =
s.onFailure match {
case Some(c) => s.copy(remainingCommands = c +: remaining, onFailure = None)
case None => noHandler
}
def addExitHook(act: => Unit): State =
s.copy(exitHooks = s.exitHooks + ExitHook(act))
def runExitHooks(): State = {
ExitHooks.runExitHooks(s.exitHooks.toSeq)
s.copy(exitHooks = Set.empty)
}
def locked[T](file: File)(t: => T): T =
s.configuration.provider.scalaProvider.launcher.globalLock.apply(file, new Callable[T] { def call = t })
def interactive = getBoolean(s, BasicKeys.interactive, false)
def setInteractive(i: Boolean) = s.put(BasicKeys.interactive, i)
def classLoaderCache: classpath.ClassLoaderCache = s get BasicKeys.classLoaderCache getOrElse newClassLoaderCache
def initializeClassLoaderCache = s.put(BasicKeys.classLoaderCache, newClassLoaderCache)
private[this] def newClassLoaderCache = new classpath.ClassLoaderCache(s.configuration.provider.scalaProvider.launcher.topLoader)
}
import ExceptionCategory._
private[sbt] def handleException(t: Throwable, s: State, log: Logger): State =
{
ExceptionCategory(t) match {
case AlreadyHandled => ()
case m: MessageOnly => log.error(m.message)
case f: Full => logFullException(f.exception, log)
}
s.fail
}
private[sbt] def logFullException(e: Throwable, log: Logger): Unit = {
log.trace(e)
log.error(ErrorHandling reducedToString e)
log.error("Use 'last' for the full log.")
}
private[sbt] def getBoolean(s: State, key: AttributeKey[Boolean], default: Boolean): Boolean =
s.get(key) getOrElse default
}
case class TemplateResolverInfo(module: ModuleID, implementationClass: String)