Skip to content

Commit 24bf189

Browse files
Merge main into feature/disable-sspc
2 parents 8fc7581 + ac3bcb3 commit 24bf189

File tree

3 files changed

+205
-127
lines changed

3 files changed

+205
-127
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 1 addition & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,17 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp
55

66
import com.google.gson.ToNumberPolicy
77
import com.intellij.execution.configurations.GeneralCommandLine
8-
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
98
import com.intellij.execution.impl.ExecutionManagerImpl
109
import com.intellij.execution.process.KillableColoredProcessHandler
1110
import com.intellij.execution.process.KillableProcessHandler
1211
import com.intellij.execution.process.ProcessEvent
1312
import com.intellij.execution.process.ProcessListener
1413
import com.intellij.execution.process.ProcessOutputType
15-
import com.intellij.notification.NotificationAction
1614
import com.intellij.openapi.Disposable
1715
import com.intellij.openapi.application.ApplicationManager
1816
import com.intellij.openapi.components.Service
1917
import com.intellij.openapi.components.service
2018
import com.intellij.openapi.components.serviceIfCreated
21-
import com.intellij.openapi.options.ShowSettingsUtil
2219
import com.intellij.openapi.project.Project
2320
import com.intellij.openapi.util.Disposer
2421
import com.intellij.openapi.util.Key
@@ -82,12 +79,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolder
8279
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
8380
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
8481
import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
85-
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
8682
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
8783
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
9184
import java.io.IOException
9285
import java.io.OutputStreamWriter
9386
import java.io.PipedInputStream
@@ -96,7 +89,6 @@ import java.net.Proxy
9689
import java.net.URI
9790
import java.nio.charset.StandardCharsets
9891
import java.nio.file.Files
99-
import java.nio.file.Path
10092
import java.util.concurrent.Future
10193
import java.util.concurrent.TimeUnit
10294
import 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 {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/NodeExePatcher.kt

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,25 @@
44
package software.aws.toolkits.jetbrains.services.amazonq.lsp
55

66
import com.intellij.execution.configurations.GeneralCommandLine
7+
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
8+
import com.intellij.execution.util.ExecUtil
9+
import com.intellij.notification.NotificationAction
10+
import com.intellij.openapi.options.ShowSettingsUtil
11+
import com.intellij.openapi.project.Project
12+
import com.intellij.openapi.util.SystemInfo
713
import com.intellij.util.system.CpuArch
814
import com.intellij.util.text.nullize
15+
import software.aws.toolkits.core.utils.debug
916
import software.aws.toolkits.core.utils.exists
1017
import software.aws.toolkits.core.utils.getLogger
1118
import software.aws.toolkits.core.utils.info
19+
import software.aws.toolkits.core.utils.warn
20+
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
21+
import software.aws.toolkits.jetbrains.settings.LspSettings
22+
import software.aws.toolkits.jetbrains.utils.notifyInfo
23+
import software.aws.toolkits.resources.message
24+
import software.aws.toolkits.telemetry.Telemetry
25+
import java.nio.file.Files
1226
import java.nio.file.Path
1327
import java.nio.file.Paths
1428

@@ -38,6 +52,128 @@ object NodeExePatcher {
3852
}
3953
}
4054

