@@ -5,20 +5,17 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp
5
5
6
6
import com.google.gson.ToNumberPolicy
7
7
import com.intellij.execution.configurations.GeneralCommandLine
8
- import com.intellij.execution.configurations.PathEnvironmentVariableUtil
9
8
import com.intellij.execution.impl.ExecutionManagerImpl
10
9
import com.intellij.execution.process.KillableColoredProcessHandler
11
10
import com.intellij.execution.process.KillableProcessHandler
12
11
import com.intellij.execution.process.ProcessEvent
13
12
import com.intellij.execution.process.ProcessListener
14
13
import com.intellij.execution.process.ProcessOutputType
15
- import com.intellij.notification.NotificationAction
16
14
import com.intellij.openapi.Disposable
17
15
import com.intellij.openapi.application.ApplicationManager
18
16
import com.intellij.openapi.components.Service
19
17
import com.intellij.openapi.components.service
20
18
import com.intellij.openapi.components.serviceIfCreated
21
- import com.intellij.openapi.options.ShowSettingsUtil
22
19
import com.intellij.openapi.project.Project
23
20
import com.intellij.openapi.util.Disposer
24
21
import com.intellij.openapi.util.Key
@@ -82,12 +79,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolder
82
79
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
83
80
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
84
81
import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
85
- import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
86
82
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
87
83
import 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
91
84
import java.io.IOException
92
85
import java.io.OutputStreamWriter
93
86
import java.io.PipedInputStream
@@ -96,7 +89,6 @@ import java.net.Proxy
96
89
import java.net.URI
97
90
import java.nio.charset.StandardCharsets
98
91
import java.nio.file.Files
99
- import java.nio.file.Path
100
92
import java.util.concurrent.Future
101
93
import java.util.concurrent.TimeUnit
102
94
import kotlin.time.Duration.Companion.seconds
@@ -460,7 +452,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
460
452
}
461
453
462
454
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))
464
456
val emptyFile = Files .createTempFile(" empty" , null ).toAbsolutePath().toString()
465
457
466
458
val cmd = NodeExePatcher .patch(nodePath)
@@ -597,124 +589,6 @@ private class AmazonQServerInstance(private val project: Project, private val cs
597
589
}
598
590
}
599
591
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
-
718
592
override fun dispose () {
719
593
if (! launcherFuture.isDone) {
720
594
try {
0 commit comments