From 43317d4f929eb0f3acf91b8dea3eb3143023fe38 Mon Sep 17 00:00:00 2001 From: cfig Date: Wed, 2 Jul 2025 18:38:15 +0800 Subject: [PATCH] chore: minor fixes on boot and lazybox --- .../src/main/kotlin/packable/IPackable.kt | 4 +- .../src/main/kotlin/packable/MiscImgParser.kt | 3 +- lazybox/src/main/kotlin/cfig/lazybox/App.kt | 19 ++ .../main/kotlin/cfig/lazybox/DmaInfoParser.kt | 155 ++++++++++++ .../kotlin/cfig/lazybox/staging/DiffCI.kt | 236 ++++++++++++++++++ 5 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 lazybox/src/main/kotlin/cfig/lazybox/DmaInfoParser.kt create mode 100644 lazybox/src/main/kotlin/cfig/lazybox/staging/DiffCI.kt diff --git a/bbootimg/src/main/kotlin/packable/IPackable.kt b/bbootimg/src/main/kotlin/packable/IPackable.kt index e9d9143..8306712 100644 --- a/bbootimg/src/main/kotlin/packable/IPackable.kt +++ b/bbootimg/src/main/kotlin/packable/IPackable.kt @@ -41,7 +41,7 @@ interface IPackable { "adb root".check_call() val abUpdateProp = "adb shell getprop ro.build.ab_update".check_output() log.info("ro.build.ab_update=$abUpdateProp") - val slotSuffix = if (abUpdateProp == "true" && !fileName.startsWith("misc.img")) { + val slotSuffix = if (abUpdateProp == "true" && !fileName.contains("misc.img")) { "adb shell getprop ro.boot.slot_suffix".check_output() } else { "" @@ -56,7 +56,7 @@ interface IPackable { "adb root".check_call() val abUpdateProp = "adb shell getprop ro.build.ab_update".check_output() log.info("ro.build.ab_update=$abUpdateProp") - val slotSuffix = if (abUpdateProp == "true" && !fileName.startsWith("misc.img")) { + val slotSuffix = if (abUpdateProp == "true" && !fileName.endsWith("misc.img")) { "adb shell getprop ro.boot.slot_suffix".check_output() } else { "" diff --git a/bbootimg/src/main/kotlin/packable/MiscImgParser.kt b/bbootimg/src/main/kotlin/packable/MiscImgParser.kt index 3753f98..066403b 100644 --- a/bbootimg/src/main/kotlin/packable/MiscImgParser.kt +++ b/bbootimg/src/main/kotlin/packable/MiscImgParser.kt @@ -65,8 +65,7 @@ class MiscImgParser : IPackable { } fun flash(fileName: String) { - val stem = fileName.substring(0, fileName.indexOf(".")) - super.flash("$fileName.new", stem) + super.flash("$fileName.new", File(fileName).nameWithoutExtension) } override fun `@verify`(fileName: String) { diff --git a/lazybox/src/main/kotlin/cfig/lazybox/App.kt b/lazybox/src/main/kotlin/cfig/lazybox/App.kt index 7bdc38d..45853df 100644 --- a/lazybox/src/main/kotlin/cfig/lazybox/App.kt +++ b/lazybox/src/main/kotlin/cfig/lazybox/App.kt @@ -1,5 +1,6 @@ package cfig.lazybox +import cfig.lazybox.staging.DiffCI import cfig.lazybox.sysinfo.BootChart import cfig.lazybox.sysinfo.CpuInfo import cfig.lazybox.sysinfo.Pidstat @@ -26,6 +27,8 @@ fun main(args: Array) { println("sysinfo : get overall system info from Android") println("\nIncubating usage:") println("apps : get apk file list from Android") + println("dmainfo : parse /d/dma_buf/bufinfo") + println("diffci : find changelist files from CI server based on date and time ranges") exitProcess(0) } if (args[0] == "cpuinfo") { @@ -83,4 +86,20 @@ fun main(args: Array) { //BootingParser.run() BootingParser.run2() } + if (args[0] == "dmainfo") { + if (args.size != 2) { + log.error("Usage: dmainfo ") + return + } + val dmainfoFile = args[1] + if (File(dmainfoFile).exists()) { + val dmaInfoParser = DmaInfoParser() + val dmaInfo = dmaInfoParser.parse(args.drop(1).toTypedArray()) + } else { + log.error("File not found: $dmainfoFile") + } + } + if (args[0] == "diffci") { + DiffCI().run(args.drop(1).toTypedArray()) + } } diff --git a/lazybox/src/main/kotlin/cfig/lazybox/DmaInfoParser.kt b/lazybox/src/main/kotlin/cfig/lazybox/DmaInfoParser.kt new file mode 100644 index 0000000..ba33345 --- /dev/null +++ b/lazybox/src/main/kotlin/cfig/lazybox/DmaInfoParser.kt @@ -0,0 +1,155 @@ +package cfig.lazybox + +import com.fasterxml.jackson.databind.ObjectMapper +import org.slf4j.LoggerFactory +import java.io.File +import java.io.FileNotFoundException + +data class DmaBufInfo( + val size: Long, + val flags: String, + val mode: String, + val count: Int, + val exp_name: String, + val ino: String, + val pid: Int?, + val tids: List, + val processName: String?, + val attachedDevices: List +) + +class DmaInfoParser { + + companion object { + private val log = LoggerFactory.getLogger(DmaInfoParser::class.java) + } + + fun parse(args: Array) { + if (args.isEmpty()) { + log.error("Usage: Provide the path to the dmainfo file as an argument.") + return + } + + val filePath = args[0] + log.info("Parsing file: {}", filePath) + + try { + val dmaInfoList = parseFile(filePath) + + if (dmaInfoList.isNotEmpty()) { + val mapper = ObjectMapper() + val writer = mapper.writerWithDefaultPrettyPrinter() + dmaInfoList.forEach { info -> + log.info("Parsed object:\n{}", writer.writeValueAsString(info)) + } + log.info("--------------------------------------------------") + log.info("Successfully parsed {} DMA buffer objects.", dmaInfoList.size) + } else { + log.warn("No valid DMA buffer objects were found in the file.") + } + } catch (e: FileNotFoundException) { + log.error("File operation failed: {}", e.message) + } catch (e: Exception) { + log.error("An unexpected error occurred during parsing.", e) + } + } + + /** + * Reads and parses a dmainfo file from the given path. + * + * @param filePath The path to the dmainfo file. + * @return A list of [DmaBufInfo] objects, one for each entry in the file. + * @throws FileNotFoundException if the file does not exist. + */ + private fun parseFile(filePath: String): List { + val file = File(filePath) + if (!file.exists()) { + throw FileNotFoundException("Error: File not found at '$filePath'") + } + + val allLines = file.readLines() + + val firstDataLineIndex = allLines.indexOfFirst { line -> + line.trim().matches(Regex("""^[0-9a-fA-F]{8}\s+.*""")) + } + + if (firstDataLineIndex == -1) { + log.warn("No data lines found in the file.") + return emptyList() + } + + val content = allLines.subList(firstDataLineIndex, allLines.size).joinToString("\n") + + val blocks = content.split(Regex("(\\r?\\n){2,}")) + .map { it.trim() } + .filter { it.isNotEmpty() } + + return blocks.mapNotNull { parseBlock(it) } + } + + /** + * Parses a single block of text representing one DMA buffer object. + */ + private fun parseBlock(block: String): DmaBufInfo? { + val lines = block.lines().filter { it.isNotBlank() } + if (lines.isEmpty()) return null + + val mainLine = lines.first() + val mainLineRegex = Regex("""^(\w+)\s+(\w+)\s+(\w+)\s+(\d+)\s+([\w-]+)\s+(\w+)\s*(.*)$""") + val match = mainLineRegex.find(mainLine) + if (match == null) { + log.warn("Skipping malformed line that doesn't match expected format: \"{}\"", mainLine) + return null + } + + val (sizeStr, flagsStr, modeStr, countStr, expName, ino, processStr) = match.destructured + + var pid: Int? = null + val tids = mutableListOf() + var processName: String? = null + + if (processStr.isNotBlank()) { + val processParts = processStr.trim().split(Regex("\\s+")) + val nameParts = mutableListOf() + var pidFound = false + + processParts.forEach { part -> + val num = part.toIntOrNull() + if (num != null) { + if (!pidFound) { + pid = num + pidFound = true + } else { + tids.add(num) + } + } else { + nameParts.add(part) + } + } + + if (nameParts.isNotEmpty()) { + processName = nameParts.joinToString(" ") + } + } + + val attachedDevices = lines.drop(1) + .dropWhile { !it.trim().equals("Attached Devices:", ignoreCase = true) } + .drop(1) + .map { it.trim() } + .takeWhile { !it.trim().startsWith("Total", ignoreCase = true) } + .filter { it.isNotEmpty() } + + return DmaBufInfo( + size = sizeStr.toLongOrNull() ?: 0L, + flags = "0x$flagsStr", + mode = "0x$modeStr", + count = countStr.toIntOrNull() ?: 0, + exp_name = expName, + ino = ino, + pid = pid, + tids = tids, + processName = processName, + attachedDevices = attachedDevices + ) + } +} diff --git a/lazybox/src/main/kotlin/cfig/lazybox/staging/DiffCI.kt b/lazybox/src/main/kotlin/cfig/lazybox/staging/DiffCI.kt new file mode 100644 index 0000000..4bfef42 --- /dev/null +++ b/lazybox/src/main/kotlin/cfig/lazybox/staging/DiffCI.kt @@ -0,0 +1,236 @@ +package cfig.lazybox.staging + +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.Duration +import java.time.format.DateTimeParseException +import kotlin.system.exitProcess + +class DiffCI { + private val FAKE_URL = "FAKE_URL" + private val baseUrl = System.getenv("diffCIbaseUrl") ?: FAKE_URL + private val finder = ChangelistFinder(baseUrl) + private val dateFormat = DateTimeFormatter.ofPattern("yyyyMMdd") + private val dateTimeFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm") + private val buildIdRegex = ".*/(\\d{12})/changelist".toRegex() + private val log = LoggerFactory.getLogger(DiffCI::class.java) + + fun run(args: Array) { + if (baseUrl == FAKE_URL) { + log.info("Error: Environment variable 'diffCIbaseUrl' is not set.") + log.info("Please set it to the base URL of the changelist directory.") + exitProcess(1) + } + when (args.size) { + 1 -> handleSingleDate(args) + 2 -> handleRange(args) + else -> printUsageAndExit() + } + } + + private fun handleSingleDate(args: Array) { + if (args[0].length != 8) { + log.info("Error: For a single argument, please provide a date in yyyyMMdd format.") + exitProcess(1) + } + val targetDate = parseDate(args[0]) ?: exitProcess(1) + + log.info("=============================================") + log.info("Searching for changelists on: ${args[0]}") + log.info("=============================================") + + val urls = finder.findChangelistUrlsForDate(targetDate) + printResults(urls) + } + + private fun handleRange(args: Array) { + val arg1 = args[0] + val arg2 = args[1] + + if (arg1.length != arg2.length) { + log.info("Error: Start and end arguments must be of the same format (both dates or both date-times).") + exitProcess(1) + } + + when (arg1.length) { + 8 -> handleDateRange(arg1, arg2) + 12 -> handleDateTimeRange(arg1, arg2) + else -> { + log.info("Error: Invalid argument format. Please use yyyyMMdd or yyyyMMddHHmm.") + exitProcess(1) + } + } + } + + private fun handleDateRange(startDateStr: String, endDateStr: String) { + val startDate = parseDate(startDateStr) ?: exitProcess(1) + val endDate = parseDate(endDateStr) ?: exitProcess(1) + + if (startDate.isAfter(endDate)) { + log.info("Error: The start date ($startDateStr) must be before or the same as the end date ($endDateStr).") + exitProcess(1) + } + + log.info("=============================================") + log.info("Searching for changelists from $startDateStr to $endDateStr") + log.info("=============================================") + + val allUrls = mutableListOf() + var currentDate = startDate + while (currentDate <= endDate) { + log.info("\n----- Processing Date: $currentDate -----") + val dailyUrls = finder.findChangelistUrlsForDate(currentDate) + allUrls.addAll(dailyUrls) + currentDate = currentDate.plusDays(1) + } + printResults(allUrls) + } + + private fun handleDateTimeRange(startDateTimeStr: String, endDateTimeStr: String) { + val startDateTime = parseDateTime(startDateTimeStr) ?: exitProcess(1) + val endDateTime = parseDateTime(endDateTimeStr) ?: exitProcess(1) + + if (startDateTime.isAfter(endDateTime)) { + log.info("Error: The start time ($startDateTimeStr) must be before or the same as the end time ($endDateTimeStr).") + exitProcess(1) + } + + log.info("=============================================") + log.info("Searching for changelists between $startDateTimeStr and $endDateTimeStr") + log.info("=============================================") + + val allUrls = mutableListOf() + var currentDate = startDateTime.toLocalDate() + while (currentDate <= endDateTime.toLocalDate()) { + log.info("\n----- Processing Date: $currentDate -----") + val dailyUrls = finder.findChangelistUrlsForDate(currentDate) + + val filteredUrls = dailyUrls.filter { url -> + val matchResult = buildIdRegex.find(url) + if (matchResult != null) { + val buildId = matchResult.groupValues[1] + val buildDateTime = parseDateTime(buildId) + buildDateTime != null && !buildDateTime.isBefore(startDateTime) && !buildDateTime.isAfter(endDateTime) + } else { + false + } + } + allUrls.addAll(filteredUrls) + currentDate = currentDate.plusDays(1) + } + printResults(allUrls, isTimeRange = true) + } + + private fun printUsageAndExit() { + log.info("Error: Invalid number of arguments.") + log.info("\nUsage:") + log.info(" Single Date: --args='diffci '") + log.info(" Date Range: --args='diffci '") + log.info(" Time Range: --args='diffci '") + log.info("\nExamples:") + log.info(" --args='diffci 20250628'") + log.info(" --args='diffci 20250627 20250628'") + log.info(" --args='diffci 202506281000 202506281430'") + exitProcess(1) + } + + private fun printResults(urls: List, isTimeRange: Boolean = false) { + log.info("\n--- Results ---") + if (urls.isNotEmpty()) { + log.info("Successfully found ${urls.size} changelist files:") + urls.forEach { log.info(it) } + } else { + val rangeType = if (isTimeRange) "time range" else "date(s)" + log.info("No changelist files were found for the specified $rangeType.") + } + log.info("---------------") + } + + private fun parseDate(dateStr: String): LocalDate? { + return try { + LocalDate.parse(dateStr, dateFormat) + } catch (e: DateTimeParseException) { + log.info("Error: Invalid date format for '$dateStr'.") + log.info("Please use the yyyyMMdd format (e.g., 20250628).") + null + } + } + + private fun parseDateTime(dateTimeStr: String): LocalDateTime? { + return try { + LocalDateTime.parse(dateTimeStr, dateTimeFormat) + } catch (e: DateTimeParseException) { + log.info("Error: Invalid date-time format for '$dateTimeStr'.") + log.info("Please use the yyyyMMddHHmm format (e.g., 202506281027).") + null + } + } + + /** + * Inner class to handle the web scraping logic. + */ + private inner class ChangelistFinder(private val baseUrl: String) { + private val client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(10)) + .build() + + fun findChangelistUrlsForDate(date: LocalDate): List { + val yearMonthPattern = DateTimeFormatter.ofPattern("yyyyMM") + val yearMonthDayPattern = DateTimeFormatter.ofPattern("yyyyMMdd") + val yearMonthStr = date.format(yearMonthPattern) + val yearMonthDayStr = date.format(yearMonthDayPattern) + val directoryUrl = "$baseUrl/$yearMonthStr/$yearMonthDayStr/" + + return try { + val htmlContent = fetchUrlContent(directoryUrl) + if (htmlContent != null) { + parseDirectoryHtmlWithRegex(htmlContent, directoryUrl, yearMonthDayStr) + } else { + emptyList() + } + } catch (e: IOException) { + log.info("An error occurred during the network request: ${e.message}") + emptyList() + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + log.info("The network request was interrupted: ${e.message}") + emptyList() + } + } + + private fun fetchUrlContent(url: String): String? { + try { + val request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build() + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() != 200) { + if (response.statusCode() != 404) { + log.info("Request for $url failed with code: ${response.statusCode()}") + } + return null + } + return response.body() + } catch (e: Exception) { + log.info("Exception while fetching URL '$url': ${e.message}") + return null + } + } + + private fun parseDirectoryHtmlWithRegex(html: String, directoryUrl: String, datePrefix: String): List { + val regex = "href=\"($datePrefix\\d+)/\"".toRegex() + return regex.findAll(html) + .map { matchResult -> + val dirName = matchResult.groupValues[1] + "$directoryUrl$dirName/changelist" + } + .toList() + } + } +}