Skip to content

Commit 9d2eb66

Browse files
authored
fix: "invalid settings.json" message #4054
2 parents 7d8094a + 1690164 commit 9d2eb66

File tree

5 files changed

+91
-34
lines changed

5 files changed

+91
-34
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Don't show \"invalid settings.json\" message if settings.json is valid but unwritable"
4+
}

src/extension.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -408,17 +408,28 @@ export function logAndShowWebviewError(err: unknown, webviewId: string, command:
408408
}
409409

410410
async function checkSettingsHealth(settings: Settings): Promise<boolean> {
411-
const ok = await settings.isValid()
412-
if (!ok) {
413-
const msg = 'User settings.json file appears to be invalid. Check settings.json for syntax errors.'
414-
const openSettingsItem = 'Open settings.json'
415-
showViewLogsMessage(msg, 'error', [openSettingsItem]).then(async resp => {
416-
if (resp === openSettingsItem) {
417-
vscode.commands.executeCommand('workbench.action.openSettingsJson')
418-
}
419-
})
411+
const r = await settings.isValid()
412+
switch (r) {
413+
case 'invalid': {
414+
const msg = 'Failed to access settings. Check settings.json for syntax errors.'
415+
const openSettingsItem = 'Open settings.json'
416+
showViewLogsMessage(msg, 'error', [openSettingsItem]).then(async resp => {
417+
if (resp === openSettingsItem) {
418+
vscode.commands.executeCommand('workbench.action.openSettingsJson')
419+
}
420+
})
421+
return false
422+
}
423+
// Don't show a message for 'nowrite' because:
424+
// - settings.json may intentionally be readonly. #4043
425+
// - vscode will show its own error if settings.json cannot be written.
426+
//
427+
// Note: isValid() already logged a warning.
428+
case 'nowrite':
429+
case 'ok':
430+
default:
431+
return true
420432
}
421-
return ok
422433
}
423434

424435
async function getMachineId(): Promise<string> {

src/shared/settings.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { ToolkitError } from './errors'
1616

1717
type Workspace = Pick<typeof vscode.workspace, 'getConfiguration' | 'onDidChangeConfiguration'>
1818

19+
/** Used by isValid(). Must be something that's defined in our package.json. */
20+
const testSetting = 'aws.samcli.lambdaTimeout'
21+
1922
/**
2023
* A class for manipulating VS Code user settings (from all extensions).
2124
*
@@ -86,39 +89,46 @@ export class Settings {
8689
}
8790

8891
/**
89-
* Checks that the user `settings.json` works correctly. #3910
92+
* Checks that user `settings.json` actually works. #3910
9093
*
91-
* Note: This checks that we can actually _write_ settings. vscode notifies the user if
92-
* settings.json is complete nonsense, but is silent if there are only "recoverable" JSON syntax
93-
* errors.
94+
* Note: This checks that we can actually "roundtrip" (read and write) settings. vscode notifies
95+
* the user if settings.json is complete nonsense, but silently fails if there are only
96+
* "recoverable" JSON syntax errors.
9497
*/
95-
public async isValid(): Promise<boolean> {
96-
const key = 'aws.samcli.lambdaTimeout'
98+
public async isValid(): Promise<'ok' | 'invalid' | 'nowrite'> {
99+
const key = testSetting
97100
const config = this.getConfig()
101+
const tempValOld = 1234 // Legacy temp value we are migrating from.
102+
const tempVal = 91234 // Temp value used to check that read/write works.
98103
const defaultVal = settingsProps[key].default
99104

100105
try {
101-
const oldVal = config.get<number>(key)
102-
// Try to write to settings.json.
103-
await config.update(key, 1234, this.updateTarget)
104-
// Restore the old value, if any.
105-
if (oldVal === undefined || defaultVal === oldVal) {
106-
// vscode will return the default even if there was no entry in settings.json.
107-
// Avoid polluting the user's settings.json unnecessarily.
106+
const userVal = config.get<number>(key)
107+
// Try to write a temporary "sentinel" value to settings.json.
108+
await config.update(key, tempVal, this.updateTarget)
109+
if (userVal === undefined || [defaultVal, tempValOld, tempVal].includes(userVal)) {
110+
// Avoid polluting the user's settings.json.
108111
await config.update(key, undefined, this.updateTarget)
109112
} else {
110-
await config.update(key, oldVal, this.updateTarget)
113+
// Restore the user's actual setting value.
114+
await config.update(key, userVal, this.updateTarget)
111115
}
112116

113-
return true
117+
return 'ok'
114118
} catch (e) {
115-
getLogger().warn(
116-
'settings: invalid "settings.json" file? failed to update "%s": %s',
117-
key,
118-
(e as Error).message
119-
)
119+
const err = e as Error
120+
// If anything tries to update an unwritable settings.json, vscode will thereafter treat
121+
// it as an "unsaved" file. #4043
122+
if (err.message.includes('EACCES') || err.message.includes('the file has unsaved changes')) {
123+
const logMsg = 'settings: unwritable settings.json: %s'
124+
getLogger().warn(logMsg, err.message)
125+
return 'nowrite'
126+
}
120127

121-
return false
128+
const logMsg = 'settings: invalid settings.json: %s'
129+
getLogger().error(logMsg, err.message)
130+
131+
return 'invalid'
122132
}
123133
}
124134

@@ -379,7 +389,9 @@ function createSettingsClass<T extends TypeDescriptor>(section: string, descript
379389
}
380390

381391
for (const key of props.filter(isDifferent)) {
382-
this.log('key "%s" changed', key)
392+
if (`${section}.${key}` !== testSetting) {
393+
this.log('key "%s" changed', key)
394+
}
383395
emitter.fire({ key })
384396
}
385397
})

src/test/shared/settingsConfiguration.test.ts

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

66
import assert from 'assert'
7+
import * as sinon from 'sinon'
78
import * as vscode from 'vscode'
89
import { DevSettings, Experiments, fromExtensionManifest, PromptSettings, Settings } from '../../shared/settings'
910
import { TestSettings } from '../utilities/testSettingsConfiguration'
@@ -26,6 +27,10 @@ describe('Settings', function () {
2627
await sut.update(settingKey, undefined)
2728
})
2829

30+
afterEach(async function () {
31+
sinon.restore()
32+
})
33+
2934
const scenarios = [
3035
{ testValue: 1234, desc: 'number' },
3136
{ testValue: 0, desc: 'default number' },
@@ -41,6 +46,31 @@ describe('Settings', function () {
4146
// Note: we don't test undefined because retrieval returns the package.json configured default value, if there is one
4247
]
4348

49+
it('isValid()', async () => {
50+
// TODO: could avoid sinon if we can force vscode to use a dummy settings.json file.
51+
const fake = {
52+
get: async () => {
53+
return 'setting-value'
54+
},
55+
update: async () => {
56+
throw Error()
57+
},
58+
} as unknown as vscode.WorkspaceConfiguration
59+
sinon.stub(vscode.workspace, 'getConfiguration').returns(fake)
60+
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+
})
73+
4474
describe('get', function () {
4575
let settings: vscode.WorkspaceConfiguration
4676

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

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

0 commit comments

Comments
 (0)