Skip to content

Commit c77fc07

Browse files
authored
fix: "Failed to access settings" when restarting multiple vscodes #4474
Problem: Shutdown-and-restart VS Code with multiple windows opened, causes AWS Toolkit to show a "Failed to access settings" error. #4453 Steps to reproduce: 1. open a bunch of different vscode instances/workspaces. 2. close all vscode instances. 3. start vscode (which resumes all the previously-closed instances). Solution: Verifying settings on startup causes too many problems, so drop it in favor of verifying just-in-time.
1 parent 941a470 commit c77fc07

File tree

6 files changed

+121
-85
lines changed

6 files changed

+121
-85
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "\"Failed to access settings\" error when restarting multiple vscode instances"
4+
}

packages/core/src/extension.ts

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,10 @@ import { EcsCredentialsProvider } from './auth/providers/ecsCredentialsProvider'
4646
import { SchemaService } from './shared/schemas'
4747
import { AwsResourceManager } from './dynamicResources/awsResourceManager'
4848
import globals from './shared/extensionGlobals'
49-
import { Experiments, Settings } from './shared/settings'
49+
import { Experiments, Settings, showSettingsFailedMsg } from './shared/settings'
5050
import { isReleaseVersion } from './shared/vscode/env'
5151
import { telemetry } from './shared/telemetry/telemetry'
5252
import { Auth } from './auth/auth'
53-
import { showViewLogsMessage } from './shared/utilities/messages'
5453
import { initializeNetworkAgent } from './codewhisperer/client/agent'
5554
import { Timeout } from './shared/utilities/timeoutUtils'
5655
import { submitFeedback } from './feedback/vue/submitFeedback'
@@ -180,7 +179,10 @@ export async function activate(context: vscode.ExtensionContext) {
180179

181180
showWelcomeMessage(context)
182181

183-
const settingsValid = await checkSettingsHealth(settings)
182+
const settingsValid = await settings.isReadable()
183+
if (!settingsValid) {
184+
void showSettingsFailedMsg('read')
185+
}
184186
recordToolkitInitialization(activationStartedOn, settingsValid, getLogger())
185187

186188
if (!isReleaseVersion()) {
@@ -238,38 +240,13 @@ function recordToolkitInitialization(activationStartedOn: number, settingsValid:
238240
if (settingsValid) {
239241
telemetry.toolkit_init.emit({ duration, result: 'Succeeded' })
240242
} else {
241-
telemetry.toolkit_init.emit({ duration, result: 'Failed', reason: 'UserSettings' })
243+
telemetry.toolkit_init.emit({ duration, result: 'Failed', reason: 'UserSettingsRead' })
242244
}
243245
} catch (err) {
244246
logger?.error(err as Error)
245247
}
246248
}
247249

248-
async function checkSettingsHealth(settings: Settings): Promise<boolean> {
249-
const r = await settings.isValid()
250-
switch (r) {
251-
case 'invalid': {
252-
const msg = 'Failed to access settings. Check settings.json for syntax errors.'
253-
const openSettingsItem = 'Open settings.json'
254-
void showViewLogsMessage(msg, 'error', [openSettingsItem]).then(async resp => {
255-
if (resp === openSettingsItem) {
256-
await vscode.commands.executeCommand('workbench.action.openSettingsJson')
257-
}
258-
})
259-
return false
260-
}
261-
// Don't show a message for 'nowrite' because:
262-
// - settings.json may intentionally be readonly. #4043
263-
// - vscode will show its own error if settings.json cannot be written.
264-
//
265-
// Note: isValid() already logged a warning.
266-
case 'nowrite':
267-
case 'ok':
268-
default:
269-
return true
270-
}
271-
}
272-
273250
// Unique extension entrypoint names, so that they can be obtained from the webpack bundle
274251
export const awsToolkitActivate = activate
275252
export const awsToolkitDeactivate = deactivate

packages/core/src/shared/sam/cli/samCliDetection.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,12 @@ export async function detectSamCli(args: { passive: boolean; showMessage: boolea
3737
// conflicts with VSCode "remote": each VSCode instance will update the
3838
// setting based on its local environment, but the user settings are
3939
// shared across VSCode instances...
40-
if (!args.passive && sam.autoDetected && sam.path) {
41-
await config.update('location', sam.path)
42-
}
40+
const update = !args.passive && sam.autoDetected && sam.path && (await config.update('location', sam.path))
4341

4442
if (args.showMessage !== false || !found) {
4543
if (!found) {
4644
notifyUserSamCliNotDetected(config)
47-
} else if (args.showMessage === true) {
45+
} else if (update && args.showMessage === true) {
4846
void vscode.window.showInformationMessage(getSettingsUpdatedMessage(sam.path ?? '?'))
4947
}
5048
}

packages/core/src/shared/settings.ts

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,50 @@ import * as vscode from 'vscode'
77
import * as packageJson from '../../package.json'
88
import * as codecatalyst from './clients/codecatalystClient'
99
import * as codewhisperer from '../codewhisperer/client/codewhisperer'
10-
import { getLogger } from './logger'
10+
import { getLogger, showLogOutputChannel } from './logger'
1111
import { cast, FromDescriptor, Record, TypeConstructor, TypeDescriptor } from './utilities/typeConstructors'
1212
import { assertHasProps, ClassToInterfaceType, keys } from './utilities/tsUtils'
1313
import { toRecord } from './utilities/collectionUtils'
1414
import { isNameMangled } from './vscode/env'
1515
import { once, onceChanged } from './utilities/functionUtils'
1616
import { ToolkitError } from './errors'
17+
import { telemetry } from './telemetry/telemetry'
1718

1819
type Workspace = Pick<typeof vscode.workspace, 'getConfiguration' | 'onDidChangeConfiguration'>
1920

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+
})
2254

2355
/**
2456
* A class for manipulating VS Code user settings (from all extensions).
@@ -78,58 +110,43 @@ export class Settings {
78110
* `vscode.ConfigurationTarget.Workspace` target requires a workspace).
79111
*/
80112
public async update(key: string, value: unknown): Promise<boolean> {
113+
const config = this.getConfig()
81114
try {
82-
await this.getConfig().update(key, value, this.updateTarget)
115+
await config.update(key, value, this.updateTarget)
83116

84117
return true
85118
} 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)
87122

88123
return false
89124
}
90125
}
91126

