Skip to content

Commit 410afb2

Browse files
Merge staging into feature/lambda-get-started
2 parents ea482f6 + a05dd40 commit 410afb2

File tree

13 files changed

+366
-75
lines changed

13 files changed

+366
-75
lines changed

packages/core/src/codewhisperer/activation.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import { ReferenceInlineProvider } from './service/referenceInlineProvider'
5050
import { disposeSecurityDiagnostic, securityScanRender } from './service/diagnosticsProvider'
5151
import { SecurityPanelViewProvider, openEditorAtRange } from './views/securityPanelViewProvider'
5252
import { RecommendationHandler } from './service/recommendationHandler'
53-
import { Commands, registerCommandsWithVSCode } from '../shared/vscode/commands2'
53+
import { Commands, registerCommandErrorHandler, registerDeclaredCommands } from '../shared/vscode/commands2'
5454
import { InlineCompletionService, refreshStatusBar } from './service/inlineCompletionService'
5555
import { isInlineCompletionEnabled } from './util/commonUtil'
5656
import { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker'
@@ -68,7 +68,7 @@ import { Container } from './service/serviceContainer'
6868
import { debounceStartSecurityScan } from './commands/startSecurityScan'
6969
import { securityScanLanguageContext } from './util/securityScanLanguageContext'
7070
import { registerWebviewErrorHandler } from '../webviews/server'
71-
import { logAndShowWebviewError } from '../shared/utilities/logAndShowUtils'
71+
import { logAndShowError, logAndShowWebviewError } from '../shared/utilities/logAndShowUtils'
7272
import { openSettings } from '../shared/settings'
7373

7474
let localize: nls.LocalizeFunc
@@ -92,8 +92,9 @@ export async function activate(context: ExtContext): Promise<void> {
9292
await enableDefaultConfigCloud9()
9393
}
9494

95-
registerCommandsWithVSCode(
96-
context.extensionContext,
95+
// TODO: is this indirection useful?
96+
registerDeclaredCommands(
97+
context.extensionContext.subscriptions,
9798
CodeWhispererCommandDeclarations.instance,
9899
new CodeWhispererCommandBackend(context.extensionContext)
99100
)
@@ -104,9 +105,13 @@ export async function activate(context: ExtContext): Promise<void> {
104105
const securityPanelViewProvider = new SecurityPanelViewProvider(context.extensionContext)
105106
activateSecurityScan()
106107

107-
/**
108-
* Register the webview error handler for Amazon Q
109-
*/
108+
// TODO: this is already done in packages/core/src/extensionCommon.ts, why doesn't amazonq use that?
109+
registerCommandErrorHandler((info, error) => {
110+
const defaultMessage = localize('AWS.generic.message.error', 'Failed to run command: {0}', info.id)
111+
void logAndShowError(localize, error, info.id, defaultMessage)
112+
})
113+
114+
// TODO: this is already done in packages/core/src/extensionCommon.ts, why doesn't amazonq use that?
110115
registerWebviewErrorHandler((error: unknown, webviewId: string, command: string) => {
111116
logAndShowWebviewError(localize, error, webviewId, command)
112117
})
@@ -293,7 +298,8 @@ export async function activate(context: ExtContext): Promise<void> {
293298

294299
if (auth.isConnectionExpired()) {
295300
auth.showReauthenticatePrompt().catch(e => {
296-
getLogger().error('showReauthenticatePrompt failed: %s', (e as Error).message)
301+
const defaulMsg = localize('AWS.generic.message.error', 'Failed to reauth:')
302+
void logAndShowError(localize, e, 'showReauthenticatePrompt', defaulMsg)
297303
})
298304
}
299305
if (auth.isValidEnterpriseSsoInUse()) {

packages/core/src/extensionCommon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { CredentialsStore } from './auth/credentials/store'
3535
import { initializeAwsCredentialsStatusBarItem } from './auth/ui/statusBarItem'
3636
import { RegionProvider, getEndpointsFromFetcher } from './shared/regions/regionProvider'
3737
import { getMachineId } from './shared/vscode/env'
38-
import { registerErrorHandler as registerCommandErrorHandler } from './shared/vscode/commands2'
38+
import { registerCommandErrorHandler } from './shared/vscode/commands2'
3939
import { registerWebviewErrorHandler } from './webviews/server'
4040
import { showQuickStartWebview } from './shared/extensionStartup'
4141
import { ExtContext } from './shared/extensions'

packages/core/src/shared/errors.ts

Lines changed: 132 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isNonNullable } from './utilities/tsUtils'
1313
import type * as fs from 'fs'
1414
import type * as os from 'os'
1515
import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming'
16+
import { isAutomation } from './vscode/env'
1617

1718
export const errorCode = {
1819
invalidConnection: 'InvalidConnection',
@@ -82,7 +83,8 @@ export interface ErrorInformation {
8283
/**
8384
* A link to documentation relevant to this error.
8485
*
85-
* TODO: implement this
86+
* TODO: use this throughout the codebase.
87+
* TODO: prefer `Error.error_uri` if present (from OIDC/OAuth service)?
8688
*/
8789
readonly documentationUri?: vscode.Uri
8890
}
@@ -234,9 +236,13 @@ export function getErrorMsg(err: Error | undefined): string | undefined {
234236
return undefined
235237
}
236238

237-
// error_description is a non-standard SDK field added by (at least) OIDC service.
238-
// If present, it has better information, so prefer it to `message`.
239-
// https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
239+
// Non-standard SDK fields added by the OIDC service, to conform to the OAuth spec
240+
// (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) :
241+
// - error: code per the OAuth spec
242+
// - error_description: improved error message provided by OIDC service. Prefer this to
243+
// `message` if present.
244+
// https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
245+
// - error_uri: not provided by OIDC currently?
240246
//
241247
// Example:
242248
//
@@ -355,16 +361,20 @@ const _preferredErrors: RegExp[] = [
355361
/^ResourceNotFoundException$/,
356362
/^ServiceQuotaExceededException$/,
357363
/^AccessDeniedException$/,
364+
/^InvalidPermissions$/,
365+
/^EPIPE$/,
366+
/^EPERM$/,
358367
]
359368

360369
/**
361370
* Searches the `cause` chain (if any) for the most useful/relevant {@link AWSError} to surface to
362371
* the user, preferring "deeper" errors (lower-level, closer to the root cause) when all else is equal.
363372
*
364373
* These conditions determine precedence (in order):
365-
* - required: AWSError type
366-
* - `error_description` field
367-
* - `code` matches one of `preferredErrors`
374+
* - is AWSError
375+
* - has `error_description` field
376+
* - has `code` matching `preferredErrors`
377+
* - is filesystem error
368378
* - cause chain depth (the deepest error wins)
369379
*
370380
* @param error Error whose `cause` chain will be searched.
@@ -375,14 +385,26 @@ const _preferredErrors: RegExp[] = [
375385
export function findBestErrorInChain(error: Error, preferredErrors = _preferredErrors): Error | undefined {
376386
// TODO: Base Error has 'cause' in ES2022. So does our own `ToolkitError`.
377387
// eslint-disable-next-line @typescript-eslint/naming-convention
378-
let bestErr: Error & { cause?: Error; error_description?: string } = error
388+
let bestErr: Error & { code?: string; cause?: Error; error_description?: string } = error
379389
let err: typeof bestErr | undefined
380390

381-
for (let i = 0; (err || i === 0) && i < 100; i++) {
391+
for (let i = 0; i < 100; i++) {
382392
err = i === 0 ? bestErr.cause : err?.cause
393+
if (!err) {
394+
break
395+
}
383396

384-
if (isAwsError(err)) {
385-
if (!isAwsError(bestErr)) {
397+
// const bestErrCode = bestErr.code?.trim() ?? ''
398+
// const preferBest = ...
399+
const errCode = err.code?.trim() ?? ''
400+
const prefer =
401+
(errCode !== '' && preferredErrors.some(re => re.test(errCode))) ||
402+
// In priority order:
403+
isFilesystemError(err) ||
404+
isNoPermissionsError(err)
405+
406+
if (isAwsError(err) || (prefer && !isAwsError(bestErr))) {
407+
if (isAwsError(err) && !isAwsError(bestErr)) {
386408
bestErr = err // Prefer AWSError.
387409
continue
388410
}
@@ -393,11 +415,7 @@ export function findBestErrorInChain(error: Error, preferredErrors = _preferredE
393415
continue
394416
}
395417

396-
// const bestErrCode = bestErr.code?.trim() ?? ''
397-
// const bestErrMatches = bestErrCode !== '' && preferredErrors.some(re => re.test(bestErrCode))
398-
const errCode = err.code?.trim() ?? ''
399-
const errMatches = errCode !== '' && preferredErrors.some(re => re.test(errCode))
400-
if (!bestErr.error_description && errMatches) {
418+
if (!bestErr.error_description && prefer) {
401419
bestErr = err
402420
}
403421
}
@@ -469,6 +487,41 @@ export function getRequestId(err: unknown): string | undefined {
469487
}
470488
}
471489

490+
export function isFilesystemError(err: unknown): boolean {
491+
if (
492+
err instanceof vscode.FileSystemError ||
493+
(hasCode(err) &&
494+
(err.code === 'EEXIST' ||
495+
err.code === 'EISDIR' ||
496+
err.code === 'ENOTDIR' ||
497+
err.code === 'EMFILE' ||
498+
err.code === 'ENOENT' ||
499+
err.code === 'ENOTEMPTY'))
500+
) {
501+
return true
502+
}
503+
504+
return false
505+
}
506+
507+
// export function isIsDirError(err: unknown): boolean {
508+
// if (err instanceof vscode.FileSystemError) {
509+
// return err.code === vscode.FileSystemError.FileIsADirectory().code
510+
// } else if (hasCode(err)) {
511+
// return err.code === 'EISDIR'
512+
// }
513+
// return false
514+
// }
515+
//
516+
// export function isFileExistsError(err: unknown): boolean {
517+
// if (err instanceof vscode.FileSystemError) {
518+
// return err.code === vscode.FileSystemError.FileExists().code
519+
// } else if (hasCode(err)) {
520+
// return err.code === 'EEXIST'
521+
// }
522+
// return false
523+
// }
524+
472525
export function isFileNotFoundError(err: unknown): boolean {
473526
if (err instanceof vscode.FileSystemError) {
474527
return err.code === vscode.FileSystemError.FileNotFound().code
@@ -487,16 +540,33 @@ export function isNoPermissionsError(err: unknown): boolean {
487540
(err.code === 'Unknown' && err.message.includes('EACCES: permission denied'))
488541
)
489542
} else if (hasCode(err)) {
543+
// " Some operating systems report EPERM in situations unrelated to permissions."
544+
// || err.code === 'EPERM'
490545
return err.code === 'EACCES'
491546
}
492547

493548
return false
494549
}
495550

496-
const modeToString = (mode: number) =>
497-
Array.from('rwxrwxrwx')
551+
function modeToString(mode: number) {
552+
return Array.from('rwxrwxrwx')
498553
.map((c, i, a) => ((mode >> (a.length - (i + 1))) & 1 ? c : '-'))
499554
.join('')
555+
}
556+
557+
function vscodeModeToString(mode: vscode.FileStat['permissions']) {
558+
// XXX: vscode.FileStat.permissions only indicates "readonly" or nothing (aka "writable").
559+
if (mode === undefined) {
560+
return 'rwx------'
561+
} else if (mode === vscode.FilePermission.Readonly) {
562+
return 'r-x------'
563+
}
564+
565+
// XXX: future-proof in case vscode.FileStat.permissions gains more granularity.
566+
if (isAutomation()) {
567+
throw new Error('vscode.FileStat.permissions gained new fields, update this logic')
568+
}
569+
}
500570

501571
function getEffectivePerms(uid: number, gid: number, stats: fs.Stats) {
502572
const mode = stats.mode
@@ -529,38 +599,71 @@ export type PermissionsTriplet = `${'r' | '-' | '*'}${'w' | '-' | '*'}${'x' | '-
529599
export class PermissionsError extends ToolkitError {
530600
public readonly actual: string // This is a resolved triplet, _not_ the full bits
531601

602+
static fromNodeFileStats(stats: fs.Stats, userInfo: os.UserInfo<string>, expected: string, source: unknown) {
603+
const mode = `${stats.isDirectory() ? 'd' : '-'}${modeToString(stats.mode)}`
604+
const owner = stats.uid === userInfo.uid ? (stats.uid === -1 ? '' : userInfo.username) : String(stats.uid)
605+
const group = String(stats.gid)
606+
const { effective, isAmbiguous } = getEffectivePerms(userInfo.uid, userInfo.gid, stats)
607+
const actual = modeToString(effective).slice(-3)
608+
const isOwner = stats.uid === -1 ? 'unknown' : userInfo.uid === stats.uid
609+
610+
return { mode, owner, group, actual, isAmbiguous, isOwner }
611+
}
612+
613+
static fromVscodeFileStats(
614+
stats: vscode.FileStat,
615+
userInfo: os.UserInfo<string>,
616+
expected: string,
617+
source: unknown
618+
) {
619+
const isDir = !!(stats.type & vscode.FileType.Directory)
620+
const mode = `${isDir ? 'd' : '-'}${vscodeModeToString(stats.permissions)}`
621+
const owner = '' // vscode.FileStat does not currently provide file owner.
622+
const group = '' // vscode.FileStat does not currently provide file group.
623+
const isAmbiguous = true // vscode.workspace.fs.stat() is currently always ambiguous.
624+
const actual = mode
625+
const isOwner = 'unknown' // vscode.FileStat does not currently provide file owner.
626+
627+
return { mode, owner, group, actual, isAmbiguous, isOwner }
628+
}
629+
630+
/**
631+
* Creates a PermissionsError from a file stat() result.
632+
*
633+
* Note: pass `fs.Stats` when possible (in a nodejs context), because it gives much better info.
634+
*/
532635
public constructor(
533636
public readonly uri: vscode.Uri,
534-
public readonly stats: fs.Stats,
637+
public readonly stats: fs.Stats | vscode.FileStat,
535638
public readonly userInfo: os.UserInfo<string>,
536639
public readonly expected: PermissionsTriplet,
537640
source?: unknown
538641
) {
539-
const mode = `${stats.isDirectory() ? 'd' : '-'}${modeToString(stats.mode)}`
540-
const owner = stats.uid === userInfo.uid ? userInfo.username : stats.uid
541-
const { effective, isAmbiguous } = getEffectivePerms(userInfo.uid, userInfo.gid, stats)
542-
const actual = modeToString(effective).slice(-3)
642+
const o = (stats as any).type
643+
? PermissionsError.fromVscodeFileStats(stats as vscode.FileStat, userInfo, expected, source)
644+
: PermissionsError.fromNodeFileStats(stats as fs.Stats, userInfo, expected, source)
645+
543646
const resolvedExpected = Array.from(expected)
544-
.map((c, i) => (c === '*' ? actual[i] : c))
647+
.map((c, i) => (c === '*' ? o.actual[i] : c))
545648
.join('')
546-
const actualText = !isAmbiguous ? actual : `${mode.slice(-6, -3)} & ${mode.slice(-3)} (ambiguous)`
649+
const actualText = !o.isAmbiguous ? o.actual : `${o.mode.slice(-6, -3)} & ${o.mode.slice(-3)} (ambiguous)`
547650

548651
// Guard against surfacing confusing error messages. If the actual perms equal the resolved
549652
// perms then odds are it wasn't really a permissions error. Some operating systems report EPERM
550653
// in situations that aren't related to permissions at all.
551-
if (actual === resolvedExpected && !isAmbiguous && source !== undefined) {
654+
if (o.actual === resolvedExpected && !o.isAmbiguous && source !== undefined) {
552655
throw source
553656
}
554657

555658
super(`${uri.fsPath} has incorrect permissions. Expected ${resolvedExpected}, found ${actualText}.`, {
556659
code: 'InvalidPermissions',
557660
details: {
558-
isOwner: stats.uid === -1 ? 'unknown' : userInfo.uid === stats.uid,
559-
mode: `${mode}${stats.uid === -1 ? '' : ` ${owner}`}${stats.gid === -1 ? '' : ` ${stats.gid}`}`,
661+
isOwner: o.isOwner,
662+
mode: `${o.mode}${o.owner === '' ? '' : ` ${o.owner}`}${o.group === '' ? '' : ` ${o.group}`}`,
560663
},
561664
})
562665

563-
this.actual = actual
666+
this.actual = o.actual
564667
}
565668
}
566669

packages/core/src/shared/extensionUtilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export function productName() {
4141
return isAmazonQ() ? 'Amazon Q' : `${getIdeProperties().company} Toolkit`
4242
}
4343

44+
/** Gets the "AWS" or "Amazon Q" prefix (in package.json: `commands.category`). */
45+
export function commandsPrefix(): string {
46+
return isAmazonQ() ? 'Amazon Q' : getIdeProperties().company
47+
}
48+
4449
export const mostRecentVersionKey: string = 'globalsMostRecentVersion'
4550

4651
export enum IDE {

packages/core/src/shared/localizedText.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as nls from 'vscode-nls'
7-
import { getIdeProperties } from './extensionUtilities'
7+
import { commandsPrefix, getIdeProperties } from './extensionUtilities'
88
const localize = nls.loadMessageBundle()
99

1010
export const yes = localize('AWS.generic.response.yes', 'Yes')
@@ -35,8 +35,8 @@ export function connectionExpired(name: string) {
3535
export const checklogs = () =>
3636
localize(
3737
'AWS.error.check.logs',
38-
'Check the logs by running the "View {0} Toolkit Logs" command from the {1}.',
39-
getIdeProperties().company,
38+
'Check the logs by running the "{0}: View Logs" command from the {1}.',
39+
commandsPrefix(),
4040
getIdeProperties().commandPalette
4141
)
4242

packages/core/src/shared/vscode/commands2.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,12 @@ export interface CommandDeclarations<T> {
113113
* @param declarations Has the mapping of command names to the backend logic
114114
* @param backend The backend logic of the commands
115115
*/
116-
export function registerCommandsWithVSCode<T>(
117-
extContext: vscode.ExtensionContext,
116+
export function registerDeclaredCommands<T>(
117+
disposables: { dispose(): any }[],
118118
declarations: CommandDeclarations<T>,
119119
backend: T
120120
): void {
121-
extContext.subscriptions.push(
122-
...Object.values<DeclaredCommand>(declarations.declared).map(c => c.register(backend))
123-
)
121+
disposables.push(...Object.values<DeclaredCommand>(declarations.declared).map(c => c.register(backend)))
124122
}
125123

126124
/**
@@ -674,7 +672,7 @@ async function runCommand<T extends Callback>(fn: T, info: CommandInfo<T>): Prom
674672
// the extension entry-point. `Commands` form the backbone of everything else in the Toolkit.
675673
// This file should contain as little application-specific logic as possible.
676674
let errorHandler: (info: Omit<CommandInfo<any>, 'args'>, error: unknown) => void
677-
export function registerErrorHandler(handler: typeof errorHandler): void {
675+
export function registerCommandErrorHandler(handler: typeof errorHandler): void {
678676
if (errorHandler !== undefined) {
679677
throw new TypeError('Error handler has already been registered')
680678
}

0 commit comments

Comments
 (0)