Skip to content
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 @@ -5,20 +5,17 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp

import com.google.gson.ToNumberPolicy
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.impl.ExecutionManagerImpl
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutputType
import com.intellij.notification.NotificationAction
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
Expand Down Expand Up @@ -82,12 +79,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolder
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
import software.aws.toolkits.jetbrains.settings.LspSettings
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Telemetry
import java.io.IOException
import java.io.OutputStreamWriter
import java.io.PipedInputStream
Expand All @@ -96,7 +89,6 @@ import java.net.Proxy
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
Expand Down Expand Up @@ -460,7 +452,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
}

val node = if (SystemInfo.isWindows) "node.exe" else "node"
val nodePath = getNodeRuntimePath(artifact.resolve(node))
val nodePath = NodeExePatcher.getNodeRuntimePath(project, artifact.resolve(node))
val emptyFile = Files.createTempFile("empty", null).toAbsolutePath().toString()

val cmd = NodeExePatcher.patch(nodePath)
Expand Down Expand Up @@ -597,124 +589,6 @@ private class AmazonQServerInstance(private val project: Project, private val cs
}
}

/**
* Resolves the path to a valid Node.js runtime in the following order of preference:
* 1. Uses the provided nodePath if it exists and is executable
* 2. Uses user-specified runtime path from LSP settings if available
* 3. Uses system Node.js if version 18+ is available
* 4. Falls back to original nodePath with a notification to configure runtime
*
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
* @return Path The resolved Node.js runtime path to use for the LSP server
*
* Side effects:
* - Logs warnings if initial runtime path is invalid
* - Logs info when using alternative runtime path
* - Shows notification to user if no valid Node.js runtime is found
*
* Note: The function will return a path even if no valid runtime is found, but the LSP server
* may fail to start in that case. The caller should handle potential runtime initialization failures.
*/
private fun getNodeRuntimePath(nodePath: Path): Path {
val resolveNodeMetric = { isBundled: Boolean, success: Boolean ->
Telemetry.languageserver.setup.use {
it.id("q")
it.metadata("languageServerSetupStage", "resolveNode")
it.metadata("credentialStartUrl", getStartUrl(project))
it.setAttribute("isBundledNode", isBundled)
it.success(success)
}
}

// attempt to use user provided node runtime path
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
if (!nodeRuntime.isNullOrEmpty()) {
LOG.info { "Using node from $nodeRuntime " }

resolveNodeMetric(false, true)
return Path.of(nodeRuntime)
}

// attempt to use bundled node
if (Files.exists(nodePath) && Files.isExecutable(nodePath) && validateNode(nodePath) != null) {
resolveNodeMetric(true, true)
return nodePath
} else {
// use alternative node runtime if it is not found
LOG.warn { "Node Runtime download failed. Fallback to user environment search" }

val localNode = locateNodeCommand()
if (localNode != null) {
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }

resolveNodeMetric(false, true)
return localNode
}
notifyInfo(
"Amazon Q",
message("amazonqFeatureDev.placeholder.node_runtime_message"),
project = project,
listOf(
NotificationAction.create(
message("codewhisperer.actions.open_settings.title")
) { _, notification ->
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
},
NotificationAction.create(
message("codewhisperer.notification.custom.simple.button.got_it")
) { _, notification -> notification.expire() }
)
)

resolveNodeMetric(false, false)
return nodePath
}
}

/**
* Locates node executable ≥18 in system PATH.
* Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
*
* @return Path? The absolute path to node ≥18 if found, null otherwise
*/
private fun locateNodeCommand(): Path? {
val exeName = if (SystemInfo.isWindows) "node.exe" else "node"

return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName)
.asSequence()
.map { it.toPath() }
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
.firstNotNullOfOrNull(::validateNode)
}

/** @return null if node is not suitable **/
private fun validateNode(path: Path) = try {
val process = ProcessBuilder(path.toString(), "--version")
.redirectErrorStream(true)
.start()

if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroy()
null
} else if (process.exitValue() == 0) {
val version = process.inputStream.bufferedReader().readText().trim()
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()

if (majorVersion != null && majorVersion >= 18) {
path.toAbsolutePath()
} else {
LOG.debug { "Node version < 18 found at: $path (version: $version)" }
null
}
} else {
LOG.debug { "Failed to get version from node at: $path" }
null
}
} catch (e: Exception) {
LOG.debug(e) { "Failed to check version for node at: $path" }
null
}

override fun dispose() {
if (!launcherFuture.isDone) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@
package software.aws.toolkits.jetbrains.services.amazonq.lsp

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.util.ExecUtil
import com.intellij.notification.NotificationAction
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.system.CpuArch
import com.intellij.util.text.nullize
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.settings.LspSettings
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Telemetry
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

Expand Down Expand Up @@ -38,6 +52,128 @@
}
}

/**
* Resolves the path to a valid Node.js runtime in the following order of preference:
* 1. Uses the provided nodePath if it exists and is executable
* 2. Uses user-specified runtime path from LSP settings if available
* 3. Uses system Node.js if version 18+ is available
* 4. Falls back to original nodePath with a notification to configure runtime
*
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
* @return Path The resolved Node.js runtime path to use for the LSP server
*
* Side effects:
* - Logs warnings if initial runtime path is invalid
* - Logs info when using alternative runtime path
* - Shows notification to user if no valid Node.js runtime is found
*
* Note: The function will return a path even if no valid runtime is found, but the LSP server
* may fail to start in that case. The caller should handle potential runtime initialization failures.
*/
fun getNodeRuntimePath(project: Project, nodePath: Path): Path {
val resolveNodeMetric = { isBundled: Boolean, success: Boolean ->
Telemetry.languageserver.setup.use {
it.id("q")
.metadata("languageServerSetupStage", "resolveNode")
.metadata("credentialStartUrl", getStartUrl(project))
.metadata("isBundledNode", isBundled.toString())
.success(success)
}
}

// attempt to use user provided node runtime path
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
if (!nodeRuntime.isNullOrEmpty()) {
LOG.info { "Using node from $nodeRuntime " }

resolveNodeMetric(false, true)
return Path.of(nodeRuntime)
}

// attempt to use bundled node
if (Files.exists(nodePath) && validateNode(nodePath) != null) {
resolveNodeMetric(true, true)
return nodePath
} else {
// use alternative node runtime if it is not found
LOG.warn { "Node Runtime download failed. Fallback to user environment search" }

val localNode = locateNodeCommand()
if (localNode != null) {
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }

resolveNodeMetric(false, true)
return localNode
}
notifyInfo(
"Amazon Q",
message("amazonqFeatureDev.placeholder.node_runtime_message"),
project = project,
listOf(
NotificationAction.create(
message("codewhisperer.actions.open_settings.title")

Check warning on line 114 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/NodeExePatcher.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Incorrect string capitalization

String 'Open Settings' is not properly capitalized. It should have sentence capitalization

Check warning

Code scanning / QDJVMC

Incorrect string capitalization Warning

String 'Open Settings' is not properly capitalized. It should have sentence capitalization
) { _, notification ->
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
},
NotificationAction.create(
message("codewhisperer.notification.custom.simple.button.got_it")
) { _, notification -> notification.expire() }
)
)

resolveNodeMetric(false, false)
return nodePath
}
}

/**
* Locates node executable ≥18 in system PATH.
* Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
*
* @return Path? The absolute path to node ≥18 if found, null otherwise
*/
private fun locateNodeCommand(): Path? {
val exeName = if (SystemInfo.isWindows) "node.exe" else "node"

return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName)
.asSequence()
.map { it.toPath() }
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
.firstNotNullOfOrNull(::validateNode)
}

/** @return null if node is not suitable **/
private fun validateNode(path: Path) = try {
val process = patch(path)
.withParameters("--version")
.withRedirectErrorStream(true)
val output = ExecUtil.execAndGetOutput(
process,
5000
)

LOG.debug { "$process: ${output.stdout.trim()}" }

if (output.exitCode == 0) {
val version = output.stdout.trim()
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()

if (majorVersion != null && majorVersion >= 18) {
LOG.debug { "Node $version found at: $path" }
path.toAbsolutePath()
} else {
LOG.debug { "Node version < 18 found at: $path (version: $version)" }
null
}
} else {
LOG.debug { "Failed to get version from node at: $path" }
null
}
} catch (e: Exception) {
LOG.debug(e) { "Failed to check version for node at: $path" }
null
}

private val linker
get() = System.getenv(GLIBC_LINKER_VAR).nullize(true) ?: let {
if (CpuArch.isArm64()) {
Expand All @@ -51,4 +187,6 @@

private val glibc
get() = System.getenv(GLIBC_PATH_VAR).nullize(true) ?: INTERNAL_GLIBC_PATH

private val LOG = getLogger<NodeExePatcher>()
}
Loading
Loading