mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-03 03:29:22 +08:00
Redesign terminal
This commit is contained in:
parent
7952f0c15d
commit
d24a5912d3
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user