Skip to content

Instantly share code, notes, and snippets.

@exaV
Last active April 28, 2020 10:08
Show Gist options
  • Save exaV/31bc6713f372e0a58655ef929cfb8e6f to your computer and use it in GitHub Desktop.
Save exaV/31bc6713f372e0a58655ef929cfb8e6f to your computer and use it in GitHub Desktop.
screeps stack trace resolver

Stack trace resolver for screeps

If you run this from the builtin terminal in IntellJ you will get clickable links to the source.

Usage

kotlin resolver.main.kts -- -h

You need the -- to separate the arguments to kotlin from the arguments to the script.

Full example:

./resolve.main.kts
> Name: screeps-kotlin-starter
> ProjectDir: ~/screeps-kotlin-starter
> enter a stacktrace (reading until EOF or empty line):

          IllegalArgumentException: oh no we crashed!
            at IllegalArgumentException_init_0 (kotlin:3551:32)
            at ConstructionOperation.run (main:396:11)
            at MiningOperation.run (main:317:82)
            at manageCity (main:275:57)
            at runEconomy (main:244:10)
            at Object.loop (main:49:5)
            at __mainLoop:1:52
            at __mainLoop:2:3
            at Object.exports.evalCode (<runtime>:15584:76)


Installation:

curl https://gist.githubusercontent.com/exaV/31bc6713f372e0a58655ef929cfb8e6f/raw/resolver.main.kts > resolver.main.kts

You need to install the kotlin executable version 1.3.70 or newer (check with kotlin -help) e.g. brew install kotlin or sdk install kotlin

#!/usr/bin/env kotlin
@file:Repository("https://maven.atlassian.com/content/repositories/atlassian-public")
@file:DependsOn("com.atlassian.sourcemap:sourcemap:1.7.7")
@file:DependsOn("com.github.ajalt:clikt:2.6.0")
import com.atlassian.sourcemap.Mapping
import com.atlassian.sourcemap.SourceMap
import com.atlassian.sourcemap.SourceMapImpl
import com.atlassian.sourcemap.Util
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.prompt
import com.github.ajalt.clikt.parameters.types.path
import java.io.File
import java.nio.file.Path
data class ProjectLayout(
val mainModuleName: String,
val sourceMapDirectory: String,
val sourceDirectory: String,
val buildDirectory: String,
val jsDirectory: String
)
data class StackFrame(
val module: String,
val symbol: String,
val line: Int,
val column: Int
)
class RunResolver : CliktCommand() {
val name: String by option(
"--name",
"-n",
help = "The name of the file published as main.js in screeps. It is usually the name of the project in settings.gradle"
).prompt()
val projectDirectory: Path by option("--projectDirectory", "-d", help = "Project directory").path(mustExist = true)
.prompt()
val debug: Boolean by option("--debug").flag()
val quiet: Boolean by option("-q", "--quiet", help = "non interactive.").flag(default = false)
val stacktrace: String? by option(help = "stacktrace")
companion object {
const val SOURCE_MAP = ".js.map"
}
fun calculatePosition(rawSourceMap: SourceMap, stackFrame: StackFrame): Mapping? {
return calculatePosition(rawSourceMap, stackFrame.line, stackFrame.column)
}
fun calculatePosition(rawSourceMap: SourceMap, line: Int, column: Int): Mapping? {
return rawSourceMap.getMapping(line, column)
}
fun resolveSourceFile(source: String, projectLayout: ProjectLayout): String {
if (source.contains("/src")) {
return projectLayout.sourceDirectory + source.substringAfter("/src")
}
if (source.contains("js/packages/")) {
// TODO it is a library
return source
}
return source
}
fun loadSourceMap(path: String?, offset: Int = 1): SourceMap? {
if (path == null) {
return null
}
val file = File(path)
if (!file.exists()) {
return null
}
val sourceMap = SourceMapImpl(file.readText())
if (offset == 0) {
return sourceMap
} else {
return Util.offset(sourceMap, offset)
}
}
fun debug(string: String? = null) {
if (debug) {
println(string)
}
}
fun resolveModulePath(
moduleName: String,
projectLayout: ProjectLayout
): String? {
val effectiveModuleName = when {
moduleName == "main" -> projectLayout.mainModuleName
moduleName.startsWith("__") -> projectLayout.mainModuleName
moduleName == "<runtime>" -> null
else -> moduleName
} ?: return null
return projectLayout.sourceMapDirectory + "/" + effectiveModuleName + SOURCE_MAP
}
fun recoverStacktrace(stacktrace: String, projectLayout: ProjectLayout): String {
fun parseLine(line: String): StackFrame {
// ...
// at ConstructionOperation.run (main:391:68)
// at __mainLoop:1:52
// ...
var remaining = line.trim().removePrefix("at ")
val symbolName = if (remaining.first() == '_') "" else remaining.substringBefore(" ")
remaining = remaining.removePrefix(symbolName).trim()
// remaining = "__mainLoop:1:52" or "(main:391:68)"
remaining = remaining.removePrefix("(").removeSuffix(")")
// remaining = "__mainLoop:1:52" or "main:391:68"
val (module, lineNo, columNo) = remaining.split(":")
return StackFrame(module, symbol = symbolName, line = lineNo.toInt(), column = columNo.toInt())
}
val sourceMaps: MutableMap<String?, SourceMap?> = mutableMapOf()
return stacktrace.lines().joinToString(System.lineSeparator()) { line ->
val canParse = line.trim().startsWith("at ")
if (!canParse) {
return@joinToString line
}
val frame = parseLine(line)
val path = resolveModulePath(frame.module, projectLayout)
val sourceMap: SourceMap? = sourceMaps.getOrPut(path) {
debug("loading module '${frame.module}' from $path")
loadSourceMap(path)
}
if (sourceMap == null) {
debug("skipping $frame because does there is no source map for the module")
return@joinToString line
}
var position = calculatePosition(sourceMap, frame)
if (position == null) {
calculatePosition(sourceMap, frame.copy(line = 1, column = 1))
}
debug("stackframe=${frame}, position = ${position}, sourcesymbol=${position?.sourceSymbolName}")
val sourceFile = resolveSourceFile(position?.sourceFileName ?: "", projectLayout)
// needs to be of the form
// <projectDir>/src/test/kotlin/Stacktrace.kt: (<line>, <column>):
val ideaClickableLink = sourceFile + ": (${position?.sourceLine}, ${position?.sourceColumn}):"
line.substringBeforeLast(" (") + " " + ideaClickableLink
}
}
override fun run() {
val projectLayout = ProjectLayout(
name,
"${projectDirectory}/build/minified-js",
"$projectDirectory/src",
"$projectDirectory/build",
"$projectDirectory/build/js"
)
debug()
debug("using $projectLayout")
val sourceMapDir = File(projectLayout.sourceMapDirectory)
if (!sourceMapDir.isDirectory || sourceMapDir.list()!!.none { it.endsWith(SOURCE_MAP) }) {
error("no source maps found in $sourceMapDir")
}
var input = stacktrace
if (input == null) {
if (!quiet) {
println("enter a stacktrace (reading until EOF or empty line):")
}
val buffer = StringBuffer()
do {
val line = readLine()?.also {
buffer.appendln(it)
}
} while (!line.isNullOrBlank())
input = buffer.toString()
}
debug("translating stacktrace...")
debug()
val translation = recoverStacktrace(input.trimIndent(), projectLayout)
println(translation)
}
}
RunResolver().main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment