IDE 现象一则
背景
Win11 21H2(OS Build 22000.2538)
IntelliJ IDEA 2023.3 (Community Edition) (Scala Plugin 2023.3.19) + JDK 21
PlayScala Project (sbt 1.9.7, Scala 3.3.1, Play 3.0.1, sbt-plugin 3.0.1)
简述
在 Windows 上使用 Intellij IDEA 打开 PlayScala 项目,使用自带 Run Configurations 启用 Use sbt shell 运行 sbt run 命令,Play Server 会自动停止。
但未启用 Use sbt shell 或直接在 sbt shell 输入 run 命令,可以正常工作。
run.log
[info] welcome to sbt 1.9.7 (Microsoft Java 21)
[info] loading global plugins from C:\Users\xxx\.sbt\1.0\plugins
[info] loading settings for project hello-play-scala-build from plugins.sbt,idea7.sbt ...
[info] loading project definition from C:\Users\xxx\Workspace\hello-play-scala\project
[info] loading settings for project root from build.sbt ...
[info] __ __
[info] \ \ ____ / /____ _ __ __
[info] \ \ / __ \ / // __ `// / / /
[info] / / / /_/ // // /_/ // /_/ /
[info] /_/ / .___//_/ \__,_/ \__, /
[info] /_/ /____/
[info]
[info] Version 3.0.1 running Java 21
[info]
[info] Play is run entirely by the community. Please consider contributing and/or donating:
[info] https://www.playframework.com/sponsors
[info]
[info] Defining Global / ideaPort
[info] The new value will be used by Compile / compile, Test / compile
[info] Reapplying settings...
[info] __ __
[info] \ \ ____ / /____ _ __ __
[info] \ \ / __ \ / // __ `// / / /
[info] / / / /_/ // // /_/ // /_/ /
[info] /_/ / .___//_/ \__,_/ \__, /
[info] /_/ /____/
[info]
[info] Version 3.0.1 running Java 21
[info]
[info] Play is run entirely by the community. Please consider contributing and/or donating:
[info] https://www.playframework.com/sponsors
[info]
[IJ][hello-play-scala] $ ~run
[info] sbt server started at local:sbt-server-4cb92cee932ea78bdec1
[info] started sbt server
--- (Running the application, auto-reloading is enabled) ---
INFO p.c.s.PekkoHttpServer - Listening for HTTP on /[0:0:0:0:0:0:0:0]:9000
(Server started, use Enter to stop and go back to the console...)
INFO p.a.i.l.c.CoordinatedShutdownSupport - Starting synchronous coordinated shutdown with ServerStoppedReason reason and 40000 milliseconds timeout
INFO p.c.s.PekkoHttpServer - Stopping Pekko HTTP server...
INFO p.c.s.PekkoHttpServer - Unbinding /[0:0:0:0:0:0:0:0]:9000
INFO p.c.s.PekkoHttpServer - Terminating server binding for /[0:0:0:0:0:0:0:0]:9000
INFO p.c.s.PekkoHttpServer - Running provided shutdown stop hooks
[success] Total time: 6 s, completed 27 Dec 2023, 13:51:04
[info] 1. Monitoring source files for root/run...
[info] Press <enter> to interrupt or '?' for more options.
[info] Received input event: CancelWatch.
细节
0x00 TL;DR
feature 对抗之战。
0x01 feature "use Enter to stop"
此 feature 归口于 PlayFramework sbt-plugin.
表现为,收到 '\r' 停止,收到 '\n' 输出换行并继续运行。
sbt-plugin!Play.sbt.PlayInteractionMode.scala
private def waitForKey(): Unit = {
withConsoleReader { consoleReader =>
@tailrec def waitEOF(): Unit = {
consoleReader.readCharacter() match {
case 4 | 13 => // STOP on Ctrl-D or Enter
case 11 => consoleReader.clearScreen(); waitEOF()
case 10 => println(); waitEOF()
case _ => waitEOF()
}
}
doWithoutEcho(waitEOF())
}
}
0x02 feature "myPatchNewline"
此 feature 归口于 JetBrains/pty4j.
表现为,The "myPatchNewline" code in WinPTYOutputStream converts an LF character to CRLF.
Converting it to CR makes much more sense. winpty interprets LF as Ctrl-RETURN rather than RETURN, and it interprets CRLF as two keypresses (RETURN followed by Ctrl-RETURN).
事实上,启用 consoleMode 会打开 myPatchNewline 特性,以及打开错误流。
com/pty4j/windows/WinPTYOutputStream.java
if (myPatchNewline) {
byte[] newBuf = new byte[len];
int newPos = 0;
for (int i = off; i < off + len; ++i) {
if (b[i] == '\n') {
newBuf[newPos++] = '\r';
} else {
newBuf[newPos++] = b[i];
}
}
b = newBuf;
off = 0;
len = newPos;
}
从代码可看出,启用此特性,'\n' 会被转为 '\r', '\r\n" 会被转为 "\r\r".
通过设置环境变量 WINPTY_DEBUG=trace,input ,打开 winpty-debugserver.exe (rprichard/winpty),可以看到 trace 信息。
input chars: run^M^M (72 75 6E 0D 0D)
keypress: R ch='r'
keypress: U ch='u'
keypress: N ch='n'
keypress: RETURN ch=0xd
keypress: RETURN ch=0xd
0x03 Sbt / projectWindowActions
此 feature 归口于 JetBrains/intellij-scala.
intellij-scala/sbt-impl/org.jetbrains.sbt.shell/communication.scala
private def processCommand(commandAndListener: (String, CommandListener[_])): Unit = {
val (cmd, listener) = commandAndListener
listener.started()
val handler = process.acquireShellProcessHandler()
handler.addProcessListener(listener)
process.usingWriter { shell =>
shell.println(cmd)
shell.flush()
}
listener.future.onComplete { _ =>
handler.removeProcessListener(listener)
}
}
此处 PrintWriter.println 会输出 System.lineSeparator, 即 CRLF.
所以,在 sbt task 节点右键运行 (SbtNodeAction) 执行命令也是相同表现。
0x04 SbtShellExecuteActionHandler
intellij-community/com.intellij.execution.console/ProcessBackedConsoleExecuteActionHandler
protected void execute(String text, LanguageConsoleView console) {
processLine(text);
}
public void processLine(String line) {
sendText(line + "\n");
}
在 IDEA 控制台输入命令执行,只有 LF.
更多
Play Framework
限 IntelliJ IDEA Ultimate, 支持 Play 2.4+.