From 5ced3727ddc185d979b877ad62829b77ba2ca776 Mon Sep 17 00:00:00 2001 From: Nikolay Chashnikov Date: Fri, 12 Apr 2024 13:43:01 +0200 Subject: [PATCH] [run configuration] fix locating log files by pattern in subdirectories (IJPL-148359) Before patterns were supported in the file name part only, now they are supported in directory names as well. GitOrigin-RevId: 607e921695b6c84d3503f1c2c921beaf0ad61d80 --- .../execution-impl/api-dump-unreviewed.txt | 1 + .../diagnostic/logging/LogFilesCollector.kt | 93 +++++++++++++++++++ .../diagnostic/logging/LogFilesManager.java | 5 +- .../logging/LogFilesCollectorTest.kt | 64 +++++++++++++ platform/execution/api-dump-unreviewed.txt | 3 - .../configurations/LogFileOptions.kt | 50 ---------- 6 files changed, 161 insertions(+), 55 deletions(-) create mode 100644 platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesCollector.kt create mode 100644 platform/execution-impl/testSources/com/intellij/diagnostic/logging/LogFilesCollectorTest.kt diff --git a/platform/execution-impl/api-dump-unreviewed.txt b/platform/execution-impl/api-dump-unreviewed.txt index 8e9145fd4a6a..e61dd4d5bdbb 100644 --- a/platform/execution-impl/api-dump-unreviewed.txt +++ b/platform/execution-impl/api-dump-unreviewed.txt @@ -90,6 +90,7 @@ a:com.intellij.diagnostic.logging.LogConsoleManagerBase - pa:getUi():com.intellij.execution.ui.RunnerLayoutUi - removeAdditionalTabComponent(com.intellij.diagnostic.logging.AdditionalTabComponent):V - removeLogConsole(java.lang.String):V +f:com.intellij.diagnostic.logging.LogFilesCollectorKt f:com.intellij.diagnostic.logging.LogFilesManager - (com.intellij.openapi.project.Project,com.intellij.diagnostic.logging.LogConsoleManager,com.intellij.openapi.Disposable):V - addLogConsoles(com.intellij.execution.configurations.RunConfigurationBase,com.intellij.execution.process.ProcessHandler):V diff --git a/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesCollector.kt b/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesCollector.kt new file mode 100644 index 000000000000..82696a680a69 --- /dev/null +++ b/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesCollector.kt @@ -0,0 +1,93 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.diagnostic.logging + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.VisibleForTesting +import java.io.File +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitor +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +import java.util.regex.Pattern +import kotlin.io.path.* + +/** + * Return paths to files which matches the given [pathPattern] in Ant format. + * @param includeAll if `true`, all matching files will be returned, otherwise only the last modified one. + */ +@RequiresBackgroundThread +@OptIn(ExperimentalPathApi::class) +@VisibleForTesting +@ApiStatus.Internal +fun collectLogPaths(pathPattern: String?, includeAll: Boolean): Set { + if (pathPattern == null) { + return emptySet() + } + + val logFile = File(pathPattern) + if (logFile.exists()) { + return setOf(pathPattern) + } + + var depth = 0 + var root: File? = logFile.parentFile + var patternString = logFile.name + while (root != null && !root.exists() && depth < 5) { + patternString = "${root.name}/$patternString" + root = root.parentFile + depth++ + } + if (root == null || !root.exists()) { + return emptySet() + } + + val pattern = Pattern.compile(FileUtil.convertAntToRegexp(patternString)) + val matchingPaths = ArrayList() + root.toPath().visitFileTree(MatchingPathsCollector(pattern, matchingPaths)) + if (matchingPaths.isEmpty()) { + return emptySet() + } + if (includeAll) { + return matchingPaths.mapTo(LinkedHashSet(matchingPaths.size)) { it.pathString } + } + else { + return setOfNotNull(matchingPaths.maxByOrNull { it.getLastModifiedTime() }?.pathString) + } +} + +private class MatchingPathsCollector(private val pattern: Pattern, private val result: ArrayList) : FileVisitor { + var relativePath = "" + var initialDir = true + + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + if (!initialDir) { + relativePath = "$relativePath${dir.name}/" + } + else { + initialDir = false + } + return FileVisitResult.CONTINUE + } + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + if (pattern.matcher(relativePath + file.name).matches()) { + result.add(file) + if (result.size > 100) { + return FileVisitResult.TERMINATE + } + } + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult { + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + relativePath = relativePath.removeSuffix("${dir.name}/") + return FileVisitResult.CONTINUE + } +} diff --git a/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesManager.java b/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesManager.java index 8cd8f0aabcec..373d44b81b2e 100644 --- a/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesManager.java +++ b/platform/execution-impl/src/com/intellij/diagnostic/logging/LogFilesManager.java @@ -40,7 +40,8 @@ public final class LogFilesManager { } final Set oldPaths = logFile.getPaths(); - final Set newPaths = logFile.getOptions().getPaths(); // should not be called in UI thread + LogFileOptions options = logFile.getOptions(); + final Set newPaths = LogFilesCollectorKt.collectLogPaths(options.getPathPattern(), options.isShowAll()); // should not be called in UI thread logFile.setPaths(newPaths); final Set obsoletePaths = new HashSet<>(oldPaths); @@ -52,7 +53,7 @@ public final class LogFilesManager { return; } - addConfigurationConsoles(logFile.getOptions(), file -> !oldPaths.contains(file), newPaths, logFile.getConfiguration()); + addConfigurationConsoles(options, file -> !oldPaths.contains(file), newPaths, logFile.getConfiguration()); for (String each : obsoletePaths) { myManager.removeLogConsole(each); } diff --git a/platform/execution-impl/testSources/com/intellij/diagnostic/logging/LogFilesCollectorTest.kt b/platform/execution-impl/testSources/com/intellij/diagnostic/logging/LogFilesCollectorTest.kt new file mode 100644 index 000000000000..697e0da44f09 --- /dev/null +++ b/platform/execution-impl/testSources/com/intellij/diagnostic/logging/LogFilesCollectorTest.kt @@ -0,0 +1,64 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.diagnostic.logging + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.io.directoryContent +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.io.path.pathString + +class LogFilesCollectorTest { + @Test + fun `collect files in root`() { + val logDir = directoryContent { + file("main.log") + file("main2.log") + }.generateInTempDir() + val root = logDir.pathString + + assertLogs("*") + assertLogs("**/**") + assertLogs("$root/x.log") + + assertLogs("$root/main.log", + "$root/main.log") + assertLogs("$root/*.log", + "$root/main.log", + "$root/main2.log") + } + + @Test + fun `collect files in subdirectories`() { + val logDir = directoryContent { + dir("subDir1") { + dir("deepSubDir") { + file("file1.log") + file("file2.log") + } + file("file1.log") + } + dir("subDir2") { + file("file1.log") + file("file2.log") + } + }.generateInTempDir() + val root = logDir.pathString + + assertLogs("$root/*/*.log", + "$root/subDir1/file1.log", + "$root/subDir2/file1.log", + "$root/subDir2/file2.log") + assertLogs("$root/*/file1.log", + "$root/subDir1/file1.log", + "$root/subDir2/file1.log") + assertLogs("$root/**/file1.log", + "$root/subDir1/deepSubDir/file1.log", + "$root/subDir1/file1.log", + "$root/subDir2/file1.log") + } + + private fun assertLogs(pathPattern: String, vararg expected: String) { + val actual = collectLogPaths(FileUtil.toSystemDependentName(pathPattern), true).toList().sorted() + Assertions.assertThatList(actual).isEqualTo(expected.toList()) + } +} \ No newline at end of file diff --git a/platform/execution/api-dump-unreviewed.txt b/platform/execution/api-dump-unreviewed.txt index ca6acc00163c..b13f6354ca0c 100644 --- a/platform/execution/api-dump-unreviewed.txt +++ b/platform/execution/api-dump-unreviewed.txt @@ -552,11 +552,9 @@ f:com.intellij.execution.configurations.LogFileOptions - (java.lang.String,java.lang.String,Z,Z,Z):V - b:(java.lang.String,java.lang.String,Z,Z,Z,I,kotlin.jvm.internal.DefaultConstructorMarker):V - sf:areEqual(com.intellij.execution.configurations.LogFileOptions,com.intellij.execution.configurations.LogFileOptions):Z -- sf:collectMatchedFiles(java.io.File,java.util.regex.Pattern,java.util.List):V - f:getCharset():java.nio.charset.Charset - f:getName():java.lang.String - f:getPathPattern():java.lang.String -- f:getPaths():java.util.Set - f:isEnabled():Z - f:isShowAll():Z - f:isSkipContent():Z @@ -569,7 +567,6 @@ f:com.intellij.execution.configurations.LogFileOptions - f:setSkipContent(Z):V f:com.intellij.execution.configurations.LogFileOptions$Companion - f:areEqual(com.intellij.execution.configurations.LogFileOptions,com.intellij.execution.configurations.LogFileOptions):Z -- f:collectMatchedFiles(java.io.File,java.util.regex.Pattern,java.util.List):V a:com.intellij.execution.configurations.ModuleBasedConfiguration - com.intellij.execution.configurations.LocatableConfigurationBase - com.intellij.execution.configurations.ModuleRunConfiguration diff --git a/platform/execution/src/com/intellij/execution/configurations/LogFileOptions.kt b/platform/execution/src/com/intellij/execution/configurations/LogFileOptions.kt index 97faf9c10782..3e1d554c0a23 100644 --- a/platform/execution/src/com/intellij/execution/configurations/LogFileOptions.kt +++ b/platform/execution/src/com/intellij/execution/configurations/LogFileOptions.kt @@ -3,15 +3,11 @@ package com.intellij.execution.configurations import com.intellij.openapi.components.BaseState import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtilRt -import com.intellij.util.SmartList import com.intellij.util.xmlb.Converter import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.Tag -import java.io.File import java.nio.charset.Charset -import java.util.regex.Pattern /** * The information about a single log file displayed in the console when the configuration @@ -20,12 +16,6 @@ import java.util.regex.Pattern @Tag("log_file") class LogFileOptions : BaseState { companion object { - @JvmStatic - fun collectMatchedFiles(root: File, pattern: Pattern, files: MutableList) { - val dirs = root.listFiles() ?: return - dirs.filterTo(files) { pattern.matcher(it.name).matches() && it.isFile } - } - @JvmStatic fun areEqual(options1: LogFileOptions?, options2: LogFileOptions?): Boolean { return if (options1 == null || options2 == null) { @@ -60,46 +50,6 @@ class LogFileOptions : BaseState { @get:Attribute(value = "charset", converter = CharsetConverter::class) var charset: Charset by property(Charset.defaultCharset()) - fun getPaths(): Set { - val logFile = File(pathPattern!!) - if (logFile.exists()) { - return setOf(pathPattern!!) - } - - val dirIndex = pathPattern!!.lastIndexOf(File.separator) - if (dirIndex == -1) { - return emptySet() - } - - val files = SmartList() - collectMatchedFiles(File(pathPattern!!.substring(0, dirIndex)), - Pattern.compile(FileUtil.convertAntToRegexp(pathPattern!!.substring(dirIndex + File.separator.length))), files) - if (files.isEmpty()) { - return emptySet() - } - - if (isShowAll) { - val result = HashSet(files.size) - files.mapTo(result) { it.path } - return result - } - else { - var lastFile: File? = null - for (file in files) { - if (lastFile != null) { - if (file.lastModified() > lastFile.lastModified()) { - lastFile = file - } - } - else { - lastFile = file - } - } - assert(lastFile != null) - return setOf(lastFile!!.path) - } - } - //read external constructor()