55+
/**
56+
* Resolves the path to a valid Node.js runtime in the following order of preference:
57+
* 1. Uses the provided nodePath if it exists and is executable
58+
* 2. Uses user-specified runtime path from LSP settings if available
59+
* 3. Uses system Node.js if version 18+ is available
60+
* 4. Falls back to original nodePath with a notification to configure runtime
61+
*
62+
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
63+
* @return Path The resolved Node.js runtime path to use for the LSP server
64+
*
65+
* Side effects:
66+
* - Logs warnings if initial runtime path is invalid
67+
* - Logs info when using alternative runtime path
68+
* - Shows notification to user if no valid Node.js runtime is found
69+
*
70+
* Note: The function will return a path even if no valid runtime is found, but the LSP server
71+
* may fail to start in that case. The caller should handle potential runtime initialization failures.
72+
*/
73+
fun getNodeRuntimePath(project: Project, nodePath: Path): Path {
74+
val resolveNodeMetric = { isBundled: Boolean, success: Boolean ->
75+
Telemetry.languageserver.setup.use {
76+
it.id("q")
77+
.metadata("languageServerSetupStage", "resolveNode")
78+
.metadata("credentialStartUrl", getStartUrl(project))
79+
.metadata("isBundledNode", isBundled.toString())
80+
.success(success)
81+
}
82+
}
83+
84+
// attempt to use user provided node runtime path
85+
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
86+
if (!nodeRuntime.isNullOrEmpty()) {
87+
LOG.info { "Using node from $nodeRuntime " }
88+
89+
resolveNodeMetric(false, true)
90+
return Path.of(nodeRuntime)
91+
}
92+
93+
// attempt to use bundled node
94+
if (Files.exists(nodePath) && validateNode(nodePath) != null) {
95+
resolveNodeMetric(true, true)
96+
return nodePath
97+
} else {
98+
// use alternative node runtime if it is not found
99+
LOG.warn { "Node Runtime download failed. Fallback to user environment search" }
100+
101+
val localNode = locateNodeCommand()
102+
if (localNode != null) {
103+
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }
104+
105+
resolveNodeMetric(false, true)
106+
return localNode
107+
}
108+
notifyInfo(
109+
"Amazon Q",
110+
message("amazonqFeatureDev.placeholder.node_runtime_message"),
111+
project = project,
112+
listOf(
113+
NotificationAction.create(
114+
message("codewhisperer.actions.open_settings.title")
115+
) { _, notification ->
116+
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
117+
},
118+
NotificationAction.create(
119+
message("codewhisperer.notification.custom.simple.button.got_it")
120+
) { _, notification -> notification.expire() }
121+
)
122+
)
123+
124+
resolveNodeMetric(false, false)
125+
return nodePath
126+
}
127+
}
128+
129+
/**
130+
* Locates node executable ≥18 in system PATH.
131+
* Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
132+
*
133+
* @return Path? The absolute path to node ≥18 if found, null otherwise
134+
*/
135+
private fun locateNodeCommand(): Path? {
136+
val exeName = if (SystemInfo.isWindows) "node.exe" else "node"
137+
138+
return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName)
139+
.asSequence()
140+
.map { it.toPath() }
141+
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
142+
.firstNotNullOfOrNull(::validateNode)
143+
}
144+
145+
/** @return null if node is not suitable **/
146+
private fun validateNode(path: Path) = try {
147+
val process = patch(path)
148+
.withParameters("--version")
149+
.withRedirectErrorStream(true)
150+
val output = ExecUtil.execAndGetOutput(
151+
process,
152+
5000
153+
)
154+
155+
LOG.debug { "$process: ${output.stdout.trim()}" }
156+
157+
if (output.exitCode == 0) {
158+
val version = output.stdout.trim()
159+
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()
160+
161+
if (majorVersion != null && majorVersion >= 18) {
162+
LOG.debug { "Node $version found at: $path" }
163+
path.toAbsolutePath()
164+
} else {
165+
LOG.debug { "Node version < 18 found at: $path (version: $version)" }
166+
null
167+
}
168+
} else {
169+
LOG.debug { "Failed to get version from node at: $path" }
170+
null
171+
}
172+
} catch (e: Exception) {
173+
LOG.debug(e) { "Failed to check version for node at: $path" }
174+
null
175+
}
176+
41177
private val linker
42178
get() = System.getenv(GLIBC_LINKER_VAR).nullize(true) ?: let {
43179
if (CpuArch.isArm64()) {
@@ -51,4 +187,6 @@ object NodeExePatcher {
51187

52188
private val glibc
53189
get() = System.getenv(GLIBC_PATH_VAR).nullize(true) ?: INTERNAL_GLIBC_PATH
190+
191+
private val LOG = getLogger<NodeExePatcher>()
54192
}

0 commit comments

Comments
 (0)