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

Allow changing terminal shell in Teleport Connect #45152

Merged
merged 39 commits into from
Aug 23, 2024
Merged

Conversation

gzdunek
Copy link
Contributor

@gzdunek gzdunek commented Aug 6, 2024

Closes #19392
Closes #20186

I have to admit that this PR has become quite big, although I think it is not that bad when reviewed commit-by-commit.
Here is a summary of how the shell switcher works:

  1. Instead of RuntimeSettings.defaultShell we have now RuntimeSettings.availableShells and RuntimeSettings.defaultOsShellId. The shells are taken from shell.ts, where we calculate them for Unix and Windows.
  • The Unix shells are read from /etc/shells.
  • The list of Windows shells is hardcoded: I check what shells from the list: powershell.exe, pwsh.exe, cmd.exe and wsl.exe are available. I didn't do anything sophisticated here, I simply check what is available in the Path.
  1. The default shell is stored in the app config, the shell for terminal tab is kept in the document.
  2. When a new terminal tab is opened, I check in useDocumentTerminal.ts what is the default shell and set its ID in that document (I don't set the shellId in DocumentsService because it would have to depend on ConfigService and currently it doesn't depend on anything).
  3. After that, I pass the shellId to the buildPtyOptions where we resolve the full Shell object and get binPath. In theory, I could send that binPath from the renderer but I didn't want to blindly spawn anything it would send.
  4. When the user changes the active shell, I recreate the terminal document with the new URI and shellId.

This is how it looks on Windows.
image

I know @ravicious that you weren't a fan of the shell switcher on non-Windows platforms, and because of that, I decided to condense both of them into submenus, so they take less space.
But also I believe that we should avoid adding platform-specific features, if possible. Such features are much more difficult to develop and test (you have to run a VM) and simply it is easy to forget that we actually have them :)

changelog: The terminal shell can now be changed in Teleport Connect by right-clicking on a terminal tab. This allows using WSL (wsl.exe) if it is installed. Also, the default shell on Windows has been changed to pwsh.exe (instead of powershell.exe).

@avatus
Copy link
Contributor

avatus commented Aug 7, 2024

I wasn't able to see the shell switcher when right clicking (on macos). From your description it seems like it should work in non-windows OSes as well yes? Am I missing something to get it open

@gzdunek
Copy link
Contributor Author

gzdunek commented Aug 8, 2024

Strange, did you click on a tab title? It needs to be a terminal or kube session tab.

friendlyName: 'Command Prompt (cmd.exe)',
},
{
id: 'wsl.exe',
Copy link
Member

Choose a reason for hiding this comment

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

With wsl.exe as the default shell, env vars are not set properly. Both the TELEPORT env vars as well as including tsh in Path. Connecting to a new kube cluster also doesn't properly show the welcome message and KUBECONFIG isn't set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With wsl.exe as the default shell, env vars are not set properly.

Ah that's right, shame on me for not checking that. Fortunately, the fix seems to be easy, we need to set a special WSLENV variable. I only need to put all variables we want to set in it.

Connecting to a new kube cluster also doesn't properly show the welcome message

This is caused by ConPTY, I have a fix here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed the problem with passing env vars to WSL, the only thing that doesn't work is using tsh from Path.
However, it seems to me that it doesn't work in regular PowerShell too, I will look into that tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This issue is a little weird and I'm not sure where is the problem exactly.
We prepend tsh.exe to the Path in

env[pathName] = [settings.binDir, env[pathName]]

I noticed that if I change Path to path for win32, everything works (tsh.exe is available in every shell). But why? On master we use Path and tsh is prepended correctly.

Btw, there was already confusion in regard to Path on Windows in node.js nodejs/node#20605.

Perhaps we should just switch to path on Windows? When I run node in a Windows terminal and type process.env, there's a path property in the returned object.

Copy link
Member

Choose a reason for hiding this comment

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

Did you check it on a packaged version of the app? Because I definitely did it only in the dev version which does not prepend anything to path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, under WSL we need to execute tsh.exe https://stackoverflow.com/a/47663269.

But now I'm really confused, why doesn't it work for me when pathName is path? I see that I don't have /mnt/c/teleport/web/packages/teleterm/build/release/win-unpacked/resources/bin in the $PATH (same for any other shell).
Maybe something is broken on my machine? I will try on a different one.

Copy link
Member

Choose a reason for hiding this comment

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

I just remembered that someone posted their workaround for using tsh in WSL: #9849 (comment)

As for the Path issues, nothing comes to my mind other than adding console.log and checking what's up. I suspect it's on our side, not necessarily the system, unless they changed how env vars work as a part of regular system update, which I guess is not that likely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I just tested this on a clean OS and it worked. I was able to use tsh/tsh.exe in powershell.exe. pwsh.exe, cmd.exe and tsh.exe in WSL.
So it seems that something was wrong with my previous machine, but I have no idea what :(
I think we can mark this issue as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just remembered that someone posted their workaround for using tsh in WSL: #9849 (comment)

I'm not sure if I see value in replacing tsh.exe with tsh. I think the official convention is to use [app-name].exe when running Win32 apps from WSL:
https://learn.microsoft.com/en-us/windows/wsl/filesystems#run-windows-tools-from-linux

Also, the users can install tsh in WSL, I think renaming tsh.exe to tsh would lead to confusion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As for the Path issues, nothing comes to my mind other than adding console.log and checking what's up. I suspect it's on our side, not necessarily the system, unless they changed how env vars work as a part of regular system update, which I guess is not that likely.

OH, I'VE GOT IT!
The problem was that I have for some reason the "user" and "os" PATH env var called differently:
image

This resulted in the PATH variable being called path, not Path in the process.env object. Node.js handles this by having its own setters and getters for PATH, so no matter what property you set, the single underlying value is updated.
Unfortunately, I missed that we merge many env vars by spreading the objects, so these setters/getters were lost.
As a result, in the env object I had both path and Path (and the value of Path was ignored).
To fix this problem, we only need to find a property with the correct name in the env object and update it.

@ravicious
Copy link
Member

I haven't looked at the code that closely, mostly just left comments about UX.

@gzdunek gzdunek requested a review from ravicious August 13, 2024 15:39

/** A utility function that allows us to mock `process.env` in tests. */
export function getNodeProcessEnv() {
return process.env;
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure how well this is going to work long term but let's try it. 😏 Usually a better option would be to pass process.env from above so that you can substitute it in tests with whatever you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to argue, but I can't really find any real benefit of it, compared to passing process.env from above lol

It's a minor thing but I changed it.

Comment on lines 57 to 68
// Add `shellId` before going further.
let docWithDefaultShell: types.DocumentTerminal;
if (
(doc.kind === 'doc.terminal_shell' || doc.kind === 'doc.gateway_kube') &&
!doc.shellId
) {
docWithDefaultShell = {
...doc,
shellId: ctx.configService.get('terminal.shell').value,
};
documentsService.update(doc.uri, docWithDefaultShell);
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this to update any existing documents which do not have this field set? It'd have been better to do this in some kind of a migration when starting the app, but I think we've never got around to adding such stuff for app_state.json.

Could we add // DELETE IN 18.0.0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this to update any existing documents which do not have this field set?

No, the purpose of it was to add a default shell to any new "shell" document (in DocumentsService we set shellId: '').
That shellId is needed later in useDocumentTerminal and in the context menu, to mark the correct option.
The problem was that DocumentsService depends only on some setters and getters from WorkspacesService. I wasn't sure about adding ConfigService there (which we would call in newTerminalDocument), so I came up with this late initialization.
But actually, after we added returning the resolved shell from buildPtyOptions (and updating the document with it), we can get rid of it. This should be probably even more correct (than updating the document with the default shell), because we don't actually know what shell we will get from buildPtyOptions, so we can update the document after we get it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I was too fast with it.
I realized that relying only on the shell returned from createPtyProcess won't work if it throws an error. Then the shellId is remains empty and in the context menu the first option is checked (at least on macOS, looks like it by default selects the first option if nothing is selected).
Because of that, I decided to revert it.

Ideally, when createPtyProcess throws an error, we should catch it, wrap in a custom object, and return something like {error, shell}.
Unfortunately, I don't have time for it, so I'd stay with what we have now (I improved the comment).

web/packages/teleterm/src/ui/TabHost/TabHost.tsx Outdated Show resolved Hide resolved
@gzdunek gzdunek requested a review from ravicious August 23, 2024 07:23
Copy link
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

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

Thanks for addressing all the feedback. This was a bunch of surprisingly tricky changes!

It might be worthwhile to keep #9849 in mind. I don't know what expectations people running WSL might have towards Connect, but it might turn out that we did not think of something that they're used to, etc.

@gzdunek
Copy link
Contributor Author

gzdunek commented Aug 23, 2024

It might be worthwhile to keep #9849 in mind. I don't know what expectations people running WSL might have towards Connect, but it might turn out that we did not think of something that they're used to, etc.

Agree!

@gzdunek gzdunek enabled auto-merge August 23, 2024 09:46
@gzdunek gzdunek added this pull request to the merge queue Aug 23, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 23, 2024
@gzdunek gzdunek added this pull request to the merge queue Aug 23, 2024
Merged via the queue into master with commit 806dcda Aug 23, 2024
39 checks passed
@gzdunek gzdunek deleted the gzdunek/shell-switcher branch August 23, 2024 10:18
@public-teleport-github-review-bot

@gzdunek See the table below for backport results.

Branch Result
branch/v16 Failed

gzdunek added a commit that referenced this pull request Aug 23, 2024
* Get available shells for the system and store them in `RuntimeSettings`

* Add a config option for `terminal.shell`

* Add `shellId` to terminal documents and pty commands

* Pass the `shellId` from pty command to pty process options (and resolve it to the shell path)

* Allow changing the active and default shell from the context menu

* Show active shell in the tab name, drop "Terminal" from the name on Windows

* Fix resolving shell env for `csh`

* Do not read unix shells from `/etc/shells`, add a config property for the custom shell

* Add a whitelist of config properties that can be modified from the renderer

* Allow selecting custom shell

* Return back the actually opened shell and creation status

* Pass variables to WSL via WSLENV

* Do not change the default shell when selecting a custom shell

* Show profile switcher only when there is more than one shell to choose from

* Show shell name in the "Default Shell" context menu as sublabel

* Remove comment about sync setup of `setUpDeepLinks`

* Replace `MainTabContextMenuOptions` type

* Improve types in `subscribeToTabContextMenuEvent`

* Use early return

* Fix condition in the schema for `terminal.shell`

* Expand the comment for `preventAutoPromiseResolveOnMenuClose`

* Render available shells without an array

* `openedShell` -> `shell`

* Add tests for resolving shell

* Remove `PtyOptions`

* Preserve user defined WSLENV

* Remove the check for `fs.access(customShellPath)`

* Mock `process.env` instead of mutating it

* Fix remaining `openedShell` -> `shell`

* Fix `PATH` issue on Windows

* Remove updating a document with the default shell in `useDocumentTerminal`

* Add `canDocChangeShell`

* Omit the default shell name on Linux too

* Convert `buildPtyOptions` parameters to object, pass `process.env` from above

* Revert "Remove updating a document with the default shell in `useDocumentTerminal`"

This reverts commit 52f0485.

* Improve comments

* Throw when the document doesn't exist

(cherry picked from commit 806dcda)
github-merge-queue bot pushed a commit that referenced this pull request Aug 23, 2024
* Get available shells for the system and store them in `RuntimeSettings`

* Add a config option for `terminal.shell`

* Add `shellId` to terminal documents and pty commands

* Pass the `shellId` from pty command to pty process options (and resolve it to the shell path)

* Allow changing the active and default shell from the context menu

* Show active shell in the tab name, drop "Terminal" from the name on Windows

* Fix resolving shell env for `csh`

* Do not read unix shells from `/etc/shells`, add a config property for the custom shell

* Add a whitelist of config properties that can be modified from the renderer

* Allow selecting custom shell

* Return back the actually opened shell and creation status

* Pass variables to WSL via WSLENV

* Do not change the default shell when selecting a custom shell

* Show profile switcher only when there is more than one shell to choose from

* Show shell name in the "Default Shell" context menu as sublabel

* Remove comment about sync setup of `setUpDeepLinks`

* Replace `MainTabContextMenuOptions` type

* Improve types in `subscribeToTabContextMenuEvent`

* Use early return

* Fix condition in the schema for `terminal.shell`

* Expand the comment for `preventAutoPromiseResolveOnMenuClose`

* Render available shells without an array

* `openedShell` -> `shell`

* Add tests for resolving shell

* Remove `PtyOptions`

* Preserve user defined WSLENV

* Remove the check for `fs.access(customShellPath)`

* Mock `process.env` instead of mutating it

* Fix remaining `openedShell` -> `shell`

* Fix `PATH` issue on Windows

* Remove updating a document with the default shell in `useDocumentTerminal`

* Add `canDocChangeShell`

* Omit the default shell name on Linux too

* Convert `buildPtyOptions` parameters to object, pass `process.env` from above

* Revert "Remove updating a document with the default shell in `useDocumentTerminal`"

This reverts commit 52f0485.

* Improve comments

* Throw when the document doesn't exist

(cherry picked from commit 806dcda)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow usage of WSL for Teleport Connect Connect: Use PowerShell 7 if available on the system
3 participants