Skip to content

Commit 4512e20

Browse files
devEnv: Fix Dev Env timeout + logs (#4980)
* fix: add more logs to Dev Env heartbeat We want more logs to help determine if the dev env heartbeat is working as expected Signed-off-by: Nikolas Komonen <[email protected]> * refactor: fix circular dependency The command that shows the login vue was having circular dependency issues. This moves that logic in to its own file to fix this. Signed-off-by: Nikolas Komonen <[email protected]> * devEnv: try start activity heartbeat on connection change Problem: We only tried to start the activity heartbeat on extension startup. So if that failed due to something like an invalid connection, the heartbeat mechanism would not start Solution: Try and start the heartbeat mechanism when the CC connection changes as well. But if it is already running then we just skip it. Signed-off-by: Nikolas Komonen <[email protected]> --------- Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 99a92e4 commit 4512e20

File tree

9 files changed

+204
-77
lines changed

9 files changed

+204
-77
lines changed

packages/core/src/auth/activation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { getLogger } from '../shared/logger'
1111
import { ExtensionUse, initAuthCommands } from './utils'
1212
import { isCloud9 } from '../shared/extensionUtilities'
1313
import { isInDevEnv } from '../shared/vscode/env'
14-
import { registerCommands, getShowManageConnections } from './ui/vue/show'
1514
import { isWeb } from '../shared/extensionGlobals'
15+
import { getShowManageConnections, registerCommands } from '../login/command'
1616

1717
export async function initialize(
1818
extensionContext: vscode.ExtensionContext,

packages/core/src/auth/ui/vue/show.ts

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* This module sets up the necessary components
66
* for the webview to be shown.
77
*/
8-
import globals, { isWeb } from '../../../shared/extensionGlobals'
8+
import globals from '../../../shared/extensionGlobals'
99
import { getIdeProperties, isCloud9 } from '../../../shared/extensionUtilities'
1010
import { VueWebview } from '../../../webviews/main'
1111
import * as vscode from 'vscode'
@@ -35,26 +35,24 @@ import {
3535
hasSso,
3636
BuilderIdKind,
3737
findSsoConnections,
38-
authCommands,
3938
} from '../../utils'
4039
import { Region } from '../../../shared/regions/endpoints'
4140
import { CancellationError } from '../../../shared/utilities/timeoutUtils'
4241
import { validateSsoUrl, validateSsoUrlFormat } from '../../sso/validation'
43-
import { awsIdSignIn, showCodeWhispererConnectionPrompt } from '../../../codewhisperer/util/showSsoPrompt'
44-
import { AuthError, ServiceItemId, isServiceItemId, authFormTelemetryMapping, userCancelled } from './types'
42+
import { awsIdSignIn } from '../../../codewhisperer/util/showSsoPrompt'
43+
import { AuthError, ServiceItemId, authFormTelemetryMapping, userCancelled } from './types'
4544
import { connectToEnterpriseSso } from '../../../codewhisperer/util/getStartUrl'
4645
import { trustedDomainCancellation } from '../../sso/model'
4746
import { CredentialSourceId, Result, telemetry } from '../../../shared/telemetry/telemetry'
4847
import { AuthFormId } from './authForms/types'
4948
import { handleWebviewError } from '../../../webviews/server'
50-
import { Commands, RegisteredCommand, VsCodeCommandArg, placeholder } from '../../../shared/vscode/commands2'
49+
import { placeholder } from '../../../shared/vscode/commands2'
5150
import { ClassToInterfaceType } from '../../../shared/utilities/tsUtils'
5251
import { debounce } from 'lodash'
5352
import { submitFeedback } from '../../../feedback/vue/submitFeedback'
5453
import { InvalidGrantException } from '@aws-sdk/client-sso-oidc'
5554
import { ExtStartUpSources } from '../../../shared/telemetry'
56-
import { CommonAuthWebview } from '../../../login/webview/vue/backend'
57-
import { AuthSource, AuthSources } from '../../../login/webview/util'
55+
import { AuthSource } from '../../../login/webview/util'
5856
import { focusAmazonQPanel } from '../../../codewhispererChat/commands/registerCommands'
5957

6058
// This file has some used functions, but most of it should be removed soon. We have a new
@@ -714,47 +712,6 @@ const Panel = VueWebview.compilePanel(AuthWebview)
714712
let activePanel: InstanceType<typeof Panel> | undefined
715713
let subscriptions: vscode.Disposable[] | undefined
716714

717-
let showManageConnections: RegisteredCommand<any> | undefined
718-
export function getShowManageConnections(): RegisteredCommand<any> {
719-
if (!showManageConnections) {
720-
throw new Error('showManageConnections not registered')
721-
}
722-
return showManageConnections
723-
}
724-
725-
export function registerCommands(context: vscode.ExtensionContext, prefix: string) {
726-
showManageConnections = Commands.register(
727-
{ id: `aws.${prefix}.auth.manageConnections`, compositeKey: { 1: 'source' } },
728-
async (_: VsCodeCommandArg, source: AuthSource, serviceToShow?: ServiceItemId) => {
729-
if (_ !== placeholder) {
730-
source = AuthSources.vscodeComponent
731-
}
732-
733-
// The auth webview page does not make sense to use in C9,
734-
// so show the auth quick pick instead.
735-
if (isCloud9('any') || isWeb()) {
736-
if (source.toLowerCase().includes('codewhisperer')) {
737-
// Show CW specific quick pick for CW connections
738-
return showCodeWhispererConnectionPrompt()
739-
}
740-
return authCommands().addConnection.execute()
741-
}
742-
743-
if (!isServiceItemId(serviceToShow)) {
744-
serviceToShow = undefined
745-
}
746-
747-
// TODO: hack
748-
if (prefix === 'toolkit') {
749-
CommonAuthWebview.authSource = source
750-
await vscode.commands.executeCommand('aws.explorer.setLoginService', serviceToShow)
751-
await vscode.commands.executeCommand('setContext', 'aws.explorer.showAuthView', true)
752-
await vscode.commands.executeCommand('aws.toolkit.AmazonCommonAuth.focus')
753-
}
754-
}
755-
)
756-
}
757-
758715
//todo: delete?
759716
export async function showAuthWebview(
760717
ctx: vscode.ExtensionContext,

packages/core/src/codecatalyst/activation.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { CodeCatalystCommands } from './commands'
1212
import { GitExtension } from '../shared/extensions/git'
1313
import { CodeCatalystAuthenticationProvider } from './auth'
1414
import { registerDevfileWatcher, updateDevfileCommand } from './devfile'
15-
import { DevEnvClient, DevEnvActivity } from '../shared/clients/devenvClient'
15+
import { DevEnvClient } from '../shared/clients/devenvClient'
1616
import { watchRestartingDevEnvs } from './reconnect'
1717
import { ToolkitPromptSettings } from '../shared/settings'
1818
import { dontShow } from '../shared/localizedText'
@@ -22,9 +22,9 @@ import { createClient, getCodeCatalystConfig } from '../shared/clients/codecatal
2222
import { isDevenvVscode } from './utils'
2323
import { codeCatalystConnectCommand, getThisDevEnv } from './model'
2424
import { getLogger } from '../shared/logger/logger'
25-
import { InactivityMessage, shouldTrackUserActivity } from './devEnv'
26-
import { getShowManageConnections } from '../auth/ui/vue/show'
25+
import { DevEnvActivityStarter } from './devEnv'
2726
import { learnMoreCommand, onboardCommand, reauth } from './explorer'
27+
import { getShowManageConnections } from '../login/command'
2828

2929
const localize = nls.loadMessageBundle()
3030

@@ -123,17 +123,10 @@ export async function activate(ctx: ExtContext): Promise<void> {
123123
}
124124
})
125125
}
126-
127-
const maxInactivityMinutes = thisDevenv.summary.inactivityTimeoutMinutes
128-
const devEnvClient = thisDevenv.devenvClient
129-
const devEnvActivity = await DevEnvActivity.instanceIfActivityTrackingEnabled(devEnvClient)
130-
if (shouldTrackUserActivity(maxInactivityMinutes) && devEnvActivity) {
131-
const inactivityMessage = new InactivityMessage()
132-
await inactivityMessage.setupMessage(maxInactivityMinutes, devEnvActivity)
133-
134-
ctx.extensionContext.subscriptions.push(inactivityMessage, devEnvActivity)
135-
}
136126
}
127+
128+
// This must always be called on activation
129+
DevEnvActivityStarter.register(authProvider)
137130
}
138131

