Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/teleterm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"private": true,
"homepage": "https://goteleport.com",
"dependencies": {
"emittery": "^1.0.1",
"node-pty": "0.10.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/teleterm/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function initializeApp(): void {
logger,
configService,
fileStorage,
windowsManager,
});

app.on(
Expand Down
2 changes: 2 additions & 0 deletions packages/teleterm/src/mainProcess/fixtures/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ export class MockMainProcessClient implements MainProcessClient {
removeKubeConfig(): Promise<void> {
return Promise.resolve(undefined);
}

forceFocusWindow() {}
}
8 changes: 8 additions & 0 deletions packages/teleterm/src/mainProcess/mainProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import {
import { subscribeToTerminalContextMenuEvent } from './contextMenus/terminalContextMenu';
import { subscribeToTabContextMenuEvent } from './contextMenus/tabContextMenu';
import { resolveNetworkAddress } from './resolveNetworkAddress';
import { WindowsManager } from './windowsManager';

type Options = {
settings: RuntimeSettings;
logger: Logger;
configService: ConfigService;
fileStorage: FileStorage;
windowsManager: WindowsManager;
};

export default class MainProcess {
Expand All @@ -42,12 +44,14 @@ export default class MainProcess {
private sharedProcess: ChildProcess;
private fileStorage: FileStorage;
private resolvedChildProcessAddresses: Promise<ChildProcessAddresses>;
private windowsManager: WindowsManager;

private constructor(opts: Options) {
this.settings = opts.settings;
this.logger = opts.logger;
this.configService = opts.configService;
this.fileStorage = opts.fileStorage;
this.windowsManager = opts.windowsManager;
}

static create(opts: Options) {
Expand Down Expand Up @@ -195,6 +199,10 @@ export default class MainProcess {
})
);

ipcMain.handle('main-process-force-focus-window', () => {
this.windowsManager.forceFocusWindow();
});

subscribeToTerminalContextMenuEvent();
subscribeToTabContextMenuEvent();
subscribeToConfigServiceEvents(this.configService);
Expand Down
3 changes: 3 additions & 0 deletions packages/teleterm/src/mainProcess/mainProcessClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ export default function createMainProcessClient(): MainProcessClient {
removeKubeConfig(options) {
return ipcRenderer.invoke('main-process-remove-kube-config', options);
},
forceFocusWindow() {
return ipcRenderer.invoke('main-process-force-focus-window');
},
};
}
1 change: 1 addition & 0 deletions packages/teleterm/src/mainProcess/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type MainProcessClient = {
relativePath: string;
isDirectory?: boolean;
}): Promise<void>;
forceFocusWindow(): void;
};

