Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run IDE for UI Tests" type="GradleRunConfiguration" factoryName="Gradle">
<configuration default="false" name="Run Plugin (IDEA)" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
Expand All @@ -11,15 +11,14 @@
</option>
<option name="taskNames">
<list>
<option value="runIdeForUiTests" />
<option value="runIdeIdea" />
</list>
</option>
<option name="vmOptions" />
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>
</component>
35 changes: 21 additions & 14 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import org.jetbrains.changelog.Changelog
import org.jetbrains.changelog.markdownToHTML
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.gradle.api.tasks.testing.TestDescriptor
import org.gradle.api.tasks.testing.TestResult
Expand All @@ -18,6 +19,19 @@ plugins {
group = providers.gradleProperty("pluginGroup").get()
version = providers.gradleProperty("pluginVersion").get()

// -Pidea switches from Gateway (GW) to IntelliJ IDEA (IU)
val isIdea = providers.gradleProperty("idea").isPresent
val resolvedPlatformType = if (isIdea) {
"IU"
} else {
providers.gradleProperty("platformType").get()
}
val resolvedBundledPlugins = if (isIdea) {
"com.jetbrains.gateway"
} else {
providers.gradleProperty("platformBundledPlugins").get()
}

// Set the JVM language level used to build the project.
kotlin {
jvmToolchain(21)
Expand Down Expand Up @@ -56,10 +70,10 @@ dependencies {

// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
intellijPlatform {
create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion"))
create(resolvedPlatformType, providers.gradleProperty("platformVersion"))

// Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins.
bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') })
bundledPlugins(resolvedBundledPlugins.split(',').filter { it.isNotBlank() })

// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace.
plugins(providers.gradleProperty("platformPlugins").map { it.split(',') })
Expand Down Expand Up @@ -227,20 +241,13 @@ tasks {

intellijPlatformTesting {
runIde {
register("runIdeForUiTests") {
task {
jvmArgumentProviders += CommandLineArgumentProvider {
listOf(
"-Drobot-server.port=8082",
"-Dide.mac.message.dialogs.as.sheets=false",
"-Djb.privacy.policy.text=<!--999.999-->",
"-Djb.consents.confirmation.enabled=false",
)
}
}
// Visible next to runIde under "intellij platform" in the Gradle tool window
register("runIdeIdea") {
type = IntelliJPlatformType.IntellijIdeaUltimate
version = providers.gradleProperty("platformVersion")

plugins {
robotServerPlugin()
bundledPlugin("com.jetbrains.gateway")
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pluginVersion = 0.0.18

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 251
#pluginUntilBuild = 252.*

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = GW
Expand All @@ -20,7 +19,6 @@ platformVersion = 2025.1.1
platformPlugins =
# Example: platformBundledPlugins = com.intellij.java
platformBundledPlugins =

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.10.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import kotlinx.coroutines.*
import java.nio.file.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.io.path.exists
import kotlin.io.path.isRegularFile

Expand All @@ -23,80 +25,144 @@
private val watchService: WatchService = FileSystems.getDefault().newWatchService()
) {
private var onFileChanged: ((Path) -> Unit)? = null
private val registeredKeys = ConcurrentHashMap<WatchKey, Path>()
private val registeredDirectories = ConcurrentHashMap<WatchKey, Path>()
private val monitoredFiles = ConcurrentHashMap.newKeySet<Path>()
private val notifyLock = ReentrantLock()
private var watchJob: Job? = null

fun start() {
this.watchJob = scope.launch(dispatcher) {
try {
while (isActive) {
val key = watchService.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS)
if (key == null) {
@Suppress("ConvertLongToDuration")
delay(100)
continue
}
val dir = registeredKeys[key] ?: continue
pollEvents(key, dir)
key.reset()
pollDirectoryEvents()
}
}

private suspend fun CoroutineScope.pollDirectoryEvents() {
try {
while (isActive) {
val key = watchService.poll(100, TimeUnit.MILLISECONDS)

Check warning on line 42 in src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcher.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Possibly blocking call in non-blocking context

Possibly blocking call in non-blocking context could lead to thread starvation
if (key == null) {
@Suppress("ConvertLongToDuration")
delay(100)
continue
}
} catch (e: ClosedWatchServiceException) {
// Watch service was closed, exit gracefully
val dir = registeredDirectories[key] ?: continue
handleDirectoryEvent(key, dir)
key.reset()
}
} catch (_: ClosedWatchServiceException) {
// Watch service was closed, exit gracefully
}
}

fun stop() {
watchJob?.cancel()
watchJob = null
registeredDirectories.keys.forEach { it.cancel() }
registeredDirectories.clear()
monitoredFiles.clear()
try {
watchService.close()
} catch (e: Exception) {
} catch (_: Exception) {
// Ignore exceptions when closing
}
}

/**
* Registers [path] for watching and invokes the [onFileChanged] callback if this is
* a new file that was not previously monitored. For non-existent files, the callback
* is invoked so listeners can track them for a later CREATE event. If [path] is
* already in the monitored set, no callback is issued — this avoids redundant refreshes.
*
* @param path the file to monitor; may be non-existent (e.g. a kubeconfig file expected
* to appear later). If [path.parent] is null, the path will not be registered.
* @return this instance for chaining.
*/
fun addFile(path: Path): FileWatcher {
if (!path.exists()
|| !path.isRegularFile()) {
return this
}
val parentDir = path.parent
if (parentDir != null
&& !monitoredFiles.contains(path)) {
val watchKey = parentDir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
)
registeredKeys[watchKey] = parentDir
registerDirectory(parentDir)
monitoredFiles.add(path)
onFileChanged?.invoke(path)
invokeOnFileChanged(path)
}
return this
}

fun removeFile(path: Path): FileWatcher {
monitoredFiles.remove(path)
if (!monitoredFiles.remove(path)) {
return this
}
val parentDir = path.parent ?: return this
if (monitoredFiles.none { it.parent == parentDir }) {
unregisterDirectory(parentDir)
}
return this
}

fun getMonitoredFiles(): Set<Path> = monitoredFiles

/** Parent directories that currently have an active [WatchKey]. For tests. */
internal fun getWatchedDirectories(): Set<Path> = registeredDirectories.values.toSet()

fun onFileChanged(action: ((Path) -> Unit)?) {
this.onFileChanged = action
notifyLock.lock()
try {
this.onFileChanged = action
} finally {
notifyLock.unlock()
}
}

private fun pollEvents(key: WatchKey, dir: Path) {
private fun registerDirectory(directory: Path) {
if (registeredDirectories.values.any { it == directory }) {
return
}
val watchKey = directory.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
)
registeredDirectories[watchKey] = directory
}

private fun unregisterDirectory(directory: Path) {
val keys = registeredDirectories.filterValues { it == directory }.keys
keys.forEach { key ->
registeredDirectories.remove(key)
key.cancel()
}
}

private fun handleDirectoryEvent(key: WatchKey, dir: Path) {
for (event in key.pollEvents()) {
val relativePath = event.context() as? Path ?: continue
val changedFile = dir.resolve(relativePath)
val kind = event.kind()

if (monitoredFiles.contains(changedFile)
&& event.kind() != StandardWatchEventKinds.OVERFLOW
if (!monitoredFiles.contains(changedFile)
|| kind == StandardWatchEventKinds.OVERFLOW
) {
onFileChanged?.invoke(changedFile)
continue
}

// DELETE: file is already gone — still notify so callers can refresh.
// CREATE/MODIFY: only notify for an existing regular file.
val shouldNotify = kind == StandardWatchEventKinds.ENTRY_DELETE
|| (changedFile.exists() && changedFile.isRegularFile())

if (shouldNotify) {
invokeOnFileChanged(changedFile)
}
}
}

private fun invokeOnFileChanged(path: Path) {
notifyLock.lock()
try {
onFileChanged?.invoke(path)
} finally {
notifyLock.unlock()
}
}
}
Loading
Loading