92127
/**
93-
* Checks that user `settings.json` actually works. #3910
128+
* Checks that user `settings.json` is readable. #3910
94129
*
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()}.
98135
*/
99-
public async isValid(): Promise<'ok' | 'invalid' | 'nowrite'> {
136+
public async isReadable(): Promise<boolean> {
100137
const key = testSetting
101138
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
105139

106140
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)
117142

118-
return 'ok'
143+
return true
119144
} catch (e) {
120145
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-
129146
const logMsg = 'settings: invalid settings.json: %s'
130147
getLogger().error(logMsg, err.message)
131148

132-
return 'invalid'
149+
return false
133150
}
134151
}
135152

@@ -321,6 +338,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
321338

322339
return true
323340
} catch (e) {
341+
const fullKey = `${section}.${key}`
342+
showSettingsUpdateFailedMsgOnce(fullKey)
324343
this._log('failed to update "%s": %s', key, (e as Error).message)
325344

326345
return false
@@ -333,6 +352,8 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
333352

334353
return true
335354
} catch (e) {
355+
const fullKey = `${section}.${key}`
356+
showSettingsUpdateFailedMsgOnce(fullKey)
336357
this._log('failed to delete "%s": %s', key, (e as Error).message)
337358

338359
return false
@@ -394,9 +415,7 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
394415
}
395416

396417
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)
400419
emitter.fire({ key })
401420
}
402421
})

packages/core/src/test/shared/settingsConfiguration.test.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66
import assert from 'assert'
77
import * as sinon from 'sinon'
88
import * as vscode from 'vscode'
9-
import { DevSettings, Experiments, fromExtensionManifest, PromptSettings, Settings } from '../../shared/settings'
9+
import {
10+
DevSettings,
11+
Experiments,
12+
fromExtensionManifest,
13+
PromptSettings,
14+
Settings,
15+
testSetting,
16+
} from '../../shared/settings'
1017
import { TestSettings } from '../utilities/testSettingsConfiguration'
1118
import { ClassToInterfaceType } from '../../shared/utilities/tsUtils'
1219
import { Optional } from '../../shared/utilities/typeConstructors'
1320
import { ToolkitError } from '../../shared/errors'
21+
import { getTestWindow } from './vscode/window'
1422

1523
const settingsTarget = vscode.ConfigurationTarget.Workspace
1624

@@ -46,10 +54,13 @@ describe('Settings', function () {
4654
// Note: we don't test undefined because retrieval returns the package.json configured default value, if there is one
4755
]
4856

49-
it('isValid()', async () => {
57+
it('isReadable()', async () => {
5058
// TODO: could avoid sinon if we can force vscode to use a dummy settings.json file.
5159
const fake = {
52-
get: async () => {
60+
get: (key: string) => {
61+
if (key === testSetting) {
62+
throw Error()
63+
}
5364
return 'setting-value'
5465
},
5566
update: async () => {
@@ -58,17 +69,7 @@ describe('Settings', function () {
5869
} as unknown as vscode.WorkspaceConfiguration
5970
sinon.stub(vscode.workspace, 'getConfiguration').returns(fake)
6071

61-
assert.deepStrictEqual(await sut.isValid(), 'invalid')
62-
63-
fake.update = async () => {
64-
throw Error('EACCES')
65-
}
66-
assert.deepStrictEqual(await sut.isValid(), 'nowrite')
67-
68-
fake.update = async () => {
69-
throw Error('xxx the file has unsaved changes xxx')
70-
}
71-
assert.deepStrictEqual(await sut.isValid(), 'nowrite')
72+
assert.deepStrictEqual(await sut.isReadable(), false)
7273
})
7374

7475
describe('get', function () {
@@ -122,6 +123,43 @@ describe('Settings', function () {
122123
assert.deepStrictEqual(savedValue, scenario.testValue)
123124
})
124125
})
126+
127+
it('shows message on failure', async () => {
128+
// TODO: could avoid sinon if we can force vscode to use a dummy settings.json file.
129+
// const fakeSettings = {}
130+
const fake = {
131+
lastValue: 'x',
132+
get: (key: string) => {
133+
return `${key}-value`
134+
},
135+
update: async (key: string, val: any) => {
136+
// Do nothing (success).
137+
// (fakeSettings as any)[key] = val
138+
},
139+
inspect: (key: string) => {
140+
return {}
141+
},
142+
} as unknown as vscode.WorkspaceConfiguration
143+
sinon.stub(vscode.workspace, 'getConfiguration').returns(fake)
144+
145+
assert.deepStrictEqual(await sut.update(testSetting, 91234), true)
146+
// No errors.
147+
assert.deepStrictEqual(getTestWindow().shownMessages.length, 0)
148+
149+
fake.update = async () => {
150+
throw Error('EACCES')
151+
}
152+
assert.deepStrictEqual(await sut.update(testSetting, 91234), false)
153+
getTestWindow()
154+
.getFirstMessage()
155+
.assertError(
156+
`Failed to update settings (key: "${testSetting}"). Check settings.json for syntax errors or insufficient permissions.`
157+
)
158+
159+
// Message skipped for duplicate error.
160+
assert.deepStrictEqual(await sut.update(testSetting, 91234), false)
161+
assert.deepStrictEqual(getTestWindow().shownMessages.length, 1)
162+
})
125163
})
126164

127165
describe('isSet', function () {

packages/core/src/test/utilities/testSettingsConfiguration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export class TestSettings implements ClassToInterfaceType<Settings> {
4848
return !type || value === undefined ? value : cast(value, type)
4949
}
5050

51-
public async isValid(): Promise<'ok' | 'invalid' | 'nowrite'> {
52-
return 'ok'
51+
public async isReadable(): Promise<boolean> {
52+
return true
5353
}
5454

5555
public async update(key: string, value: unknown): Promise<boolean> {

0 commit comments

Comments
 (0)