Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update shell detection #3466

Merged
merged 41 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a541ffa
Update shell detection
MDrakos Aug 27, 2024
1807cee
Fix Windows detection
MDrakos Aug 27, 2024
958ba7f
Remove test code
MDrakos Aug 27, 2024
2a9fbc2
Add newline
MDrakos Aug 27, 2024
14e274f
Debug test failures
MDrakos Aug 27, 2024
27932e1
Debug
MDrakos Aug 27, 2024
e4f6e33
Change order in shell detection
MDrakos Aug 27, 2024
3f56d39
Add bash support on Windows
MDrakos Aug 28, 2024
84ba928
Remove debug logs
MDrakos Aug 28, 2024
05ff825
Merge branch 'version/0-47-0-RC1' into DX-3006
MDrakos Aug 28, 2024
efe42e9
Add back debug logging
MDrakos Aug 28, 2024
46e9a00
Set shell on Windows
MDrakos Aug 28, 2024
198b05a
Merge branch 'DX-3006' of github.com:ActiveState/cli into DX-3006
MDrakos Aug 28, 2024
9882ea8
Run Windows CI tests inside subshell
MDrakos Aug 29, 2024
d1a9f8a
Attempt to fix Windows tests
MDrakos Aug 29, 2024
976c6fc
Address more tests
MDrakos Aug 29, 2024
0f6e7d5
Revert install scripts test change
MDrakos Aug 29, 2024
408e7dd
Change powershell expect
MDrakos Aug 30, 2024
73fec74
Try another expect string
MDrakos Aug 30, 2024
471f509
Remove maybe option
MDrakos Sep 3, 2024
8d8234f
Use env var
MDrakos Sep 3, 2024
77c970f
Fix shells tests
MDrakos Sep 3, 2024
b0886ea
Shells test fix
MDrakos Sep 3, 2024
8af3168
Remove opt
MDrakos Sep 3, 2024
e2df1a7
Remove logging calls
MDrakos Sep 3, 2024
53f8d17
Merge branch 'DX-3006' of github.com:ActiveState/cli into DX-3006
MDrakos Sep 3, 2024
e6eeb7b
Fix env var name
MDrakos Sep 4, 2024
b6ddb92
Make shell name comparison case-insensitive
MDrakos Sep 4, 2024
7907c12
Handle filepaths
MDrakos Sep 4, 2024
723b520
Update argument name
MDrakos Sep 4, 2024
c97aadb
Use configured first
MDrakos Sep 4, 2024
4cf5cda
Clear configured shell in test
MDrakos Sep 4, 2024
f00d77c
Clear configured shell on Windows test
MDrakos Sep 4, 2024
e532c53
Revert config setting on Windows
MDrakos Sep 4, 2024
df47edd
Respect override
MDrakos Sep 5, 2024
4f23d47
Test revert backtick change
MDrakos Sep 5, 2024
7cb0820
Disable override for install scripts tests
MDrakos Sep 5, 2024
396fe68
Update internal/subshell/subshell.go
MDrakos Sep 5, 2024
f57b755
Make configured a fallback
MDrakos Sep 5, 2024
bc1e2ec
Make process error a multilog error
MDrakos Sep 5, 2024
a7ac5d8
Add back powershell change
MDrakos Sep 6, 2024
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
6 changes: 3 additions & 3 deletions cmd/state-installer/test/integration/installer_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() {
suite.NoError(err)

// Run installer with source-path flag (ie. install from this local path)
cp := ts.SpawnCmdWithOpts(
cp := ts.SpawnCmdInsideShellWithOpts(
suite.installerExe,
e2e.OptArgs(installationDir(ts), "-n"),
e2e.OptAppendEnv(constants.DisableUpdates+"=false"),
Expand All @@ -56,7 +56,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() {
cp.ExpectExitCode(0)

// Ensure installing overtop doesn't result in errors
cp = ts.SpawnCmdWithOpts(
cp = ts.SpawnCmdInsideShellWithOpts(
suite.installerExe,
e2e.OptArgs(installationDir(ts), "-n"),
e2e.OptAppendEnv(constants.DisableUpdates+"=false"),
Expand Down Expand Up @@ -171,7 +171,7 @@ func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() {
dir, err := os.MkdirTemp("", "system*")
suite.NoError(err)

cp := ts.SpawnCmdWithOpts(
cp := ts.SpawnCmdInsideShellWithOpts(
suite.installerExe,
e2e.OptArgs(installationDir(ts), "--activate", "ActiveState-CLI/Python3", "-n"),
e2e.OptAppendEnv(constants.DisableUpdates+"=true"),
Expand Down
31 changes: 14 additions & 17 deletions internal/subshell/subshell.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,20 +182,18 @@ func DetectShell(cfg sscommon.Configurable) (string, string) {
}
}()

binary = os.Getenv("SHELL")
if binary == "" && runtime.GOOS == "windows" {
binary = detectShellWindows()
binary = detectShellParent()
logging.Debug("Configured shell: %s", binary)
if binary == "" {
binary = configured
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I realized upon reading the changes that configured had a different connotation than I remembered. In my mind this was "what the user configured", but actually this is just a fallback we ourselves set when we detect the shell.

This is awkward and should probably be revisited, but given the current behaviour I this should probably be a fallback after shell parent and environment.

}

if binary == "" {
binary = configured
binary = os.Getenv(SHELL_ENV_VAR)
}

if binary == "" {
if runtime.GOOS == "windows" {
binary = "cmd.exe"
} else {
binary = "bash"
}
binary = OS_DEFULAT
}

path := resolveBinaryPath(binary)
Expand Down Expand Up @@ -239,24 +237,23 @@ func DetectShell(cfg sscommon.Configurable) (string, string) {
return name, path
}

func detectShellWindows() string {
// Windows does not provide a way of identifying which shell we are running in, so we have to look at the parent
// process.

func detectShellParent() string {
logging.Debug("Detecting shell from parent process")
p, err := process.NewProcess(int32(os.Getppid()))
if err != nil && !errors.As(err, ptr.To(&os.PathError{})) {
panic(err)
logging.Error("Failed to get parent process: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this was a panic, but we went from panicking to essentially ignoring it. Please return the error up the chain instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was originally a windows only function which may be a reason for the panic?

No other functions in this package return errors either as DetectShell has fallbacks so it always returns a shell name. This function is also used but subshell.New. So even if we get a fallback shell from either the SHELL env var or the system default we would error out because we couldn't detect the parent.

Again, I just want to check before making this change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, let's go with this for now, but please make it a multilog error so we can track this on rollbar. If this is happening a lot we'll want to know about it.

MDrakos marked this conversation as resolved.
Show resolved Hide resolved
}

for p != nil {
for p != nil && p.Pid != 0 {
name, err := p.Name()
if err == nil {
if strings.Contains(name, "cmd.exe") || strings.Contains(name, "powershell.exe") {
logging.Debug("Searching for supported shell in parent process: %s", name)
if supportedShellName(name) {
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
return name
}
}
p, _ = p.Parent()
}

return os.Getenv("ComSpec")
return ""
}
14 changes: 14 additions & 0 deletions internal/subshell/subshell_lin_mac.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,17 @@ var supportedShells = []SubShell{
&fish.SubShell{},
&cmd.SubShell{},
}

const (
SHELL_ENV_VAR = "SHELL"
OS_DEFULAT = "bash"
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
)

func supportedShellName(name string) bool {
for _, subshell := range supportedShells {
if name == subshell.Shell() {
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
return true
}
}
return false
}
24 changes: 23 additions & 1 deletion internal/subshell/subshell_win.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,30 @@

package subshell

import "github.com/ActiveState/cli/internal/subshell/cmd"
import (
"fmt"

"github.com/ActiveState/cli/internal/subshell/bash"
"github.com/ActiveState/cli/internal/subshell/cmd"
"github.com/ActiveState/cli/internal/subshell/pwsh"
)

var supportedShells = []SubShell{
&cmd.SubShell{},
&pwsh.SubShell{},
&bash.SubShell{},
}

const (
SHELL_ENV_VAR = "COMSPEC"
OS_DEFULAT = "cmd.exe"
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
)

func supportedShellName(name string) bool {
for _, subshell := range supportedShells {
if name == fmt.Sprintf("%s.exe", subshell.Shell()) {
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
return true
}
}
return false
}
6 changes: 6 additions & 0 deletions internal/testhelpers/e2e/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session
require.NoError(session.T, err)
}

if runtime.GOOS == "windows" {
if err := cfg.Set(subshell.ConfigKeyShell, "cmd.exe"); err != nil {
require.NoError(session.T, err)
}
}
MDrakos marked this conversation as resolved.
Show resolved Hide resolved

return session
}

Expand Down
18 changes: 18 additions & 0 deletions internal/testhelpers/e2e/session_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,21 @@ var (
RuntimeSourcingTimeoutOpt = termtest.OptExpectTimeout(3 * time.Minute)
RuntimeBuildSourcingTimeoutOpt = termtest.OptExpectTimeout(6 * time.Minute)
)

// SpawnInsideShell spawns the state tool executable to be tested with arguments.
// On Unix systems, this function is equivalent to Spawn.
func (s *Session) SpawnInsideShell(args ...string) *SpawnedCmd {
return s.SpawnCmd(s.Exe, args...)
}

// SpawnCmdInsideShellWithOpts spawns the state tool executable to be tested with arguments and options.
// On Unix systems, this function is equivalent to SpawnCmdWithOpts.
func (s *Session) SpawnCmdInsideShellWithOpts(exe string, opts ...SpawnOptSetter) *SpawnedCmd {
return s.SpawnCmdWithOpts(exe, opts...)
}

// SpawnInsideShell spawns the state tool executable to be tested with arguments.
// On Unix systems, this function is equivalent to SpawnWithOpts.
func (s *Session) SpawnInsideShellWithOpts(opts ...SpawnOptSetter) *SpawnedCmd {
return s.SpawnCmdWithOpts(s.Exe, opts...)
}
36 changes: 36 additions & 0 deletions internal/testhelpers/e2e/session_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,48 @@
package e2e

import (
"runtime"
"time"

"github.com/ActiveState/cli/internal/condition"
"github.com/ActiveState/termtest"
)

var (
RuntimeSourcingTimeoutOpt = termtest.OptExpectTimeout(3 * time.Minute)
RuntimeBuildSourcingTimeoutOpt = termtest.OptExpectTimeout(6 * time.Minute)
)

// SpawnInsideShell spawns the state tool executable to be tested with arguments.
// This function differs from Spawn in that it runs the command in a shell on Windows CI.
// Our Windows integration tests are run on bash. Due to the way the PTY library runs a new
// command we need to run the command inside a shell in order to setup the correct process
// tree. Without this integration tests run in bash will incorrectly identify the partent shell
// as bash, rather than the actual shell that is running the command
func (s *Session) SpawnInsideShell(args ...string) *SpawnedCmd {
opts := []SpawnOptSetter{OptArgs(args...)}
if runtime.GOOS == "windows" && condition.OnCI() {
opts = append(opts, OptRunInsideShell(true))
}
return s.SpawnCmdWithOpts(s.Exe, opts...)
}

// SpawnCmdInsideShellWithOpts spawns the executable to be tested with arguments and options.
// This function differs from SpawnCmdWithOpts in that it runs the command in a shell on Windows CI.
// See SpawnInsideShell for more information.
func (s *Session) SpawnCmdInsideShellWithOpts(exe string, opts ...SpawnOptSetter) *SpawnedCmd {
if runtime.GOOS == "windows" && condition.OnCI() {
opts = append(opts, OptRunInsideShell(true))
}
return s.SpawnCmdWithOpts(exe, opts...)
}

// SpawnInsideShell spawns the state tool executable to be tested with arguments.
// This function differs from SpawnWithOpts in that it runs the command in a shell on Windows CI.
// See SpawnInsideShell for more information.
func (s *Session) SpawnInsideShellWithOpts(opts ...SpawnOptSetter) *SpawnedCmd {
if runtime.GOOS == "windows" && condition.OnCI() {
opts = append(opts, OptRunInsideShell(true))
}
return s.SpawnCmdWithOpts(s.Exe, opts...)
}
2 changes: 1 addition & 1 deletion internal/testhelpers/e2e/spawn.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (s *SpawnedCmd) ExpectInput(opts ...termtest.SetExpectOpt) error {
expect := `expect'input from posix shell`
if cmdName != "bash" && shellName != "bash" && runtime.GOOS == "windows" {
if strings.Contains(cmdName, "powershell") || strings.Contains(shellName, "powershell") {
send = "echo \"`<expect input from powershell`>\""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this works? IIRC the backtick is an escape on powershell.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, I was running into a failure with this because it was defaulting back to cmd.exe and not powershell.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, I was right. Here's the output on powershell

[ActiveState-CLI/small-python] C:\Users\runneradmin\AppData\Local\Temp\2179854665\work\small-python>echo "`<expect input from powershell`>" 
"`<expect input from powershell`>"    

And the expect

'Expected: "<expect input from powershell>": after 40s: timeout':
              'after 40s: timeout': timeout

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is it depends on which shell you started from. So your new tests may want this variant but older tests will want the escaping? Worth perhaps looking for other places where we run ExpectInput and running those tests on Windows before merging your PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the integration tests that start powershell are also critical. Only one of them expects input which is the one that flagged this change.

send = "echo \"<expect input from powershell>\""
expect = `<expect input from powershell>`
} else {
send = `echo ^<expect input from cmd prompt^>`
Expand Down
22 changes: 11 additions & 11 deletions test/integration/activate_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivateWithoutRuntime() {
close := suite.addForegroundSvc(ts)
defer close()

cp := ts.Spawn("activate", "ActiveState-CLI/Empty")
cp := ts.SpawnInsideShell("activate", "ActiveState-CLI/Empty")
cp.Expect("Activated")
cp.ExpectInput()

Expand Down Expand Up @@ -134,7 +134,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivateUsingCommitID() {
close := suite.addForegroundSvc(ts)
defer close()

cp := ts.Spawn("activate", "ActiveState-CLI/Empty#6d79f2ae-f8b5-46bd-917a-d4b2558ec7b8", "--path", ts.Dirs.Work)
cp := ts.SpawnInsideShell("activate", "ActiveState-CLI/Empty#6d79f2ae-f8b5-46bd-917a-d4b2558ec7b8", "--path", ts.Dirs.Work)
cp.Expect("Activated")
cp.ExpectInput()

Expand All @@ -149,7 +149,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivateNotOnPath() {
close := suite.addForegroundSvc(ts)
defer close()

cp := ts.Spawn("activate", "activestate-cli/empty", "--path", ts.Dirs.Work)
cp := ts.SpawnInsideShell("activate", "activestate-cli/empty", "--path", ts.Dirs.Work)
cp.Expect("Activated")
cp.ExpectInput()

Expand Down Expand Up @@ -177,7 +177,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivatePythonByHostOnly() {
defer close()

projectName := "Python-LinuxWorks"
cp := ts.Spawn("activate", "cli-integration-tests/"+projectName, "--path="+ts.Dirs.Work)
cp := ts.SpawnInsideShell("activate", "cli-integration-tests/"+projectName, "--path="+ts.Dirs.Work)

if runtime.GOOS == "linux" {
cp.Expect("Creating a Virtual Environment")
Expand Down Expand Up @@ -218,7 +218,7 @@ func (suite *ActivateIntegrationTestSuite) activatePython(version string, extraE

namespace := "ActiveState-CLI/Python" + version

cp := ts.SpawnWithOpts(
cp := ts.SpawnInsideShellWithOpts(
e2e.OptArgs("activate", namespace),
e2e.OptAppendEnv(extraEnv...),
)
Expand Down Expand Up @@ -294,7 +294,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_PythonPath() {

namespace := "ActiveState-CLI/Python3"

cp := ts.Spawn("activate", namespace)
cp := ts.SpawnInsideShell("activate", namespace)

cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt)
// ensure that shell is functional
Expand Down Expand Up @@ -334,7 +334,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_SpaceInCacheDir() {
err := fileutils.MkdirUnlessExists(cacheDir)
suite.Require().NoError(err)

cp := ts.SpawnWithOpts(
cp := ts.SpawnInsideShellWithOpts(
e2e.OptArgs("activate", "ActiveState-CLI/Python3"),
e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.CacheEnvVarName, cacheDir)),
)
Expand All @@ -358,7 +358,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivatePerlCamel() {
close := suite.addForegroundSvc(ts)
defer close()

cp := ts.Spawn("activate", "ActiveState-CLI/Perl")
cp := ts.SpawnInsideShell("activate", "ActiveState-CLI/Perl")

cp.Expect("Downloading", termtest.OptExpectTimeout(40*time.Second))
cp.Expect("Installing", termtest.OptExpectTimeout(140*time.Second))
Expand Down Expand Up @@ -399,7 +399,7 @@ version: %s
ts.PrepareCommitIdFile("6d79f2ae-f8b5-46bd-917a-d4b2558ec7b8")

// Activate in the subdirectory
c2 := ts.SpawnWithOpts(
c2 := ts.SpawnInsideShellWithOpts(
e2e.OptArgs("activate"),
e2e.OptWD(filepath.Join(ts.Dirs.Work, "foo", "bar", "baz")),
)
Expand Down Expand Up @@ -474,15 +474,15 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_FromCache() {

// Note: cannot use Empty project since we need artifacts to download and install.
// Pick the langless project, which just has some small, non-language artifacts.
cp := ts.Spawn("activate", "ActiveState-CLI/langless", "--path", ts.Dirs.Work)
cp := ts.SpawnInsideShell("activate", "ActiveState-CLI/langless", "--path", ts.Dirs.Work)
cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt)

suite.assertCompletedStatusBarReport(cp.Output())
cp.SendLine("exit")
cp.ExpectExitCode(0)

// next activation is cached
cp = ts.Spawn("activate", "ActiveState-CLI/langless", "--path", ts.Dirs.Work)
cp = ts.SpawnInsideShell("activate", "ActiveState-CLI/langless", "--path", ts.Dirs.Work)

cp.ExpectInput(e2e.RuntimeSourcingTimeoutOpt)
cp.SendLine("exit")
Expand Down
1 change: 1 addition & 0 deletions test/integration/deploy_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func (suite *DeployIntegrationTestSuite) TestDeployPython() {
"cmd.exe",
e2e.OptArgs("/k", filepath.Join(targetPath, "bin", "shell.bat")),
e2e.OptAppendEnv("PATHEXT=.COM;.EXE;.BAT;.LNK", "SHELL="),
e2e.OptRunInsideShell(true),
)
} else {
cp = ts.SpawnCmdWithOpts(
Expand Down
2 changes: 1 addition & 1 deletion test/integration/shell_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ events:`, lang, splat), 1)
suite.Require().NoError(fileutils.WriteFile(asyFilename, []byte(contents)))

// Verify that running a script as a command with an argument containing special characters works.
cp = ts.Spawn("shell")
cp = ts.SpawnInsideShell("shell")
cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt)
cp.ExpectInput()
cp.SendLine(`args "<3"`)
Expand Down
Loading