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

Multi-line status prompt ticker #3577

Merged
merged 121 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
d532b49
wip
lihaoyi Sep 18, 2024
82a2ab0
Merge branch 'main' into multi-prompt
lihaoyi Sep 18, 2024
52d4163
.
lihaoyi Sep 18, 2024
35fcf43
works when not scrolling, but scrolling breaks save/restore cursor
lihaoyi Sep 18, 2024
8cdca12
.
lihaoyi Sep 18, 2024
2c615ab
.
lihaoyi Sep 18, 2024
277feef
wip trying to buffer output
lihaoyi Sep 18, 2024
c9bc44a
track newlines in wrapped multiline prompt stream
lihaoyi Sep 19, 2024
6c8f3c1
wip
lihaoyi Sep 19, 2024
da050ab
works
lihaoyi Sep 19, 2024
4ff88de
wip
lihaoyi Sep 19, 2024
771be38
wip
lihaoyi Sep 19, 2024
81311f2
wip
lihaoyi Sep 19, 2024
641f55e
.
lihaoyi Sep 19, 2024
64f8973
.
lihaoyi Sep 19, 2024
3c98ee2
.
lihaoyi Sep 19, 2024
486be7d
.
lihaoyi Sep 19, 2024
038aae9
.
lihaoyi Sep 19, 2024
239f76a
.
lihaoyi Sep 19, 2024
a70766d
Merge branch 'main' into multi-prompt
lihaoyi Sep 19, 2024
bd68251
.
lihaoyi Sep 19, 2024
bd895a0
119 width, preserve ending header
lihaoyi Sep 19, 2024
36b97f8
.
lihaoyi Sep 19, 2024
990f7d1
.
lihaoyi Sep 19, 2024
c447df0
wip
lihaoyi Sep 19, 2024
515e704
.
lihaoyi Sep 19, 2024
cb6f9b6
.
lihaoyi Sep 19, 2024
f2cd440
.
lihaoyi Sep 19, 2024
d1c41d7
.
lihaoyi Sep 19, 2024
8ab7918
.
lihaoyi Sep 19, 2024
b6ad58c
make use of org.jline.terminal
lihaoyi Sep 19, 2024
7fda88f
prompt title centering
lihaoyi Sep 19, 2024
4c5efd5
preserve prompt header for final render during shutdown
lihaoyi Sep 19, 2024
3269c72
renderHeader tests
lihaoyi Sep 19, 2024
d67b456
more debouncing on removal
lihaoyi Sep 19, 2024
616dea6
Long.MaxValue
lihaoyi Sep 19, 2024
a5e1385
transfer terminfo from client
lihaoyi Sep 19, 2024
325ccc1
renderPrompt unit tests
lihaoyi Sep 19, 2024
e4f3ea6
wip removalDelay test
lihaoyi Sep 19, 2024
d44b0d5
fix rendering
lihaoyi Sep 19, 2024
d79906e
fix-compile
lihaoyi Sep 19, 2024
12ec229
.
lihaoyi Sep 19, 2024
085ede3
.
lihaoyi Sep 19, 2024
611c7d4
use ProxyStream to prepare for batch prompt updates
lihaoyi Sep 19, 2024
34c7208
.
lihaoyi Sep 19, 2024
6842986
move StateStream logic into Pumper hooks for debouncing
lihaoyi Sep 20, 2024
816b800
avoid duplicate preWrite clearScreens
lihaoyi Sep 20, 2024
432b1fc
buffer up prompts
lihaoyi Sep 20, 2024
7db0c81
write final prompt before closing streams
lihaoyi Sep 20, 2024
36ac687
simplify termDimensions
lihaoyi Sep 20, 2024
306ec44
simplify termDimensions
lihaoyi Sep 20, 2024
1df4fe4
fix more threads reporting and remove unnecessary blank line at bottom
lihaoyi Sep 20, 2024
8f8ed1c
more fixes
lihaoyi Sep 20, 2024
289c8dd
fix+format
lihaoyi Sep 20, 2024
b40bba8
improve non-interactive prompting
lihaoyi Sep 20, 2024
1e0110f
.
lihaoyi Sep 20, 2024
1573a96
tweaks
lihaoyi Sep 20, 2024
3e51dab
try and fix exit code
lihaoyi Sep 20, 2024
9ed81b3
avoid closing downstream streams in ProxyStreams.Pumper
lihaoyi Sep 20, 2024
a3c0fe6
.
lihaoyi Sep 20, 2024
35907a3
fixes
lihaoyi Sep 20, 2024
b5a7083
fixes
lihaoyi Sep 20, 2024
3156586
wip
lihaoyi Sep 21, 2024
2eab6a2
add global override for systemstreams to prevent threading problems
lihaoyi Sep 21, 2024
7199e05
fix stream original comparison
lihaoyi Sep 21, 2024
1a05e1c
fix missing trailing output
lihaoyi Sep 21, 2024
7ccdea3
fix
lihaoyi Sep 21, 2024
e8420a0
avoid infinite recursion with system.out
lihaoyi Sep 21, 2024
e3de328
swap out Piped*Stream with our on PipeStreams
lihaoyi Sep 22, 2024
e23fc07
.
lihaoyi Sep 22, 2024
aabc13d
put back line prefixes
lihaoyi Sep 22, 2024
6da90c0
fixes
lihaoyi Sep 22, 2024
5d85b47
fixes
lihaoyi Sep 22, 2024
e4cd4f6
fixes
lihaoyi Sep 22, 2024
2ae3478
fix
lihaoyi Sep 22, 2024
985c691
.
lihaoyi Sep 22, 2024
414e727
try to fix circular system stream
lihaoyi Sep 22, 2024
68b0d1a
.
lihaoyi Sep 23, 2024
7f94afd
.
lihaoyi Sep 23, 2024
7fcd03d
.
lihaoyi Sep 23, 2024
21d3704
Merge branch 'main' into multi-prompt
lihaoyi Sep 23, 2024
d2d2b1e
.
lihaoyi Sep 23, 2024
516182d
.
lihaoyi Sep 23, 2024
7726da3
.
lihaoyi Sep 23, 2024
38a2b1a
always notify in PipeStreams
lihaoyi Sep 23, 2024
fd3acf9
.
lihaoyi Sep 23, 2024
4704013
task log headers
lihaoyi Sep 23, 2024
6dd4e17
.
lihaoyi Sep 24, 2024
0cce6b1
split out MultilinePromptLoggerUtil
lihaoyi Sep 24, 2024
f4dbbe0
add simple integration test for MultilinePromptLogger
lihaoyi Sep 24, 2024
101802a
implement test terminal
lihaoyi Sep 24, 2024
048f00b
wip
lihaoyi Sep 24, 2024
470caf5
wip
lihaoyi Sep 24, 2024
a211350
disable prompt updater in integration tests
lihaoyi Sep 24, 2024
66b389d
wip
lihaoyi Sep 24, 2024
9989a08
.
lihaoyi Sep 24, 2024
35c0638
.
lihaoyi Sep 24, 2024
4fdfdbf
use raw outputstreams in withPromptPaused
lihaoyi Sep 24, 2024
4965555
.
lihaoyi Sep 24, 2024
c61c914
Merge branch 'main' into multi-prompt
lihaoyi Sep 25, 2024
7f6c713
rename
lihaoyi Sep 25, 2024
b042c77
rename
lihaoyi Sep 25, 2024
ae88d2b
remove unnecessary sleep
lihaoyi Sep 25, 2024
93f6fae
.
lihaoyi Sep 25, 2024
5f74864
wip
lihaoyi Sep 25, 2024
1ca0c71
wip
lihaoyi Sep 25, 2024
b3a69f7
wip
lihaoyi Sep 25, 2024
7057570
wip
lihaoyi Sep 25, 2024
5482bfb
wip
lihaoyi Sep 25, 2024
b7579d1
avoid using SystemStreams.original due to it's uncapturability in ser…
lihaoyi Sep 25, 2024
c127015
disable prompt integration tests on windows due to flakiness
lihaoyi Sep 25, 2024
32cc9c4
.
lihaoyi Sep 25, 2024
c4a9578
make prompt based on prefix keys rather than thread IDs
lihaoyi Sep 26, 2024
38386bc
renames
lihaoyi Sep 26, 2024
0a475d6
.
lihaoyi Sep 26, 2024
b62f9e7
color prompt details
lihaoyi Sep 26, 2024
c9d0f7a
merge
lihaoyi Sep 26, 2024
f95f5b6
mima
lihaoyi Sep 26, 2024
3a8a4e7
merge
lihaoyi Sep 26, 2024
7a029da
.
lihaoyi Sep 26, 2024
bcf1056
.
lihaoyi Sep 26, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/run-mill-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
if: inputs.millargs != '' && startsWith(inputs.os, 'windows')

