Files
arduino-ide/arduino-ide-extension/src/browser/theia/core/connection-status-service.ts
2025-03-29 01:33:25 +09:00

337 lines
10 KiB
TypeScript

import {
ConnectionStatus,
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { WebSocketConnectionSource } from '@theia/core/lib/browser/messaging/ws-connection-source';
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { Disposable } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import debounce from 'lodash.debounce';
import { ArduinoDaemon } from '../../../common/protocol';
import { assertUnreachable } from '../../../common/utils';
import { CreateFeatures } from '../../create/create-features';
import { NotificationCenter } from '../../notification-center';
@injectable()
export class IsOnline implements FrontendApplicationContribution {
private readonly onDidChangeOnlineEmitter = new Emitter<boolean>();
private _online = false;
private stopped = false;
onStart(): void {
import('is-online').then((module) => {
const checkOnline = async () => {
if (!this.stopped) {
try {
const online = await module.default();
this.setOnline(online);
} finally {
window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval
}
}
};
checkOnline();
});
}
onStop(): void {
this.stopped = true;
this.onDidChangeOnlineEmitter.dispose();
}
get online(): boolean {
return this._online;
}
get onDidChangeOnline(): Event<boolean> {
return this.onDidChangeOnlineEmitter.event;
}
private setOnline(online: boolean): void {
const oldOnline = this._online;
this._online = online;
if (!this.stopped && this._online !== oldOnline) {
this.onDidChangeOnlineEmitter.fire(this._online);
}
}
}
@injectable()
export class DaemonPort implements FrontendApplicationContribution {
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
private readonly onPortDidChangeEmitter = new Emitter<number | undefined>();
private _port: number | undefined;
onStart(): void {
this.daemon.tryGetPort().then(
(port) => this.setPort(port),
(reason) =>
console.warn('Could not retrieve the CLI daemon port.', reason)
);
this.notificationCenter.onDaemonDidStart((port) => this.setPort(port));
this.notificationCenter.onDaemonDidStop(() => this.setPort(undefined));
}
onStop(): void {
this.onPortDidChangeEmitter.dispose();
}
get port(): number | undefined {
return this._port;
}
get onDidChangePort(): Event<number | undefined> {
return this.onPortDidChangeEmitter.event;
}
private setPort(port: number | undefined): void {
const oldPort = this._port;
this._port = port;
if (this._port !== oldPort) {
this.onPortDidChangeEmitter.fire(this._port);
}
}
}
@injectable()
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
@inject(DaemonPort)
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@inject(WebSocketConnectionSource)
private readonly connectionSource: WebSocketConnectionSource;
@postConstruct()
protected override init(): void {
this.schedulePing();
const refresh = debounce(() => {
this.updateStatus(Boolean(this.daemonPort.port) && this.isOnline.online);
this.schedulePing();
}, this.options.offlineTimeout - 10);
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
}
protected override async performPingRequest(): Promise<void> {
if (!this.connectionSource['socket'].connected) {
this.updateStatus(false);
return;
}
try {
await this.pingService.ping();
this.updateStatus(this.isOnline.online);
} catch (e) {
this.updateStatus(false);
this.logger.error(e);
}
}
}
const connectionStatusStatusBar = 'connection-status';
const theiaOffline = 'theia-mod-offline';
export type OfflineConnectionStatus =
/**
* There is no websocket connection between the frontend and the backend.
*/
| 'backend'
/**
* The CLI daemon port is not available. Could not establish the gRPC connection between the backend and the CLI.
*/
| 'daemon'
/**
* Cloud not connect to the Internet from the browser.
*/
| 'internet';
@injectable()
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
@inject(DaemonPort)
private readonly daemonPort: DaemonPort;
@inject(IsOnline)
private readonly isOnline: IsOnline;
@inject(MessageService)
private readonly messageService: MessageService;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@inject(WebSocketConnectionSource)
private readonly connectionSource: WebSocketConnectionSource;
private readonly offlineStatusDidChangeEmitter = new Emitter<
OfflineConnectionStatus | undefined
>();
private noInternetConnectionNotificationId: string | undefined;
private _offlineStatus: OfflineConnectionStatus | undefined;
get offlineStatus(): OfflineConnectionStatus | undefined {
return this._offlineStatus;
}
get onOfflineStatusDidChange(): Event<OfflineConnectionStatus | undefined> {
return this.offlineStatusDidChangeEmitter.event;
}
protected override onStateChange(state: ConnectionStatus): void {
if (
(!Boolean(this.daemonPort.port) || !this.isOnline.online) &&
state === ConnectionStatus.ONLINE
) {
return;
}
super.onStateChange(state);
}
protected override handleOffline(): void {
const params = <OfflineMessageParams>{
port: this.daemonPort.port,
online: this.isOnline.online,
backendConnected: this.connectionSource['socket'].connected, // https://github.com/arduino/arduino-ide/issues/2081
};
this._offlineStatus = offlineConnectionStatusType(params);
const { text, tooltip } = offlineMessage(params);
this.statusBar.setElement(connectionStatusStatusBar, {
alignment: StatusBarAlignment.LEFT,
text,
tooltip,
priority: 5000,
});
document.body.classList.add(theiaOffline);
this.toDisposeOnOnline.pushAll([
Disposable.create(() =>
this.statusBar.removeElement(connectionStatusStatusBar)
),
Disposable.create(() => document.body.classList.remove(theiaOffline)),
Disposable.create(() => {
this._offlineStatus = undefined;
this.fireStatusDidChange();
}),
]);
if (!this.isOnline.online) {
const text = nls.localize(
'arduino/connectionStatus/connectionLost',
"Connection lost. Cloud sketch actions and updates won't be available."
);
this.noInternetConnectionNotificationId = this.notificationManager[
'getMessageId'
]({ text, type: MessageType.Warning });
if (this.createFeatures.enabled) {
this.messageService.warn(text);
}
this.toDisposeOnOnline.push(
Disposable.create(() => this.clearNoInternetConnectionNotification())
);
}
this.fireStatusDidChange();
}
private clearNoInternetConnectionNotification(): void {
if (this.noInternetConnectionNotificationId) {
this.notificationManager.clear(this.noInternetConnectionNotificationId);
this.noInternetConnectionNotificationId = undefined;
}
}
private fireStatusDidChange(): void {
if (this.createFeatures.enabled) {
return this.offlineStatusDidChangeEmitter.fire(this._offlineStatus);
}
}
}
interface OfflineMessageParams {
readonly port: string | undefined;
readonly online: boolean;
readonly backendConnected: boolean;
}
interface OfflineMessage {
readonly text: string;
readonly tooltip: string;
}
/**
* (non-API) exported for testing
*
* The precedence of the offline states are the following:
* - No connection to the Theia backend,
* - CLI daemon is offline, and
* - There is no Internet connection.
*/
export function offlineMessage(params: OfflineMessageParams): OfflineMessage {
const statusType = offlineConnectionStatusType(params);
const text = getOfflineText(statusType);
const tooltip = getOfflineTooltip(statusType);
return { text, tooltip };
}
function offlineConnectionStatusType(
params: OfflineMessageParams
): OfflineConnectionStatus {
const { port, online, backendConnected } = params;
if (!backendConnected || (port && online)) {
return 'backend';
}
if (!port) {
return 'daemon';
}
return 'internet';
}
export const backendOfflineText = nls.localize('theia/core/offline', 'Offline');
export const daemonOfflineText = nls.localize(
'theia/core/daemonOffline',
'CLI Daemon Offline'
);
export const offlineText = nls.localize('theia/core/offlineText', 'Offline');
export const backendOfflineTooltip = nls.localize(
'theia/core/cannotConnectBackend',
'Cannot connect to the backend.'
);
export const daemonOfflineTooltip = nls.localize(
'theia/core/cannotConnectDaemon',
'Cannot connect to the CLI daemon.'
);
export const offlineTooltip = offlineText;
function getOfflineText(statusType: OfflineConnectionStatus): string {
switch (statusType) {
case 'backend':
return backendOfflineText;
case 'daemon':
return '$(bolt) ' + daemonOfflineText;
case 'internet':
return '$(alert) ' + offlineText;
default:
assertUnreachable(statusType);
}
}
function getOfflineTooltip(statusType: OfflineConnectionStatus): string {
switch (statusType) {
case 'backend':
return backendOfflineTooltip;
case 'daemon':
return daemonOfflineTooltip;
case 'internet':
return offlineTooltip;
default:
assertUnreachable(statusType);
}
}