Skip to content

Commit 2df1c49

Browse files
committed
feat(settings): "invalid settings.json" message for readonly settings
Problem: settings.json may be intentionally readonly, then "invalid settings.json" is not helpful. #4043 Solution: Only log a warning if settings.json is not writable, don't show a message. If settings.json is invalid a message is still shown.
1 parent f201a47 commit 2df1c49

File tree

5 files changed

+75
-24
lines changed

5 files changed

+75
-24
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: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,13 @@ export class Settings {
8686
}
8787

8888
/**
89-
* Checks that the user `settings.json` works correctly. #3910
89+
* Checks that user `settings.json` actually works. #3910
9090
*
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.
91+
* Note: This checks that we can actually "roundtrip" (read and write) settings. vscode notifies
92+
* the user if settings.json is complete nonsense, but silently fails if there are only
93+
* "recoverable" JSON syntax errors.
9494
*/
95-
public async isValid(): Promise<boolean> {
95+
public async isValid(): Promise<'ok' | 'invalid' | 'nowrite'> {
9696
const key = 'aws.samcli.lambdaTimeout'
9797
const config = this.getConfig()
9898
const tempValOld = 1234 // Legacy temp value we are migrating from.
@@ -111,15 +111,21 @@ export class Settings {
111111
await config.update(key, userVal, this.updateTarget)
112112
}
113113

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

122-
return false
125+
const logMsg = 'settings: invalid "settings.json": %s'
126+
getLogger().error(logMsg, err.message)
127+
128+
return 'invalid'
123129
}
124130
}
125131

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)