[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
This commit is contained in:
Nikolay Chashnikov
2024-04-12 13:43:01 +02:00
committed by intellij-monorepo-bot
parent 2338c10c80
commit 5ced3727dd
6 changed files with 161 additions and 55 deletions

View File

@@ -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
- <init>(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

View File

@@ -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<String> {
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<Path>()
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<Path>) : FileVisitor<Path> {
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
}
}

View File

@@ -40,7 +40,8 @@ public final class LogFilesManager {
}
final Set<String> oldPaths = logFile.getPaths();
final Set<String> newPaths = logFile.getOptions().getPaths(); // should not be called in UI thread
LogFileOptions options = logFile.getOptions();
final Set<String> newPaths = LogFilesCollectorKt.collectLogPaths(options.getPathPattern(), options.isShowAll()); // should not be called in UI thread
logFile.setPaths(newPaths);
final Set<String> 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);
}

View File

@@ -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())
}
}

View File

@@ -552,11 +552,9 @@ f:com.intellij.execution.configurations.LogFileOptions
- <init>(java.lang.String,java.lang.String,Z,Z,Z):V
- b:<init>(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

View File

@@ -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<File>) {
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<String> {
val logFile = File(pathPattern!!)
if (logFile.exists()) {
return setOf(pathPattern!!)
}
val dirIndex = pathPattern!!.lastIndexOf(File.separator)
if (dirIndex == -1) {
return emptySet()
}
val files = SmartList<File>()
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<String>(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()