export type ChildProcessAddresses = {
Expand Down
65 changes: 64 additions & 1 deletion packages/teleterm/src/mainProcess/windowsManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';

import { BrowserWindow, Menu, Rectangle, screen } from 'electron';
import { app, BrowserWindow, Menu, Rectangle, screen } from 'electron';

import { FileStorage } from 'teleterm/services/fileStorage';
import { RuntimeSettings } from 'teleterm/mainProcess/types';
Expand Down Expand Up @@ -78,6 +78,11 @@ export class WindowsManager {
this.window = window;
}

/**
* focusWindow is for situations where the app has privileges to do so, for example in a scenario
* where the user attempts to launch a second instance of the app – the same process that the user
* interacted with asks for its window to receive focus.
*/
focusWindow(): void {
if (!this.window) {
return;
Expand All @@ -90,6 +95,64 @@ export class WindowsManager {
this.window.focus();
}

/**
* forceFocusWindow if for situations where Connect wants to essentially steal focus.
*
* One example would be 3rd party apps interacting with resources exposed by Connect, e.g.
* gateways. If the user attempts to make a connection through a gateway but the certs have
* expired, Connect should receive focus and show an appropriate message to the user.
*/
forceFocusWindow(): void {
if (!this.window) {
return;
}

if (this.window.isFocused()) {
return;
}

// What follows is a special focus handler for windows.
//
// On Windows, app.focus() doesn't work as expected so instead we call win.focus().
// If the window is minimized, win.focus() will bring it to the front and give it focus.
// If the window is not minimized but simply covered by other another window, win.focus() will
// flash the icon of Connect in the task bar.
//
// Ideally, we'd like the not minimized window to receive focus too. We considered two
// workarounds to bring focus to a window that's not minimized:
//
// * win.minimized() followed by win.focus() – this reportedly doesn't work anymore (see the
// comment linked below) though it did work at the time of implementing forceFocusWindow.
// Admittedly, this seems like a hack and does cause the window to first minimize and then show
// up which feels weird.
// * win.setAlwaysOnTop(true) followed by win.show() – this does bring the window to the top
// but doesn't give it focus. Super awkward because Connect shows up over another app that you
// were using, you start typing to fill out whatever form Connect has shown you. But your
// keystrokes go to the app that the Connect window just covered.
//
// Since we cannot reliably steal focus, let's just not attempt to do it and instead defer to
// flashing the icon in the task bar.
//
// https://github.com/electron/electron/issues/2867#issuecomment-1080573240
//
// I don't understand why calling win.focus() on a minimized window gives it focus in the
// first place. In theory it shouldn't work, see the links below:
//
// https://stackoverflow.com/a/72620653/742872
// https://devblogs.microsoft.com/oldnewthing/20090220-00/?p=19083
// https://github.com/electron/electron/issues/2867#issuecomment-142480964
// https://github.com/electron/electron/issues/2867#issuecomment-142511956
if (this.settings.platform === 'win32') {
this.window.focus();
return;
}

app.dock?.bounce('informational');
// app.focus() alone doesn't un-minimize the window if the window is minimized.
this.window.show();
app.focus({ steal: true });
}

private saveWindowState(window: BrowserWindow): void {
const windowState: WindowState = {
...window.getNormalBounds(),
Expand Down
49 changes: 49 additions & 0 deletions packages/teleterm/src/services/relogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MainProcessClient } from 'teleterm/types';
import { ReloginRequest } from 'teleterm/services/tshdEvents';
import {
ModalsService,
ClusterConnectReason,
} from 'teleterm/ui/services/modals';
import { ClustersService } from 'teleterm/ui/services/clusters';

export class ReloginService {
constructor(
private mainProcessClient: MainProcessClient,
private modalsService: ModalsService,
private clustersService: ClustersService
) {}

relogin(
request: ReloginRequest,
onRequestCancelled: (callback: () => void) => void
): Promise<void> {
this.mainProcessClient.forceFocusWindow();
let reason: ClusterConnectReason;

if (request.gatewayCertExpired) {
const gateway = this.clustersService.findGateway(
request.gatewayCertExpired.gatewayUri
);
reason = {
kind: 'reason.gateway-cert-expired',
targetUri: request.gatewayCertExpired.targetUri,
gateway: gateway,
};
}

return new Promise((resolve, reject) => {
// GatewayCertReissuer in tshd makes sure that we only ever have one concurrent request to the
// relogin event. So at the moment, ReloginService won't ever call openImportantDialog twice.
const { closeDialog } = this.modalsService.openImportantDialog({
kind: 'cluster-connect',
clusterUri: request.rootClusterUri,
reason,
onSuccess: () => resolve(),
onCancel: () =>
reject(new Error('Login process was canceled by the user')),
});

onRequestCancelled(closeDialog);
});
}
}
13 changes: 0 additions & 13 deletions packages/teleterm/src/services/tshd/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,19 +589,6 @@ export default function createClient(
});
},

async restartGateway(gatewayUri = '') {
const req = new api.RestartGatewayRequest().setGatewayUri(gatewayUri);
return new Promise<void>((resolve, reject) => {
tshd.restartGateway(req, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
},

async setGatewayTargetSubresourceName(
gatewayUri = '',
targetSubresourceName = ''
Expand Down
13 changes: 12 additions & 1 deletion packages/teleterm/src/services/tshd/fixtures/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export class MockTshClient implements TshClient {
listGateways: () => Promise<Gateway[]>;
createGateway: (params: CreateGatewayParams) => Promise<Gateway>;
removeGateway: (gatewayUri: string) => Promise<undefined>;
restartGateway: (gatewayUri: string) => Promise<undefined>;
setGatewayTargetSubresourceName: (
gatewayUri: string,
targetSubresourceName: string
Expand Down Expand Up @@ -91,3 +90,15 @@ export class MockTshClient implements TshClient {
logout: (clusterUri: string) => Promise<undefined>;
transferFile: () => undefined;
}

export const gateway: Gateway = {
uri: '/gateways/gateway1',
targetName: 'postgres',
targetUri: '/clusters/teleport-local/dbs/postgres',
targetUser: 'alice',
targetSubresourceName: '',
localAddress: 'localhost',
localPort: '59116',
protocol: 'postgres',
cliCommand: 'psql postgres://alice@localhost:59116',
};
1 change: 0 additions & 1 deletion packages/teleterm/src/services/tshd/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export type TshClient = {
listGateways: () => Promise<Gateway[]>;
createGateway: (params: CreateGatewayParams) => Promise<Gateway>;
removeGateway: (gatewayUri: string) => Promise<void>;
restartGateway: (gatewayUri: string) => Promise<void>;
setGatewayTargetSubresourceName: (
gatewayUri: string,
targetSubresourceName: string
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading