Skip to content

Commit

Permalink
Handle unexpected shutdown in Electron app
Browse files Browse the repository at this point in the history
  • Loading branch information
ravicious committed Jun 5, 2024
1 parent 1a072cb commit f8d0664
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 10 deletions.
4 changes: 4 additions & 0 deletions web/packages/teleterm/src/services/tshdEvents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ function createService(logger: Logger): {
getUsageReportingSettings: (call, callback) => {
processEvent('getUsageReportingSettings', call, callback);
},

reportUnexpectedVnetShutdown: (call, callback) => {
processEvent('reportUnexpectedVnetShutdown', call, callback);
},
};

return { service, setupTshdEventContextBridgeService };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,37 @@ export function VnetError() {
);
}

export function VnetUnexpectedShutdown() {
const appContext = new MockAppContext();
prepareAppContext(appContext);

appContext.statePersistenceService.putState({
...appContext.statePersistenceService.getState(),
vnet: { autoStart: true },
});
appContext.workspacesService.setState(draft => {
draft.isInitialized = true;
});
appContext.vnet.start = () => {
setTimeout(() => {
appContext.unexpectedVnetShutdownListener({
error: 'lorem ipsum dolor sit amet',
});
}, 0);
return new MockedUnaryCall({});
};

return (
<AppContextProvider value={appContext}>
<ConnectionsContextProvider>
<VnetContextProvider>
<Connections />
</VnetContextProvider>
</ConnectionsContextProvider>
</AppContextProvider>
);
}

