@@ -92,6 +92,7 @@ import java.nio.charset.StandardCharsets
9292import java.nio.file.Files
9393import java.nio.file.Path
9494import java.util.concurrent.Future
95+ import java.util.concurrent.TimeUnit
9596import 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 {
0 commit comments