From 530fb3f4c821ac2e96b6395efcbc145281d3e329 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Mon, 30 Jun 2025 22:11:55 +0200 Subject: [PATCH] PY-82318: Unlock files locked by `pip` and `python` on Windows. GitOrigin-RevId: 7b72a365964314707b28f6a7f20e3cedbe4bc194 --- .../framework/winLockedFile/api.kt | 2 +- .../framework/winLockedFile/impl/delete.kt | 5 ++-- .../unit/showCase/WinLockedFilesTest.kt | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/api.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/api.kt index f18175ca882b..2eeaffec1685 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/api.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/api.kt @@ -26,5 +26,5 @@ fun getProcessLockedPath(path: Path): Result, @NlsSafe Strin fun deleteCheckLocking(path: Path, vararg processesToKillIfLocked: Regex = arrayOf(PYTHON)): Unit = deleteCheckLockingImpl(path, *processesToKillIfLocked) -private val PYTHON = Regex("^python[0-9.]*\\.exe$") +private val PYTHON = Regex("^(python|pip)[0-9.]*\\.exe$") diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/impl/delete.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/impl/delete.kt index 64ab913d88ad..0b8c04d74b2e 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/impl/delete.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/winLockedFile/impl/delete.kt @@ -14,6 +14,8 @@ import kotlin.io.path.isDirectory import kotlin.jvm.optionals.getOrNull +private val fileLogger = fileLogger() + @OptIn(ExperimentalPathApi::class) @Throws(IOException::class, FileLockedException::class) internal fun deleteCheckLockingImpl(path: Path, vararg processesToKill: Regex) { @@ -26,7 +28,6 @@ internal fun deleteCheckLockingImpl(path: Path, vararg processesToKill: Regex) { } val paths = listOf(path) + if (path.isDirectory()) Files.walk(path).use { it.toList() } else emptyList() - // First, kill processes for (child in paths) { for (process in getProcessLockedPath(child).orThrow()) { @@ -34,7 +35,7 @@ internal fun deleteCheckLockingImpl(path: Path, vararg processesToKill: Regex) { val fileName = Path.of(command).fileName.toString() if (processesToKill.any { it.matches(fileName) }) { val processInfo = WinProcessInfo.get(process.pid()) - fileLogger().warn("Killing ${process.pid()} ${processInfo}") + fileLogger.warn("Killing ${process.pid()} ${processInfo}") killProcess(process) } } diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/unit/showCase/WinLockedFilesTest.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/unit/showCase/WinLockedFilesTest.kt index 4afb8db760f3..8f7effc397f3 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/unit/showCase/WinLockedFilesTest.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/unit/showCase/WinLockedFilesTest.kt @@ -3,6 +3,9 @@ package com.intellij.python.junit5Tests.unit.showCase import com.intellij.python.junit5Tests.framework.winLockedFile.FileLockedException import com.intellij.python.junit5Tests.framework.winLockedFile.deleteCheckLocking import com.intellij.python.junit5Tests.framework.winLockedFile.getProcessLockedPath +import com.intellij.util.io.awaitExit +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers @@ -16,7 +19,9 @@ import org.junit.jupiter.api.io.TempDir import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteIfExists import kotlin.io.path.writeText +import kotlin.time.Duration.Companion.seconds class WinLockedFilesTest { @@ -60,4 +65,24 @@ class WinLockedFilesTest { process.waitFor() deleteCheckLocking(path) } + + @OptIn(ExperimentalPathApi::class) + @EnabledOnOs(value = [OS.WINDOWS]) + @Test + fun testDirectoryLockedUnlock(@TempDir path: Path): Unit = runBlocking { + val systemRoot = System.getenv("SystemRoot") ?: "c:\\windows" + val process = ProcessBuilder("$systemRoot/system32/cmd.exe") + .directory(path.toFile()) + .start() + try { + deleteCheckLocking(path, Regex("cmd\\.exe")) + withTimeoutOrNull(10.seconds) { + process.awaitExit() + } + path.deleteIfExists() + } + finally { + process.destroyForcibly() + } + } } \ No newline at end of file