Merge pull request #163 from Karlatemp/no-console

Working without Terminal
This commit is contained in:
Him188 2020-09-13 02:12:29 +08:00 committed by GitHub
commit 77bc76be24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 338 additions and 3 deletions

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
/*
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*/
package net.mamoe.mirai.console.pure
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.CONSTRUCTOR
)
@MustBeDocumented
annotation class ConsolePureExperimentalApi
@ConsolePureExperimentalApi
public object ConsolePureSettings {
@JvmField
var setupAnsi: Boolean = System.getProperty("os.name")
.toLowerCase()
.contains("windows") // Just for Windows
@JvmField
var noConsole: Boolean = false
@JvmField
var noAnsi: Boolean = false
@JvmField
var noConsoleSafeReading: Boolean = false
@JvmField
var noConsoleReadingReplacement: String = ""
}

View File

@ -27,8 +27,10 @@ import org.jline.reader.UserInterruptException
val consoleLogger by lazy { DefaultLogger("console") }
@OptIn(ConsoleInternalApi::class)
@OptIn(ConsoleInternalApi::class, ConsolePureExperimentalApi::class)
internal fun startupConsoleThread() {
if (ConsolePureSettings.noConsole) return
MiraiConsole.launch(CoroutineName("Input")) {
while (true) {
try {

View File

@ -17,7 +17,7 @@
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING",
"EXPOSED_SUPER_CLASS"
)
@file:OptIn(ConsoleInternalApi::class, ConsoleFrontEndImplementation::class)
@file:OptIn(ConsoleInternalApi::class, ConsoleFrontEndImplementation::class, ConsolePureExperimentalApi::class)
package net.mamoe.mirai.console.pure
@ -34,6 +34,8 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader
import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.pure.ConsoleInputImpl.requestInput
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.pure.noconsole.AllEmptyLineReader
import net.mamoe.mirai.console.pure.noconsole.NoConsole
import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.NamedSupervisorJob
@ -115,6 +117,8 @@ private object ConsoleInputImpl : ConsoleInput {
}
val lineReader: LineReader by lazy {
if (ConsolePureSettings.noConsole) return@lazy AllEmptyLineReader
LineReaderBuilder.builder()
.terminal(terminal)
.completer(NullCompleter())
@ -122,6 +126,8 @@ val lineReader: LineReader by lazy {
}
val terminal: Terminal = run {
if (ConsolePureSettings.noConsole) return@run NoConsole
val dumb = System.getProperty("java.class.path")
.contains("idea_rt.jar") || System.getProperty("mirai.idea") !== null || System.getenv("mirai.idea") !== null

View File

@ -15,7 +15,7 @@
"INVISIBLE_GETTER",
"INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER",
)
@file:OptIn(ConsoleInternalApi::class)
@file:OptIn(ConsoleInternalApi::class, ConsolePureExperimentalApi::class)
package net.mamoe.mirai.console.pure
@ -26,6 +26,7 @@ import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleImplementation
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
import net.mamoe.mirai.console.data.AutoSavePluginDataHolder
import net.mamoe.mirai.console.pure.noconsole.SystemOutputPrintStream
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
@ -33,6 +34,7 @@ import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.minutesToMillis
import java.io.PrintStream
import kotlin.system.exitProcess
/**
* mirai-console-pure CLI 入口点
@ -40,6 +42,7 @@ import java.io.PrintStream
object MiraiConsolePureLoader {
@JvmStatic
fun main(args: Array<String>) {
parse(args, exitProcess = true)
startAsDaemon()
try {
runBlocking {
@ -50,6 +53,97 @@ object MiraiConsolePureLoader {
}
}
@ConsolePureExperimentalApi
fun printHelpMessage() {
val help = listOf(
"" to "Mirai-Console[Pure FrontEnd] v" + kotlin.runCatching {
net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version
}.getOrElse { "<unknown>" },
"" to "",
"--help" to "显示此帮助",
"" to "",
"--no-console" to "使用无终端操作环境",
"--dont-setup-terminal-ansi" to
"[NoConsole] [Windows Only] 不进行ansi console初始化工作",
"--no-ansi" to "[NoConsole] 禁用 ansi",
"--safe-reading" to
"[NoConsole] 如果启动此选项, console在获取用户输入的时候会获得一个安全的替换符\n" +
" 如果不启动, 将会直接 error",
"--reading-replacement <string>" to
"[NoConsole] Console尝试读取命令的替换符, 默认是空字符串\n" +
" 使用此选项会自动开启 --safe-reading",
)
val prefixPlaceholder = String(CharArray(
help.maxOfOrNull { it.first.length }!! + 3
) { ' ' })
fun printOption(optionName: String, value: String) {
if (optionName == "") {
println(value)
return
}
print(optionName)
print(prefixPlaceholder.substring(optionName.length))
val lines = value.split('\n').iterator()
if (lines.hasNext()) println(lines.next())
lines.forEach { line ->
print(prefixPlaceholder)
println(line)
}
}
help.forEach { (optionName, value) ->
printOption(optionName, value)
}
}
@ConsolePureExperimentalApi
fun parse(args: Array<String>, exitProcess: Boolean = false) {
val iterator = args.iterator()
while (iterator.hasNext()) {
when (val option = iterator.next()) {
"--help" -> {
printHelpMessage()
if (exitProcess) exitProcess(0)
return
}
"--no-console" -> {
ConsolePureSettings.noConsole = true
}
"--dont-setup-terminal-ansi" -> {
ConsolePureSettings.setupAnsi = false
}
"--no-ansi" -> {
ConsolePureSettings.noAnsi = true
ConsolePureSettings.setupAnsi = false
}
"--reading-replacement" -> {
ConsolePureSettings.noConsoleSafeReading = true
if (iterator.hasNext()) {
ConsolePureSettings.noConsoleReadingReplacement = iterator.next()
} else {
println("Bad option `--reading-replacement`")
println("Usage: --reading-replacement <string>")
if (exitProcess)
exitProcess(1)
return
}
}
"--safe-reading" -> {
ConsolePureSettings.noConsoleSafeReading = true
}
else -> {
println("Unknown option `$option`")
printHelpMessage()
if (exitProcess)
exitProcess(1)
return
}
}
}
if (ConsolePureSettings.noConsole)
SystemOutputPrintStream // Setup Output Channel
}
@Suppress("MemberVisibilityCanBePrivate")
@ConsoleExperimentalApi
fun startAsDaemon(instance: MiraiConsoleImplementationPure = MiraiConsoleImplementationPure()) {

View File

@ -0,0 +1,187 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
/*
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*/
@file:OptIn(ConsolePureExperimentalApi::class)
package net.mamoe.mirai.console.pure.noconsole
import net.mamoe.mirai.console.pure.ConsolePureExperimentalApi
import net.mamoe.mirai.console.pure.ConsolePureSettings
import org.jline.keymap.KeyMap
import org.jline.reader.*
import org.jline.terminal.Attributes
import org.jline.terminal.MouseEvent
import org.jline.terminal.Size
import org.jline.terminal.Terminal
import org.jline.terminal.impl.AbstractTerminal
import org.jline.utils.AttributedString
import org.jline.utils.NonBlockingReader
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.io.PrintWriter
private const val LN_INT = '\n'.toInt()
private const val LN_BYTE = '\n'.toByte()
internal object NoConsoleNonBlockingReader : NonBlockingReader() {
override fun read(timeout: Long, isPeek: Boolean): Int {
return LN_INT
}
override fun close() {}
override fun readBuffered(b: CharArray?): Int {
return 0
}
}
internal object AllNextLineInputStream : InputStream() {
override fun read(): Int = LN_INT
override fun available(): Int = 1
override fun read(b: ByteArray, off: Int, len: Int): Int {
for (i in off until (off + len)) {
b[i] = LN_BYTE
}
return len
}
override fun close() {}
}
internal object AllIgnoredOutputStream : OutputStream() {
override fun close() {}
override fun write(b: ByteArray, off: Int, len: Int) {}
override fun write(b: ByteArray) {}
override fun write(b: Int) {}
override fun flush() {}
}
internal val SystemOutputPrintStream by lazy {
@OptIn(ConsolePureExperimentalApi::class)
if (ConsolePureSettings.setupAnsi) {
org.fusesource.jansi.AnsiConsole.systemInstall()
}
System.out
}
private val ANSI_REGEX = """\u001b\[[0-9a-zA-Z;]*?m""".toRegex()
internal object AllEmptyLineReader : LineReader {
override fun printAbove(str: String?) {
if (str == null) return
@OptIn(ConsolePureExperimentalApi::class)
if (ConsolePureSettings.noAnsi) {
SystemOutputPrintStream.println(ANSI_REGEX.replace(str, ""))
} else SystemOutputPrintStream.println(str)
}
@OptIn(ConsolePureExperimentalApi::class)
override fun readLine(): String =
if (ConsolePureSettings.noConsoleSafeReading) ConsolePureSettings.noConsoleReadingReplacement
else error("Unsupported Reading line when console front-end closed.")
// region
private fun <T> ignored(): T = error("Ignored")
override fun readLine(mask: Char?): String = readLine()
override fun readLine(prompt: String?): String = readLine()
override fun readLine(prompt: String?, mask: Char?): String = readLine()
override fun readLine(prompt: String?, mask: Char?, buffer: String?): String = readLine()
override fun readLine(prompt: String?, rightPrompt: String?, mask: Char?, buffer: String?): String = readLine()
override fun readLine(
prompt: String?,
rightPrompt: String?,
maskingCallback: MaskingCallback?,
buffer: String?
): String = readLine()
override fun printAbove(str: AttributedString?) {
str?.let { printAbove(it.toAnsi()) }
}
override fun defaultKeyMaps(): MutableMap<String, KeyMap<Binding>> = ignored()
override fun isReading(): Boolean = false
override fun variable(name: String?, value: Any?) = this
override fun option(option: LineReader.Option?, value: Boolean) = this
override fun callWidget(name: String?) {}
override fun getVariables(): MutableMap<String, Any> = ignored()
override fun getVariable(name: String?): Any = ignored()
override fun setVariable(name: String?, value: Any?) {}
override fun isSet(option: LineReader.Option?): Boolean = ignored()
override fun setOpt(option: LineReader.Option?) {}
override fun unsetOpt(option: LineReader.Option?) {}
override fun getTerminal(): Terminal = NoConsole
override fun getWidgets(): MutableMap<String, Widget> = ignored()
override fun getBuiltinWidgets(): MutableMap<String, Widget> = ignored()
override fun getBuffer(): Buffer = ignored()
override fun getAppName(): String = "Mirai Console"
override fun runMacro(macro: String?) {}
override fun readMouseEvent(): MouseEvent = ignored()
override fun getHistory(): History = ignored()
override fun getParser(): Parser = ignored()
override fun getHighlighter(): Highlighter = ignored()
override fun getExpander(): Expander = ignored()
override fun getKeyMaps(): MutableMap<String, KeyMap<Binding>> = ignored()
override fun getKeyMap(): String = ignored()
override fun setKeyMap(name: String?): Boolean = ignored()
override fun getKeys(): KeyMap<Binding> = ignored()
override fun getParsedLine(): ParsedLine = ignored()
override fun getSearchTerm(): String = ignored()
override fun getRegionActive(): LineReader.RegionType = ignored()
override fun getRegionMark(): Int = ignored()
override fun addCommandsInBuffer(commands: MutableCollection<String>?) {}
override fun editAndAddInBuffer(file: File?) {}
override fun getLastBinding(): String = ignored()
override fun getTailTip(): String = ignored()
override fun setTailTip(tailTip: String?) {}
override fun setAutosuggestion(type: LineReader.SuggestionType?) {}
override fun getAutosuggestion(): LineReader.SuggestionType = ignored()
// endregion
}
internal object NoConsole : AbstractTerminal(
"No Console", "No Console"
) {
override fun reader(): NonBlockingReader = NoConsoleNonBlockingReader
private val AllIgnoredPrintWriter = object : PrintWriter(AllIgnoredOutputStream) {
override fun close() {}
override fun flush() {}
}
// We don't need it. Mirai-Console using LineReader to print messages.
override fun writer(): PrintWriter = AllIgnoredPrintWriter
override fun input(): InputStream = AllNextLineInputStream
override fun output(): OutputStream = AllIgnoredOutputStream
private val attributes0 = Attributes()
override fun getAttributes(): Attributes {
return Attributes(attributes0)
}
override fun setAttributes(attr: Attributes?) {
attr?.let { attributes0.copy(it) }
}
private val size0 = Size(189, 53)
override fun getSize(): Size {
return Size().also { it.copy(size0) }
}
override fun setSize(size: Size?) {
size?.let { size0.copy(it) }
}
}