@@ -7,18 +7,50 @@ import * as vscode from 'vscode'
7
7
import * as packageJson from '../../package.json'
8
8
import * as codecatalyst from './clients/codecatalystClient'
9
9
import * as codewhisperer from '../codewhisperer/client/codewhisperer'
10
- import { getLogger } from './logger'
10
+ import { getLogger , showLogOutputChannel } from './logger'
11
11
import { cast , FromDescriptor , Record , TypeConstructor , TypeDescriptor } from './utilities/typeConstructors'
12
12
import { assertHasProps , ClassToInterfaceType , keys } from './utilities/tsUtils'
13
13
import { toRecord } from './utilities/collectionUtils'
14
14
import { isNameMangled } from './vscode/env'
15
15
import { once , onceChanged } from './utilities/functionUtils'
16
16
import { ToolkitError } from './errors'
17
+ import { telemetry } from './telemetry/telemetry'
17
18
18
19
type Workspace = Pick < typeof vscode . workspace , 'getConfiguration' | 'onDidChangeConfiguration' >
19
20
20
- /** Used by isValid(). Must be something that's defined in our package.json. */
21
- const testSetting = 'aws.samcli.lambdaTimeout'
21
+ /** Used by isReadable(). Must be something that's defined in our package.json. */
22
+ export const testSetting = 'aws.samcli.lambdaTimeout'
23
+
24
+ export async function showSettingsFailedMsg ( kind : 'read' | 'update' , key ?: string ) {
25
+ const keyMsg = key ? ` (key: "${ key } ")` : ''
26
+ const msg = `Failed to ${ kind } settings${ keyMsg } . Check settings.json for syntax errors or insufficient permissions.`
27
+ const openSettingsItem = 'Open settings.json'
28
+ const logsItem = 'View Logs...'
29
+
30
+ const items = [ openSettingsItem , logsItem ]
31
+ const p = vscode . window . showErrorMessage ( msg , { } , ...items )
32
+ return p . then < string | undefined > ( async selection => {
33
+ if ( selection === logsItem ) {
34
+ showLogOutputChannel ( )
35
+ } else if ( selection === openSettingsItem ) {
36
+ await vscode . commands . executeCommand ( 'workbench.action.openSettingsJson' )
37
+ }
38
+ return selection
39
+ } )
40
+ }
41
+
42
+ /**
43
+ * Shows an error message if we couldn't update settings, unless the last message was for the same `key`.
44
+ */
45
+ const showSettingsUpdateFailedMsgOnce = onceChanged ( key => {
46
+ // Edge cases:
47
+ // - settings.json may intentionally be readonly. #4043
48
+ // - settings.json may be open in multiple vscodes. #4453
49
+ // - vscode will show its own error if settings.json cannot be written.
50
+ void showSettingsFailedMsg ( 'update' , key )
51
+
52
+ telemetry . aws_modifySetting . emit ( { result : 'Failed' , reason : 'UserSettingsWrite' , settingId : key } )
53
+ } )
22
54
23
55
/**
24
56
* A class for manipulating VS Code user settings (from all extensions).
@@ -78,58 +110,43 @@ export class Settings {
78
110
* `vscode.ConfigurationTarget.Workspace` target requires a workspace).
79
111
*/
80
112
public async update ( key : string , value : unknown ) : Promise < boolean > {
113
+ const config = this . getConfig ( )
81
114
try {
82
- await this . getConfig ( ) . update ( key , value , this . updateTarget )
115
+ await config . update ( key , value , this . updateTarget )
83
116
84
117
return true
85
118
} catch ( e ) {
86
- getLogger ( ) . warn ( 'settings: failed to update "%s": %s' , key , ( e as Error ) . message )
119
+ const fullKey = config . inspect ( key ) ?. key ?? key
120
+ getLogger ( ) . warn ( 'settings: failed to update "%s": %s' , fullKey , ( e as Error ) . message )
121
+ showSettingsUpdateFailedMsgOnce ( fullKey )
87
122
88
123
return false
89
124
}
90
125
}
91
126
92
127
/**
93
- * Checks that user `settings.json` actually works . #3910
128
+ * Checks that user `settings.json` is readable . #3910
94
129
*
95
- * Note: This checks that we can actually "roundtrip" (read and write) settings. vscode notifies
96
- * the user if settings.json is complete nonsense, but silently fails if there are only
97
- * "recoverable" JSON syntax errors.
130
+ * Note: Does NOT check that we can "roundtrip" (read-and-write) settings. vscode notifies the
131
+ * user if settings.json is complete nonsense, but silently fails if there are only
132
+ * "recoverable" JSON syntax errors. We can't test for "roundtrip" on _startup_ because it causes
133
+ * race conditions if multiple VSCode instances start simultaneously. #4453
134
+ * Instead we handle that in {@link Settings#update()}.
98
135
*/
99
- public async isValid ( ) : Promise < 'ok' | 'invalid' | 'nowrite' > {
136
+ public async isReadable ( ) : Promise < boolean > {
100
137
const key = testSetting
101
138
const config = this . getConfig ( )
102
- const tempValOld = 1234 // Legacy temp value we are migrating from.
103
- const tempVal = 91234 // Temp value used to check that read/write works.
104
- const defaultVal = settingsProps [ key ] . default
105
139
106
140
try {
107
- const userVal = config . get < number > ( key )
108
- // Try to write a temporary "sentinel" value to settings.json.
109
- await config . update ( key , tempVal , this . updateTarget )
110
- if ( userVal === undefined || [ defaultVal , tempValOld , tempVal ] . includes ( userVal ) ) {
111
- // Avoid polluting the user's settings.json.
112
- await config . update ( key , undefined , this . updateTarget )
113
- } else {
114
- // Restore the user's actual setting value.
115
- await config . update ( key , userVal , this . updateTarget )
116
- }
141
+ config . get < number > ( key )
117
142
118
- return 'ok'
143
+ return true
119
144
} catch ( e ) {
120
145
const err = e as Error
121
- // If anything tries to update an unwritable settings.json, vscode will thereafter treat
122
- // it as an "unsaved" file. #4043
123
- if ( err . message . includes ( 'EACCES' ) || err . message . includes ( 'the file has unsaved changes' ) ) {
124
- const logMsg = 'settings: unwritable settings.json: %s'
125
- getLogger ( ) . warn ( logMsg , err . message )
126
- return 'nowrite'
127
- }
128
-
129
146
const logMsg = 'settings: invalid settings.json: %s'
130
147
getLogger ( ) . error ( logMsg , err . message )
131
148
132
- return 'invalid'
149
+ return false
133
150
}
134
151
}
135
152
@@ -321,6 +338,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
321
338
322
339
return true
323
340
} catch ( e ) {
341
+ const fullKey = `${ section } .${ key } `
342
+ showSettingsUpdateFailedMsgOnce ( fullKey )
324
343
this . _log ( 'failed to update "%s": %s' , key , ( e as Error ) . message )
325
344
326
345
return false
@@ -333,6 +352,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
333
352
334
353
return true
335
354
} catch ( e ) {
355
+ const fullKey = `${ section } .${ key } `
356
+ showSettingsUpdateFailedMsgOnce ( fullKey )
336
357
this . _log ( 'failed to delete "%s": %s' , key , ( e as Error ) . message )
337
358
338
359
return false
@@ -394,9 +415,7 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
394
415
}
395
416
396
417
for ( const key of props . filter ( isDifferent ) ) {
397
- if ( `${ section } .${ key } ` !== testSetting ) {
398
- this . _log ( 'key "%s" changed' , key )
399
- }
418
+ this . _log ( 'key "%s" changed' , key )
400
419
emitter . fire ( { key } )
401
420
}
402
421
} )
0 commit comments