Skip to content

Commit d0b5a1b

Browse files
committed
get node from env
1 parent 2e07596 commit d0b5a1b

File tree

2 files changed

+122
-26
lines changed

2 files changed

+122
-26
lines changed

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

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import java.nio.charset.StandardCharsets
9292
import java.nio.file.Files
9393
import java.nio.file.Path
9494
import java.util.concurrent.Future
95+
import java.util.concurrent.TimeUnit
9596
import kotlin.time.Duration.Companion.seconds
9697

9798
// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
@@ -377,32 +378,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
377378
}
378379

379380
val node = if (SystemInfo.isWindows) "node.exe" else "node"
380-
var nodePath = artifact.resolve(node)
381-
// download node runtime if it is not found
382-
if (!Files.exists(nodePath) || !Files.isExecutable(nodePath)) {
383-
LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " }
384-
// attempt to use user provided node runtime path
385-
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
386-
if (!nodeRuntime.isNullOrEmpty()) {
387-
nodePath = Path.of(nodeRuntime)
388-
} else {
389-
notifyInfo(
390-
"Amazon Q",
391-
message("amazonqFeatureDev.placeholder.node_runtime_message"),
392-
project = project,
393-
listOf(
394-
NotificationAction.create(
395-
message("codewhisperer.actions.open_settings.title")
396-
) { _, notification ->
397-
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
398-
},
399-
NotificationAction.create(
400-
message("codewhisperer.notification.custom.simple.button.got_it")
401-
) { _, notification -> notification.expire() }
402-
)
403-
)
404-
}
405-
}
381+
var nodePath = getNodeRuntimePath(artifact.resolve(node))
406382

407383
val cmd = NodeExePatcher.patch(nodePath)
408384
.withParameters(
@@ -524,6 +500,125 @@ private class AmazonQServerInstance(private val project: Project, private val cs
524500
}
525501
}
526502

503+
/**
504+
* Resolves the path to a valid Node.js runtime in the following order of preference:
505+
* 1. Uses the provided nodePath if it exists and is executable
506+
* 2. Uses user-specified runtime path from LSP settings if available
507+
* 3. Uses system Node.js if version 18+ is available
508+
* 4. Falls back to original nodePath with a notification to configure runtime
509+
*
510+
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
511+
* @return Path The resolved Node.js runtime path to use for the LSP server
512+
*
513+
* Side effects:
514+
* - Logs warnings if initial runtime path is invalid
515+
* - Logs info when using alternative runtime path
516+
* - Shows notification to user if no valid Node.js runtime is found
517+
*
518+
* Note: The function will return a path even if no valid runtime is found, but the LSP server
519+
* may fail to start in that case. The caller should handle potential runtime initialization failures.
520+
*/
521+
private fun getNodeRuntimePath(nodePath: Path): Path {
522+
if (Files.exists(nodePath) && Files.isExecutable(nodePath)) {
523+
return nodePath
524+
}
525+
// use alternative node runtime if it is not found
526+
LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " }
527+
// attempt to use user provided node runtime path
528+
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
529+
if (!nodeRuntime.isNullOrEmpty()) {
530+
LOG.info { "Using node from $nodeRuntime " }
531+
return Path.of(nodeRuntime)
532+
} else {
533+
val localNode = locateNodeCommand()
534+
if (localNode != null) {
535+
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }
536+
return localNode
537+
}
538+
notifyInfo(
539+
"Amazon Q",
540+
message("amazonqFeatureDev.placeholder.node_runtime_message"),
541+
project = project,
542+
listOf(
543+
NotificationAction.create(
544+
message("codewhisperer.actions.open_settings.title")
545+
) { _, notification ->
546+
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
547+
},
548+
NotificationAction.create(
549+
message("codewhisperer.notification.custom.simple.button.got_it")
550+
) { _, notification -> notification.expire() }
551+
)
552+
)
553+
return nodePath
554+
}
555+
}
556+
557+
/**
558+
* Locates node executable using platform-specific command.
559+
* Uses 'where' on Windows and 'which' on Unix-like systems.
560+
* Only gets node newer than node 18!
561+
* @return Path? The absolute path to node if found via where/which, null otherwise
562+
*/
563+
private fun locateNodeCommand(): Path? {
564+
val command = if (SystemInfo.isWindows) {
565+
arrayOf("where", "node.exe")
566+
} else {
567+
arrayOf("which", "node")
568+
}
569+
570+
return try {
571+
val process = ProcessBuilder(*command)
572+
.redirectErrorStream(true)
573+
.start()
574+
575+
if (!process.waitFor(2, TimeUnit.SECONDS)) {
576+
process.destroy()
577+
return null
578+
}
579+
580+
if (process.exitValue() != 0) {
581+
return null
582+
}
583+
584+
// where/which can return multiple lines - check each path until we find node ≥18
585+
process.inputStream.bufferedReader()
586+
.lineSequence()
587+
.map { Path.of(it.trim()) }
588+
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
589+
.firstNotNullOfOrNull { path ->
590+
// Check version for each found node
591+
val versionProcess = ProcessBuilder(path.toString(), "--version")
592+
.redirectErrorStream(true)
593+
.start()
594+
595+
try {
596+
if (!versionProcess.waitFor(2, TimeUnit.SECONDS)) {
597+
versionProcess.destroy()
598+
null
599+
} else if (versionProcess.exitValue() == 0) {
600+
val version = versionProcess.inputStream.bufferedReader().readText().trim()
601+
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()
602+
603+
if (majorVersion != null && majorVersion >= 18) {
604+
path.toAbsolutePath()
605+
} else {
606+
null
607+
}
608+
} else {
609+
null
610+
}
611+
} catch (e: Exception) {
612+
LOG.debug(e) { "Failed to check version for node at: $path" }
613+
null
614+
}
615+
}
616+
} catch (e: Exception) {
617+
LOG.debug(e) { "Failed to locate node using ${command.joinToString(" ")}" }
618+
null
619+
}
620+
}
621+
527622
override fun dispose() {
528623
if (!launcherFuture.isDone) {
529624
try {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
114114
logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" }
115115

116116
try {
117+
throw Error("ee")
117118
return withBackgroundProgress(
118119
project,
119120
AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"),

0 commit comments

Comments
 (0)