Skip to content

Commit 80d5b5a

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
fix: propagate monitor errors to the frontend
- Handle when the board's platform is not installed (Closes #1974) - UX: Smoother monitor widget reset (Closes #1985) - Fixed monitor <input> readOnly state (Closes #1984) - Set monitor widget header color (Ref #682) Closes #1508 Signed-off-by: Akos Kitta <[email protected]>
1 parent ab5c63c commit 80d5b5a

16 files changed

+718
-353
lines changed

Diff for: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -496,15 +496,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
496496
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
497497
bind(WidgetFactory).toDynamicValue((context) => ({
498498
id: MonitorWidget.ID,
499-
createWidget: () => {
500-
return new MonitorWidget(
501-
context.container.get<MonitorModel>(MonitorModel),
502-
context.container.get<MonitorManagerProxyClient>(
503-
MonitorManagerProxyClient
504-
),
505-
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
506-
);
507-
},
499+
createWidget: () => context.container.get(MonitorWidget),
508500
}));
509501

510502
bind(MonitorManagerProxyFactory).toFactory(

Diff for: arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts

+65-27
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
2-
CommandRegistry,
2+
ApplicationError,
33
Disposable,
44
Emitter,
55
MessageService,
66
nls,
77
} from '@theia/core';
8+
import { Deferred } from '@theia/core/lib/common/promise-util';
89
import { inject, injectable } from '@theia/core/shared/inversify';
10+
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
11+
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
912
import { Board, Port } from '../common/protocol';
1013
import {
1114
Monitor,
@@ -23,21 +26,31 @@ import { BoardsServiceProvider } from './boards/boards-service-provider';
2326
export class MonitorManagerProxyClientImpl
2427
implements MonitorManagerProxyClient
2528
{
29+
@inject(MessageService)
30+
private readonly messageService: MessageService;
31+
// This is necessary to call the backend methods from the frontend
32+
@inject(MonitorManagerProxyFactory)
33+
private readonly server: MonitorManagerProxyFactory;
34+
@inject(BoardsServiceProvider)
35+
private readonly boardsServiceProvider: BoardsServiceProvider;
36+
@inject(NotificationManager)
37+
private readonly notificationManager: NotificationManager;
38+
2639
// When pluggable monitor messages are received from the backend
2740
// this event is triggered.
2841
// Ideally a frontend component is connected to this event
2942
// to update the UI.
30-
protected readonly onMessagesReceivedEmitter = new Emitter<{
43+
private readonly onMessagesReceivedEmitter = new Emitter<{
3144
messages: string[];
3245
}>();
3346
readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;
3447

35-
protected readonly onMonitorSettingsDidChangeEmitter =
48+
private readonly onMonitorSettingsDidChangeEmitter =
3649
new Emitter<MonitorSettings>();
3750
readonly onMonitorSettingsDidChange =
3851
this.onMonitorSettingsDidChangeEmitter.event;
3952

40-
protected readonly onMonitorShouldResetEmitter = new Emitter();
53+
private readonly onMonitorShouldResetEmitter = new Emitter<void>();
4154
readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event;
4255

4356
// WebSocket used to handle pluggable monitor communication between
@@ -51,29 +64,16 @@ export class MonitorManagerProxyClientImpl
5164
return this.wsPort;
5265
}
5366

54-
constructor(
55-
@inject(MessageService)
56-
protected messageService: MessageService,
57-
58-
// This is necessary to call the backend methods from the frontend
59-
@inject(MonitorManagerProxyFactory)
60-
protected server: MonitorManagerProxyFactory,
61-
62-
@inject(CommandRegistry)
63-
protected readonly commandRegistry: CommandRegistry,
64-
65-
@inject(BoardsServiceProvider)
66-
protected readonly boardsServiceProvider: BoardsServiceProvider
67-
) {}
68-
6967
/**
7068
* Connects a localhost WebSocket using the specified port.
7169
* @param addressPort port of the WebSocket
7270
*/
7371
async connect(addressPort: number): Promise<void> {
74-
if (!!this.webSocket) {
75-
if (this.wsPort === addressPort) return;
76-
else this.disconnect();
72+
if (this.webSocket) {
73+
if (this.wsPort === addressPort) {
74+
return;
75+
}
76+
this.disconnect();
7777
}
7878
try {
7979
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
@@ -87,6 +87,9 @@ export class MonitorManagerProxyClientImpl
8787
return;
8888
}
8989

90+
const opened = new Deferred<void>();
91+
this.webSocket.onopen = () => opened.resolve();
92+
this.webSocket.onerror = () => opened.reject();
9093
this.webSocket.onmessage = (message) => {
9194
const parsedMessage = JSON.parse(message.data);
9295
if (Array.isArray(parsedMessage))
@@ -99,19 +102,26 @@ export class MonitorManagerProxyClientImpl
99102
}
100103
};
101104
this.wsPort = addressPort;
105+
return opened.promise;
102106
}
103107

104108
/**
105109
* Disconnects the WebSocket if connected.
106110
*/
107111
disconnect(): void {
108-
if (!this.webSocket) return;
112+
if (!this.webSocket) {
113+
return;
114+
}
109115
this.onBoardsConfigChanged?.dispose();
110116
this.onBoardsConfigChanged = undefined;
111117
try {
112-
this.webSocket?.close();
118+
this.webSocket.close();
113119
this.webSocket = undefined;
114-
} catch {
120+
} catch (err) {
121+
console.error(
122+
'Could not close the websocket connection for the monitor.',
123+
err
124+
);
115125
this.messageService.error(
116126
nls.localize(
117127
'arduino/monitor/unableToCloseWebSocket',
@@ -126,6 +136,7 @@ export class MonitorManagerProxyClientImpl
126136
}
127137

128138
async startMonitor(settings?: PluggableMonitorSettings): Promise<void> {
139+
await this.boardsServiceProvider.reconciled;
129140
this.lastConnectedBoard = {
130141
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
131142
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort,
@@ -150,11 +161,11 @@ export class MonitorManagerProxyClientImpl
150161
? Port.keyOf(this.lastConnectedBoard.selectedPort)
151162
: undefined)
152163
) {
153-
this.onMonitorShouldResetEmitter.fire(null);
154164
this.lastConnectedBoard = {
155165
selectedBoard: selectedBoard,
156166
selectedPort: selectedPort,
157167
};
168+
this.onMonitorShouldResetEmitter.fire();
158169
} else {
159170
// a board is plugged and it's the same as prev, rerun "this.startMonitor" to
160171
// recreate the listener callback
@@ -167,7 +178,14 @@ export class MonitorManagerProxyClientImpl
167178
const { selectedBoard, selectedPort } =
168179
this.boardsServiceProvider.boardsConfig;
169180
if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return;
170-
await this.server().startMonitor(selectedBoard, selectedPort, settings);
181+
try {
182+
this.clearVisibleNotification();
183+
await this.server().startMonitor(selectedBoard, selectedPort, settings);
184+
} catch (err) {
185+
const message = ApplicationError.is(err) ? err.message : String(err);
186+
this.previousNotificationId = this.notificationId(message);
187+
this.messageService.error(message);
188+
}
171189
}
172190

173191
getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings> {
@@ -199,4 +217,24 @@ export class MonitorManagerProxyClientImpl
199217
})
200218
);
201219
}
220+
221+
/**
222+
* This is the internal (Theia) ID of the notification that is currently visible.
223+
* It's stored here as a field to be able to close it before starting a new monitor connection. It's a hack.
224+
*/
225+
private previousNotificationId: string | undefined;
226+
private clearVisibleNotification(): void {
227+
if (this.previousNotificationId) {
228+
this.notificationManager.clear(this.previousNotificationId);
229+
this.previousNotificationId = undefined;
230+
}
231+
}
232+
233+
private notificationId(message: string, ...actions: string[]): string {
234+
return this.notificationManager['getMessageId']({
235+
text: message,
236+
actions,
237+
type: MessageType.Error,
238+
});
239+
}
202240
}

Diff for: arduino-ide-extension/src/browser/monitor-model.ts

+37-47
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import {
44
LocalStorageService,
55
} from '@theia/core/lib/browser';
66
import { inject, injectable } from '@theia/core/shared/inversify';
7-
import { MonitorManagerProxyClient } from '../common/protocol';
7+
import {
8+
isMonitorConnected,
9+
MonitorConnectionStatus,
10+
monitorConnectionStatusEquals,
11+
MonitorEOL,
12+
MonitorManagerProxyClient,
13+
MonitorState,
14+
} from '../common/protocol';
815
import { isNullOrUndefined } from '../common/utils';
916
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';
1017

@@ -19,48 +26,48 @@ export class MonitorModel implements FrontendApplicationContribution {
1926
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
2027

2128
protected readonly onChangeEmitter: Emitter<
22-
MonitorModel.State.Change<keyof MonitorModel.State>
29+
MonitorState.Change<keyof MonitorState>
2330
>;
2431

2532
protected _autoscroll: boolean;
2633
protected _timestamp: boolean;
27-
protected _lineEnding: MonitorModel.EOL;
34+
protected _lineEnding: MonitorEOL;
2835
protected _interpolate: boolean;
2936
protected _darkTheme: boolean;
3037
protected _wsPort: number;
3138
protected _serialPort: string;
32-
protected _connected: boolean;
39+
protected _connectionStatus: MonitorConnectionStatus;
3340

3441
constructor() {
3542
this._autoscroll = true;
3643
this._timestamp = false;
3744
this._interpolate = false;
38-
this._lineEnding = MonitorModel.EOL.DEFAULT;
45+
this._lineEnding = MonitorEOL.DEFAULT;
3946
this._darkTheme = false;
4047
this._wsPort = 0;
4148
this._serialPort = '';
42-
this._connected = true;
49+
this._connectionStatus = 'not-connected';
4350

4451
this.onChangeEmitter = new Emitter<
45-
MonitorModel.State.Change<keyof MonitorModel.State>
52+
MonitorState.Change<keyof MonitorState>
4653
>();
4754
}
4855

4956
onStart(): void {
5057
this.localStorageService
51-
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
58+
.getData<MonitorState>(MonitorModel.STORAGE_ID)
5259
.then(this.restoreState.bind(this));
5360

5461
this.monitorManagerProxy.onMonitorSettingsDidChange(
5562
this.onMonitorSettingsDidChange.bind(this)
5663
);
5764
}
5865

59-
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
66+
get onChange(): Event<MonitorState.Change<keyof MonitorState>> {
6067
return this.onChangeEmitter.event;
6168
}
6269

63-
protected restoreState(state: MonitorModel.State): void {
70+
protected restoreState(state: MonitorState): void {
6471
if (!state) {
6572
return;
6673
}
@@ -125,11 +132,11 @@ export class MonitorModel implements FrontendApplicationContribution {
125132
this.timestamp = !this._timestamp;
126133
}
127134

128-
get lineEnding(): MonitorModel.EOL {
135+
get lineEnding(): MonitorEOL {
129136
return this._lineEnding;
130137
}
131138

132-
set lineEnding(lineEnding: MonitorModel.EOL) {
139+
set lineEnding(lineEnding: MonitorEOL) {
133140
if (lineEnding === this._lineEnding) return;
134141
this._lineEnding = lineEnding;
135142
this.monitorManagerProxy.changeSettings({
@@ -211,19 +218,26 @@ export class MonitorModel implements FrontendApplicationContribution {
211218
);
212219
}
213220

214-
get connected(): boolean {
215-
return this._connected;
221+
get connectionStatus(): MonitorConnectionStatus {
222+
return this._connectionStatus;
216223
}
217224

218-
set connected(connected: boolean) {
219-
if (connected === this._connected) return;
220-
this._connected = connected;
225+
set connectionStatus(connectionStatus: MonitorConnectionStatus) {
226+
if (
227+
monitorConnectionStatusEquals(connectionStatus, this.connectionStatus)
228+
) {
229+
return;
230+
}
231+
this._connectionStatus = connectionStatus;
221232
this.monitorManagerProxy.changeSettings({
222-
monitorUISettings: { connected },
233+
monitorUISettings: {
234+
connectionStatus,
235+
connected: isMonitorConnected(connectionStatus),
236+
},
223237
});
224238
this.onChangeEmitter.fire({
225-
property: 'connected',
226-
value: this._connected,
239+
property: 'connectionStatus',
240+
value: this._connectionStatus,
227241
});
228242
}
229243

@@ -238,7 +252,7 @@ export class MonitorModel implements FrontendApplicationContribution {
238252
darkTheme,
239253
wsPort,
240254
serialPort,
241-
connected,
255+
connectionStatus,
242256
} = monitorUISettings;
243257

244258
if (!isNullOrUndefined(autoscroll)) this.autoscroll = autoscroll;
@@ -248,31 +262,7 @@ export class MonitorModel implements FrontendApplicationContribution {
248262
if (!isNullOrUndefined(darkTheme)) this.darkTheme = darkTheme;
249263
if (!isNullOrUndefined(wsPort)) this.wsPort = wsPort;
250264
if (!isNullOrUndefined(serialPort)) this.serialPort = serialPort;
251-
if (!isNullOrUndefined(connected)) this.connected = connected;
265+
if (!isNullOrUndefined(connectionStatus))
266+
this.connectionStatus = connectionStatus;
252267
};
253268
}
254-
255-
// TODO: Move this to /common
256-
export namespace MonitorModel {
257-
export interface State {
258-
autoscroll: boolean;
259-
timestamp: boolean;
260-
lineEnding: EOL;
261-
interpolate: boolean;
262-
darkTheme: boolean;
263-
wsPort: number;
264-
serialPort: string;
265-
connected: boolean;
266-
}
267-
export namespace State {
268-
export interface Change<K extends keyof State> {
269-
readonly property: K;
270-
readonly value: State[K];
271-
}
272-
}
273-
274-
export type EOL = '' | '\n' | '\r' | '\r\n';
275-
export namespace EOL {
276-
export const DEFAULT: EOL = '\n';
277-
}
278-
}

0 commit comments

Comments
 (0)