Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework local settings file support #1000

Merged
merged 3 commits into from
Jan 14, 2025
Merged
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
Expand Up @@ -15,6 +15,7 @@ import com.jetbrains.rider.projectView.solution
import com.microsoft.azure.toolkit.intellij.legacy.function.coreTools.FunctionsVersionMsBuildService
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionLocalSettingsService
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionWorkerRuntime
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.getWorkerRuntime
import com.microsoft.azure.toolkit.lib.appservice.model.OperatingSystem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -67,7 +68,7 @@ suspend fun PublishableProjectModel.getFunctionStack(
val functionLocalSettings = FunctionLocalSettingsService
.getInstance(project)
.getFunctionLocalSettings(this)
val workerRuntime = functionLocalSettings?.values?.workerRuntime ?: FunctionWorkerRuntime.DOTNET_ISOLATED
val workerRuntime = functionLocalSettings?.getWorkerRuntime() ?: FunctionWorkerRuntime.DOTNET_ISOLATED
val azureFunctionVersion = withContext(Dispatchers.EDT) {
FunctionsVersionMsBuildService
.getInstance(project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ package com.microsoft.azure.toolkit.intellij.legacy.function

import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.openapi.rd.util.lifetime
import com.intellij.openapi.startup.ProjectActivity
import com.jetbrains.rd.util.reactive.adviseOnce
import com.jetbrains.rd.util.threading.coroutines.nextNotNullValue
import com.jetbrains.rider.run.configurations.runnableProjectsModelIfAvailable
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionLocalSettingsService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class FunctionWarmupStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
withContext(Dispatchers.EDT) {
project.runnableProjectsModelIfAvailable?.projects?.adviseOnce(project.lifetime) {
FunctionLocalSettingsService.getInstance(project).initialize(it)
}
val projectList = withContext(Dispatchers.EDT) {
project.runnableProjectsModelIfAvailable?.projects?.nextNotNullValue()
} ?: return

withContext(Dispatchers.Main) {
FunctionLocalSettingsService
.getInstance(project)
.initialize(projectList)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,36 @@

package com.microsoft.azure.toolkit.intellij.legacy.function.localsettings

import com.google.gson.annotations.SerializedName

//Return back when `allowComments` is available.
//@Serializable
//data class FunctionLocalSettings(
// @SerialName("IsEncrypted") val isEncrypted: Boolean?,
// @SerialName("Values") val values: FunctionValuesModel?,
// @SerialName("Host") val host: FunctionHostModel?,
// @SerialName("ConnectionStrings") val connectionStrings: Map<String, String>?
//)
//
//@Serializable
//data class FunctionValuesModel(
// @SerialName("FUNCTIONS_WORKER_RUNTIME") val workerRuntime: FunctionWorkerRuntime?,
// @SerialName("AzureWebJobsStorage") val webJobsStorage: String?,
// @SerialName("AzureWebJobsDashboard") val webJobsDashboard: String?,
// @SerialName("AzureWebJobs.HttpExample.Disabled") val webJobsHttpExampleDisabled: Boolean?,
// @SerialName("MyBindingConnection") val bindingConnection: String?
//)
//
//@Serializable
//data class FunctionHostModel(
// @SerialName("LocalHttpPort") val localHttpPort: Int?,
// @SerialName("CORS") val cors: String?,
// @SerialName("CORSCredentials") val corsCredentials: Boolean?
//)
//
//@Serializable
//enum class FunctionWorkerRuntime {
// @SerialName("DOTNET") DOTNET {
// override fun value() = "DOTNET"
// },
// @SerialName("DOTNET-ISOLATED") DOTNET_ISOLATED {
// override fun value() = "DOTNET-ISOLATED"
// };
//
// abstract fun value(): String
//}
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@Serializable
data class FunctionLocalSettings(
@SerializedName("IsEncrypted") val isEncrypted: Boolean?,
@SerializedName("Values") val values: FunctionValuesModel?,
@SerializedName("Host") val host: FunctionHostModel?,
@SerializedName("ConnectionStrings") val connectionStrings: Map<String, String>?
)

data class FunctionValuesModel(
@SerializedName("FUNCTIONS_WORKER_RUNTIME") val workerRuntime: FunctionWorkerRuntime?,
@SerializedName("AzureWebJobsStorage") val webJobsStorage: String?,
@SerializedName("AzureWebJobsDashboard") val webJobsDashboard: String?,
@SerializedName("AzureWebJobs.HttpExample.Disabled") val webJobsHttpExampleDisabled: Boolean?,
@SerializedName("MyBindingConnection") val bindingConnection: String?
@SerialName("IsEncrypted") val isEncrypted: Boolean?,
@SerialName("Values") val values: Map<String, String>?,
@SerialName("Host") val host: FunctionHostModel?,
)

@Serializable
data class FunctionHostModel(
@SerializedName("LocalHttpPort") val localHttpPort: Int?,
@SerializedName("CORS") val cors: String?,
@SerializedName("CORSCredentials") val corsCredentials: Boolean?
@SerialName("LocalHttpPort") val localHttpPort: Int?,
@SerialName("CORS") val cors: String?,
@SerialName("CORSCredentials") val corsCredentials: Boolean?
)

fun FunctionLocalSettings.getWorkerRuntime(): FunctionWorkerRuntime? {
val runtime = values?.get("FUNCTIONS_WORKER_RUNTIME") ?: return null
return when {
runtime.equals(FunctionWorkerRuntime.DOTNET_ISOLATED.value(), true) -> FunctionWorkerRuntime.DOTNET_ISOLATED
runtime.equals(FunctionWorkerRuntime.DOTNET.value(), true) -> FunctionWorkerRuntime.DOTNET
else -> null
}
}

enum class FunctionWorkerRuntime {
@SerializedName(value = "DOTNET", alternate = ["dotnet"])
DOTNET {
override fun value() = "DOTNET"
},
@SerializedName(value = "DOTNET-ISOLATED", alternate = ["dotnet-isolated"])
DOTNET_ISOLATED {
override fun value() = "DOTNET-ISOLATED"
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2018-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the MIT license.
*/

package com.microsoft.azure.toolkit.intellij.legacy.function.localsettings

import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.ModificationTracker
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import com.intellij.psi.util.CachedValueProvider
import kotlinx.serialization.json.Json

class FunctionLocalSettingsCachedValueProvider(val virtualFile: VirtualFile, private val json: Json) :
CachedValueProvider<FunctionLocalSettings> {

companion object {
private val LOG = logger<FunctionLocalSettingsCachedValueProvider>()
}

override fun compute(): CachedValueProvider.Result<FunctionLocalSettings?> {
if (!virtualFile.exists() || !virtualFile.isValid) {
return CachedValueProvider.Result(null, ModificationTracker.NEVER_CHANGED)
}

try {
val text = virtualFile.readText()
val localSettings = json.decodeFromString<FunctionLocalSettings>(text)
return CachedValueProvider.Result.create(localSettings, virtualFile)
} catch (e: Exception) {
LOG.warn("Failed to load local settings from $virtualFile", e)
return CachedValueProvider.Result(null, ModificationTracker.NEVER_CHANGED)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@

package com.microsoft.azure.toolkit.intellij.legacy.function.localsettings

import com.google.gson.Gson
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValuesManager
import com.jetbrains.rider.model.PublishableProjectModel
import com.jetbrains.rider.model.RunnableProject
import com.microsoft.azure.toolkit.intellij.legacy.function.daemon.AzureRunnableProjectKinds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists

@Service(Service.Level.PROJECT)
class FunctionLocalSettingsService {
class FunctionLocalSettingsService(private val project: Project) {
companion object {
fun getInstance(project: Project): FunctionLocalSettingsService = project.service()
}
Expand All @@ -33,58 +35,53 @@ class FunctionLocalSettingsService {
explicitNulls = false
ignoreUnknownKeys = true
allowTrailingComma = true
allowComments = true
}

private val cache = ConcurrentHashMap<String, Pair<Long, FunctionLocalSettings>>()

fun initialize(runnableProjects: List<RunnableProject>) {
runnableProjects.forEach {
val localSettingsFile = getLocalSettingFilePathInternal(Path(it.projectFilePath).parent)
if (!localSettingsFile.exists()) return@forEach
val pathString = localSettingsFile.absolutePathString()
if (cache.containsKey(pathString)) return@forEach
val virtualFile = VfsUtil.findFile(localSettingsFile, true) ?: return@forEach
val localSettingsFileStamp = localSettingsFile.toFile().lastModified()
val localSettings = getFunctionLocalSettings(virtualFile)
cache[pathString] = Pair(localSettingsFileStamp, localSettings)
}
private val cache = ConcurrentHashMap<Path, CachedValue<FunctionLocalSettings>>()

suspend fun initialize(runnableProjects: List<RunnableProject>) {
runnableProjects
.filter { it.kind == AzureRunnableProjectKinds.AzureFunctions }
.forEach { getFunctionLocalSettings(it) }
}

fun getFunctionLocalSettings(publishableProject: PublishableProjectModel) =
suspend fun getFunctionLocalSettings(publishableProject: PublishableProjectModel) =
getFunctionLocalSettingsInternal(Path(publishableProject.projectFilePath).parent)

fun getFunctionLocalSettings(runnableProject: RunnableProject) =
suspend fun getFunctionLocalSettings(runnableProject: RunnableProject) =
getFunctionLocalSettingsInternal(Path(runnableProject.projectFilePath).parent)

fun getFunctionLocalSettings(projectPath: Path) =
suspend fun getFunctionLocalSettings(projectPath: Path) =
getFunctionLocalSettingsInternal(projectPath.parent)

private fun getFunctionLocalSettingsInternal(basePath: Path): FunctionLocalSettings? {
private suspend fun getFunctionLocalSettingsInternal(basePath: Path): FunctionLocalSettings? {
val localSettingsFile = getLocalSettingFilePathInternal(basePath)
if (!localSettingsFile.exists()) return null

val localSettingsFileStamp = localSettingsFile.toFile().lastModified()
val pathString = localSettingsFile.absolutePathString()
val existingLocalSettings = cache[pathString]
if (existingLocalSettings == null || localSettingsFileStamp != existingLocalSettings.first) {
val virtualFile = VfsUtil.findFile(localSettingsFile, true) ?: return null
val localSettings = getFunctionLocalSettings(virtualFile)
cache[pathString] = Pair(localSettingsFileStamp, localSettings)
return localSettings
}
val virtualFile = withContext(Dispatchers.IO) {
VfsUtil.findFile(localSettingsFile, true)
} ?: return null

return existingLocalSettings.second
val localSettings = getFunctionLocalSettings(virtualFile)
return localSettings
}

fun getLocalSettingFilePath(projectPath: Path) =
getLocalSettingFilePathInternal(projectPath.parent)

private fun getLocalSettingFilePathInternal(basePath: Path): Path = basePath.resolve("local.settings.json")
private fun getLocalSettingFilePathInternal(basePath: Path): Path =
basePath.resolve("local.settings.json")

private fun getFunctionLocalSettings(localSettingsFile: VirtualFile): FunctionLocalSettings? {
val path = localSettingsFile.toNioPath()
val localSettings = cache.getOrPut(path) {
val cachedValuesManager = CachedValuesManager.getManager(project)
val provider = FunctionLocalSettingsCachedValueProvider(localSettingsFile, json)
val cachedValue = cachedValuesManager.createCachedValue(provider, false)
cachedValue
}

private fun getFunctionLocalSettings(localSettingsFile: VirtualFile): FunctionLocalSettings {
val content = localSettingsFile.readText()
//Return back when `allowComments` is available.
//return json.decodeFromString<FunctionLocalSettings>(content)
return Gson().fromJson(content, FunctionLocalSettings::class.java)
return localSettings.value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class FunctionRunConfigurationType : ConfigurationTypeBase(
return settings
}

private fun generateConfigurationForRunnableProject(
private suspend fun generateConfigurationForRunnableProject(
runnableProject: RunnableProject,
runManager: RunManager,
autoGeneratedRunConfigurationManager: AutoGeneratedRunConfigurationManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.microsoft.azure.toolkit.intellij.legacy.function.launchProfiles.getWo
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionLocalSettings
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionLocalSettingsService
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionWorkerRuntime
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.getWorkerRuntime
import kotlinx.coroutines.Dispatchers
import java.io.File

Expand Down Expand Up @@ -141,7 +142,7 @@ class FunctionRunConfigurationViewModel(
launchProfileSelector.isLoading.set(false)
}

private fun readLocalSettingsForProject(runnableProject: RunnableProject) {
private suspend fun readLocalSettingsForProject(runnableProject: RunnableProject) {
functionLocalSettings = FunctionLocalSettingsService
.getInstance(project)
.getFunctionLocalSettings(runnableProject)
Expand Down Expand Up @@ -337,7 +338,7 @@ class FunctionRunConfigurationViewModel(
readLocalSettingsForProject(runnableProject)

// Disable "Use external console" for Isolated worker
val workerRuntime = functionLocalSettings?.values?.workerRuntime ?: FunctionWorkerRuntime.DOTNET_ISOLATED
val workerRuntime = functionLocalSettings?.getWorkerRuntime() ?: FunctionWorkerRuntime.DOTNET_ISOLATED
val workerRuntimeSupportsExternalConsole = workerRuntime != FunctionWorkerRuntime.DOTNET_ISOLATED
useExternalConsoleEditor.isVisible.set(workerRuntimeSupportsExternalConsole)
useExternalConsoleEditor.isSelected.set(workerRuntimeSupportsExternalConsole && useExternalConsole)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.Functi
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionLocalSettingsService
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.FunctionWorkerRuntime
import com.microsoft.azure.toolkit.intellij.legacy.function.launchProfiles.getProjectLaunchProfileByName
import com.microsoft.azure.toolkit.intellij.legacy.function.localsettings.getWorkerRuntime
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
Expand Down Expand Up @@ -84,11 +85,9 @@ class FunctionRunExecutorFactory(
.getInstance(project)
.getFunctionLocalSettings(projectFilePath)
}
val workerRuntime = if (functionLocalSettings?.values?.workerRuntime == null) {
getFunctionWorkerRuntimeFromBackendOrDefault(projectFilePath)
} else {
functionLocalSettings.values.workerRuntime
}

val workerRuntime = functionLocalSettings?.getWorkerRuntime()
?: getFunctionWorkerRuntimeFromBackendOrDefault(projectFilePath)
LOG.debug { "Worker runtime: $workerRuntime" }

val functionsRuntimeVersion = calculateFunctionsRuntimeVersion(msBuildVersionProperty, workerRuntime)
Expand Down
Loading