diff --git a/src/commonMain/kotlin/file.kt b/src/commonMain/kotlin/file.kt index 57df7b2..de5f50f 100644 --- a/src/commonMain/kotlin/file.kt +++ b/src/commonMain/kotlin/file.kt @@ -16,6 +16,7 @@ expect class File(rawPath: String) { val length : Long fun ls(): List fun lines(): List + fun write(s: String) fun mv(dest:File): Boolean fun rm() : Boolean fun rmdir() : Boolean diff --git a/src/commonMain/kotlin/logging.kt b/src/commonMain/kotlin/logging.kt index ba3d4cf..7f7d34c 100644 --- a/src/commonMain/kotlin/logging.kt +++ b/src/commonMain/kotlin/logging.kt @@ -5,6 +5,9 @@ import platform.posix.exit private const val EXIT_CODE_ON_FAIL = 20 var debugMode = getenv("DEBUG") !in listOf(null, "", "0", "false", "FALSE") +var logFilePath = getenv("JAUNCH_LOGFILE") ?: "jaunch.log" + +private var logFile: File? = null fun debug(vararg args: Any) { if (debugMode) report("DEBUG", *args) } @@ -37,8 +40,18 @@ fun fail(message: String): Nothing { } private fun report(prefix: String, vararg args: Any) { - printlnErr(buildString { + val s = buildString { append("[$prefix] ") args.forEach { append(it) } - }) + } + printlnErr(s) + if (debugMode) { + val log = logFile ?: File(logFilePath) + if (logFile == null) { + // Overwrite any log file from previous run. + if (log.exists) log.rm() + logFile = log + } + log.write("$s$NL") + } } diff --git a/src/commonMain/kotlin/main.kt b/src/commonMain/kotlin/main.kt index 3f7e0c5..69a4a60 100644 --- a/src/commonMain/kotlin/main.kt +++ b/src/commonMain/kotlin/main.kt @@ -109,8 +109,9 @@ fun main(args: Array) { } // Finally, execute all the directives! \^_^/ - executeDirectives(nonGlobalDirectives, launchDirectives, runtimes, userArgs, - argsInContext) + executeDirectives(nonGlobalDirectives, launchDirectives, runtimes, argsInContext) + + debugBanner("JAUNCH CONFIGURATION COMPLETE") } // -- Program flow functions -- @@ -128,6 +129,11 @@ private fun parseArguments(args: Array): Pair> { // Enable debug mode when --debug flag is present. debugMode = inputArgs.contains("--debug") + // Note: We need to let parseArguments set the debugMode + // flag in response to the --debug argument being passed. + // So we wait until now to emit this initial debugging bookend message. + debugBanner("PROCEEDING WITH JAUNCH CONFIGURATION") + debug("executable -> ", executable ?: "") debug("inputArgs -> ", inputArgs) @@ -531,7 +537,6 @@ private fun executeDirectives( configDirectives: List, launchDirectives: List, runtimes: List, - userArgs: ProgramArgs, argsInContext: Map ) { debugBanner("EXECUTING DIRECTIVES") diff --git a/src/posixMain/kotlin/file.kt b/src/posixMain/kotlin/file.kt index b535f0b..c425d23 100644 --- a/src/posixMain/kotlin/file.kt +++ b/src/posixMain/kotlin/file.kt @@ -34,7 +34,7 @@ actual class File actual constructor(private val rawPath: String) { actual fun ls(): List { if (!isDirectory) throw IllegalArgumentException("Not a directory: $path") - val directory = opendir(path) ?: throw IllegalArgumentException("Failed to open directory") + val directory = opendir(path) ?: throw IllegalArgumentException("Failed to open directory: $path") val files = mutableListOf() try { @@ -55,7 +55,7 @@ actual class File actual constructor(private val rawPath: String) { actual fun lines(): List { val lines = mutableListOf() memScoped { - val file = fopen(path, "r") ?: throw RuntimeException("Failed to open file") + val file = fopen(path, "r") ?: throw RuntimeException("Failed to open file: $this") try { while (true) { val buffer = ByteArray(BUFFER_SIZE) @@ -70,8 +70,16 @@ actual class File actual constructor(private val rawPath: String) { return lines } - override fun toString(): String { - return path + @OptIn(ExperimentalForeignApi::class) + actual fun write(s: String) { + val file = fopen(path, "a") ?: + throw RuntimeException("Failed to open file: $this") + try { + fputs(s, file) + } + finally { + fclose(file) + } } @OptIn(ExperimentalForeignApi::class) @@ -94,6 +102,10 @@ actual class File actual constructor(private val rawPath: String) { return rmdir(path) == 0 } } + + override fun toString(): String { + return path + } } @OptIn(ExperimentalForeignApi::class) diff --git a/src/windowsMain/kotlin/file.kt b/src/windowsMain/kotlin/file.kt index 044fc46..bf874fd 100644 --- a/src/windowsMain/kotlin/file.kt +++ b/src/windowsMain/kotlin/file.kt @@ -19,7 +19,9 @@ actual class File actual constructor(private val rawPath: String) { actual val isRoot: Boolean = // Is it a drive letter plus backslash (e.g. `C:\`)? - path.length == 3 && (path[0] in 'a'..'z' || path[0] in 'A'..'Z') && path[1] == ':' && path[2] == '\\' + path.length == 3 && + (path[0] in 'a'..'z' || path[0] in 'A'..'Z') && + path[1] == ':' && path[2] == '\\' @OptIn(ExperimentalForeignApi::class) actual val length: Long get() { @@ -82,7 +84,7 @@ actual class File actual constructor(private val rawPath: String) { content.append(buffer.decodeToString(0, readCount)); } } else { - printlnErr("Error reading file: ${GetLastError()}") + printlnErr("Error reading file '$this': ${lastError()}") } } } while (bytesRead.value > 0U) @@ -94,8 +96,33 @@ actual class File actual constructor(private val rawPath: String) { return lines } - override fun toString(): String { - return path + @OptIn(ExperimentalForeignApi::class) + actual fun write(s: String) { + val handle = openFile(path, write = true) ?: + throw RuntimeException("Failed to open file: $this") + try { + memScoped { + val bytes = s.encodeToByteArray() + bytes.usePinned { pinnedBytes -> + val bytesWritten = alloc() + + val result = WriteFile( + handle, + pinnedBytes.addressOf(0).reinterpret(), + bytes.size.toUInt(), + bytesWritten.ptr, + null + ) + + if (result == 0) { // 0 indicates failure in Windows + throw RuntimeException("Error writing to file '$this': ${lastError()}") + } + } + } + } + finally { + CloseHandle(handle) + } } @OptIn(ExperimentalForeignApi::class) @@ -122,25 +149,29 @@ actual class File actual constructor(private val rawPath: String) { } } + override fun toString(): String { + return path + } + private fun isMode(modeBits: Int): Boolean { val attrs = GetFileAttributesA(path) return attrs != INVALID_FILE_ATTRIBUTES && (attrs.toInt() and modeBits) != 0 } @OptIn(ExperimentalForeignApi::class) - private fun openFile(path: String): HANDLE? { + private fun openFile(path: String, write: Boolean = false): HANDLE? { val fileHandle = CreateFileW( path, - GENERIC_READ, - FILE_SHARE_READ.toUInt(), + if (write) FILE_APPEND_DATA.toUInt() else GENERIC_READ, + if (write) FILE_SHARE_WRITE.toUInt() else FILE_SHARE_READ.toUInt(), null, - OPEN_EXISTING.toUInt(), + if (write) OPEN_ALWAYS.toUInt() else OPEN_EXISTING.toUInt(), FILE_ATTRIBUTE_NORMAL.toUInt(), null ) if (fileHandle == INVALID_HANDLE_VALUE) { - warn("Error opening file: ${GetLastError()}") + warn("Error opening file '$this': ${lastError()}") return null } return fileHandle @@ -150,7 +181,7 @@ actual class File actual constructor(private val rawPath: String) { private fun fileSize(fileHandle: HANDLE): Long? = memScoped { val fileSize = alloc() if (GetFileSizeEx(fileHandle, fileSize.ptr) == 0) { - warn("Error getting file size: ${GetLastError()}") + warn("Error getting size of file '$this': ${lastError()}") return null } return fileSize.QuadPart @@ -173,7 +204,31 @@ private fun canonicalize(path: String): String { memScoped { val buffer = allocArray(bufferLength) val fullPathLength = GetFullPathName!!(p.wcstr.ptr, bufferLength.toUInt(), buffer, null) - if (fullPathLength == 0u) throw RuntimeException("Failed to get full path: ${GetLastError()}") + if (fullPathLength == 0u) throw RuntimeException("Failed to get full path of '$path': ${lastError()}") return buffer.toKString() } } + +/** Converts Windows numeric error codes to human-friendly strings. */ +@OptIn(ExperimentalForeignApi::class) +private fun lastError(): String { + memScoped { + val errorCode = GetLastError() + val buffer = alloc>() + + val messageLength = FormatMessageW( + (FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS).toUInt(), + null, + errorCode, + 0.toUInt(), // Default language + buffer.ptr.reinterpret(), + 0.toUInt(), + null + ) + val errorMessage = if (messageLength > 0U) buffer.value!!.toKString().trim() else "Unknown error" + + if (messageLength > 0U) LocalFree(buffer.value) + + return errorMessage; + } +}