Add crash handler and add various optimizations

This commit is contained in:
JeremyStar™ 2024-12-15 19:16:43 +01:00
parent 2cfc8cad9d
commit 2b8cf723f6
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
10 changed files with 536 additions and 72 deletions

View file

@ -0,0 +1,134 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
* with an exception allowing classpath linking.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.engine.logging
import de.staropensource.engine.logging.implementable.Adapter
import de.staropensource.engine.logging.implementable.CrashCategory
import de.staropensource.engine.logging.implementable.Formatter
import de.staropensource.engine.logging.implementation.KotlinShutdownHandler
import de.staropensource.engine.logging.type.Call
import de.staropensource.engine.logging.type.ChannelSettings
/**
* Handles crashes.
*
* @since v1-alpha10
*/
class CrashHandler private constructor() {
/**
* Companion object of [CrashHandler].
*
* @since v1-alpha10
*/
companion object {
/**
* Handles crashes.
*
* @param call [Call] metadata
* @param throwable the [Throwable] which caused the crash
* @param fatal terminates the engine and application if `true`
* @since v1-alpha10
*/
internal fun handle(call: Call, throwable: Throwable? = null, fatal: Boolean = true) {
val format: StringBuilder = StringBuilder()
var formatFinalized: String? = null
val channelconf: ChannelSettings? = LoggerConfiguration.channelSettings[call.channel]
if (ChannelSettings.getSetting(channelconf, "permitFormatting") as Boolean)
format.append("<red>")
format
.append("--------------------\nOh no! ")
.append(ChannelSettings.getSetting(channelconf, "applicationName"))
.append(" crashed!\nIf you're a user of this application, then please report this crash to the developer.")
for (category: CrashCategory in LoggerConfiguration.crashCategories)
if (category.check())
format
.append("\n\n${category.getName()}")
.append(compileCategory(category.execute(call, channelconf, throwable, fatal)))
format
.append("\n\n... ")
.append(ChannelSettings.getSetting(channelconf, "applicationName"))
.append(" unfortunately crashed.\nIf you're a user of this application, then please ")
.append("report this crash to the developer.\n--------------------")
// Format format
formatFinalized = if (ChannelSettings.getSetting(channelconf, "permitFormatting") as Boolean)
(ChannelSettings.getSetting(channelconf, "formatter") as Formatter).formatString(format.toString())
else
format.toString()
// Pass format to adapter
(ChannelSettings.getSetting(channelconf, "adapter") as Adapter).handle(call, formatFinalized)
if (fatal)
(LoggerConfiguration.shutdownHandler ?: KotlinShutdownHandler.instance).exit(exitcode = 69)
}
/**
* Compiles a [CrashCategory].
*
* @param category [CrashCategory] to compile
* @return compiled output
* @since v1-alpha10
*/
private fun compileCategory(map: LinkedHashMap<*, *>, indent: Int = 1): String {
val builder: StringBuilder = StringBuilder()
var entryString: String? = null
// Iterate over all entries
for (entry in map.keys) {
// Check if key is a string
if (entry !is String)
continue
builder.append("\n${" ".repeat(indent)}-> ${entry}")
if (map[entry] is LinkedHashMap<*, *>) // Value is a map
builder.append(
compileCategory(
map[entry] as LinkedHashMap<*, *>,
indent = indent + 3 // increase the 2nd addend to change the indent size during recursion
)
)
else {
entryString = map[entry].toString()
// Put on separate line if contains newline
if (entryString.contains("\n"))
builder
.append("\n${entryString}"
.replace(
"\n",
"\n ${" ".repeat(indent)}"
)
)
else
builder.append(": ${entryString}")
}
}
return builder.toString()
}
}
}

View file

@ -61,6 +61,7 @@ class Logger {
* @since v1-alpha10
*/
@JvmStatic
@Suppress("unused")
val instance = Logger()
}
@ -90,34 +91,41 @@ class Logger {
/**
* Logs a message.
*
* @param level level to use
* @param message message to log
* @param stackTraceDistance determines which [StackTraceElement] will
* be used as the call's origin. Just fiddle
* with this number until it's correct.
* Using this method is highly discouraged as it is
* considered internal and should only be accessed
* if necessary. It provides direct access to the
* internal logging facility and can cause breakage
* if used improperly.
*
* @param level level to use
* @param message message to log
* @param levelData data specific to a [Level]
* @param stackTraceDistance determines which [StackTraceElement] will be used as the call's origin. Just fiddle with this number until it's correct.
* @since v1-alpha10
*/
@Suppress("JoinDeclarationAndAssignment")
fun log(level: Level, message: String, stackTraceDistance: Int = 0) {
fun log(level: Level, message: String, levelData: Map<String, Object?> = emptyMap(), stackTraceDistance: Int = 0) {
val origin: StackTraceElement
var call: Call
// Set 'origin'
try {
origin = Throwable().stackTrace[1 + stackTraceDistance]
} catch (exception: IndexOutOfBoundsException) {
} catch (_: IndexOutOfBoundsException) {
return
}
// Set 'call'
call = Call(origin, level, message, channel)
// Create 'Call' instance
var call: Call = Call(origin, level, message, channel)
// Run processing
if (Processor.check(call))
return
if (level == Level.CRASH)
CrashHandler.handle(call, levelData["throwable"] as Throwable?, levelData.getOrDefault("fatal", true) as Boolean)
else {
if (Processor.check(call))
return
if (LoggerConfiguration.threadingHandler?.queue(call) == null)
Processor.process(call)
if (LoggerConfiguration.threadingHandler?.queue(call) == null)
Processor.process(call)
}
}
/**
@ -127,7 +135,7 @@ class Logger {
* @since v1-alpha10
*/
fun diag(message: String) {
log(Level.DIAGNOSTIC, message, 1)
log(Level.DIAGNOSTIC, message, stackTraceDistance = 1)
}
/**
@ -137,7 +145,7 @@ class Logger {
* @since v1-alpha10
*/
fun verb(message: String) {
log(Level.VERBOSE, message, 1)
log(Level.VERBOSE, message, stackTraceDistance = 1)
}
/**
@ -147,7 +155,7 @@ class Logger {
* @since v1-alpha10
*/
fun sarn(message: String) {
log(Level.SILENT_WARNING, message, 1)
log(Level.SILENT_WARNING, message, stackTraceDistance = 1)
}
/**
@ -157,7 +165,7 @@ class Logger {
* @since v1-alpha10
*/
fun info(message: String) {
log(Level.INFORMATIONAL, message, 1)
log(Level.INFORMATIONAL, message, stackTraceDistance = 1)
}
/**
@ -167,7 +175,7 @@ class Logger {
* @since v1-alpha10
*/
fun warn(message: String) {
log(Level.WARNING, message, 1)
log(Level.WARNING, message, stackTraceDistance = 1)
}
/**
@ -177,17 +185,22 @@ class Logger {
* @since v1-alpha10
*/
fun error(message: String) {
log(Level.ERROR, message, 1)
log(Level.ERROR, message, stackTraceDistance = 1)
}
/**
* Logs a fatal error.
*
* @param message message
* @param error the error which caused the crash
* @param throwable the [Throwable] which caused the crash
* @param fatal terminates the engine and application if `true`
* @since v1-alpha10
*/
fun crash(message: String) {
log(Level.CRASH, message, 1)
fun crash(error: String, throwable: Throwable? = null, fatal: Boolean = true) {
log(Level.CRASH, error, levelData = mapOf(
Pair("throwable", throwable as Object?),
Pair("fatal", fatal as Object?)
), stackTraceDistance = 1)
}

View file

@ -20,10 +20,13 @@
package de.staropensource.engine.logging
import de.staropensource.engine.logging.implementable.CrashCategory
import de.staropensource.engine.logging.implementation.SOSLSv2FormatBuilder
import de.staropensource.engine.logging.type.ChannelSettings
import de.staropensource.engine.logging.type.Feature
import de.staropensource.engine.logging.implementable.FormatBuilder
import de.staropensource.engine.logging.implementable.ShutdownHandler
import de.staropensource.engine.logging.implementation.crashcategory.InfoCrashCategory
import de.staropensource.engine.logging.type.Level
import de.staropensource.engine.logging.type.OperationMode
import kotlinx.datetime.TimeZone
@ -79,32 +82,6 @@ class LoggerConfiguration private constructor() {
Feature.LINE_NUMBER,
)
/**
* Controls the [ThreadingHandler] to use.
*
* This determines how multithreading
* shall be performed. Set to `null` for
* a single-threaded logger.
*
* @see ThreadingHandler
* @since v1-alpha10
*/
@JvmStatic
val threadingHandler: ThreadingHandler? = null
/**
* Controls the [FormatBuilder] to use.
*
* This determines how formats are built
* and how the final log output looks like.
* Set to `null` to default to [SOSLSv2FormatBuilder].
*
* @see FormatBuilder
* @since v1-alpha10
*/
@JvmStatic
val formatBuilder: KClass<FormatBuilder>? = null
/**
* Controls how fast the logging thread
* shall wait until processing the log
@ -125,7 +102,57 @@ class LoggerConfiguration private constructor() {
*
* @since v1-alpha10
*/
val channelSettings: MutableMap<String, ChannelSettings> = mutableMapOf()
@JvmStatic
var channelSettings: MutableMap<String, ChannelSettings> = mutableMapOf()
/**
* Contains all registered [CrashCategory]s.
*
* @since v1-alpha10
*/
@JvmStatic
var crashCategories: LinkedHashSet<CrashCategory> = linkedSetOf(
InfoCrashCategory.instance
)
/**
* Controls the [ThreadingHandler] to use.
*
* This determines how multithreading
* shall be performed. Set to `null` for
* a single-threaded logger.
*
* @see ThreadingHandler
* @since v1-alpha10
*/
@JvmStatic
var threadingHandler: ThreadingHandler? = null
/**
* Controls the [FormatBuilder] to use.
*
* This determines how formats are built
* and how the final log output looks like.
* Set to `null` to default to [SOSLSv2FormatBuilder].
*
* @see FormatBuilder
* @since v1-alpha10
*/
@JvmStatic
var formatBuilder: KClass<FormatBuilder>? = null
/**
* Controls the [ShutdownHandler] to use.
*
* This determines how the
* application is shut down
* after crashing fatally.
*
* @see ShutdownHandler
* @since v1-alpha10
*/
@JvmStatic
var shutdownHandler: ShutdownHandler? = null
// -----> Feature settings

View file

@ -20,13 +20,13 @@
package de.staropensource.engine.logging
import de.staropensource.engine.logging.implementation.NoOperationFormatter
import de.staropensource.engine.logging.implementation.PrintlnAdapter
import de.staropensource.engine.logging.implementable.Adapter
import de.staropensource.engine.logging.implementable.FormatBuilder
import de.staropensource.engine.logging.implementable.Formatter
import de.staropensource.engine.logging.implementation.SOSLSv2FormatBuilder
import de.staropensource.engine.logging.type.Call
import de.staropensource.engine.logging.type.ChannelSettings
import de.staropensource.engine.logging.type.Feature
import de.staropensource.engine.logging.implementable.FormatBuilder
import de.staropensource.engine.logging.type.OperationMode
import kotlin.reflect.full.primaryConstructor
@ -51,6 +51,7 @@ class Processor private constructor() {
*
* Invoked by [Logger.log].
*
* @param call [Call] metadata
* @return terminate processing?
* @since v1-alpha10
*/
@ -81,6 +82,7 @@ class Processor private constructor() {
* Invoked by the configured
* [ThreadingHandler].
*
* @param call [Call] metadata
* @see LoggerConfiguration.threadingHandler
* @see ChannelSettings.formatter
* @see ChannelSettings.adapter
@ -91,7 +93,7 @@ class Processor private constructor() {
fun process(call: Call) {
val format: FormatBuilder
var formatFinalized: String = ""
val channelconf: ChannelSettings = LoggerConfiguration.channelSettings[call.channel] ?: ChannelSettings.global
val channelconf: ChannelSettings? = LoggerConfiguration.channelSettings[call.channel]
var message: String = call.message
// Set 'format'
@ -103,7 +105,7 @@ class Processor private constructor() {
}
// Stop if channel does not permit execution
if ((channelconf.enable ?: ChannelSettings.global.enable) == false)
if (!(ChannelSettings.getSetting(channelconf, "enable") as Boolean))
return
// Build format
@ -113,11 +115,11 @@ class Processor private constructor() {
// Update message
if (
(channelconf.sanitizeMessage ?: ChannelSettings.global.sanitizeMessage) != false
&& (channelconf.permitFormatting ?: ChannelSettings.global.permitFormatting) != false
ChannelSettings.getSetting(channelconf, "sanitizeMessage") as Boolean
&& ChannelSettings.getSetting(channelconf, "permitFormatting") as Boolean
) message = message.replace("<", "\\<")
if (
((channelconf.permitFormatting ?: ChannelSettings.global.permitFormatting) != false)
ChannelSettings.getSetting(channelconf, "permitFormatting") as Boolean
&& LoggerConfiguration.features.contains(Feature.FORMATTING)
) format.addFeature(Feature.FORMATTING)
@ -125,13 +127,13 @@ class Processor private constructor() {
format.message = message
// Format format
formatFinalized = if ((channelconf.permitFormatting ?: ChannelSettings.global.permitFormatting) != false)
(channelconf.formatter ?: ChannelSettings.global.formatter ?: NoOperationFormatter.instance).formatString(format.toString())
formatFinalized = if (ChannelSettings.getSetting(channelconf, "permitFormatting") as Boolean)
(ChannelSettings.getSetting(channelconf, "formatter") as Formatter).formatString(format.toString())
else
format.toString()
// Pass format to adapter
(channelconf.adapter ?: ChannelSettings.global.adapter ?: PrintlnAdapter.instance).handle(call, formatFinalized)
(ChannelSettings.getSetting(channelconf, "adapter") as Adapter).handle(call, formatFinalized)
}
}
}

View file

@ -0,0 +1,72 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
* with an exception allowing classpath linking.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.engine.logging.implementable
import de.staropensource.engine.logging.CrashHandler
import de.staropensource.engine.logging.type.Call
import de.staropensource.engine.logging.type.ChannelSettings
/**
* Used by the [CrashHandler] to
* print useful information.
*
* @since v1-alpha10
*/
interface CrashCategory {
/**
* Checks if this category
* shall be executed.
*
* @return execute?
* @since v1-alpha10
*/
fun check(): Boolean
/**
* Returns the name of this crash category.
*
* @return category name
* @since v1-alpha10
*/
fun getName(): String
/**
* Executes this crash category.
*
* The value of this map can either
* be a [String] or recurse downwards
* with `Map<String, Any>`. Any other
* values will be ignored.
*
* @param call [Call] metadata
* @param channelconf [ChannelSettings] instance or `null`. Use this to access [ChannelSettings.getSetting]
* @param throwable the [Throwable] which caused the crash
* @param fatal terminates the engine and application if `true`
* @return crash category items
* @since v1-alpha10
*/
fun execute(
call: Call,
channelconf: ChannelSettings?,
throwable: Throwable?,
fatal: Boolean,
): LinkedHashMap<String, Any>
}

View file

@ -0,0 +1,36 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
* with an exception allowing classpath linking.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.engine.logging.implementable
/**
* Handles shutdowns.
*
* @since v1-alpha10
*/
interface ShutdownHandler {
/**
* Shuts everything down.
*
* @param exitcode the code to exit with
* @since v1-alpha10
*/
fun exit(exitcode: Byte = 0)
}

View file

@ -0,0 +1,51 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
* with an exception allowing classpath linking.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.engine.logging.implementation
import de.staropensource.engine.logging.implementable.ShutdownHandler
import kotlin.system.exitProcess
/**
* [ShutdownHandler] implementation using
* Kotlin's [kotlin.system.exitProcess] method.
*
* @since v1-alpha10
*/
class KotlinShutdownHandler private constructor() : ShutdownHandler {
/**
* Companion object of [KotlinShutdownHandler].
*
* @since v1-alpha10
*/
companion object {
/**
* Global instance of [KotlinShutdownHandler].
*
* @since v1-alpha10
*/
@JvmStatic
val instance: KotlinShutdownHandler = KotlinShutdownHandler()
}
override fun exit(exitcode: Byte) {
exitProcess(exitcode.toInt())
}
}

View file

@ -0,0 +1,76 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
* with an exception allowing classpath linking.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.engine.logging.implementation.crashcategory
import de.staropensource.engine.logging.implementable.CrashCategory
import de.staropensource.engine.logging.type.Call
import de.staropensource.engine.logging.type.ChannelSettings
/**
* [CrashCategory] implementation
* providing insight into a crash.
*
* @since v1-alpha10
*/
class InfoCrashCategory private constructor() : CrashCategory {
/**
* Companion object of [InfoCrashCategory].
*
* @since v1-alpha10
*/
companion object {
/**
* Global instance of [InfoCrashCategory].
*
* @since v1-alpha10
*/
@JvmStatic
val instance: InfoCrashCategory = InfoCrashCategory()
}
override fun check(): Boolean {
return true
}
override fun getName(): String {
return "Crash"
}
override fun execute(
call: Call,
channelconf: ChannelSettings?,
throwable: Throwable?,
fatal: Boolean,
): LinkedHashMap<String, Any> {
return linkedMapOf(
Pair("Origin", linkedMapOf<String, Any>(
Pair("Class", call.origin.className),
Pair("Method", call.origin.methodName),
Pair("Line", call.origin.lineNumber),
Pair("Native", if (call.origin.isNativeMethod) "yes" else "false")
)),
Pair("Channel", call.channel),
Pair("Fatal", if (fatal) "yes" else "no"),
Pair("Message", call.message),
Pair("Stacktrace", throwable?.toString() ?: "Not available."), // TODO report correct stacktrace
)
}
}

View file

@ -0,0 +1,28 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Authors
* Licensed under the GNU Affero General Public License v3
* with an exception allowing classpath linking.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Implementations of the [CrashCategory] interface.
*
* @since v1-alpha10
*/
package de.staropensource.engine.logging.implementation.crashcategory
import de.staropensource.engine.logging.implementable.CrashCategory

View file

@ -12,16 +12,18 @@ import de.staropensource.engine.logging.implementation.PrintlnAdapter
* @param enable enables or disables all log messages utilising this channel. Defaults to the global value or `true` if set to `null`
* @param sanitizeMessage if message sanitization (escaping `<`) should be performed. Defaults to the global value or `true` if set to `null`
* @param permitFormatting if formatting shall be permitted for the entire format. For disallowing formatting for the message only, see [sanitizeMessage]. Defaults to the global value or `true` if set to `null`
* @param applicationName name of the application. Used in crash reports. Defaults to the global value or `"This application"` if `null`
* @param formatter determines how messages are formatted and stylized. Defaults to the global value or [NoOperationFormatter] if set to `null`
* @param adapter used for printing the finalized log format somewhere. Defaults to the global value [PrintlnAdapter] if set to `null`
* @since v1-alpha10
*/
data class ChannelSettings(
val enable: Boolean? = null,
val sanitizeMessage: Boolean? = null,
val permitFormatting: Boolean? = null,
val formatter: Formatter? = null,
val adapter: Adapter? = null,
private val enable: Boolean? = null,
private val sanitizeMessage: Boolean? = null,
private val permitFormatting: Boolean? = null,
private val applicationName: String? = null,
private val formatter: Formatter? = null,
private val adapter: Adapter? = null,
) {
/**
* Companion object of [ChannelSettings].
@ -47,11 +49,34 @@ data class ChannelSettings(
*/
@JvmStatic
val global: ChannelSettings = ChannelSettings(
true,
true,
true,
null,
null
enable = null,
sanitizeMessage = null,
permitFormatting = null,
applicationName = null,
formatter = null,
adapter = null,
)
/**
* Returns a setting's value.
*
* @param settings [ChannelSettings] instance to access. Set to `null` to only access the global settings
* @param setting setting to get
* @return setting value or `null`
* @since v1-alpha10
*/
@JvmStatic
@Suppress("CyclomaticComplexMethod")
fun getSetting(settings: ChannelSettings?, setting: String): Any? {
return when (setting) {
"enable" -> (settings?.enable ?: global.enable) != false
"sanitizeMessage" -> (settings?.sanitizeMessage ?: global.sanitizeMessage) != false
"permitFormatting" -> (settings?.permitFormatting ?: global.permitFormatting) != false
"applicationName" -> settings?.applicationName ?: global.applicationName ?: "This application"
"formatter" -> settings?.formatter ?: global.formatter ?: NoOperationFormatter.instance
"adapter" -> settings?.adapter ?: global.adapter ?: PrintlnAdapter.instance
else -> null
}
}
}
}