Redesign terminal

This commit is contained in:
Karlatemp 2022-08-31 22:43:54 +08:00
parent 7952f0c15d
commit d24a5912d3
No known key found for this signature in database
GPG Key ID: BA173CA2B9956C59
7 changed files with 486 additions and 426 deletions

View File

@ -9,67 +9,11 @@
package net.mamoe.mirai.console.terminal
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import net.mamoe.mirai.console.util.ConsoleInput
import org.fusesource.jansi.Ansi
import org.jline.reader.EndOfFileException
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
internal object ConsoleInputImpl : ConsoleInput {
private val format = DateTimeFormatter.ofPattern("HH:mm:ss")
internal val thread = Executors.newSingleThreadExecutor { task ->
Thread(task, "Mirai Console Input Thread").also {
it.isDaemon = false
}
}
internal var executingCoroutine: CancellableContinuation<String>? = null
override suspend fun requestInput(hint: String): String {
return suspendCancellableCoroutine { coroutine ->
if (thread.isShutdown || thread.isTerminated) {
coroutine.resumeWithException(EndOfFileException())
return@suspendCancellableCoroutine
}
executingCoroutine = coroutine
kotlin.runCatching {
thread.submit {
kotlin.runCatching {
waitDownloadingProgressEmpty()
lineReader.readLine(
if (hint.isNotEmpty()) {
prePrintNewLog()
lineReader.printAbove(
Ansi.ansi()
.fgCyan()
.a(
LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
.format(format)
)
.a(" ")
.fgMagenta().a(hint)
.reset()
.toString()
)
"$hint > "
} else "> "
)
}.let { result ->
executingCoroutine = null
coroutine.resumeWith(result)
}
}
}.onFailure { error ->
executingCoroutine = null
kotlin.runCatching { coroutine.resumeWithException(EndOfFileException(error)) }
}
}
return JLineInputDaemon.nextInput(hint)
}
}

View File