- name: Run Mill (on Windows) Worker Cleanup
run: 'taskkill -f -im java* && rm -rf out/mill-worker-*'
run: 'taskkill -f -im java* && rm -rf out/mill-server/*'
if: inputs.millargs != '' && startsWith(inputs.os, 'windows')
shell: bash
continue-on-error: true
Expand Down
1 change: 1 addition & 0 deletions bsp/src/mill/bsp/BspContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ private[mill] class BspContext(
override def info(s: String): Unit = streams.err.println(s)
override def error(s: String): Unit = streams.err.println(s)
override def ticker(s: String): Unit = streams.err.println(s)
override def ticker(key: String, s: String): Unit = streams.err.println(s)
override def debug(s: String): Unit = streams.err.println(s)

override def debugEnabled: Boolean = true
Expand Down
2 changes: 1 addition & 1 deletion build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import mill.define.Cross
import $meta._
import $file.ci.shared
import $file.ci.upload

//import $packages._
object Settings {
val pomOrg = "com.lihaoyi"
val githubOrg = "com-lihaoyi"
Expand Down
6 changes: 3 additions & 3 deletions docs/modules/ROOT/pages/Out_Dir.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Mill puts all its output in the top-level `out/` folder.
== Structure of the `out/` Directory

The `out/` folder contains all the generated files & metadata for your build.
It holds some files needed to manage Mill's longer running server instances (`out/mill-worker-*`) as well as a directory and file structure resembling the project's module structure.
It holds some files needed to manage Mill's longer running server instances (`out/mill-server/*`) as well as a directory and file structure resembling the project's module structure.

.Example of the `out/` directory after running `mill main.compile`
[source,text]
Expand Down Expand Up @@ -47,7 +47,7 @@ out/
│ ├── unmanagedClasspath.json
│ └── upstreamCompileOutput.json
├── mill-profile.json
└── mill-worker-VpZubuAK6LQHHN+3ojh1LsTZqWY=-1/
└── mill-server/VpZubuAK6LQHHN+3ojh1LsTZqWY=-1/
----

<1> The `main` directory contains all files associated with target and submodules of the `main` module.
Expand Down Expand Up @@ -109,5 +109,5 @@ This is very useful if Mill is being unexpectedly slow, and you want to find out
`mill-chrome-profile.json`::
This file is only written if you run Mill in parallel mode, e.g. `mill --jobs 4`. This file can be opened in Google Chrome with the built-in `tracing:` protocol even while Mill is still running, so you get a nice chart of what's going on in parallel.

`mill-worker-*/`::
`mill-server/*`::
Each Mill server instance needs to keep some temporary files in one of these directories. Deleting it will also terminate the associated server instance, if it is still running.
4 changes: 2 additions & 2 deletions example/depth/sandbox/1-task/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ def osProcTask = Task {
//
// Lastly, there is the possibily of calling `os.pwd` outside of a task. When outside of
// a task there is no `.dest/` folder associated, so instead Mill will redirect `os.pwd`
// towards an empty `sandbox/` folder in `out/mill-worker.../`:
// towards an empty `sandbox/` folder in `out/mill-server/...`:

val externalPwd = os.pwd
def externalPwdTask = Task { println(externalPwd.toString) }

/** Usage
> ./mill externalPwdTask
.../out/mill-worker-.../sandbox/sandbox
.../out/mill-server/.../sandbox
*/


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ object MissingBuildFileTests extends UtestIntegrationTestSuite {
test - integrationTest { tester =>
val res = tester.eval(("resolve", "_"))
assert(!res.isSuccess)
val s"build.mill file not found in $msg. Are you in a Mill project folder?" = res.err
val s"${prefix}build.mill file not found in $msg. Are you in a Mill project folder?" = res.err
}
}
}
16 changes: 16 additions & 0 deletions main/api/src/mill/api/Logger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ trait Logger {
def info(s: String): Unit
def error(s: String): Unit
def ticker(s: String): Unit
def ticker(key: String, s: String): Unit = ticker(s)
private[mill] def reportPrefix(s: String): Unit = ()
private[mill] def promptLine(key: String, identSuffix: String, message: String): Unit =
ticker(s"$key $message")
private[mill] def globalTicker(s: String): Unit = ()
private[mill] def clearAllTickers(): Unit = ()
private[mill] def endTicker(key: String): Unit = ()

def debug(s: String): Unit

/**
Expand All @@ -53,4 +61,12 @@ trait Logger {
def debugEnabled: Boolean = false

def close(): Unit = ()

/**
* Used to disable the terminal UI prompt without a certain block of code so you
* can run stuff like REPLs or other output-sensitive code in a clean terminal
*/
def withPromptPaused[T](t: => T) = t

def enableTicker: Boolean = false
}
120 changes: 91 additions & 29 deletions main/api/src/mill/api/SystemStreams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package mill.api
import java.io.{InputStream, OutputStream, PrintStream}
import mill.main.client.InputPumper

import scala.util.DynamicVariable

/**
* Represents a set of streams that look similar to those provided by the
* operating system. These may internally be proxied/redirected/processed, but
Expand All @@ -28,11 +30,12 @@ object SystemStreams {
* stdout/stderr/stdin.
*/
def isOriginal(): Boolean = {
(System.out eq original.out) &&
(System.err eq original.err) &&
(System.in eq original.in) &&
(Console.out eq original.out) &&
(Console.err eq original.err)
(Console.out eq original.out) && (Console.err eq original.err)
// We do not check System.* for equality because they are always overridden by
// `ThreadLocalStreams`
// (System.out eq original.out) &&
// (System.err eq original.err) &&
// (System.in eq original.in) &&

// We do not check `Console.in` for equality, because `Console.withIn` always wraps
// `Console.in` in a `new BufferedReader` each time, and so it is impossible to check
Expand Down Expand Up @@ -62,30 +65,24 @@ object SystemStreams {
Some(new InputPumper(() => processOut.wrapped, () => dest, false, () => true))
}
def withStreams[T](systemStreams: SystemStreams)(t: => T): T = {
val in = System.in
val out = System.out
val err = System.err
try {
// If we are setting a stream back to its original value, make sure we reset
// `os.Inherit` to `os.InheritRaw` for that stream. This direct inheritance
// ensures that interactive applications involving console IO work, as the
// presence of a `PumpedProcess` would cause most interactive CLIs (e.g.
// scala console, REPL, etc.) to misbehave
val inheritIn =
if (systemStreams.in eq original.in) os.InheritRaw
else new PumpedProcessInput

val inheritOut =
if (systemStreams.out eq original.out) os.InheritRaw
else new PumpedProcessOutput(systemStreams.out)

val inheritErr =
if (systemStreams.err eq original.err) os.InheritRaw
else new PumpedProcessOutput(systemStreams.err)

System.setIn(systemStreams.in)
System.setOut(systemStreams.out)
System.setErr(systemStreams.err)
// If we are setting a stream back to its original value, make sure we reset
// `os.Inherit` to `os.InheritRaw` for that stream. This direct inheritance
// ensures that interactive applications involving console IO work, as the
// presence of a `PumpedProcess` would cause most interactive CLIs (e.g.
// scala console, REPL, etc.) to misbehave
val inheritIn =
if (systemStreams.in eq original.in) os.InheritRaw
else new PumpedProcessInput

val inheritOut =
if (systemStreams.out eq original.out) os.InheritRaw
else new PumpedProcessOutput(systemStreams.out)

val inheritErr =
if (systemStreams.err eq original.err) os.InheritRaw
else new PumpedProcessOutput(systemStreams.err)

ThreadLocalStreams.current.withValue(systemStreams) {
Console.withIn(systemStreams.in) {
Console.withOut(systemStreams.out) {
Console.withErr(systemStreams.err) {
Expand All @@ -99,10 +96,75 @@ object SystemStreams {
}
}
}
}
}

/**
* Manages the global override of `System.{in,out,err}`. Overrides of those streams are
* global, so we cannot just override them per-use-site in a multithreaded environment
* because different threads may interleave and stomp over each other's over-writes.
* Instead, we over-write them globally with a set of streams that does nothing but
* forward to the per-thread [[ThreadLocalStreams.current]] streams, allowing callers
* to each reach their own thread-local streams without clashing across multiple threads
*/
def withTopLevelSystemStreamProxy[T](t: => T): T = {
val in = System.in
val out = System.out
val err = System.err

try {
setTopLevelSystemStreamProxy()
t
} finally {
System.setErr(err)
System.setOut(out)
System.setIn(in)
}
}
def setTopLevelSystemStreamProxy(): Unit = {
// Make sure to initialize `Console` to cache references to the original
// `System.{in,out,err}` streams before we redirect them
Console.out
Console.err
Console.in
System.setIn(ThreadLocalStreams.In)
System.setOut(ThreadLocalStreams.Out)
System.setErr(ThreadLocalStreams.Err)
}

private[mill] object ThreadLocalStreams {
val current = new DynamicVariable(original)

object Out extends PrintStream(new ProxyOutputStream { def delegate() = current.value.out })
object Err extends PrintStream(new ProxyOutputStream { def delegate() = current.value.err })
object In extends ProxyInputStream { def delegate() = current.value.in }

abstract class ProxyOutputStream extends OutputStream {
def delegate(): OutputStream
override def write(b: Array[Byte], off: Int, len: Int): Unit = delegate().write(b, off, len)
override def write(b: Array[Byte]): Unit = delegate().write(b)
def write(b: Int): Unit = delegate().write(b)
override def flush(): Unit = delegate().flush()
override def close(): Unit = delegate().close()
}
abstract class ProxyInputStream extends InputStream {
def delegate(): InputStream
override def read(): Int = delegate().read()
override def read(b: Array[Byte], off: Int, len: Int): Int = delegate().read(b, off, len)
override def read(b: Array[Byte]): Int = delegate().read(b)
override def readNBytes(b: Array[Byte], off: Int, len: Int): Int =
delegate().readNBytes(b, off, len)
override def readNBytes(len: Int): Array[Byte] = delegate().readNBytes(len)
override def readAllBytes(): Array[Byte] = delegate().readAllBytes()
override def mark(readlimit: Int): Unit = delegate().mark(readlimit)
override def markSupported(): Boolean = delegate().markSupported()
override def available(): Int = delegate().available()
override def reset(): Unit = delegate().reset()
override def skip(n: Long): Long = delegate().skip(n)
// Not present in some versions of Java
// override def skipNBytes(n: Long): Unit = delegate().skipNBytes(n)
override def close(): Unit = delegate().close()
override def transferTo(out: OutputStream): Long = delegate().transferTo(out)
}
}
}
20 changes: 20 additions & 0 deletions main/client/src/mill/main/client/DebugLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package mill.main.client;
import java.io.IOException;
import java.nio.file.*;

/**
* Used to add `println`s in scenarios where you can't figure out where on earth
* your stdout/stderr/logs are going and so we just dump them in a file in your
* home folder so you can find them
*/
public class DebugLog{
synchronized public static void println(String s){
Path path = Paths.get(System.getProperty("user.home"), "mill-debug-log.txt");
try {
if (!Files.exists(path)) Files.createFile(path);
Files.writeString(path, s + "\n", StandardOpenOption.APPEND);
}catch (IOException e){
throw new RuntimeException(e);
}
}
}
3 changes: 2 additions & 1 deletion main/client/src/mill/main/client/OutFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ public class OutFiles {
* Subfolder of `out/` that contains the machinery necessary for a single Mill background
* server: metadata files, pipes, logs, etc.
*/
final public static String millWorker = "mill-worker-";
final public static String millServer = "mill-server";

/**
* Subfolder of `out/` used to contain the Mill subprocess when run in no-server mode
*/
final public static String millNoServer = "mill-no-server";


}
19 changes: 13 additions & 6 deletions main/client/src/mill/main/client/ProxyStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
* Logic to capture a pair of streams (typically stdout and stderr), combining
Expand Down Expand Up @@ -106,11 +108,16 @@ public Pumper(InputStream src, OutputStream destOut, OutputStream destErr){
this.destErr = destErr;
}

public void preRead(InputStream src){}

public void preWrite(){}

public void run() {

byte[] buffer = new byte[1024];
while (true) {
try {
this.preRead(src);
int header = src.read();
// -1 means socket was closed, 0 means a ProxyStream.END was sent. Note
// that only header values > 0 represent actual data to read:
Expand All @@ -124,6 +131,7 @@ public void run() {
int offset = 0;
int delta = -1;
while (offset < quantity) {
this.preRead(src);
delta = src.read(buffer, offset, quantity - offset);
if (delta == -1) {
break;
Expand All @@ -133,26 +141,25 @@ public void run() {
}

if (delta != -1) {
this.preWrite();
switch(stream){
case ProxyStream.OUT: destOut.write(buffer, 0, offset); break;
case ProxyStream.ERR: destErr.write(buffer, 0, offset); break;
}

flush();
}
}
} catch (org.newsclub.net.unix.ConnectionResetSocketException e) {
// This happens when you run mill shutdown and the server exits gracefully
break;
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
// This happens when the upstream pipe was closed
break;
}
}

try {
destOut.close();
destErr.close();
destOut.flush();
destErr.flush();
} catch(IOException e) {}
}

Expand Down
11 changes: 8 additions & 3 deletions main/client/src/mill/main/client/ServerFiles.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package mill.main.client;

/**
* Central place containing all the files that live inside the `out/mill-worker-*` folder
* Central place containing all the files that live inside the `out/mill-server-*` folder
* and documentation about what they do
*/
public class ServerFiles {
final public static String serverId = "serverId";
final public static String sandbox = "sandbox";

/**
* Ensures only a single client is manipulating each mill-worker folder at
* Ensures only a single client is manipulating each mill-server folder at
* a time, either spawning the server or submitting a command. Also used by
* the server to detect when a client disconnects, so it can terminate execution
*/
final public static String clientLock = "clientLock";

/**
* Lock file ensuring a single server is running in a particular mill-worker
* Lock file ensuring a single server is running in a particular mill-server
* folder. If multiple servers are spawned in the same folder, only one takes
* the lock and the others fail to do so and terminate immediately.
*/
Expand Down Expand Up @@ -67,4 +67,9 @@ public static String pipe(String base) {
* Where the server's stderr is piped to
*/
final public static String stderr = "stderr";

/**
* Terminal information that we need to propagate from client to server
*/
final public static String terminfo = "terminfo";
}
Loading
Loading