Skip to content

Commit ad1c5a0

Browse files
authored
Fix 'not recognized as an internal or external command' for dev envs on windows (#3853)
1 parent 773a115 commit ad1c5a0

File tree

8 files changed

+122
-77
lines changed

8 files changed

+122
-77
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Fix 'not recognzied as an ... command' when connecting to CodeCatalyst Dev Environments on Windows"
4+
}

jetbrains-gateway/src/software/aws/toolkits/jetbrains/gateway/connection/AbstractSsmCommandExecutor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ abstract class AbstractSsmCommandExecutor(private val region: AwsRegion, protect
161161

162162
private fun newSshCommand() = SsmCommandLineFactory(ssmTarget, startSsh(), region).sshCommand()
163163

164-
fun proxyCommand() = SsmCommandLineFactory(ssmTarget, startSsh(), region).generateProxyCommand()
164+
fun proxyCommand() = SsmCommandLineFactory(ssmTarget, startSsh(), region).proxyCommand()
165165

166166
private companion object {
167167
private val LOG = getLogger<AbstractSsmCommandExecutor>()

jetbrains-gateway/src/software/aws/toolkits/jetbrains/gateway/connection/SsmCommandLineFactory.kt

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
package software.aws.toolkits.jetbrains.gateway.connection
55

6+
import com.intellij.execution.CommandLineUtil
7+
import com.intellij.execution.Platform
68
import com.intellij.execution.configurations.GeneralCommandLine
79
import com.intellij.openapi.util.SystemInfo
810
import software.aws.toolkits.core.region.AwsRegion
9-
import software.aws.toolkits.jetbrains.AwsToolkit
1011
import software.aws.toolkits.jetbrains.core.tools.ToolManager
1112
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin
1213

@@ -23,56 +24,59 @@ class SsmCommandLineFactory(
2324
private val ssmTarget: String,
2425
private val sessionParameters: StartSessionResponse,
2526
private val region: AwsRegion,
26-
private val overrideSsmPlugin: String? = null,
27-
private val overrideWindowsWrapper: String? = null
27+
private val overrideSsmPlugin: String? = null
2828
) {
2929
fun sshCommand(): SshCommandLine {
3030
val command = SshCommandLine(ssmTarget)
31-
val proxyCommand = generateProxyCommand()
32-
command.addSshOption("-o", "ProxyCommand=${proxyCommand.commandString}")
31+
command.addSshOption("-o", "ProxyCommand=${proxyCommand()}")
3332
command.addSshOption("-o", "ServerAliveInterval=60")
34-
command.withEnvironment(proxyCommand.environment)
3533

3634
return command
3735
}
3836

3937
fun scpCommand(remotePath: String, recursive: Boolean = false): ScpCommandLine {
4038
val command = ScpCommandLine(ssmTarget, remotePath, recursive)
41-
val proxyCommand = generateProxyCommand()
42-
command.addSshOption("-o", "ProxyCommand=${proxyCommand.commandString}")
43-
command.withEnvironment(proxyCommand.environment)
39+
command.addSshOption("-o", "ProxyCommand=${proxyCommand()}")
4440

4541
return command
4642
}
4743

44+
/**
45+
* This returns a GeneralCommandLine is meant to be executed directly.
46+
* Use [proxyCommand] instead if you need a value for the SSH "ProxyCommand" property
47+
*/
4848
fun rawCommand(): GeneralCommandLine = generateProxyCommand().let {
4949
GeneralCommandLine(it.exePath)
50-
.withEnvironment(it.environment)
51-
.apply {
52-
if (it.args != null) {
53-
withParameters(it.args)
54-
}
55-
}
50+
.withParameters(it.args)
5651
}
5752

5853
inner class ProxyCommand(
5954
val exePath: String,
60-
val ssmPayload: String? = null,
61-
val environment: Map<String, String> = emptyMap()
62-
) {
63-
val commandString by lazy {
55+
val args: List<String>
56+
)
57+
58+
/**
59+
* This is meant to be passed directly as a value into the SSH "ProxyCommand" property
60+
* Use [rawCommand] instead for command execution
61+
*/
62+
fun proxyCommand(): String {
63+
val rawCommand = rawCommand()
64+
65+
return if (SystemInfo.isWindows) {
66+
// see [GeneralCommandLine.getPreparedCommandLine]
67+
CommandLineUtil.toCommandLine(rawCommand.exePath, rawCommand.parametersList.list, Platform.current()).joinToString(separator = " ")
68+
} else {
69+
// on *nix, the quoting on getPreparedCommandLine is not quite correct since the arguments aren't being passed directly to execv
6470
buildString {
65-
append(exePath)
66-
if (ssmPayload != null) {
67-
append(""" '$ssmPayload' ${region.id} StartSession""")
71+
append(rawCommand.exePath)
72+
rawCommand.parametersList.list.forEach {
73+
append(" '$it'")
6874
}
6975
}
7076
}
71-
72-
val args = ssmPayload?.let { listOf(it, region.id, "StartSession") }
7377
}
7478

75-
fun generateProxyCommand(): ProxyCommand {
79+
private fun generateProxyCommand(): ProxyCommand {
7680
val ssmPluginJson = """
7781
{
7882
"streamUrl":"${sessionParameters.streamUrl}",
@@ -84,21 +88,9 @@ class SsmCommandLineFactory(
8488
val ssmPath = overrideSsmPlugin
8589
?: ToolManager.getInstance().getOrInstallTool(SsmPlugin).path.toAbsolutePath().toString()
8690

87-
return if (SystemInfo.isWindows) {
88-
ProxyCommand(
89-
exePath = overrideWindowsWrapper
90-
?: AwsToolkit.pluginPath().resolve("gateway-resources").resolve("caws-proxy-command.bat").toAbsolutePath().toString(),
91-
environment = mapOf(
92-
"sessionManagerExe" to ssmPath,
93-
"sessionManagerJson" to '"' + ssmPluginJson.replace("\"", "\\\"") + '"',
94-
"region" to region.id
95-
)
96-
)
97-
} else {
98-
ProxyCommand(
99-
exePath = ssmPath,
100-
ssmPayload = ssmPluginJson
101-
)
102-
}
91+
return ProxyCommand(
92+
ssmPath,
93+
listOf(ssmPluginJson, region.id, "StartSession")
94+
)
10395
}
10496
}

jetbrains-gateway/src/software/aws/toolkits/jetbrains/gateway/connection/caws/CawsCommandExecutor.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class CawsCommandExecutor(
1515
ssmTarget: String,
1616
private val spaceName: String,
1717
private val projectName: String
18-
) : AbstractSsmCommandExecutor(REGION, ssmTarget) {
18+
) : AbstractSsmCommandExecutor(AwsRegion.GLOBAL, ssmTarget) {
1919
override fun startSsh(): StartSessionResponse =
2020
startSession {
2121
it.sessionConfiguration { session ->
@@ -49,9 +49,4 @@ class CawsCommandExecutor(
4949
tokenValue = session.accessDetails().tokenValue()
5050
)
5151
}
52-
53-
companion object {
54-
// TODO: devWorkspace APIs are only in us-west-2 at the moment
55-
private val REGION = AwsRegion("us-west-2", "us-west-2", "aws")
56-
}
5752
}

jetbrains-gateway/src/software/aws/toolkits/jetbrains/gateway/connection/caws/CawsSshConnectionConfigModifier.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.intellij.ssh.config.SshConnectionConfigService
99
import com.intellij.ssh.config.SshProxyConfig
1010
import software.aws.toolkits.jetbrains.core.awsClient
1111
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
12+
import software.aws.toolkits.jetbrains.gateway.connection.AbstractSsmCommandExecutor
1213

1314
class CawsSshConnectionConfigModifier : SshConnectionConfigService.Modifier {
1415
override fun modify(initialHost: String, connectionConfig: SshConnectionConfig): SshConnectionConfig {
@@ -24,13 +25,16 @@ class CawsSshConnectionConfigModifier : SshConnectionConfigService.Modifier {
2425
projectName = project
2526
)
2627

27-
return connectionConfig.copy(
28-
proxyConfig = SshProxyConfig.Command(executor.proxyCommand().commandString),
29-
hostKeyVerifier = PromiscuousSshHostKeyVerifier
30-
)
28+
return modify(executor, connectionConfig)
3129
}
3230

3331
companion object {
3432
const val HOST_PREFIX = "aws.codecatalyst:"
33+
34+
fun modify(executor: AbstractSsmCommandExecutor, connectionConfig: SshConnectionConfig): SshConnectionConfig =
35+
connectionConfig.copy(
36+
proxyConfig = SshProxyConfig.Command(executor.proxyCommand()),
37+
hostKeyVerifier = PromiscuousSshHostKeyVerifier
38+
)
3539
}
3640
}

jetbrains-gateway/src/software/aws/toolkits/jetbrains/gateway/connection/workflow/v2/StartBackendV2.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class StartBackendV2(
3232
override val stepName: String = message("gateway.connection.workflow.start_ide")
3333

3434
override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
35+
stepEmitter.emitMessageLine("Waiting for IDE to start on Dev Environment. See Gateway logs for details.", false)
36+
3537
val creds = RemoteCredentialsHolder().apply {
3638
setHost("${CawsSshConnectionConfigModifier.HOST_PREFIX}${identifier.friendlyString}")
3739
}

jetbrains-gateway/tst/software/aws/toolkits/jetbrains/gateway/connection/SsmCommandLineTest.kt

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,13 @@ class SsmCommandLineTest {
4040
"target",
4141
StartSessionResponse("session", "stream", "token"),
4242
AwsRegion.GLOBAL,
43-
overrideSsmPlugin = "session manager plugin",
44-
overrideWindowsWrapper = "session manager plugin"
43+
overrideSsmPlugin = "session manager plugin"
4544
).sshCommand()
4645

4746
if (SystemInfo.isWindows) {
4847
assertThat(sut.constructCommandLine().commandLineString).matches(
4948
"""
50-
(.*)?-o "ProxyCommand=session manager plugin"(.*)?
49+
(.*)?-o "ProxyCommand=session manager plugin (.*)?"(.*)?
5150
""".trimIndent().toPattern()
5251
)
5352
} else {
@@ -65,7 +64,7 @@ class SsmCommandLineTest {
6564
val sut = sutFactory.sshCommand()
6665
assertThat(sut.constructCommandLine().commandLineString).matches(
6766
"""
68-
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' aws-global StartSession" -o ServerAliveInterval=60
67+
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' 'aws-global' 'StartSession'" -o ServerAliveInterval=60
6968
""".trimIndent().toPattern()
7069
)
7170
}
@@ -76,18 +75,9 @@ class SsmCommandLineTest {
7675
val sut = sutFactory.sshCommand()
7776
assertThat(sut.constructCommandLine().commandLineString).matches(
7877
"""
79-
$prefix -o ProxyCommand=(.*)?caws-proxy-command.bat -o ServerAliveInterval=60
78+
$prefix -o "ProxyCommand=session-manager-plugin \\"\{\\\\"streamUrl\\\\":\\\\"stream\\\\",\\\\"tokenValue\\\\":\\\\"token\\\\",\\\\"sessionId\\\\":\\\\"session\\\\"}\\" aws-global StartSession" -o ServerAliveInterval=60
8079
""".trimIndent().toPattern()
8180
)
82-
83-
assertThat(sut.constructCommandLine().effectiveEnvironment)
84-
.containsAllEntriesOf(
85-
mapOf(
86-
"sessionManagerExe" to "session-manager-plugin",
87-
"sessionManagerJson" to """"{\"streamUrl\":\"stream\",\"tokenValue\":\"token\",\"sessionId\":\"session\"}"""",
88-
"region" to "aws-global"
89-
)
90-
)
9181
}
9282

9383
@Test
@@ -98,7 +88,7 @@ class SsmCommandLineTest {
9888

9989
assertThat(sut.constructCommandLine().commandLineString).matches(
10090
"""
101-
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' aws-global StartSession" localPath target:remote
91+
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' 'aws-global' 'StartSession'" localPath target:remote
10292
""".trimIndent().toPattern()
10393
)
10494
}
@@ -111,17 +101,8 @@ class SsmCommandLineTest {
111101

112102
assertThat(sut.constructCommandLine().commandLineString).matches(
113103
"""
114-
$prefix -o ProxyCommand=(.*)?caws-proxy-command.bat localPath target:remote
104+
$prefix-o "ProxyCommand=session-manager-plugin \\"\{\\\\"streamUrl\\\\":\\\\"stream\\\\",\\\\"tokenValue\\\\":\\\\"token\\\\",\\\\"sessionId\\\\":\\\\"session\\\\"}\\" aws-global StartSession" localPath target:remote
115105
""".trimIndent().toPattern()
116106
)
117-
118-
assertThat(sut.constructCommandLine().effectiveEnvironment)
119-
.containsAllEntriesOf(
120-
mapOf(
121-
"sessionManagerExe" to "session-manager-plugin",
122-
"sessionManagerJson" to """"{\"streamUrl\":\"stream\",\"tokenValue\":\"token\",\"sessionId\":\"session\"}"""",
123-
"region" to "aws-global"
124-
)
125-
)
126107
}
127108
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.gateway.connection.caws
5+
6+
import com.intellij.openapi.util.SystemInfo
7+
import com.intellij.ssh.PromiscuousSshHostKeyVerifier
8+
import com.intellij.ssh.config.SshConnectionConfig
9+
import com.intellij.ssh.config.SshProxyConfig
10+
import org.assertj.core.api.Assertions.assertThat
11+
import org.junit.Rule
12+
import org.junit.Test
13+
import org.mockito.kotlin.doReturn
14+
import org.mockito.kotlin.mock
15+
import software.aws.toolkits.core.region.AwsRegion
16+
import software.aws.toolkits.jetbrains.core.tools.MockToolManagerRule
17+
import software.aws.toolkits.jetbrains.core.tools.Tool
18+
import software.aws.toolkits.jetbrains.gateway.connection.AbstractSsmCommandExecutor
19+
import software.aws.toolkits.jetbrains.gateway.connection.StartSessionResponse
20+
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin
21+
import java.nio.file.Path
22+
23+
class CawsSshConnectionConfigModifierTest {
24+
@Rule
25+
@JvmField
26+
val toolManager = MockToolManagerRule()
27+
28+
@Test
29+
fun `modify only mutates CodeCatalyst targets`() {
30+
val initial = SshConnectionConfig("test")
31+
val sut = CawsSshConnectionConfigModifier()
32+
33+
assertThat(sut.modify(initial.host, initial)).isEqualTo(initial)
34+
}
35+
36+
@Test
37+
fun `modify adds proxy command to CodeCatalyst targets`() {
38+
val dummyExecutor = object : AbstractSsmCommandExecutor(AwsRegion.GLOBAL, "test") {
39+
val response = StartSessionResponse("session", "stream", "token")
40+
41+
override fun startSsh() = response
42+
override fun startSsm(exe: String, vararg args: String) = response
43+
}
44+
val mockPath = Path.of("ssm")
45+
val mockTool = mock<Tool<SsmPlugin>> {
46+
on {
47+
path
48+
}.doReturn(mockPath)
49+
}
50+
51+
toolManager.registerTool(SsmPlugin, mockTool)
52+
53+
val proxyCommand = if (SystemInfo.isWindows) {
54+
"""${mockPath.toAbsolutePath()} "{\"streamUrl\":\"stream\",\"tokenValue\":\"token\",\"sessionId\":\"session\"}" aws-global StartSession"""
55+
} else {
56+
"""${mockPath.toAbsolutePath()} '{"streamUrl":"stream","tokenValue":"token","sessionId":"session"}' 'aws-global' 'StartSession'"""
57+
}
58+
59+
assertThat(CawsSshConnectionConfigModifier.modify(dummyExecutor, SshConnectionConfig("test")))
60+
.isEqualTo(
61+
SshConnectionConfig("test").copy(
62+
proxyConfig = SshProxyConfig.Command(command = proxyCommand),
63+
hostKeyVerifier = PromiscuousSshHostKeyVerifier
64+
)
65+
)
66+
}
67+
}

0 commit comments

Comments
 (0)