Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better revolver output #1614

Merged
merged 3 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package scala.cli.commands

import scala.annotation.tailrec

object WatchUtil {

lazy val isDevMode: Boolean =
Expand All @@ -9,18 +11,47 @@ object WatchUtil {
def waitMessage(message: String): String = {
// Both short cuts actually always work, but Ctrl+C also exits mill in mill watch mode.
val shortCut = if (isDevMode) "Ctrl+D" else "Ctrl+C"
val gray = "\u001b[90m"
val reset = Console.RESET
s"$gray$message, press $shortCut to exit, or press Enter to re-run.$reset"
gray(s"$message, press $shortCut to exit, or press Enter to re-run.")
}

private def gray(message: String): String = {
val gray = "\u001b[90m"
val reset = Console.RESET
s"$gray$message$reset"
}

def printWatchMessage(): Unit =
System.err.println(waitMessage("Watching sources"))

def waitForCtrlC(onPressEnter: () => Unit = () => ()): Unit = {
def printWatchWhileRunningMessage(): Unit =
System.err.println(gray("Watching sources while your program is running."))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT I'm thinking the logger could be passed here and used to print the watch messages, to respect verbosity...
But it's not a big deal, can be done separately.


def waitForCtrlC(
onPressEnter: () => Unit = () => (),
shouldReadInput: () => Boolean = () => true
): Unit = synchronized {

@tailrec
def readNextChar(): Int =
if (shouldReadInput())
try System.in.read()
catch {
case _: InterruptedException =>
// Actually never called, as System.in.read isn't interruptible…
// That means we sometimes read input when we shouldn't.
readNextChar()
}
else {
try wait()
catch {
case _: InterruptedException =>
}
readNextChar()
}

var readKey = -1
while ({
readKey = System.in.read()
readKey = readNextChar()
readKey != -1
})
if (readKey == '\n')
Expand Down
52 changes: 41 additions & 11 deletions modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,22 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
val onExitProcess = process.onExit().thenApply { p1 =>
val retCode = p1.exitValue()
onExitOpt.foreach(_())
if (retCode != 0)
if (allowTerminate)
(retCode, allowTerminate) match {
case (0, true) =>
case (0, false) =>
val gray = "\u001b[90m"
val reset = Console.RESET
System.err.println(s"${gray}Program exited with return code $retCode.$reset")
case (_, true) =>
sys.exit(retCode)
else {
case (_, false) =>
val red = Console.RED
val lightRed = "\u001b[91m"
val reset = Console.RESET
System.err.println(
s"${red}Program exited with return code $lightRed$retCode$red.$reset"
)
}
}
}

Some((process, onExitProcess))
Expand Down Expand Up @@ -194,7 +199,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
)

if (options.sharedRun.watch.watchMode) {
var processOpt = Option.empty[(Process, CompletableFuture[_])]
var processOpt = Option.empty[(Process, CompletableFuture[_])]
var shouldReadInput = false
var mainThreadOpt = Option.empty[Thread]
val watcher = Build.watch(
inputs,
initialBuildOptions,
Expand All @@ -205,16 +212,23 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
buildTests = false,
partial = None,
actionableDiagnostics = actionableDiagnostics,
postAction = () => WatchUtil.printWatchMessage()
postAction = () =>
if (processOpt.exists(_._1.isAlive()))
WatchUtil.printWatchWhileRunningMessage()
else
WatchUtil.printWatchMessage()
) { res =>
for ((process, onExitProcess) <- processOpt) {
onExitProcess.cancel(true)
ProcUtil.interruptProcess(process, logger)
}
res.orReport(logger).map(_.main).foreach {
case s: Build.Successful =>
for ((proc, _) <- processOpt) // If the process doesn't exit, send SIGKILL
if (proc.isAlive) ProcUtil.forceKillProcess(proc, logger)
for ((proc, _) <- processOpt if proc.isAlive)
// If the process doesn't exit, send SIGKILL
ProcUtil.forceKillProcess(proc, logger)
shouldReadInput = false
mainThreadOpt.foreach(_.interrupt())
val maybeProcess = maybeRun(
s,
allowTerminate = false,
Expand All @@ -224,18 +238,34 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
)
.orReport(logger)
.flatten
.map {
case (proc, onExit) =>
if (options.sharedRun.watch.restart)
onExit.thenApply { _ =>
shouldReadInput = true
mainThreadOpt.foreach(_.interrupt())
}
(proc, onExit)
}
s.copyOutput(options.shared)
if (options.sharedRun.watch.restart)
processOpt = maybeProcess
else
else {
for ((proc, onExit) <- maybeProcess)
ProcUtil.waitForProcess(proc, onExit)
shouldReadInput = true
mainThreadOpt.foreach(_.interrupt())
}
case _: Build.Failed =>
System.err.println("Compilation failed")
}
}
try WatchUtil.waitForCtrlC(() => watcher.schedule())
finally watcher.dispose()
mainThreadOpt = Some(Thread.currentThread())
try WatchUtil.waitForCtrlC(() => watcher.schedule(), () => shouldReadInput)
finally {
mainThreadOpt = None
watcher.dispose()
}
}
else {
val builds =
Expand Down