diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 8f6201bd12c..366527cec5f 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -2,6 +2,7 @@ import iconv from "iconv-lite"; import childProcess from "node:child_process"; import os from "node:os"; import { ContinueError, ContinueErrorReason } from "../../util/errors"; +import * as path from "node:path"; // Default timeout for terminal commands (2 minutes) const DEFAULT_TOOL_TIMEOUT_MS = 120_000; @@ -21,8 +22,42 @@ function getDecodedOutput(data: Buffer): string { } else { return data.toString(); } -} // Simple helper function to use login shell on Unix/macOS and PowerShell on Windows -function getShellCommand(command: string): { shell: string; args: string[] } { +} + +// Simple helper function to use login shell on Unix/macOS and PowerShell on Windows +// Detects WSL paths and uses the appropriate WSL shell +function getShellCommand( + command: string, + cwd?: string, +): { shell: string; args: string[] } { + // Check if the working directory is in WSL on Intellij + const isWslPath = + cwd && (cwd.includes("\\wsl.localhost\\") || cwd.includes("\\wsl$\\")); + + if (isWslPath) { + // Extract the WSL distribution name from the path + // Path format: \\wsl.localhost\UbuntuAny\home\user\project + // or: \\wsl$\UbuntuAny\home\user\project + let distroName = "Ubuntu"; // Default fallback + try { + const wslPathSegments = cwd.split(/\\+/); + // Find the segment after wsl.localhost or wsl$ + const wslIndex = wslPathSegments.findIndex( + (seg) => seg.includes("wsl.localhost") || seg.includes("wsl$"), + ); + if (wslIndex !== -1 && wslIndex + 1 < wslPathSegments.length) { + distroName = wslPathSegments[wslIndex + 1]; + } + } catch { + // If parsing fails, use default Ubuntu + } + // Use wsl.exe to run commands in the specific distribution + return { + shell: "wsl.exe", + args: ["-d", distroName, "-e", "bash", "-c", command], + }; + } + if (process.platform === "win32") { // Windows: Use PowerShell return { @@ -52,6 +87,17 @@ import { getBooleanArg, getStringArg } from "../parseArgs"; * Falls back to home directory or temp directory if no workspace is available. */ function resolveWorkingDirectory(workspaceDirs: string[]): string { + // Check for valid existing local paths first (IntelliJ support) so \\wsl$\ or \\wsl.local\ can pass + for (const dir of workspaceDirs) { + try { + if (dir.startsWith("file:////wsl")) { + return path.normalize(dir.replace("file://", "")); + } + } catch { + // Ignore errors (e.g. invalid path formats) + } + } + // Handle file:// URIs (local workspaces) const fileWorkspaceDir = workspaceDirs.find((dir) => dir.startsWith("file:/"), @@ -145,7 +191,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { } // Use spawn with color environment - const { shell, args } = getShellCommand(command); + const { shell, args } = getShellCommand(command, cwd); const childProc = childProcess.spawn(shell, args, { cwd, env: getColorEnv(), // Add enhanced environment for colors @@ -376,7 +422,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { try { // Use spawn approach for consistency with streaming version const { shell: nonStreamingShell, args: nonStreamingArgs } = - getShellCommand(command); + getShellCommand(command, cwd); const output = await new Promise<{ stdout: string; stderr: string }>( (resolve, reject) => { let timeoutId: ReturnType | undefined; @@ -503,8 +549,10 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { // but don't attach any listeners other than error try { // Use spawn with color environment - const { shell: detachedShell, args: detachedArgs } = - getShellCommand(command); + const { shell: detachedShell, args: detachedArgs } = getShellCommand( + command, + cwd, + ); const childProc = childProcess.spawn(detachedShell, detachedArgs, { cwd, env: getColorEnv(), // Add color environment diff --git a/extensions/intellij/.run/Run Continue WSL (CE).run.xml b/extensions/intellij/.run/Run Continue WSL (CE).run.xml new file mode 100644 index 00000000000..38bfd7038d7 --- /dev/null +++ b/extensions/intellij/.run/Run Continue WSL (CE).run.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/extensions/intellij/.run/Run Continue WSL.run.xml b/extensions/intellij/.run/Run Continue WSL.run.xml new file mode 100644 index 00000000000..9158883d891 --- /dev/null +++ b/extensions/intellij/.run/Run Continue WSL.run.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml b/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml new file mode 100644 index 00000000000..fb946b626c1 --- /dev/null +++ b/extensions/intellij/.run/Run Extension WSL (use TCP).run.xml @@ -0,0 +1,31 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts index 86231350ff7..529deb84cc9 100644 --- a/extensions/intellij/build.gradle.kts +++ b/extensions/intellij/build.gradle.kts @@ -119,10 +119,49 @@ tasks { } runIde { - val openProject = "$projectDir/../../manual-testing-sandbox" + val wslKernel = environment("WSL_KERNEL").getOrElse("") + val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") + val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 + val currentDrive = projectDir.absolutePath.first().lowercase() + + val openProject = if (wslKernel.isNotEmpty()) { + //If gradle.properties >= 2025.X -- can direct run mount from WSL windows path in Intellij + // instead of copy to /tmp -- IE \\wsl?\Ubuntu\mnt\c\Users + if (majorVersion >= 2025) { + "\\\\wsl$\\$wslKernel\\mnt\\$currentDrive" + projectDir.absolutePath.substring(2).replace("/extensions/intellij/", "") + .replace("/", "\\") + "\\..\\..\\manual-testing-sandbox" + } + //For now we must copy the test file to WSL filesystem to be cleaned up at exit + else { + "\\\\wsl$\\$wslKernel\\tmp\\manual-testing-sandbox" + } + } else { + "$projectDir/../../manual-testing-sandbox" + } + + doFirst { + if (wslKernel.isNotEmpty() && majorVersion < 2025) { + val wslSourcePath = "/mnt/$currentDrive" + projectDir.absolutePath.substring(2) + .replace("\\", "/") + "/../../manual-testing-sandbox" + val wslDestPath = "/tmp/manual-testing-sandbox" + val command = listOf("wsl", "-d", wslKernel, "-e", "cp", "-r", wslSourcePath, wslDestPath) + val process = ProcessBuilder(command).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + throw GradleException( + "Failed to copy $wslSourcePath to $wslKernel:$wslDestPath\" via WSL: ${ + process.errorStream.bufferedReader().readText() + }" + ) + } else { + logger.lifecycle("Successfully copied $wslSourcePath to $wslKernel:$wslDestPath") + } + } + } argumentProviders += CommandLineArgumentProvider { listOf(openProject, "$openProject/test.kt") } + finalizedBy("cleanupAfterRunIde") } test { @@ -132,6 +171,39 @@ tasks { } } +tasks.register("cleanupAfterRunIde") { + doLast { + val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1") + val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024 + //cleanup the temp file created in runIde if WSL test was used + if (majorVersion < 2025) { + val wslKernel = environment("WSL_KERNEL").getOrElse("") + if (wslKernel.isNotEmpty()) { + val wslDestPath = "/tmp/manual-testing-sandbox" + logger.lifecycle("Cleaning up WSL temp file: $wslDestPath in $wslKernel") + val command = listOf("wsl", "-d", wslKernel, "-e", "rm", "-rf", wslDestPath) + try { + val process = ProcessBuilder(command).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + logger.lifecycle( + "Warning: Failed to cleanup WSL temp file: ${ + process.errorStream.bufferedReader().readText() + }" + ) + } else { + logger.lifecycle("Successfully cleaned up $wslDestPath") + } + } catch (e: Exception) { + logger.lifecycle("Error during cleanup: ${e.message}") + } + + } + logger.lifecycle("IDE stopped. Performing cleanup...") + } + } +} + val testIntegration = task("testIntegration") { val integrationTestSourceSet = sourceSets.getByName("testIntegration") testClassesDirs = integrationTestSourceSet.output.classesDirs