export function WithScroll() {
const appContext = new MockAppContext();
prepareAppContext(appContext);
Expand Down
5 changes: 4 additions & 1 deletion web/packages/teleterm/src/ui/Vnet/VnetConnectionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ const VnetConnectionItemBase = forwardRef(
startAttempt.status === 'processing' ||
stopAttempt.status === 'processing';
const indicatorStatus =
startAttempt.status === 'error' || stopAttempt.status === 'error'
startAttempt.status === 'error' ||
stopAttempt.status === 'error' ||
(status.value === 'stopped' &&
status.reason.value === 'unexpected-shutdown')
? 'error'
: status.value === 'running'
? 'on'
Expand Down
16 changes: 13 additions & 3 deletions web/packages/teleterm/src/ui/Vnet/VnetSliderStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,19 @@ export const VnetSliderStep = (props: StepComponentProps) => {
<Text>Could not stop VNet: {stopAttempt.statusText}</Text>
)}

{status.value === 'stopped' && (
<Text>VNet automatically authenticates connections to TCP apps.</Text>
)}
{status.value === 'stopped' &&
(status.reason.value === 'unexpected-shutdown' ? (
<Text>
VNet unexpectedly shut down:{' '}
{status.reason.errorMessage ||
'no direct reason was given, please check logs'}
.
</Text>
) : (
<Text>
VNet automatically authenticates connections to TCP apps.
</Text>
))}
</Flex>

{status.value === 'running' &&
Expand Down
52 changes: 51 additions & 1 deletion web/packages/teleterm/src/ui/Vnet/vnetContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import { IAppContext } from 'teleterm/ui/types';
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';

import { VnetContextProvider, useVnetContext } from './vnetContext';
import {
VnetContextProvider,
VnetStatus,
VnetStoppedReason,
useVnetContext,
} from './vnetContext';

describe('autostart', () => {
it('starts VNet if turned on', async () => {
Expand Down Expand Up @@ -143,6 +148,51 @@ describe('autostart', () => {
});
});

it('registers a callback for unexpected shutdown', async () => {
const appContext = new MockAppContext();
appContext.workspacesService.setState(draft => {
draft.isInitialized = true;
});
appContext.statePersistenceService.putState({
...appContext.statePersistenceService.getState(),
vnet: { autoStart: true },
});

const { result } = renderHook(() => useVnetContext(), {
wrapper: createWrapper(Wrapper, { appContext }),
});

await waitFor(
() => expect(result.current.startAttempt.status).toEqual('success'),
{ interval: 5 }
);

// Trigger unexpected shutdown.
act(() => {
appContext.unexpectedVnetShutdownListener({
error: 'lorem ipsum dolor sit amet',
});
});

await waitFor(
() => {
expect(result.current.status.value).toEqual('stopped');
},
{ interval: 5 }
);

const status = result.current.status as Extract<
VnetStatus,
{ value: 'stopped' }
>;
expect(status.reason.value).toEqual('unexpected-shutdown');
const reason = status.reason as Extract<
VnetStoppedReason,
{ value: 'unexpected-shutdown' }
>;
expect(reason.errorMessage).toEqual('lorem ipsum dolor sit amet');
});

const Wrapper = (props: PropsWithChildren<{ appContext: IAppContext }>) => (
<MockAppContextProvider appContext={props.appContext}>
<VnetContextProvider>{props.children}</VnetContextProvider>
Expand Down
44 changes: 40 additions & 4 deletions web/packages/teleterm/src/ui/Vnet/vnetContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,23 @@ export type VnetContext = {
stopAttempt: Attempt<void>;
};

export type VnetStatus = { value: 'running' } | { value: 'stopped' };
export type VnetStatus =
| { value: 'running' }
| { value: 'stopped'; reason: VnetStoppedReason };

export type VnetStoppedReason =
| { value: 'regular-shutdown-or-not-started' }
| { value: 'unexpected-shutdown'; errorMessage: string };

export const VnetContext = createContext<VnetContext>(null);

export const VnetContextProvider: FC<PropsWithChildren> = props => {
const [status, setStatus] = useState<VnetStatus>({ value: 'stopped' });
const { vnet, mainProcessClient } = useAppContext();
const [status, setStatus] = useState<VnetStatus>({
value: 'stopped',
reason: { value: 'regular-shutdown-or-not-started' },
});
const appCtx = useAppContext();
const { vnet, mainProcessClient, notificationsService } = appCtx;
const isWorkspaceStateInitialized = useStoreSelector(
'workspacesService',
useCallback(state => state.isInitialized, [])
Expand All @@ -80,7 +90,10 @@ export const VnetContextProvider: FC<PropsWithChildren> = props => {
const [stopAttempt, stop] = useAsync(
useCallback(async () => {
await vnet.stop({});
setStatus({ value: 'stopped' });
setStatus({
value: 'stopped',
reason: { value: 'regular-shutdown-or-not-started' },
});
setAppState({ autoStart: false });
}, [vnet, setAppState])
);
Expand All @@ -106,6 +119,29 @@ export const VnetContextProvider: FC<PropsWithChildren> = props => {
handleAutoStart();
}, [isWorkspaceStateInitialized]);

useEffect(
function handleUnexpectedShutdown() {
const removeListener = appCtx.addUnexpectedVnetShutdownListener(
({ error }) => {
setStatus({
value: 'stopped',
reason: { value: 'unexpected-shutdown', errorMessage: error },
});

notificationsService.notifyError({
title: 'VNet has unexpectedly shut down',
description: error
? `Reason: ${error}`
: 'No reason was given, check the logs for more details.',
});
}
);

return removeListener;
},
[appCtx, notificationsService]
);

return (
<VnetContext.Provider
value={{
Expand Down
33 changes: 32 additions & 1 deletion web/packages/teleterm/src/ui/appContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { ResourcesService } from 'teleterm/ui/services/resources';
import { ConnectMyComputerService } from 'teleterm/ui/services/connectMyComputer';
import { ConfigService } from 'teleterm/services/config';
import { TshdClient, VnetClient } from 'teleterm/services/tshd/createClient';
import { IAppContext } from 'teleterm/ui/types';
import { IAppContext, UnexpectedVnetShutdownListener } from 'teleterm/ui/types';
import { DeepLinksService } from 'teleterm/ui/services/deepLinks';
import { parseDeepLink } from 'teleterm/deepLinks';

Expand Down Expand Up @@ -82,6 +82,9 @@ export default class AppContext implements IAppContext {
configService: ConfigService;
connectMyComputerService: ConnectMyComputerService;
deepLinksService: DeepLinksService;
private _unexpectedVnetShutdownListener:
| UnexpectedVnetShutdownListener
| undefined;

constructor(config: ElectronGlobals) {
const { tshClient, ptyServiceClient, mainProcessClient } = config;
Expand Down Expand Up @@ -175,6 +178,34 @@ export default class AppContext implements IAppContext {
await this.clustersService.syncRootClustersAndCatchErrors();
}

/**
* addUnexpectedVnetShutdownListener sets the listener and returns a cleanup function which
* removes the listener.
*/
addUnexpectedVnetShutdownListener(
listener: UnexpectedVnetShutdownListener
): () => void {
this._unexpectedVnetShutdownListener = listener;

return () => {
this._unexpectedVnetShutdownListener = undefined;
};
}

/**
* unexpectedVnetShutdownListener gets called by tshd events service when it gets a report about
* said shutdown from tsh daemon.
*
* The communication between tshd events service and VnetContext is done through a callback on
* AppContext. That's because tshd events service lives outside of React but within the same
* process (renderer).
*/
// To force callsites to use addUnexpectedVnetShutdownListener instead of setting the property
// directly on appContext, we use a getter which exposes a private property.
get unexpectedVnetShutdownListener(): UnexpectedVnetShutdownListener {
return this._unexpectedVnetShutdownListener;
}

private subscribeToDeepLinkLaunch() {
this.mainProcessClient.subscribeToDeepLinkLaunch(result => {
this.deepLinksService.launchDeepLink(result).catch(error => {
Expand Down
14 changes: 14 additions & 0 deletions web/packages/teleterm/src/ui/tshdEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@

import { TshdEventContextBridgeService } from 'teleterm/types';
import { IAppContext } from 'teleterm/ui/types';
import Logger from 'teleterm/logger';

export function createTshdEventsContextBridgeService(
ctx: IAppContext
): TshdEventContextBridgeService {
const logger = new Logger('tshd events UI');

return {
relogin: async ({ request, onRequestCancelled }) => {
await ctx.reloginService.relogin(request, onRequestCancelled);
Expand Down Expand Up @@ -87,5 +90,16 @@ export function createTshdEventsContextBridgeService(
},
};
},

reportUnexpectedVnetShutdown: async ({ request }) => {
if (!ctx.unexpectedVnetShutdownListener) {
logger.warn(
`Dropping unexpected VNet shutdown event, no listener present; error: ${request.error}`
);
} else {
ctx.unexpectedVnetShutdownListener(request);
}
return {};
},
};
}
21 changes: 21 additions & 0 deletions web/packages/teleterm/src/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import * as tshdEventsApi from 'gen-proto-ts/teleport/lib/teleterm/v1/tshd_events_service_pb';

import {
MainProcessClient,
Expand Down Expand Up @@ -65,4 +66,24 @@ export interface IAppContext {
vnet: VnetClient;

pullInitialState(): Promise<void>;

/**
* addUnexpectedVnetShutdownListener sets the listener and returns a cleanup function.
*/
addUnexpectedVnetShutdownListener: (
listener: UnexpectedVnetShutdownListener
) => () => void;
/**
* unexpectedVnetShutdownListener gets called by tshd events service when it gets a report about
* said shutdown from tsh daemon.
*
* The communication between tshd events service and VnetContext is done through a callback on
* AppContext. That's because tshd events service lives outside of React but within the same
* process (renderer).
*/
unexpectedVnetShutdownListener: UnexpectedVnetShutdownListener | undefined;
}

export type UnexpectedVnetShutdownListener = (
request: tshdEventsApi.ReportUnexpectedVnetShutdownRequest
) => void;

0 comments on commit f8d0664

Please sign in to comment.