Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.

Add commands for linking tsh on macOS#1480

Merged
ravicious merged 11 commits intoravicious/link-tshfrom
ravicious/link-tsh-macos
Jan 4, 2023
Merged

Add commands for linking tsh on macOS#1480
ravicious merged 11 commits intoravicious/link-tshfrom
ravicious/link-tsh-macos

Conversation

@ravicious
Copy link
Copy Markdown
Member

This will be merged into #1445.

First I added some missing stories in order to be confidently make changes to how suggestions are displayed. Then I added the command bar commands and later added the necessary main process IPC handlers to take care of executing osascript that creates the symlinks.

It's based on how VSCode does this:

@ravicious ravicious requested review from avatus and gzdunek January 3, 2023 11:35
}
});

ipcMain.handle('main-process-remove-tsh-symlink', async () => {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I just copy-pasted the other handler instead of adding some abstractions here, IMHO once we have another use case for osascript we can think about abstracting osascript prompts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I love this thinking. Always reminds me of this.

return commands.map(cmd => {
const acceptsNoArguments =
this.parserRegistry.get(cmd.displayName) instanceof
QuickNoArgumentsParser;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The way it worked before is that tsh ssh and tsh proxy db had their own parsers assigned to them:

this.quickCommandParser.registerParserForCommand(
'tsh ssh',
new parsers.QuickTshSshParser(sshLoginSuggester, serverSuggester)
);
this.quickCommandParser.registerParserForCommand(
'tsh proxy db',
new parsers.QuickTshProxyDbParser(databaseSuggester)
);

However, tsh install and tsh uninstall don't need custom parsers as they don't accept any extra arguments. So I added QuickNoArgumentsParser that I use for those two commands.

If that parser is used for the given command, then QuickInputService is not going to append space to the input.

// Importing electron breaks the fixtures if that's done from within storybook.
import { createConfigService } from 'teleterm/services/config/configService';

const platform = 'darwin';
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I had to remove the hardcoded platform in order to be able to test config service for different platforms.

@ravicious ravicious force-pushed the ravicious/link-tsh-macos branch from fd30885 to 7600ae8 Compare January 3, 2023 11:48

const defaultRuntimeSettings = {
platform: 'darwin' as const,
dev: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know this is a mock, but what are the consequences of having our mock be in dev mode instead of not? Could this lead to any false positives/negatives?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I know this is a mock, but what are the consequences of having our mock be in dev mode instead of not? Could this lead to any false positives/negatives?

dev is mostly used during the build & startup of the app, it shouldn't have much effect overall on tests.

In general I'm of the opinion that we shouldn't use dev for business logic, I was talking with Grzegorz about this recently. And for any other scenario, our setup code in tests should probably be closer to the dev build than the prod build (for example in terms of what's getting logged and what's not, etc.).

}
});

ipcMain.handle('main-process-remove-tsh-symlink', async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I love this thinking. Always reminds me of this.

ipcMain.handle('main-process-remove-tsh-symlink', async () => {
const target = '/usr/local/bin/tsh';
const prompt =
'Teleport Connect wants to remove a symlink for tsh from /usr/local/bin.';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if this language could be changed at all. I know this is exactly what the script is doing, and I know our audience is technical enough to understand thing, but it just seems so... destructive? Idk what the word is. I'm ok with not changing it, but I wonder if there is simpler terms to describe what is happening. Maybe I'm overreacting

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm ok with not changing it, but I wonder if there is simpler terms to describe what is happening.

We could say "Connect wants to uninstall tsh from PATH" but personally I'd prefer the app to tell me exactly what it wants to do when it asks me for a password.

Another option would be "Connect wants to modify the contents of /usr/local/bin" which doesn't use the notion of removing/uninstalling but again fails at specifying what exactly is going to happen and potentially sounds even more dangerous.

I'd probably keep this, the way I look at it is we ask the user for permission to do something so we better explain that as succinctly as possible.

// Used in the `tsh install` command on macOS to make the bundled tsh available in PATH.
// Returns true if tsh got successfully installed, false if the user closed the osascript
// prompt. Throws an error when osascript fails.
ipcMain.handle('main-process-symlink-tsh', async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this is specific to macOS, should we indicate that in the name of the event?

Suggested change
ipcMain.handle('main-process-symlink-tsh', async () => {
ipcMain.handle('main-process-symlink-tsh-macos', async () => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since this is specific to macOS, should we indicate that in the name of the event?

I was thinking about it but in the end I didn't do it for some reason, I'll add that.

@@ -0,0 +1,36 @@
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing license here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I haven't been adding license to any file since I've joined. 🙊

If that's necessary then once the merge is done we should just modify the ignored paths of make fix-license.

return (
<>
{/* Extra bottom margin to accommodate the suggestions displayed under the input. */}
<QuickInputDemo description="Pristine state" mb={350} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I'd consider creating separate stories for each demo. The "scroll into viewport" mechanism in QuickInput causes the whole story to jump when you want to use an input at the bottom of the viewport.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The "scroll into viewport" mechanism in QuickInput causes the whole story to jump when you want to use an input at the bottom of the viewport.

Right, it's annoying but the reason I created the story in the first place is that I wanted to have an easy way to inspect all available suggestions.

But I suppose a better way would be to create a separate story which uses QuickInputList alone rather than setting up the whole QuickInput component. I'll make that change soon.

}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const exhaustiveCheck: never = command;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't you want to use assertUnreachable here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Don't you want to use assertUnreachable here?

I was one the fence about it, especially when I remembered the problems we had with throwing errors when the doc type was not included in the switch.

But I think the difference between switching on docs vs commands here is that docs are loaded from a JSON file whereas commands are hardcoded.

We should be able to throw when switching on docs too but the actual issue with docs is that we don't parse the payload coming from the JSON file in any way. This allows objects of invalid type to slip through.

But that's not a problem here so yeah I think we can just always prefer assertUnreachable.

Comment on lines +16 to +36
it('does not return tsh install & uninstall autocomplete command on Linux', () => {
const appContext = new MockAppContext({ platform: 'linux' });
const commandLauncher = new CommandLauncher(appContext);
const autocompleteCommandNames = commandLauncher
.getAutocompleteCommands()
.map(c => c.name);

expect(autocompleteCommandNames).not.toContain('autocomplete.tsh-install');
expect(autocompleteCommandNames).not.toContain('autocomplete.tsh-uninstall');
});

it('does not return tsh install & uninstall autocomplete command on Windows', () => {
const appContext = new MockAppContext({ platform: 'win32' });
const commandLauncher = new CommandLauncher(appContext);
const autocompleteCommandNames = commandLauncher
.getAutocompleteCommands()
.map(c => c.name);

expect(autocompleteCommandNames).not.toContain('autocomplete.tsh-install');
expect(autocompleteCommandNames).not.toContain('autocomplete.tsh-uninstall');
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can avoid repeating the same test code with test.each.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We can avoid repeating the same test code with test.each.

I think I'm actually going to keep it, it's just three tests and idk how to neatly refactor this with test.each without introducing additional logic since one test requires commands to be in the array and others do not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was thinking about using it only for windows/linux 😛

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Then I'd say it's not worth it for just two short tests. 😏

description: 'Start a local proxy for a database connection',
run() {},
},
'autocomplete.tsh-install': {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know that QuickInput was quickly adjusted to the new requirements before the preview release and needs some refactoring, but maybe it can be cleaned up a bit?
I'm thinking about moving autocomplete.* commands to the new variable (like autocompleteCommands). It is a bit confusing that we mix commands that actually trigger some actions with commands used only for autocomplete. Then we could add types to these autocomplete commands, so you would be able to get rid of (Array.isArray(platforms) check in commandLauncher.ts.

error => {
ctx.notificationsService.notifyError({
title: 'Could not install tsh in PATH',
description: `Ran into an error: ${error}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't check it, but it seems to me that the error here is not a plain string, but an object with a message field (as we throw error from the IPC handler), so we will get a runtime error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't check it, but it seems to me that the error here is not a plain string, but an object with a message field (as we throw error from the IPC handler), so we will get a runtime error.

Errors that pass through IPC or the context bridge are still instances of Error with message, they're just stripped of any custom properties that might be attached to them. See the A word on error handling section.

The place where we discussed returning plain objects instead of errors was tshd client but we never got around to that (gravitational/webapps.e#309) 😏.

And I don't think we'd get a runtime error in any case:

const err = {message: "test"}; `err: ${err}`
// > "err: [object Object]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Errors that pass through IPC or the context bridge are still instances of Error with message, they're just stripped of any custom properties

I see, I was sure that name is passed too, as it is standard Error property.

And I don't think we'd get a runtime error in any case

Ah right, Objects are not valid as a react child happens only when rendering objects directly in JSX.

Comment on lines +244 to +248
if (error instanceof Error && error.message.includes('-128')) {
return false;
}
this.logger.error(error);
throw error;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have mixed feelings about this, it would be nice to return the errors in the same way.
Maybe we could create a new custom error, like PromptCancelledByUserError and throw (or even return) it instead of false? Then on the renderer side we would have to create our own instanceOf (as the the classes are serialized) that would compare errors by name.

IMO this would make the code more elegant, but we can live with what we have, it is quite simple and documented.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have mixed feelings about this, it would be nice to return the errors in the same way.

The way I think about this is that closing the prompt is not really an error/exception so I wanted to reserve rejecting the promise for actual errors that might happen during the execution of the script.

As seen in your suggestion, using a custom error would be much more complicated. But even more important is that it wouldn't work because errors are not only stripped of any custom properties, they lose their name as well and become simple Error instances. So we couldn't inspect name AFAIR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So the only way is to return plain objects, but if we don't think of closing the prompt as an error then it's fine to stay with the current solution.

@ravicious ravicious requested a review from gzdunek January 4, 2023 15:57
@@ -32,10 +32,10 @@ export default function createMainProcessClient(): MainProcessClient {
return ipcRenderer.invoke('main-process-force-focus-window');
},
symlinkTsh() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I somehow forgot to send a comment earlier, but I'd also add macOs part to symlinkTsh/removeTshSymlink.

@ravicious ravicious merged commit 2daafe9 into ravicious/link-tsh Jan 4, 2023
@ravicious ravicious deleted the ravicious/link-tsh-macos branch January 4, 2023 16:24
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants