Skip to content

Commit

Permalink
Merge pull request #149 from rafaelldi/hot-reload
Browse files Browse the repository at this point in the history
Enable hot reload
  • Loading branch information
rafaelldi authored May 10, 2024
2 parents e00143f + b458566 commit 4a207b4
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 41 deletions.
3 changes: 3 additions & 0 deletions .fleet/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"backend.maxHeapSizeMb": 3072
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Added

- Stop Resource action to the dashboard
- Hot reload while running

## [0.7.0] - 2024-05-08

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ data class AspireHostConfig(
val resourceServiceEndpointUrl: String?,
val resourceServiceApiKey: String?,
val openTelemetryProtocolEndpointUrl: String?,
val openTelemetryProtocolServerPort: Int,
val openTelemetryProtocolServerPort: Int?,
val aspireHostLifetime: Lifetime
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlinx.coroutines.withContext
import me.rafaelldi.aspire.generated.aspireSessionHostModel
import me.rafaelldi.aspire.services.AspireServiceManager
import me.rafaelldi.aspire.sessionHost.SessionHostManager
import me.rafaelldi.aspire.settings.AspireSettings
import me.rafaelldi.aspire.util.DEBUG_SESSION_PORT
import me.rafaelldi.aspire.util.DEBUG_SESSION_TOKEN
import me.rafaelldi.aspire.util.DOTNET_DASHBOARD_OTLP_ENDPOINT_URL
Expand Down Expand Up @@ -65,7 +66,9 @@ class AspireHostProgramRunner : DotNetProgramRunner() {
val resourceServiceApiKey = environmentVariables[DOTNET_DASHBOARD_RESOURCESERVICE_APIKEY]

val openTelemetryProtocolEndpointUrl = environmentVariables[DOTNET_DASHBOARD_OTLP_ENDPOINT_URL]
val openTelemetryProtocolServerPort = NetUtils.findFreePort(57100)
val openTelemetryProtocolServerPort =
if (AspireSettings.getInstance().collectTelemetry) NetUtils.findFreePort(57100)
else null

val debuggingMode = environment.executor.id == DefaultDebugExecutor.EXECUTOR_ID

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import com.intellij.util.io.systemIndependentPath
import com.jetbrains.rider.model.RunnableProject
import com.jetbrains.rider.model.runnableProjectsModel
import com.jetbrains.rider.projectView.solution
import com.jetbrains.rider.run.configurations.RunnableProjectKinds
import com.jetbrains.rider.runtime.DotNetExecutable
import com.jetbrains.rider.runtime.dotNetCore.DotNetCoreRuntimeType
import me.rafaelldi.aspire.generated.SessionModel
import me.rafaelldi.aspire.settings.AspireSettings
import me.rafaelldi.aspire.util.MSBuildPropertyService
import java.nio.file.Path
import kotlin.io.path.Path
Expand All @@ -21,41 +19,30 @@ import kotlin.io.path.Path
class SessionExecutableFactory(private val project: Project) {
companion object {
fun getInstance(project: Project) = project.service<SessionExecutableFactory>()

private const val OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT"
private fun getOtlpEndpoint(port: Int) = "http://localhost:$port"
}

suspend fun createExecutable(sessionModel: SessionModel, openTelemetryPort: Int): DotNetExecutable? {
val runnableProjects = project.solution.runnableProjectsModel.projects.valueOrNull
suspend fun createExecutable(sessionModel: SessionModel): DotNetExecutable? {
val sessionProjectPath = Path(sessionModel.projectPath)
val sessionProjectPathString = sessionProjectPath.systemIndependentPath
val runnableProject = runnableProjects?.singleOrNull {
it.projectFilePath == sessionProjectPathString && it.kind == RunnableProjectKinds.DotNetCore
}
val runnableProject = project.solution.runnableProjectsModel.findBySessionProject(sessionProjectPath)

return if (runnableProject != null) {
getExecutableForRunnableProject(runnableProject, sessionModel, openTelemetryPort)
getExecutableForRunnableProject(runnableProject, sessionModel)
} else {
getExecutableForExternalProject(sessionProjectPath, sessionModel, openTelemetryPort)
getExecutableForExternalProject(sessionProjectPath, sessionModel)
}
}

private fun getExecutableForRunnableProject(
runnableProject: RunnableProject,
sessionModel: SessionModel,
openTelemetryPort: Int
sessionModel: SessionModel
): DotNetExecutable? {
val output = runnableProject.projectOutputs.firstOrNull() ?: return null
val executablePath = output.exePath
val arguments =
if (sessionModel.args?.isNotEmpty() == true) sessionModel.args.toList()
else output.defaultArguments
val params = ParametersListUtil.join(arguments)
val envs = sessionModel.envs?.associate { it.key to it.value }?.toMutableMap() ?: mutableMapOf()
if (AspireSettings.getInstance().collectTelemetry) {
envs[OTEL_EXPORTER_OTLP_ENDPOINT] = getOtlpEndpoint(openTelemetryPort)
}
val envs = sessionModel.envs?.associate { it.key to it.value } ?: mapOf()

return DotNetExecutable(
executablePath,
Expand All @@ -76,8 +63,7 @@ class SessionExecutableFactory(private val project: Project) {

private suspend fun getExecutableForExternalProject(
sessionProjectPath: Path,
sessionModel: SessionModel,
openTelemetryPort: Int
sessionModel: SessionModel
): DotNetExecutable? {
val propertyService = MSBuildPropertyService.getInstance(project)
val properties = propertyService.getProjectRunProperties(sessionProjectPath) ?: return null
Expand All @@ -86,10 +72,7 @@ class SessionExecutableFactory(private val project: Project) {
if (sessionModel.args?.isNotEmpty() == true) sessionModel.args.toList()
else properties.arguments
val params = ParametersListUtil.join(arguments)
val envs = sessionModel.envs?.associate { it.key to it.value }?.toMutableMap() ?: mutableMapOf()
if (AspireSettings.getInstance().collectTelemetry) {
envs[OTEL_EXPORTER_OTLP_ENDPOINT] = getOtlpEndpoint(openTelemetryPort)
}
val envs = sessionModel.envs?.associate { it.key to it.value } ?: mapOf()

return DotNetExecutable(
executablePath,
Expand Down
150 changes: 137 additions & 13 deletions src/main/kotlin/me/rafaelldi/aspire/sessionHost/SessionLauncher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,26 @@ import com.jetbrains.rd.framework.*
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.lifetime.isNotAlive
import com.jetbrains.rd.util.put
import com.jetbrains.rd.util.reactive.hasTrueValue
import com.jetbrains.rd.util.threading.coroutines.nextTrueValueAsync
import com.jetbrains.rdclient.protocol.RdDispatcher
import com.jetbrains.rider.RiderEnvironment
import com.jetbrains.rider.RiderEnvironment.createRunCmdForLauncherInfo
import com.jetbrains.rider.debugger.DebuggerWorkerProcessHandler
import com.jetbrains.rider.debugger.RiderDebuggerWorkerModelManager
import com.jetbrains.rider.debugger.createAndStartSession
import com.jetbrains.rider.debugger.editAndContinue.DotNetRunHotReloadProcess
import com.jetbrains.rider.debugger.editAndContinue.hotReloadManager
import com.jetbrains.rider.debugger.targets.DEBUGGER_WORKER_LAUNCHER
import com.jetbrains.rider.hotReload.HotReloadHost
import com.jetbrains.rider.model.HotReloadSupportedInfo
import com.jetbrains.rider.model.debuggerWorker.*
import com.jetbrains.rider.model.debuggerWorkerConnectionHelperModel
import com.jetbrains.rider.model.runnableProjectsModel
import com.jetbrains.rider.projectView.solution
import com.jetbrains.rider.run.*
import com.jetbrains.rider.run.configurations.RunnableProjectKinds
import com.jetbrains.rider.run.configurations.launchSettings.LaunchSettingsJsonService
import com.jetbrains.rider.runtime.DotNetExecutable
import com.jetbrains.rider.runtime.DotNetRuntime
import com.jetbrains.rider.runtime.RiderDotNetActiveRuntimeHost
Expand All @@ -38,6 +46,8 @@ import kotlinx.coroutines.withContext
import me.rafaelldi.aspire.generated.SessionModel
import me.rafaelldi.aspire.util.decodeAnsiCommandsToString
import org.jetbrains.annotations.Nls
import java.nio.file.Path
import java.util.*
import kotlin.io.path.Path
import kotlin.io.path.nameWithoutExtension

Expand All @@ -47,6 +57,13 @@ class SessionLauncher(private val project: Project) {
fun getInstance(project: Project) = project.service<SessionLauncher>()

private val LOG = logger<SessionLauncher>()

private const val DOTNET_MODIFIABLE_ASSEMBLIES = "DOTNET_MODIFIABLE_ASSEMBLIES"
private const val DOTNET_HOTRELOAD_NAMEDPIPE_NAME = "DOTNET_HOTRELOAD_NAMEDPIPE_NAME"
private const val DOTNET_STARTUP_HOOKS = "DOTNET_STARTUP_HOOKS"
private const val OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT"

private fun getOtlpEndpoint(port: Int) = "http://localhost:$port"
}

suspend fun launchSession(
Expand All @@ -55,7 +72,7 @@ class SessionLauncher(private val project: Project) {
sessionLifetime: Lifetime,
sessionEvents: MutableSharedFlow<SessionEvent>,
debuggingMode: Boolean,
openTelemetryPort: Int
openTelemetryPort: Int?
) {
LOG.info("Starting a session for the project ${sessionModel.projectPath}")

Expand All @@ -65,7 +82,7 @@ class SessionLauncher(private val project: Project) {
}

val factory = SessionExecutableFactory.getInstance(project)
val executable = factory.createExecutable(sessionModel, openTelemetryPort)
val executable = factory.createExecutable(sessionModel)
if (executable == null) {
LOG.warn("Unable to create executable for $sessionId (project: ${sessionModel.projectPath})")
return
Expand All @@ -84,38 +101,52 @@ class SessionLauncher(private val project: Project) {
return
}

val sessionProjectPath = Path(sessionModel.projectPath)
if (debuggingMode || sessionModel.debug) {
launchDebugSession(
sessionId,
Path(sessionModel.projectPath).nameWithoutExtension,
sessionProjectPath.nameWithoutExtension,
executable,
runtime,
openTelemetryPort,
sessionLifetime,
sessionEvents
)
} else {
launchRunSession(
sessionId,
sessionProjectPath,
executable,
runtime,
sessionModel.launchProfile,
openTelemetryPort,
sessionLifetime,
sessionEvents
)
}
}

private fun launchRunSession(
private suspend fun launchRunSession(
sessionId: String,
sessionProjectPath: Path,
executable: DotNetExecutable,
runtime: DotNetCoreRuntime,
launchProfile: String?,
openTelemetryPort: Int?,
sessionLifetime: Lifetime,
sessionEvents: MutableSharedFlow<SessionEvent>
) {
LOG.trace("Starting the session in the run mode")
val commandLine = executable.createRunCommandLine(runtime)

val executableToRun =
modifyExecutableToRun(executable, sessionProjectPath, launchProfile, openTelemetryPort, sessionLifetime)

val commandLine = executableToRun.createRunCommandLine(runtime)
val handler = KillableProcessHandler(commandLine)
subscribeToSessionEvents(sessionId, handler, sessionEvents)

addHotReloadListener(handler, sessionLifetime, commandLine.environment)

sessionLifetime.onTermination {
if (!handler.isProcessTerminating && !handler.isProcessTerminated) {
LOG.trace("Killing session process (id: $sessionId)")
Expand All @@ -126,25 +157,100 @@ class SessionLauncher(private val project: Project) {
handler.startNotify()
}

private suspend fun modifyExecutableToRun(
executable: DotNetExecutable,
sessionProjectPath: Path,
launchProfile: String?,
openTelemetryPort: Int?,
lifetime: Lifetime
): DotNetExecutable {
val envs = executable.environmentVariables.toMutableMap()

val isHotReloadAvailable = isHotReloadAvailable(executable, sessionProjectPath, launchProfile, lifetime)
if (isHotReloadAvailable) {
val pipeName = UUID.randomUUID().toString()
envs[DOTNET_MODIFIABLE_ASSEMBLIES] = "debug"
envs[DOTNET_HOTRELOAD_NAMEDPIPE_NAME] = pipeName
val deltaApplierPath =
RiderEnvironment.getBundledFile("JetBrains.Microsoft.Extensions.DotNetDeltaApplier.dll").absolutePath
envs[DOTNET_STARTUP_HOOKS] = deltaApplierPath
}

val isOpenTelemetryAvailable = openTelemetryPort != null
if (isOpenTelemetryAvailable) {
envs[OTEL_EXPORTER_OTLP_ENDPOINT] = getOtlpEndpoint(requireNotNull(openTelemetryPort))
}

return if (isHotReloadAvailable || isOpenTelemetryAvailable) {
executable.copy(environmentVariables = envs)
} else {
executable
}
}

private suspend fun isHotReloadAvailable(
executable: DotNetExecutable,
sessionProjectPath: Path,
launchProfile: String?,
lifetime: Lifetime
): Boolean {
if (executable.projectTfm == null) return false

val hotReloadHost = HotReloadHost.getInstance(project)
if (!hotReloadHost.runtimeHotReloadEnabled.hasTrueValue) return false

val runnableProject =
project.solution.runnableProjectsModel.findBySessionProject(sessionProjectPath) ?: return false

if (launchProfile != null) {
val launchSettings = LaunchSettingsJsonService.loadLaunchSettings(runnableProject)
if (launchSettings != null) {
val profile = launchSettings.profiles?.get(launchProfile)
if (profile?.hotReloadEnabled == false) return false
}
}

val info = HotReloadSupportedInfo(runnableProject.name, requireNotNull(executable.projectTfm).presentableName)
return withContext(Dispatchers.EDT) {
hotReloadHost.checkProjectConfigRuntimeHotReload(lifetime, info)
}
}

private fun addHotReloadListener(handler: KillableProcessHandler, lifetime: Lifetime, envs: Map<String, String>) {
val pipeName = envs[DOTNET_HOTRELOAD_NAMEDPIPE_NAME]
if (pipeName.isNullOrEmpty()) return

handler.addProcessListener(object : ProcessAdapter() {
override fun startNotified(event: ProcessEvent) {
val runSession = DotNetRunHotReloadProcess(lifetime, pipeName)
project.hotReloadManager.addProcess(runSession)
}
})
}

private suspend fun launchDebugSession(
sessionId: String,
@Nls sessionName: String,
executable: DotNetExecutable,
runtime: DotNetCoreRuntime,
openTelemetryPort: Int?,
sessionLifetime: Lifetime,
sessionEvents: MutableSharedFlow<SessionEvent>
) {
LOG.trace("Starting the session in the debug mode")

val executableToDebug = modifyExecutableToDebug(executable, openTelemetryPort)

val startInfo = DotNetCoreExeStartInfo(
DotNetCoreInfo(runtime.cliExePath),
executable.projectTfm?.let { EncInfo(it) },
executable.exePath,
executable.workingDirectory,
executable.programParameterString,
executable.environmentVariables.toModelMap,
executable.runtimeArguments,
executable.executeAsIs,
executable.useExternalConsole
executableToDebug.projectTfm?.let { EncInfo(it) },
executableToDebug.exePath,
executableToDebug.workingDirectory,
executableToDebug.programParameterString,
executableToDebug.environmentVariables.toModelMap,
executableToDebug.runtimeArguments,
executableToDebug.executeAsIs,
executableToDebug.useExternalConsole
)

withContext(Dispatchers.EDT) {
Expand All @@ -158,6 +264,24 @@ class SessionLauncher(private val project: Project) {
}
}

private suspend fun modifyExecutableToDebug(

Check warning on line 267 in src/main/kotlin/me/rafaelldi/aspire/sessionHost/SessionLauncher.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant 'suspend' modifier

Redundant 'suspend' modifier
executable: DotNetExecutable,
openTelemetryPort: Int?
): DotNetExecutable {
val envs = executable.environmentVariables.toMutableMap()

val isOpenTelemetryAvailable = openTelemetryPort != null
if (isOpenTelemetryAvailable) {
envs[OTEL_EXPORTER_OTLP_ENDPOINT] = getOtlpEndpoint(requireNotNull(openTelemetryPort))
}

return if (isOpenTelemetryAvailable) {
executable.copy(environmentVariables = envs)
} else {
executable
}
}

private suspend fun createAndStartDebugSession(
sessionId: String,
@Nls sessionName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class SessionManager(private val project: Project, scope: CoroutineScope) {
val model: SessionModel,
val lifetimes: SequentialLifetimes,
val events: MutableSharedFlow<SessionEvent>,
val openTelemetryProtocolServerPort: Int
val openTelemetryProtocolServerPort: Int?
)

interface LaunchSessionCommand
Expand Down
Loading

0 comments on commit 4a207b4

Please sign in to comment.