Skip to content
This repository was archived by the owner on Oct 1, 2024. It is now read-only.

Commit 2d52e52

Browse files
elektronikworkshopadiazulay
authored andcommitted
Fixed handling of invalid manual configurations. Improved code locality for board configuration manipulation and fixed a race condition with fine grained event handling. Details:
Added missing checks when board configurations are loaded from the configuration file: * Up to now vscode-arduino blindly loaded any board configuration from `arduino.json` even if this would result in invalid board configurations which in turn lead to compilation (verify, upload) failure. * Up to now this state couldn't be recovered by simply removing the offending configuration from the configuration file. Even worse: it stored the wrong configuration in between board changes. To reproduce the bug in 0.2.29 1. Select Arduino Nano with the *Arduino Board Configuration* 2. Set configuration in `arduino.json` to `cpu=cray2` and save 3. Verify -> fails 4. Switch board to Arduino Uno 5. Switch back to Arduino Nano: The wrong configuration is back and now the user can't even select another (correct) configuration from the *Arduino Board Configuration* window 6. Delete the wrong configuration and save -> verify still fails `vscode-arduino` does not fall back to a default configuration. The user has now two options: find the correct configuration by himself and set it within arduino.json. Very experienced users could probably accomplish that. Everone else can just restart vscode. I corrected that by enhancing IBoard.loadConfig and IBoard.updateConfig member functions to * check for proper formatting of the config string loaded from `arduino.json` * check if the configuration IDs and the option IDs are valid If any of the above fails, the functions bail out and return the error. The board manager then loads a default configuration and issues a warning, that the configuration is invalid. This way the user gets the chance to fix her/his configuration but gets informed at the same time, that a different configuration than the intended is loaded (prevents surprises). This situation is only relevant, when users start fiddling with the configuration values in `arduino.json`. As long as they just set the board and the configurations from within the *Arduino Board Configuration Window* nothing bad can happen. But now custom configurations are handled in a cleaner way. The DeviceContext's board configuration was set in board.ts and boardManager.ts in different places - even when it was loaded after a DeviceContext's configuration update event which is prone to infinite loops. This has been resolved and it's not re-written/re-set during loading a configuration on change. This is valid for board manager's updateStatusBar function which fiddled with the board and the configuration. Now updateStatusBar really just updates the status bar. And it isn't necessary to call it from outside the board manager anymore due to proper event handling which identifies the situations during which the status bar has to be updated. Therefore this member is now private. In board manager itself operations that affect device context and current board now happen only within doChangeBoardType and the event handlers of DeviceContext callbacks onDeviceContextConfigurationChange and onDeviceContextBoardChange. This prevents the accidental creation of infinite event loops, makes the code more understandable, maintainable and therefore resilient against future bugs.
1 parent 640f269 commit 2d52e52

File tree

5 files changed

+223
-54
lines changed

5 files changed

+223
-54
lines changed

src/arduino/arduinoContentProvider.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ArduinoActivator from "../arduinoActivator";
88
import ArduinoContext from "../arduinoContext";
99
import * as Constants from "../common/constants";
1010
import * as JSONHelper from "../common/cycle";
11+
import { DeviceContext } from "../deviceContext";
1112
import * as Logger from "../logger/logger";
1213
import LocalWebServer from "./localWebServer";
1314