139132
async function showReadmeFileOnFirstLoad(workspaceState: vscode.ExtensionContext['workspaceState']): Promise<void> {

packages/core/src/codecatalyst/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import { AccountStatus } from '../shared/telemetry/telemetryClient'
2525
import { CreateDevEnvironmentRequest, UpdateDevEnvironmentRequest } from 'aws-sdk/clients/codecatalyst'
2626
import { Auth } from '../auth/auth'
2727
import { SsoConnection } from '../auth/connection'
28-
import { getShowManageConnections } from '../auth/ui/vue/show'
2928
import { isInDevEnv, isRemoteWorkspace } from '../shared/vscode/env'
29+
import { getShowManageConnections } from '../login/command'
3030

3131
/** "List CodeCatalyst Commands" command. */
3232
export async function listCommands(): Promise<void> {

packages/core/src/codecatalyst/devEnv.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,123 @@ import { DevEnvironment } from '../shared/clients/codecatalystClient'
77
import { DevEnvActivity } from '../shared/clients/devenvClient'
88
import globals from '../shared/extensionGlobals'
99
import * as vscode from 'vscode'
10-
import { Timeout } from '../shared/utilities/timeoutUtils'
10+
import { Timeout, waitUntil } from '../shared/utilities/timeoutUtils'
1111
import { showMessageWithCancel } from '../shared/utilities/messages'
1212
import { isCloud9 } from '../shared/extensionUtilities'
1313
import { getLogger } from '../shared/logger'
14+
import { CodeCatalystAuthenticationProvider } from './auth'
15+
import { getThisDevEnv } from './model'
16+
import { isInDevEnv } from '../shared/vscode/env'
17+
import { shared } from '../shared/utilities/functionUtils'
18+
19+
/**
20+
* This class exists due to complexities with starting the {@link DevEnvActivity} Hearbeat mechanism.
21+
* All logic to start it is contained within this class.
22+
*/
23+
export class DevEnvActivityStarter {
24+
/**
25+
* Trys to start the Dev Env Activity Heartbeat mechanism.
26+
* This mechanism keeps the Dev Env from timing out.
27+
*/
28+
public static register(authProvider: CodeCatalystAuthenticationProvider) {
29+
if (!isInDevEnv()) {
30+
getLogger().debug('codecatalyst: not in a devenv, not registering DevEnvActivityHeartbeatStarter')
31+
return
32+
}
33+
34+
DevEnvActivityStarter.authProvider = authProvider
35+
void DevEnvActivityStarter.instance.tryStartDevEnvActivityHeartbeatWithRetry()
36+
}
37+
38+
/** If true, the Activity Heartbeat is running */
39+
private didStart: boolean = false
40+
41+
protected constructor(private readonly authProvider: CodeCatalystAuthenticationProvider) {}
42+
43+
private static _instance: DevEnvActivityStarter | undefined = undefined
44+
private static get instance(): DevEnvActivityStarter {
45+
return (DevEnvActivityStarter._instance ??= new DevEnvActivityStarter(DevEnvActivityStarter.getAuthProvider()))
46+
}
47+
48+
/**
49+
* Keeps executing {@link DevEnvActivityStarter.tryStartDevEnvActivityHeartbeat} on an interval until it succeeds.
50+
* After a certain amount of time this will stop retrying.
51+
*/
52+
private async tryStartDevEnvActivityHeartbeatWithRetry() {
53+
return waitUntil(
54+
async () => {
55+
await this.tryStartDevEnvActivityHeartbeat()
56+
return this.didStart
57+
},
58+
{ interval: 20_000, timeout: 60_000 * 5 }
59+
)
60+
}
61+
62+
/** Trys to start the Activity Heartbeat mechanism */
63+
private tryStartDevEnvActivityHeartbeat = shared(async () => {
64+
if (this.didStart) {
65+
return
66+
}
67+
68+
const thisDevenv = (await getThisDevEnv(this.authProvider))?.unwrapOrElse(err => {
69+
getLogger().warn('codecatalyst: failed to get current Dev Enviroment: %s', err)
70+
return undefined
71+
})
72+
73+
if (!thisDevenv) {
74+
const connection = this.authProvider.activeConnection
75+
if (connection) {
76+
void vscode.window
77+
.showErrorMessage(
78+
'CodeCatalyst: Reauthenticate your connection or the Dev Environment will time out.',
79+
'Reauthenticate'
80+
)
81+
.then(async res => {
82+
if (res !== 'Reauthenticate') {
83+
return
84+
}
85+
await this.authProvider.auth.reauthenticate(connection)
86+
void this.tryStartDevEnvActivityHeartbeat()
87+
})
88+
89+
getLogger().warn('codecatalyst: dev env needs reauth to not time out')
90+
} else {
91+
getLogger().warn(`codecatalyst: dev env needs a connection to not time out`)
92+
}
93+
return
94+
}
95+
96+
const maxInactivityMinutes = thisDevenv.summary.inactivityTimeoutMinutes
97+
if (!shouldTrackUserActivity(maxInactivityMinutes)) {
98+
getLogger().debug(
99+
`codecatalyst: not tracking user inactivity due to inactivity minutes being: ${maxInactivityMinutes}`
100+
)
101+
return
102+
}
103+
104+
const devEnvActivity = await DevEnvActivity.instanceIfActivityTrackingEnabled(thisDevenv.devenvClient)
105+
if (!devEnvActivity) {
106+
getLogger().debug(`codecatalyst: not tracking user inactivity since activity api is not enabled`)
107+
return
108+
}
109+
110+
// Everything is good, we can start the activity heartbeat now
111+
devEnvActivity.setUpdateActivityOnIdeActivity(true)
112+
const inactivityMessage = new InactivityMessage()
113+
await inactivityMessage.setupMessage(maxInactivityMinutes, devEnvActivity)
114+
115+
globals.context.subscriptions.push(inactivityMessage, devEnvActivity)
116+
this.didStart = true
117+
})
118+
119+
private static authProvider: CodeCatalystAuthenticationProvider | undefined = undefined
120+
private static getAuthProvider() {
121+
if (!this.authProvider) {
122+
throw new Error('DevEnvActivityHeartbeatStarter authProvider is not set')
123+
}
124+
return this.authProvider
125+
}
126+
}
14127

15128
/** If we should be sending the dev env activity timestamps to track user activity */
16129
export function shouldTrackUserActivity(maxInactivityMinutes: DevEnvironment['inactivityTimeoutMinutes']): boolean {

packages/core/src/codecatalyst/explorer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as codecatalyst from './model'
1616
import { getLogger } from '../shared/logger'
1717
import { Connection } from '../auth/connection'
1818
import { openUrl } from '../shared/utilities/vsCodeUtils'
19-
import { getShowManageConnections } from '../auth/ui/vue/show'
19+
import { getShowManageConnections } from '../login/command'
2020

2121
export const learnMoreCommand = Commands.declare('aws.learnMore', () => async (docsUrl: vscode.Uri) => {
2222
return openUrl(docsUrl)

packages/core/src/login/command.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import vscode from 'vscode'
6+
import { Commands, RegisteredCommand, VsCodeCommandArg, placeholder } from '../shared/vscode/commands2'
7+
import { ServiceItemId, isServiceItemId } from '../auth/ui/vue/types'
8+
import { authCommands } from '../auth/utils'
9+
import { showCodeWhispererConnectionPrompt } from '../codewhisperer/util/showSsoPrompt'
10+
import { AuthSource, AuthSources } from './webview/util'
11+
import { isCloud9 } from '../shared/extensionUtilities'
12+
import { isWeb } from '../shared/extensionGlobals'
13+
import { CommonAuthWebview } from './webview/vue/backend'
14+
15+
let showManageConnections: RegisteredCommand<any> | undefined
16+
export function getShowManageConnections(): RegisteredCommand<any> {
17+
if (!showManageConnections) {
18+
throw new Error('showManageConnections not registered')
19+
}
20+
return showManageConnections
21+
}
22+
23+
export function registerCommands(context: vscode.ExtensionContext, prefix: string) {
24+
showManageConnections = Commands.register(
25+
{ id: `aws.${prefix}.auth.manageConnections`, compositeKey: { 1: 'source' } },
26+
async (_: VsCodeCommandArg, source: AuthSource, serviceToShow?: ServiceItemId) => {
27+
if (_ !== placeholder) {
28+
source = AuthSources.vscodeComponent
29+
}
30+
31+
// The auth webview page does not make sense to use in C9,
32+
// so show the auth quick pick instead.
33+
if (isCloud9('any') || isWeb()) {
34+
if (source.toLowerCase().includes('codewhisperer')) {
35+
// Show CW specific quick pick for CW connections
36+
return showCodeWhispererConnectionPrompt()
37+
}
38+
return authCommands().addConnection.execute()
39+
}
40+
41+
if (!isServiceItemId(serviceToShow)) {
42+
serviceToShow = undefined
43+
}
44+
45+
// TODO: hack
46+
if (prefix === 'toolkit') {
47+
CommonAuthWebview.authSource = source
48+
await vscode.commands.executeCommand('aws.explorer.setLoginService', serviceToShow)
49+
await vscode.commands.executeCommand('setContext', 'aws.explorer.showAuthView', true)
50+
await vscode.commands.executeCommand('aws.toolkit.AmazonCommonAuth.focus')
51+
}
52+
}
53+
)
54+
}

0 commit comments

Comments
 (0)