@ -24,7 +24,6 @@ import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.terminal.noconsole.NoConsole
import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.cast
import net.mamoe.mirai.console.util.requestInput
import net.mamoe.mirai.console.util.safeCast
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.warning
@ -47,16 +46,12 @@ internal fun startupConsoleThread() {
runCatching<Unit> {
// 应该仅关闭用户输入
terminal.reader().shutdown()
ConsoleInputImpl.thread.shutdownNow()
runCatching {
ConsoleInputImpl.executingCoroutine?.cancel(EndOfFileException())
}
}.exceptionOrNull()?.printStackTrace()
}
MiraiConsole.launch(CoroutineName("Console Command")) {
while (true) {
val next = try {
MiraiConsole.requestInput("").let {
JLineInputDaemon.nextCmd().let {
when {
it.isBlank() -> it
it.startsWith(CommandManager.commandPrefix) -> it

View File

@ -0,0 +1,218 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.console.terminal
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine
import net.mamoe.mirai.console.terminal.noconsole.NoConsole
import net.mamoe.mirai.utils.ConcurrentLinkedDeque
import net.mamoe.mirai.utils.cast
import org.jline.reader.MaskingCallback
import org.jline.reader.impl.LineReaderImpl
import org.jline.utils.AttributedStringBuilder
import org.jline.utils.AttributedStyle
import java.util.*
internal object JLineInputDaemon : Runnable {
lateinit var terminal0: MiraiConsoleImplementationTerminal
private val readerImpl: LineReaderImpl get() = lineReader.cast()
private var pausedByDaemon: Boolean = false
private var canResumeByNewRequest: Boolean = false
class Request(
val masked: Boolean = false,
val delayable: Boolean = false,
val coroutine: CancellableContinuation<String>,
val prompt: String? = null,
)
private val pwdMasker = object : MaskingCallback {
override fun display(line: String): String {
return buildString(line.length) { repeat(line.length) { append('*') } }
}
override fun history(line: String?): String? {
return null
}
}
val queue = ConcurrentLinkedDeque<Request>()
val queueDelayable = ConcurrentLinkedDeque<Request>()
val queueStateChangeNoticer = Object()
var processing: Request? = null
override fun run() {
while (terminal0.isActive) {
val nextTask = queue.poll() ?: queueDelayable.poll()
if (nextTask == null) {
synchronized(queueStateChangeNoticer) {
if (queue.isEmpty() && queueDelayable.isEmpty()) {
queueStateChangeNoticer.wait()
}
}
continue
}
if (nextTask.coroutine.isCancelled) continue
synchronized(queueStateChangeNoticer) {
processing = nextTask
updateFlags(nextTask)
}
val rsp = kotlin.runCatching {
lineReader.readLine(
nextTask.prompt ?: "> ",
null,
if (nextTask.masked) pwdMasker else null,
null
)
}
val crtProcessing: Request
synchronized(queueStateChangeNoticer) {
crtProcessing = processing ?: error("!processing lost")
processing = null
}
crtProcessing.coroutine.resumeWith(rsp)
}
}
internal fun sendRequest(req: Request) {
if (terminal is NoConsole) {
req.coroutine.resumeWith(kotlin.runCatching {
lineReader.readLine()
})
return
}
req.coroutine.invokeOnCancellation {
if (req.delayable) {
queueDelayable
} else {
queue
}.remove(req)
synchronized(queueStateChangeNoticer) {
if (processing !== req) return@invokeOnCancellation
val nnextTask: Request
while (true) {
val nnextTask2 = queue.poll() ?: queueDelayable.poll()
if (nnextTask2 == null) {
suspendReader(true)
return@invokeOnCancellation
}
if (nnextTask2.coroutine.isCancelled) continue
nnextTask = nnextTask2
break
}
processing = nnextTask
updateFlags(nnextTask)
}
}
synchronized(queueStateChangeNoticer) {
val crtProcessing = processing
if (crtProcessing != null) {
if (crtProcessing.delayable) {
processing = req
queueDelayable.addLast(crtProcessing)
updateFlags(req)
if (lineReader.isReading) {
readerImpl.redisplay()
}
return@synchronized
}
}
if (req.delayable) {
queueDelayable
} else {
queue
}.addLast(req)
queueStateChangeNoticer.notify()
}
tryResumeReader(true)
}
private fun updateFlags(req: Request) {
if (req.masked) {
lineReaderMaskingCallback[lineReader] = pwdMasker
} else {
lineReaderMaskingCallback[lineReader] = null
}
readerImpl.setPrompt(req.prompt ?: "> ")
}
@Synchronized
internal fun suspendReader(canResumeByNewRequest: Boolean) {
if (!lineReader.isReading) return
terminal.pause()
pausedByDaemon = true
this.canResumeByNewRequest = canResumeByNewRequest
lineReaderReadingField.setBoolean(lineReader, false)
terminalDisplay.update(Collections.emptyList(), 0)
}
@Synchronized
internal fun tryResumeReader(byNewReq: Boolean) {
if (!pausedByDaemon) return
if (byNewReq && !canResumeByNewRequest) return
pausedByDaemon = false
terminal.resume()
lineReaderReadingField.setBoolean(lineReader, true)
readerImpl.redisplay()
}
suspend fun nextInput(hint: String): String = suspendCancellableCoroutine { cort ->
sendRequest(
Request(
masked = false,
delayable = false,
coroutine = cort,
prompt = "$hint> "
)
)
}
suspend fun nextCmd(): String = suspendCancellableCoroutine { cort ->
sendRequest(Request(masked = false, delayable = true, coroutine = cort))
}
suspend fun nextPwd(): String = suspendCancellableCoroutine { cort ->
sendRequest(
Request(
masked = true,
delayable = false,
coroutine = cort,
prompt = AttributedStringBuilder()
.style(AttributedStyle.DEFAULT.background(AttributedStyle.CYAN).foreground(AttributedStyle.WHITE))
.append("PASSWORD")
.style(AttributedStyle.DEFAULT)
.append("> ")
.toAnsi()
)
)
}
}

View File

@ -9,23 +9,9 @@
package net.mamoe.mirai.console.terminal
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.utils.TestOnly
import net.mamoe.mirai.utils.systemProp
import java.io.File
import java.io.RandomAccessFile
import java.nio.file.Files
import java.time.Instant
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
// Used by https://github.com/iTXTech/soyuz, change with care.
internal sealed class LoggingService {
@ -45,108 +31,14 @@ internal class LoggingServiceNoop : LoggingService() {
}
}
@OptIn(ConsoleTerminalExperimentalApi::class)
internal class LoggingServiceI(
private val scope: CoroutineScope,
scope: CoroutineScope,
) : LoggingService() {
private val threadPool = Executors.newScheduledThreadPool(3, object : ThreadFactory {
private val group = ThreadGroup("mirai console terminal logging")
private val counter = AtomicInteger(0)
override fun newThread(r: Runnable): Thread {
return Thread(
group,
r,
"Mirai Console Terminal Logging Thread#" + counter.getAndIncrement()
).also { thread ->
thread.isDaemon = true
}
}
})
private val threadDispatcher = threadPool.asCoroutineDispatcher()
internal val pipelineSize = systemProp("mirai.console.terminal.log.buffer", 2048).toInt()
private val pipeline = Channel<String>(capacity = pipelineSize)
internal lateinit var autoSplitTask: Future<*>
@Suppress("BlockingMethodInNonBlockingContext")
fun startup(logDir: File) {
logDir.mkdirs()
val outputLock = Any()
val output = AtomicReference<RandomAccessFile>()
fun switchLogFile() {
val latestLogFile = logDir.resolve("latest.log")
var targetFile: File
if (latestLogFile.isFile) {
var counter = 0
do {
targetFile = logDir.resolve("log-$counter.log")
counter++
} while (targetFile.exists())
} else {
targetFile = latestLogFile
}
synchronized(outputLock) {
output.get()?.close()
if (latestLogFile !== targetFile) {
Files.move(latestLogFile.toPath(), targetFile.toPath())
}
output.set(RandomAccessFile(latestLogFile, "rw").also { it.seek(it.length()) })
}
}
switchLogFile()
@OptIn(TestOnly::class)
switchLogFileNow = ::switchLogFile
scope.launch(threadDispatcher) {
while (isActive) {
val nextLine = pipeline.receive()
synchronized(outputLock) {
output.get().let { out ->
out.write(nextLine.toByteArray())
out.write('\n'.code)
}
}
}
}
// Daily split log files
val nextDayTimeSec = Instant.now()
.atZone(ZoneId.systemDefault())
.plus(1, ChronoUnit.DAYS)
.withHour(0)
.withMinute(0)
.withSecond(0)
.toEpochSecond()
autoSplitTask = threadPool.scheduleAtFixedRate(
::switchLogFile,
nextDayTimeSec * 1000 - System.currentTimeMillis(),
TimeUnit.DAYS.toMillis(1), TimeUnit.MILLISECONDS
)
scope.coroutineContext.job.invokeOnCompletion {
threadPool.shutdown()
synchronized(outputLock) {
output.get()?.close()
}
}
}
private fun pushInPool(line: String) {
scope.launch(threadDispatcher, start = CoroutineStart.UNDISPATCHED) {
pipeline.send(line)
}
}
override fun pushLine(line: String) {
pipeline.trySend(line).onFailure {
pushInPool(line)
}
}
}

View File

@ -22,41 +22,42 @@
package net.mamoe.mirai.console.terminal
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import net.mamoe.mirai.console.ConsoleFrontEndImplementation
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription
import net.mamoe.mirai.console.MiraiConsoleImplementation
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.data.MultiFilePluginDataStorage
import net.mamoe.mirai.console.data.PluginDataStorage
import net.mamoe.mirai.console.fontend.ProcessProgress
import net.mamoe.mirai.console.frontendbase.AbstractMiraiConsoleFrontendImplementation
import net.mamoe.mirai.console.frontendbase.FrontendBase
import net.mamoe.mirai.console.frontendbase.logging.AllDroppedLogRecorder
import net.mamoe.mirai.console.frontendbase.logging.LogRecorder
import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader
import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.terminal.ConsoleInputImpl.requestInput
import net.mamoe.mirai.console.terminal.noconsole.AllEmptyLineReader
import net.mamoe.mirai.console.terminal.noconsole.NoConsole
import net.mamoe.mirai.console.terminal.noconsole.SystemOutputPrintStream
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.SemVersion
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.StandardCharImageLoginSolver
import org.fusesource.jansi.Ansi
import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder
import org.jline.reader.impl.LineReaderImpl
import org.jline.reader.impl.completer.NullCompleter
import org.jline.terminal.Terminal
import org.jline.terminal.TerminalBuilder
import org.jline.terminal.impl.AbstractWindowsTerminal
import org.jline.utils.AttributedString
import org.jline.utils.Display
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.concurrent.withLock
import kotlin.coroutines.Continuation
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
/**
* mirai-console-terminal 后端实现
@ -74,28 +75,40 @@ open class MiraiConsoleImplementationTerminal
override val dataStorageForBuiltIns: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("data")),
override val configStorageForJvmPluginLoader: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("config")),
override val configStorageForBuiltIns: PluginDataStorage = MultiFilePluginDataStorage(rootPath.resolve("config")),
) : MiraiConsoleImplementation, CoroutineScope by CoroutineScope(
SupervisorJob() + CoroutineName("MiraiConsoleImplementationTerminal") +
CoroutineExceptionHandler { coroutineContext, throwable ->
if (throwable is CancellationException) {
return@CoroutineExceptionHandler
}
val coroutineName = coroutineContext[CoroutineName]?.name ?: "<unnamed>"
MiraiConsole.mainLogger.error("Exception in coroutine $coroutineName", throwable)
}) {
override val jvmPluginLoader: JvmPluginLoader by lazy { backendAccess.createDefaultJvmPluginLoader(coroutineContext) }
override val commandManager: CommandManager by lazy { backendAccess.createDefaultCommandManager(coroutineContext) }
override val consoleInput: ConsoleInput get() = ConsoleInputImpl
override val isAnsiSupported: Boolean get() = true
override val consoleDataScope: MiraiConsoleImplementation.ConsoleDataScope by lazy {
MiraiConsoleImplementation.ConsoleDataScope.createDefault(
coroutineContext,
dataStorageForBuiltIns,
configStorageForBuiltIns
)
) : AbstractMiraiConsoleFrontendImplementation("MiraiConsoleImplementationTerminal") {
@Suppress("MemberVisibilityCanBePrivate")
override val frontendBase = object : FrontendBase() {
override val scope: CoroutineScope
get() = this@MiraiConsoleImplementationTerminal
override val workingDirectory: Path
get() = this@MiraiConsoleImplementationTerminal.rootPath
override fun initLogRecorder(): LogRecorder {
if (ConsoleTerminalSettings.noLogging) {
return AllDroppedLogRecorder
}
return super.initLogRecorder()
}
override fun initScreen_forwardStdToScreen() {
lineReader
super.initScreen_forwardStdToScreen()
}
override fun printToScreenDirectly(msg: String) {
printToScreen(msg)
@Suppress("DEPRECATION")
logService.pushLine(msg)
}
}
// used in test
override val consoleInput: ConsoleInput get() = ConsoleInputImpl
override val isAnsiSupported: Boolean get() = true
@Deprecated("Used by iTXTech; for binary compatibility")
internal val logService: LoggingService
override fun createLoginSolver(requesterBot: Long, configuration: BotConfiguration): LoginSolver {
@ -103,58 +116,14 @@ open class MiraiConsoleImplementationTerminal
return StandardCharImageLoginSolver(input = { requestInput("LOGIN> ") })
}
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(
"Deprecated for removal. Implement the other overload, or use MiraiConsole.createLogger instead.",
level = DeprecationLevel.ERROR
)
override fun createLogger(identity: String?): MiraiLogger {
return MiraiLogger.Factory.create(MiraiConsoleImplementationTerminal::class, identity)
// return PlatformLogger(identity = identity, output = { line ->
// val text = line + ANSI_RESET
// lineReader.printAbove(text)
// logService.pushLine(text)
// })
}
override fun createLoggerFactory(context: MiraiConsoleImplementation.FrontendLoggingInitContext): MiraiLogger.Factory {
// platformImplementation is not used by Terminal
return object : MiraiLogger.Factory {
override fun create(requester: Class<*>, identity: String?): MiraiLogger {
return PlatformLogger(identity = identity ?: requester.simpleName, output = { line ->
val text = line + ANSI_RESET
prePrintNewLog()
lineReader.printAbove(text)
postPrintNewLog()
logService.pushLine(text)
})
}
override fun create(requester: KClass<*>, identity: String?): MiraiLogger {
return PlatformLogger(identity = identity ?: requester.simpleName, output = { line ->
val text = line + ANSI_RESET
prePrintNewLog()
lineReader.printAbove(text)
postPrintNewLog()
logService.pushLine(text)
})
}
}
}
init {
with(rootPath.toFile()) {
mkdir()
require(isDirectory) { "rootDir $absolutePath is not a directory" }
logService = if (ConsoleTerminalSettings.noLogging) {
LoggingServiceNoop()
} else {
LoggingServiceI(childScope("Log Service")).also { service ->
service.startup(resolve("logs"))
}
}
@Suppress("DEPRECATION")
logService = LoggingServiceNoop()
}
}
@ -163,7 +132,12 @@ open class MiraiConsoleImplementationTerminal
override fun preStart() {
registerSignalHandler()
overrideSTD(this)
JLineInputDaemon.terminal0 = this
if (terminal !is NoConsole) {
frontendBase.newDaemon("JLine Input Daemon", JLineInputDaemon).start()
}
launch(CoroutineName("Mirai Console Terminal Downloading Progress Bar Updater")) {
while (isActive) {
downloadingProgressDaemonStub()
@ -178,7 +152,10 @@ open class MiraiConsoleImplementationTerminal
kotlin.runCatching {
downloadingProgressCoroutine?.resumeWith(Result.success(Unit))
}
return TerminalProcessProgress(lineReader).also { terminalDownloadingProgresses.add(it) }
return TerminalProcessProgress(lineReader).also {
terminalDownloadingProgresses.add(it)
terminal.writer().print("\u001B[?25l") // hide cursor
}
}
}
@ -192,149 +169,12 @@ val lineReader: LineReader by lazy {
.build()
}
internal val terminalDisplay: Display by object : kotlin.properties.ReadOnlyProperty<Any?, Display> {
val delegate: () -> Display by lazy {
val terminal = terminal
if (terminal is NoConsole) {
val display = Display(terminal, false)
return@lazy { display }
}
val lr = lineReader
val field = LineReaderImpl::class.java.declaredFields.first { it.type == Display::class.java }
field.isAccessible = true
return@lazy { field[lr] as Display }
}
override fun getValue(thisRef: Any?, property: KProperty<*>): Display {
return delegate()
}
}
internal val terminalExecuteLock: java.util.concurrent.locks.Lock by lazy {
val terminal = terminal
if (terminal is NoConsole) return@lazy java.util.concurrent.locks.ReentrantLock()
val lr = lineReader
val field = LineReaderImpl::class.java.declaredFields.first {
java.util.concurrent.locks.Lock::class.java.isAssignableFrom(it.type)
}
field.isAccessible = true
field[lr].cast()
}
private val terminalDownloadingProgressesNoticer = Object()
private var containDownloadingProgress: Boolean = false
get() = field || terminalDownloadingProgresses.isNotEmpty()
internal val terminalDownloadingProgresses = mutableListOf<TerminalProcessProgress>()
private var downloadingProgressCoroutine: Continuation<Unit>? = null
private suspend fun downloadingProgressDaemonStub() {
delay(500L)
if (containDownloadingProgress) {
updateTerminalDownloadingProgresses()
} else {
suspendCancellableCoroutine<Unit> { cp ->
downloadingProgressCoroutine = cp
}
downloadingProgressCoroutine = null
}
}
internal fun updateTerminalDownloadingProgresses() {
if (!containDownloadingProgress) return
runCatching { downloadingProgressCoroutine?.resumeWith(Result.success(Unit)) }
terminalExecuteLock.withLock {
if (terminalDownloadingProgresses.isNotEmpty()) {
val wid = terminal.width
if (wid == 0) { // Run in idea
if (terminalDownloadingProgresses.removeIf { it.pendingErase }) {
updateTerminalDownloadingProgresses()
return
}
terminalDisplay.update(listOf(AttributedString.EMPTY), 0, false)
// Error in idea when more than one bar displaying
terminalDisplay.update(listOf(terminalDownloadingProgresses[0].let {
it.updateTxt(0); it.ansiMsg
}), 0)
} else {
if (terminalDownloadingProgresses.size > 4) {
// to mush. delete some completed status
var allowToDelete = terminalDownloadingProgresses.size - 4
terminalDownloadingProgresses.removeIf { pg ->
if (allowToDelete == 0) {
return@removeIf false
}
if (pg.pendingErase) {
allowToDelete--
return@removeIf true
}
return@removeIf false
}
}
terminalDisplay.update(terminalDownloadingProgresses.map {
it.updateTxt(wid); it.ansiMsg
}, 0)
cleanupErase()
}
} else {
terminalDisplay.update(emptyList(), 0)
(lineReader as LineReaderImpl).let { lr ->
if (lr.isReading) {
lr.redisplay()
}
}
noticeDownloadingProgressEmpty()
}
}
}
internal fun prePrintNewLog() {
if (!containDownloadingProgress) return
if (terminalDownloadingProgresses.isNotEmpty()) {
terminalExecuteLock.withLock {
terminalDisplay.update(emptyList(), 0)
}
}
}
internal fun cleanupErase() {
val now = currentTimeMillis()
terminalDownloadingProgresses.removeIf { pg ->
if (!pg.pendingErase) return@removeIf false
if (now > pg.eraseTimestamp) {
pg.ansiMsg = AttributedString.EMPTY
return@removeIf true
}
return@removeIf false
}
}
internal fun postPrintNewLog() {
if (!containDownloadingProgress) return
updateTerminalDownloadingProgresses()
cleanupErase()
}
private fun noticeDownloadingProgressEmpty() {
synchronized(terminalDownloadingProgressesNoticer) {
containDownloadingProgress = false
if (terminalDownloadingProgresses.isEmpty()) {
terminalDownloadingProgressesNoticer.notifyAll()
}
}
}
internal fun waitDownloadingProgressEmpty() {
synchronized(terminalDownloadingProgressesNoticer) {
if (containDownloadingProgress) {
terminalDownloadingProgressesNoticer.wait()
}
}
}
val terminal: Terminal = run {
if (ConsoleTerminalSettings.noConsole) return@run NoConsole
if (ConsoleTerminalSettings.noConsole) {
SystemOutputPrintStream // init value
return@run NoConsole
}
TerminalBuilder.builder()
.name("Mirai Console")

View File

@ -286,38 +286,11 @@ internal fun exitProcessAndForceHalt(code: Int): Nothing {
}
}
internal fun overrideSTD(terminal: MiraiConsoleImplementation) {
if (ConsoleTerminalSettings.noConsole) {
SystemOutputPrintStream // Avoid StackOverflowError when launch with no console mode
}
lineReader // Initialize real frontend first. #1936
System.setOut(
PrintStream(
BufferedOutputStream(
logger = MiraiLogger.Factory.create(terminal::class, "stdout")::info
),
false,
"UTF-8"
)
)
System.setErr(
PrintStream(
BufferedOutputStream(
logger = MiraiLogger.Factory.create(terminal::class, "stderr")::warning
),
false,
"UTF-8"
)
)
}
internal object ConsoleCommandSenderImplTerminal : MiraiConsoleImplementation.ConsoleCommandSenderImpl {
override suspend fun sendMessage(message: String) {
kotlin.runCatching {
prePrintNewLog()
lineReader.printAbove(message + ANSI_RESET)
postPrintNewLog()
printToScreen(message + ANSI_RESET)
}.onFailure { exception ->
// If failed. It means JLine Terminal not working...
PrintStream(FileOutputStream(FileDescriptor.err)).use {

View File

@ -9,10 +9,22 @@
package net.mamoe.mirai.console.terminal
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import net.mamoe.mirai.console.fontend.ProcessProgress
import net.mamoe.mirai.console.terminal.noconsole.NoConsole
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.currentTimeMillis
import org.jline.reader.MaskingCallback
import org.jline.reader.impl.LineReaderImpl
import org.jline.utils.AttributedString
import org.jline.utils.AttributedStringBuilder
import org.jline.utils.AttributedStyle
import org.jline.utils.Display
import java.lang.reflect.Field
import kotlin.concurrent.withLock
import kotlin.coroutines.Continuation
import kotlin.reflect.KProperty
internal class TerminalProcessProgress(
private val reader: org.jline.reader.LineReader,
@ -145,10 +157,8 @@ internal class TerminalProcessProgress(
updateTxt(reader.terminal.width)
if (failed) {
terminalDownloadingProgresses.remove(this)
prePrintNewLog()
reader.printAbove(ansiMsg)
printToScreen(ansiMsg)
ansiMsg = AttributedString.EMPTY
postPrintNewLog()
return
}
// terminalDownloadingProgresses.remove(this)
@ -161,4 +171,192 @@ internal class TerminalProcessProgress(
// reader.printAbove(ansiMsg)
// ansiMsg = AttributedString.EMPTY
}
}
}
internal val terminalDisplay: Display by object : kotlin.properties.ReadOnlyProperty<Any?, Display> {
val delegate: () -> Display by lazy {
val terminal = terminal
if (terminal is NoConsole) {
val display = Display(terminal, false)
return@lazy { display }
}
val lr = lineReader
val field = LineReaderImpl::class.java.declaredFields.first { it.type == Display::class.java }
field.isAccessible = true
return@lazy { field[lr] as Display }
}
override fun getValue(thisRef: Any?, property: KProperty<*>): Display {
return delegate()
}
}
internal val lineReaderMaskingCallback: Field by lazy {
val field = LineReaderImpl::class.java.declaredFields.first {
MaskingCallback::class.java.isAssignableFrom(it.type)
}
field.isAccessible = true
field
}
internal val lineReaderReadingField: Field by lazy {
val field = LineReaderImpl::class.java.getDeclaredField("reading")
field.isAccessible = true
field
}
internal val terminalExecuteLock: java.util.concurrent.locks.Lock by lazy {
val terminal = terminal
if (terminal is NoConsole) return@lazy java.util.concurrent.locks.ReentrantLock()
val lr = lineReader
val field = LineReaderImpl::class.java.declaredFields.first {
java.util.concurrent.locks.Lock::class.java.isAssignableFrom(it.type)
}
field.isAccessible = true
field[lr].cast()
}
private val terminalDownloadingProgressesNoticer = Object()
internal var containDownloadingProgress: Boolean = false
get() = field || terminalDownloadingProgresses.isNotEmpty()
internal val terminalDownloadingProgresses = mutableListOf<TerminalProcessProgress>()
internal var downloadingProgressCoroutine: Continuation<Unit>? = null
internal suspend fun downloadingProgressDaemonStub() {
delay(500L)
if (containDownloadingProgress) {
updateTerminalDownloadingProgresses()
} else {
suspendCancellableCoroutine<Unit> { cp ->
downloadingProgressCoroutine = cp
}
downloadingProgressCoroutine = null
}
}
internal fun updateTerminalDownloadingProgresses() {
if (!containDownloadingProgress) return
runCatching { downloadingProgressCoroutine?.resumeWith(Result.success(Unit)) }
JLineInputDaemon.suspendReader(false)
terminalExecuteLock.withLock {
if (terminalDownloadingProgresses.isNotEmpty()) {
val wid = terminal.width
if (wid == 0) { // Run in idea
if (terminalDownloadingProgresses.removeIf { it.pendingErase }) {
updateTerminalDownloadingProgresses()
return
}
terminalDisplay.update(listOf(AttributedString.EMPTY), 0, false)
// Error in idea when more than one bar displaying
terminalDisplay.update(listOf(terminalDownloadingProgresses[0].let {
it.updateTxt(0); it.ansiMsg
}), 0)
} else {
if (terminalDownloadingProgresses.size > 4) {
// to mush. delete some completed status
var allowToDelete = terminalDownloadingProgresses.size - 4
terminalDownloadingProgresses.removeIf { pg ->
if (allowToDelete == 0) {
return@removeIf false
}
if (pg.pendingErase) {
allowToDelete--
return@removeIf true
}
return@removeIf false
}
}
terminalDisplay.update(terminalDownloadingProgresses.map {
it.updateTxt(wid); it.ansiMsg
}, 0)
cleanupErase()
}
} else {
terminalDisplay.update(emptyList(), 0)
(lineReader as LineReaderImpl).let { lr ->
if (lr.isReading) {
lr.redisplay()
}
}
noticeDownloadingProgressEmpty()
terminal.writer().print("\u001B[?25h") // show cursor
}
}
}
internal fun printToScreen(msg: String) {
if (!containDownloadingProgress) {
if (msg.endsWith(ANSI_RESET)) {
lineReader.printAbove(msg)
} else {
lineReader.printAbove(msg + ANSI_RESET)
}
return
}
terminalExecuteLock.withLock {
terminalDisplay.update(emptyList(), 0)
if (msg.endsWith(ANSI_RESET)) {
lineReader.printAbove(msg)
} else {
lineReader.printAbove(msg + ANSI_RESET)
}
updateTerminalDownloadingProgresses()
cleanupErase()
}
}
internal fun printToScreen(msg: AttributedString) {
if (!containDownloadingProgress) {
return lineReader.printAbove(msg)
}
terminalExecuteLock.withLock {
terminalDisplay.update(emptyList(), 0)
lineReader.printAbove(msg)
updateTerminalDownloadingProgresses()
cleanupErase()
}
}
internal fun cleanupErase() {
val now = currentTimeMillis()
terminalDownloadingProgresses.removeIf { pg ->
if (!pg.pendingErase) return@removeIf false
if (now > pg.eraseTimestamp) {
pg.ansiMsg = AttributedString.EMPTY
return@removeIf true
}
return@removeIf false
}
}
private fun noticeDownloadingProgressEmpty() {
synchronized(terminalDownloadingProgressesNoticer) {
containDownloadingProgress = false
if (terminalDownloadingProgresses.isEmpty()) {
terminalDownloadingProgressesNoticer.notifyAll()
}
JLineInputDaemon.tryResumeReader(false)
}
}
internal fun waitDownloadingProgressEmpty() {
synchronized(terminalDownloadingProgressesNoticer) {
if (containDownloadingProgress) {
terminalDownloadingProgressesNoticer.wait()
}
}
}