@@ -5,20 +5,17 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp
55
66import com.google.gson.ToNumberPolicy
77import com.intellij.execution.configurations.GeneralCommandLine
8- import com.intellij.execution.configurations.PathEnvironmentVariableUtil
98import com.intellij.execution.impl.ExecutionManagerImpl
109import com.intellij.execution.process.KillableColoredProcessHandler
1110import com.intellij.execution.process.KillableProcessHandler
1211import com.intellij.execution.process.ProcessEvent
1312import com.intellij.execution.process.ProcessListener
1413import com.intellij.execution.process.ProcessOutputType
15- import com.intellij.notification.NotificationAction
1614import com.intellij.openapi.Disposable
1715import com.intellij.openapi.application.ApplicationManager
1816import com.intellij.openapi.components.Service
1917import com.intellij.openapi.components.service
2018import com.intellij.openapi.components.serviceIfCreated
21- import com.intellij.openapi.options.ShowSettingsUtil
2219import com.intellij.openapi.project.Project
2320import com.intellij.openapi.util.Disposer
2421import com.intellij.openapi.util.Key
@@ -82,12 +79,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolder
8279import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
8380import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
8481import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
85- import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
8682import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
8783import software.aws.toolkits.jetbrains.settings.LspSettings
88- import software.aws.toolkits.jetbrains.utils.notifyInfo
89- import software.aws.toolkits.resources.message
90- import software.aws.toolkits.telemetry.Telemetry
9184import java.io.IOException
9285import java.io.OutputStreamWriter
9386import java.io.PipedInputStream
@@ -96,7 +89,6 @@ import java.net.Proxy
9689import java.net.URI
9790import java.nio.charset.StandardCharsets
9891import java.nio.file.Files
99- import java.nio.file.Path
10092import java.util.concurrent.Future
10193import java.util.concurrent.TimeUnit
10294import kotlin.time.Duration.Companion.seconds
@@ -460,7 +452,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
460452 }
461453
462454 val node = if (SystemInfo .isWindows) " node.exe" else " node"
463- val nodePath = getNodeRuntimePath(artifact.resolve(node))
455+ val nodePath = NodeExePatcher . getNodeRuntimePath(project, artifact.resolve(node))
464456 val emptyFile = Files .createTempFile(" empty" , null ).toAbsolutePath().toString()
465457
466458 val cmd = NodeExePatcher .patch(nodePath)
@@ -597,124 +589,6 @@ private class AmazonQServerInstance(private val project: Project, private val cs
597589 }
598590 }
599591
600- /* *
601- * Resolves the path to a valid Node.js runtime in the following order of preference:
602- * 1. Uses the provided nodePath if it exists and is executable
603- * 2. Uses user-specified runtime path from LSP settings if available
604- * 3. Uses system Node.js if version 18+ is available
605- * 4. Falls back to original nodePath with a notification to configure runtime
606- *
607- * @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
608- * @return Path The resolved Node.js runtime path to use for the LSP server
609- *
610- * Side effects:
611- * - Logs warnings if initial runtime path is invalid
612- * - Logs info when using alternative runtime path
613- * - Shows notification to user if no valid Node.js runtime is found
614- *
615- * Note: The function will return a path even if no valid runtime is found, but the LSP server
616- * may fail to start in that case. The caller should handle potential runtime initialization failures.
617- */
618- private fun getNodeRuntimePath (nodePath : Path ): Path {
619- val resolveNodeMetric = { isBundled: Boolean , success: Boolean ->
620- Telemetry .languageserver.setup.use {
621- it.id(" q" )
622- it.metadata(" languageServerSetupStage" , " resolveNode" )
623- it.metadata(" credentialStartUrl" , getStartUrl(project))
624- it.setAttribute(" isBundledNode" , isBundled)
625- it.success(success)
626- }
627- }
628-
629- // attempt to use user provided node runtime path
630- val nodeRuntime = LspSettings .getInstance().getNodeRuntimePath()
631- if (! nodeRuntime.isNullOrEmpty()) {
632- LOG .info { " Using node from $nodeRuntime " }
633-
634- resolveNodeMetric(false , true )
635- return Path .of(nodeRuntime)
636- }
637-
638- // attempt to use bundled node
639- if (Files .exists(nodePath) && Files .isExecutable(nodePath) && validateNode(nodePath) != null ) {
640- resolveNodeMetric(true , true )
641- return nodePath
642- } else {
643- // use alternative node runtime if it is not found
644- LOG .warn { " Node Runtime download failed. Fallback to user environment search" }
645-
646- val localNode = locateNodeCommand()
647- if (localNode != null ) {
648- LOG .info { " Using node from ${localNode.toAbsolutePath()} " }
649-
650- resolveNodeMetric(false , true )
651- return localNode
652- }
653- notifyInfo(
654- " Amazon Q" ,
655- message(" amazonqFeatureDev.placeholder.node_runtime_message" ),
656- project = project,
657- listOf (
658- NotificationAction .create(
659- message(" codewhisperer.actions.open_settings.title" )
660- ) { _, notification ->
661- ShowSettingsUtil .getInstance().showSettingsDialog(project, message(" aws.settings.codewhisperer.configurable.title" ))
662- },
663- NotificationAction .create(
664- message(" codewhisperer.notification.custom.simple.button.got_it" )
665- ) { _, notification -> notification.expire() }
666- )
667- )
668-
669- resolveNodeMetric(false , false )
670- return nodePath
671- }
672- }
673-
674- /* *
675- * Locates node executable ≥18 in system PATH.
676- * Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
677- *
678- * @return Path? The absolute path to node ≥18 if found, null otherwise
679- */
680- private fun locateNodeCommand (): Path ? {
681- val exeName = if (SystemInfo .isWindows) " node.exe" else " node"
682-
683- return PathEnvironmentVariableUtil .findAllExeFilesInPath(exeName)
684- .asSequence()
685- .map { it.toPath() }
686- .filter { Files .isRegularFile(it) && Files .isExecutable(it) }
687- .firstNotNullOfOrNull(::validateNode)
688- }
689-
690- /* * @return null if node is not suitable **/
691- private fun validateNode (path : Path ) = try {
692- val process = ProcessBuilder (path.toString(), " --version" )
693- .redirectErrorStream(true )
694- .start()
695-
696- if (! process.waitFor(5 , TimeUnit .SECONDS )) {
697- process.destroy()
698- null
699- } else if (process.exitValue() == 0 ) {
700- val version = process.inputStream.bufferedReader().readText().trim()
701- val majorVersion = version.removePrefix(" v" ).split(" ." )[0 ].toIntOrNull()
702-
703- if (majorVersion != null && majorVersion >= 18 ) {
704- path.toAbsolutePath()
705- } else {
706- LOG .debug { " Node version < 18 found at: $path (version: $version )" }
707- null
708- }
709- } else {
710- LOG .debug { " Failed to get version from node at: $path " }
711- null
712- }
713- } catch (e: Exception ) {
714- LOG .debug(e) { " Failed to check version for node at: $path " }
715- null
716- }
717-
718592 override fun dispose () {
719593 if (! launcherFuture.isDone) {
720594 try {
0 commit comments