@@ -262,6 +263,8 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide
262263
} else {
263264
try {
264265
ArduinoContext.boardManager.currentBoard.updateConfig(req.body.configId, req.body.optionId);
266+
const dc = DeviceContext.getInstance();
267+
dc.configuration = ArduinoContext.boardManager.currentBoard.customConfig;
265268
return res.json({
266269
status: "OK",
267270
});

src/arduino/board.ts

+70-22
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4-
import { DeviceContext } from "../deviceContext";
5-
import { IBoard, IBoardConfigItem, IPlatform } from "./package";
4+
import { BoardConfigResult, IBoard, IBoardConfigItem, IPlatform } from "./package";
65

76
export function parseBoardDescriptor(boardDescriptor: string, plat: IPlatform): Map<string, IBoard> {
87
const boardLineRegex = /([^\.]+)\.(\S+)=(.+)/;
@@ -52,15 +51,15 @@ export class Board implements IBoard {
5251
this._configItems = [];
5352
}
5453

55-
public get board(): string {
54+
public get board() {
5655
return this._board;
5756
}
5857

59-
public get platform(): IPlatform {
58+
public get platform() {
6059
return this._platform;
6160
}
6261

63-
public addParameter(key: string, value: string): void {
62+
public addParameter(key: string, value: string) {
6463
const match = key.match(MENU_REGEX);
6564
if (match) {
6665
const existingItem = this._configItems.find((item) => item.id === match[1]);
@@ -82,8 +81,7 @@ export class Board implements IBoard {
8281
}
8382
}
8483
}
85-
86-
public getBuildConfig(): string {
84+
public getBuildConfig() {
8785
return `${this.getPackageName()}:${this.platform.architecture}:${this.board}${this.customConfig ? ":" + this.customConfig : ""}`;
8886
}
8987

@@ -100,36 +98,86 @@ export class Board implements IBoard {
10098
}
10199
}
102100

103-
public get configItems(): IBoardConfigItem[] {
101+
public get configItems() {
104102
return this._configItems;
105103
}
106104

107-
public loadConfig(configString: string): void {
105+
public loadConfig(configString: string) {
106+
// An empty or undefined config string resets the configuration
107+
if (!configString) {
108+
this.resetConfig();
109+
return BoardConfigResult.Success;
110+
}
108111
const configSections = configString.split(",");
109112
const keyValueRegex = /(\S+)=(\S+)/;
110-
configSections.forEach((configSection) => {
111-
const match = configSection.match(keyValueRegex);
112-
if (match && match.length >= 2) {
113-
this.updateConfig(match[1], match[2]);
113+
let result = BoardConfigResult.Success;
114+
for (const section of configSections) {
115+
const match = section.match(keyValueRegex);
116+
if (!match) {
117+
return BoardConfigResult.InvalidFormat;
114118
}
115-
});
119+
const r = this.updateConfig(match[1], match[2]);
120+
switch (r) {
121+
case BoardConfigResult.SuccessNoChange:
122+
result = r;
123+
break;
124+
case BoardConfigResult.Success:
125+
break;
126+
default:
127+
return r;
128+
}
129+
};
130+
return result;
116131
}
117132

118-
public updateConfig(configId: string, optionId: string): boolean {
133+
/**
134+
* For documentation see the documentation on IBoard.updateConfig().
135+
*/
136+
public updateConfig(configId: string, optionId: string) {
119137
const targetConfig = this._configItems.find((config) => config.id === configId);
120138
if (!targetConfig) {
121-
return false;
139+
return BoardConfigResult.InvalidConfigID;
140+
}
141+
// Iterate through all options and ...
142+
for (const o of targetConfig.options) {
143+
// Make sure that we only set valid options, e.g. when loading
144+
// from config files.
145+
if (o.id === optionId) {
146+
if (targetConfig.selectedOption !== optionId) {
147+
targetConfig.selectedOption = optionId;
148+
return BoardConfigResult.Success;
149+
}
150+
return BoardConfigResult.SuccessNoChange;
151+
} else {
152+
return BoardConfigResult.InvalidOptionID;
153+
}
122154
}
123-
if (targetConfig.selectedOption !== optionId) {
124-
targetConfig.selectedOption = optionId;
125-
const dc = DeviceContext.getInstance();
126-
dc.configuration = this.customConfig;
127-
return true;
155+
return BoardConfigResult.InvalidOptionID;
156+
}
157+
158+
public resetConfig() {
159+
for (const c of this._configItems) {
160+
c.selectedOption = c.options[0].id;
128161
}
129-
return false;
130162
}
131163

132164
public getPackageName() {
133165
return this.platform.packageName ? this.platform.packageName : this.platform.package.name;
134166
}
135167
}
168+
169+
/**
170+
* Test if two boards are of the same type, i.e. have the same key.
171+
* @param {IBoard | undefined} a A board.
172+
* @param {IBoard | undefined} b And another board.
173+
* @returns {boolean} true if two boards are of the same type, else false.
174+
*/
175+
export function boardEqual(a: IBoard | undefined,
176+
b: IBoard | undefined) {
177+
if (a && b) {
178+
return a.key === b.key;
179+
} else if (a || b) {
180+
return false;
181+
}
182+
return true;
183+
}

src/arduino/boardManager.ts

+99-26
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { arduinoChannel } from "../common/outputChannel";
1212
import { DeviceContext } from "../deviceContext";
1313
import { ArduinoApp } from "./arduino";
1414
import { IArduinoSettings } from "./arduinoSettings";
15-
import { parseBoardDescriptor } from "./board";
16-
import { IBoard, IPackage, IPlatform } from "./package";
15+
import { boardEqual, parseBoardDescriptor } from "./board";
16+
import { BoardConfigResult, IBoard, IPackage, IPlatform } from "./package";
1717
import { VscodeSettings } from "./vscodeSettings";
1818

1919
export class BoardManager {
@@ -67,18 +67,17 @@ export class BoardManager {
6767
// Load default platforms from arduino installation directory and user manually installed platforms.
6868
this.loadInstalledPlatforms();
6969

70-
// Load all supported boards type.
70+
// Load all supported board types
7171
this.loadInstalledBoards();
72-
this.updateStatusBar();
73-
this._boardConfigStatusBar.show();
7472

7573
const dc = DeviceContext.getInstance();
76-
dc.onChangeBoard(() => {
77-
this.updateStatusBar();
78-
});
79-
dc.onChangeConfiguration(() => {
80-
this.updateStatusBar();
81-
});
74+
dc.onChangeBoard(() => this.onDeviceContextBoardChange());
75+
dc.onChangeConfiguration(() => this.onDeviceContextConfigurationChange());
76+
77+
// load initial board from DeviceContext by emulating
78+
// a board change event.
79+
this.onDeviceContextBoardChange();
80+
this.updateStatusBar(true);
8281
}
8382

8483
public async changeBoardType() {
@@ -122,14 +121,24 @@ export class BoardManager {
122121

123122
public doChangeBoardType(targetBoard: IBoard) {
124123
const dc = DeviceContext.getInstance();
124+
125+
if (dc.board === targetBoard.key) {
126+
return;
127+
}
128+
129+
// Resetting the board first that we don't overwrite the configuration
130+
// of the previous board.
131+
this._currentBoard = null;
132+
// This will cause a configuration changed event which will have no
133+
// effect because no current board is set.
134+
dc.configuration = targetBoard.customConfig;
135+
// This will generate a device context board event which will set the
136+
// correct board and configuration. We know that it will trigger - we
137+
// made sure above that the boards actually differ
125138
dc.board = targetBoard.key;
126-
this._currentBoard = targetBoard;
127-
dc.configuration = this._currentBoard.customConfig;
128-
this._boardConfigStatusBar.text = targetBoard.name;
139+
129140
// IS-REMOVE: to be removed completely when IntelliSense implementation is merged
130141
// this._arduinoApp.addLibPath(null);
131-
132-
this._onBoardTypeChanged.fire();
133142
}
134143

135144
public get packages(): IPackage[] {
@@ -251,26 +260,90 @@ export class BoardManager {
251260
}
252261
}
253262

254-
public updateStatusBar(show: boolean = true): void {
263+
private updateStatusBar(show: boolean = true): void {
255264
if (show) {
256265
this._boardConfigStatusBar.show();
257-
const dc = DeviceContext.getInstance();
258-
const selectedBoard = this._boards.get(dc.board);
259-
if (selectedBoard) {
260-
this._currentBoard = selectedBoard;
261-
this._boardConfigStatusBar.text = selectedBoard.name;
262-
if (dc.configuration) {
263-
this._currentBoard.loadConfig(dc.configuration);
264-
}
266+
if (this._currentBoard) {
267+
this._boardConfigStatusBar.text = this._currentBoard.name;
265268
} else {
266-
this._currentBoard = null;
267269
this._boardConfigStatusBar.text = "<Select Board Type>";
268270
}
269271
} else {
270272
this._boardConfigStatusBar.hide();
271273
}
272274
}
273275

276+
/**
277+
* Event callback if DeviceContext detected a new board - either when
278+
* loaded from configuration file or when set by the doChangeBoardType
279+
* member.
280+
*/
281+
private onDeviceContextBoardChange() {
282+
const dc = DeviceContext.getInstance();
283+
const newBoard = this._boards.get(dc.board);
284+
if (boardEqual(newBoard, this._currentBoard)) {
285+
return;
286+
}
287+
if (newBoard) {
288+
this._currentBoard = newBoard;
289+
if (dc.configuration) {
290+
// In case the configuration is incompatible, we reset it as
291+
// setting partially valid configurations can lead to nasty
292+
// surprises. When setting a new board this is acceptable
293+
const r = this._currentBoard.loadConfig(dc.configuration);
294+
if (r !== BoardConfigResult.Success && r !== BoardConfigResult.SuccessNoChange) {
295+
this._currentBoard.resetConfig();
296+
// we don't reset dc.configuration to give the user a
297+
// chance to fix her/his configuration
298+
this.invalidConfigWarning(r);
299+
}
300+
} else {
301+
this._currentBoard.resetConfig();
302+
dc.configuration = undefined;
303+
}
304+
} else {
305+
this._currentBoard = null;
306+
}
307+
this._onBoardTypeChanged.fire();
308+
this.updateStatusBar();
309+
}
310+
311+
/**
312+
* Event callback if DeviceContext detected a configuration change
313+
* - either when loaded from configuration file or when set by the
314+
* doChangeBoardType member.
315+
*/
316+
private onDeviceContextConfigurationChange() {
317+
const dc = DeviceContext.getInstance();
318+
if (this._currentBoard) {
319+
const r = this._currentBoard.loadConfig(dc.configuration);
320+
if (r !== BoardConfigResult.Success && r !== BoardConfigResult.SuccessNoChange) {
321+
this._currentBoard.resetConfig();
322+
// We reset the configuration here but do not write it back
323+
// to the configuration file - this can be annoying when
324+
// someone tries to set a special configuration and doesn't
325+
// get it right the first time.
326+
this.invalidConfigWarning(r);
327+
}
328+
}
329+
}
330+
331+
private invalidConfigWarning(result: BoardConfigResult) {
332+
let what = "";
333+
switch (result) {
334+
case BoardConfigResult.InvalidFormat:
335+
what = ": Invalid format must be of the form \"key1=value2,key1=value2,...\"";
336+
break;
337+
case BoardConfigResult.InvalidConfigID:
338+
what = ": Invalid configuration key";
339+
break;
340+
case BoardConfigResult.InvalidOptionID:
341+
what = ": Invalid configuration value";
342+
break;
343+
}
344+
vscode.window.showWarningMessage(`Invalid board configuration detected in configuration file${what}. Falling back to defaults.`);
345+
}
346+
274347
private loadInstalledPlatforms() {
275348
const installed = this.getInstalledPlatforms();
276349
installed.forEach((platform) => {

src/arduino/package.ts

+51-4
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,35 @@ export interface IBoardConfigItem {
152152
options: IBoardConfigOption[];
153153
}
154154

155+
/**
156+
* Return values of calls to IBoard.loadConfig() and IBoard.updateConfig().
157+
*/
158+
export enum BoardConfigResult {
159+
/**
160+
* Setting configuration value(s) was successful
161+
*/
162+
Success,
163+
/**
164+
* Setting configuration value(s) was successful. All or some items
165+
* were already set to the requested values.
166+
*/
167+
SuccessNoChange,
168+
/**
169+
* One or more configuration keys were invalid.
170+
*/
171+
InvalidConfigID,
172+
/**
173+
* One or more options were invalid.
174+
*/
175+
InvalidOptionID,
176+
/**
177+
* Can only happen when calling IBoard.loadConfig() and when
178+
* the raw configuration string did contain invalid/unparsable
179+
* elements.
180+
*/
181+
InvalidFormat,
182+
}
183+
155184
/**
156185
* Interface for classes that represent an Arduino supported board.
157186
*
@@ -206,14 +235,32 @@ export interface IBoard {
206235
getBuildConfig(): string;
207236

208237
/**
209-
* Load default configuration from saved context.
238+
* Load configuration from saved context.
239+
* Parses the configuration string and tries to set the individual
240+
* configuration values. It will bail out on any error.
241+
* @param {string} configString The configuration string from the
242+
* configuration file.
243+
* @returns {BoardConfigResult} Result of the operation - for more
244+
* information see documentation of BoardConfigResult.
245+
*/
246+
loadConfig(configString: string): BoardConfigResult;
247+
248+
/**
249+
* Set configuration value.
250+
* This function makes sure, that the configuration ID and the option ID
251+
* are actually valid. It will bail out on any error
252+
* @param {string} configId The ID of the configuration value
253+
* @param {string} optionId The ID to which the option of the configuration
254+
* value should be set to.
255+
* @returns {BoardConfigResult} Result of the operation - for more
256+
* information see documentation of BoardConfigResult.
210257
*/
211-
loadConfig(configString: string): void;
258+
updateConfig(configId: string, optionId: string): BoardConfigResult;
212259

213260
/**
214-
* Update the configuration
261+
* Reset configuration to defaults and update configuration file.
215262
*/
216-
updateConfig(configId: string, optionId: string): boolean;
263+
resetConfig();
217264

218265
/**
219266
* Get the board package name

0 commit comments

Comments
 (0)