Files
arduino-ide/arduino-ide-extension/src/browser/contributions/debug.ts
2024-11-21 14:21:30 +01:00

443 lines
13 KiB
TypeScript

import { Emitter, Event } from '@theia/core/lib/common/event';
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
import { nls } from '@theia/core/lib/common/nls';
import { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import { noBoardSelected } from '../../common/nls';
import {
BoardDetails,
BoardIdentifier,
BoardsService,
CheckDebugEnabledParams,
ExecutableService,
SketchRef,
isBoardIdentifierChangeEvent,
isCompileSummary,
} from '../../common/protocol';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { HostedPluginSupport } from '../hosted/hosted-plugin-support';
import { ArduinoMenus } from '../menu/arduino-menus';
import { NotificationCenter } from '../notification-center';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
Command,
CommandRegistry,
SketchContribution,
TabBarToolbarRegistry,
URI,
} from './contribution';
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
interface StartDebugParams {
/**
* Absolute filesystem path to the Arduino CLI executable.
*/
readonly cliPath: string;
/**
* The the board to debug.
*/
readonly board: Readonly<{ fqbn: string; name?: string }>;
/**
* Absolute filesystem path of the sketch to debug.
*/
readonly sketchPath: string;
/**
* Location where the `launch.json` will be created on the fly before starting every debug session.
* If not defined, it falls back to `sketchPath/.vscode/launch.json`.
*/
readonly launchConfigsDirPath?: string;
/**
* Absolute path to the `arduino-cli.yaml` file. If not specified, it falls back to `~/.arduinoIDE/arduino-cli.yaml`.
*/
readonly cliConfigPath?: string;
/**
* Programmer for the debugging.
*/
readonly programmer?: string;
/**
* Custom progress title to use when getting the debug information from the CLI.
*/
readonly title?: string;
}
type StartDebugResult = boolean;
export const DebugDisabledStatusMessageSource = Symbol(
'DebugDisabledStatusMessageSource'
);
export interface DebugDisabledStatusMessageSource {
/**
* `undefined` if debugging is enabled (for the currently selected board + programmer + config options).
* Otherwise, it's the human readable message why it's disabled.
*/
get message(): string | undefined;
/**
* Emits an event when {@link message} changes.
*/
get onDidChangeMessage(): Event<string | undefined>;
}
@injectable()
export class Debug
extends SketchContribution
implements DebugDisabledStatusMessageSource
{
@inject(HostedPluginSupport)
private readonly hostedPluginSupport: HostedPluginSupport;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(ExecutableService)
private readonly executableService: ExecutableService;
@inject(BoardsService)
private readonly boardService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
/**
* If `undefined`, debugging is enabled. Otherwise, the human-readable reason why it's disabled.
*/
private _message?: string = noBoardSelected; // Initial pessimism.
private readonly didChangeMessageEmitter = new Emitter<string | undefined>();
readonly onDidChangeMessage = this.didChangeMessageEmitter.event;
get message(): string | undefined {
return this._message;
}
private set message(message: string | undefined) {
this._message = message;
this.didChangeMessageEmitter.fire(this._message);
}
private readonly debugToolbarItem = {
id: Debug.Commands.START_DEBUGGING.id,
command: Debug.Commands.START_DEBUGGING.id,
tooltip: `${
this.message
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.message
)
: Debug.Commands.START_DEBUGGING.label
}`,
priority: 3,
onDidChange: this.onDidChangeMessage as Event<void>,
};
override onStart(): void {
this.onDidChangeMessage(
() =>
(this.debugToolbarItem.tooltip = `${
this.message
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.message
)
: Debug.Commands.START_DEBUGGING.label
}`)
);
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateMessage();
}
});
this.notificationCenter.onPlatformDidInstall(() => this.updateMessage());
this.notificationCenter.onPlatformDidUninstall(() => this.updateMessage());
this.boardsDataStore.onDidChange((event) => {
const selectedFqbn =
this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn;
if (event.changes.find((change) => change.fqbn === selectedFqbn)) {
this.updateMessage();
}
});
this.commandService.onDidExecuteCommand((event) => {
const { commandId, args } = event;
if (
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
isCompileSummary(args[0])
) {
this.updateMessage();
}
});
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() => this.updateMessage());
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
execute: () => this.startDebug(),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.message,
});
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
execute: () => this.toggleCompileForDebug(),
isToggled: () => this.compileForDebug,
});
registry.registerCommand(Debug.Commands.IS_OPTIMIZE_FOR_DEBUG, {
execute: () => this.compileForDebug,
});
}
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem(this.debugToolbarItem);
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.id,
label: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.label,
order: '5',
});
}
private async updateMessage(): Promise<void> {
try {
await this.isDebugEnabled();
this.message = undefined;
} catch (err) {
let message = String(err);
if (err instanceof Error) {
message = err.message;
}
this.message = message;
}
}
private async isDebugEnabled(
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard
): Promise<string> {
const debugFqbn = await isDebugEnabled(
board,
(fqbn) => this.boardService.getBoardDetails({ fqbn }),
(fqbn) => this.boardsDataStore.getData(fqbn),
(fqbn) => this.boardsDataStore.appendConfigToFqbn(fqbn),
(params) => this.boardService.checkDebugEnabled(params)
);
return debugFqbn;
}
private async startDebug(
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
.selectedBoard,
sketch:
| CurrentSketch
| undefined = this.sketchServiceClient.tryGetCurrentSketch()
): Promise<StartDebugResult> {
if (!CurrentSketch.isValid(sketch)) {
return false;
}
const params = await this.createStartDebugParams(board);
if (!params) {
return false;
}
await this.hostedPluginSupport.didStart;
try {
const result = await this.debug(params);
return Boolean(result);
} catch (err) {
if (await this.isSketchNotVerifiedError(err, sketch)) {
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
const answer = await this.messageService.error(
sketchIsNotCompiled(sketch.name),
yes
);
if (answer === yes) {
this.commandService.executeCommand('arduino-verify-sketch');
}
} else {
this.messageService.error(
err instanceof Error ? err.message : String(err)
);
}
}
return false;
}
private async debug(
params: StartDebugParams
): Promise<StartDebugResult | undefined> {
return this.commandService.executeCommand<StartDebugResult>(
'arduino.debug.start',
params
);
}
get compileForDebug(): boolean {
const value = window.localStorage.getItem(COMPILE_FOR_DEBUG_KEY);
return value === 'true';
}
private toggleCompileForDebug(): void {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
this.menuManager.update();
}
private async isSketchNotVerifiedError(
err: unknown,
sketch: SketchRef
): Promise<boolean> {
if (err instanceof Error) {
try {
const buildPaths = await this.sketchesService.getBuildPath(sketch);
return buildPaths.some((tempBuildPath) =>
err.message.includes(tempBuildPath)
);
} catch {
return false;
}
}
return false;
}
private async createStartDebugParams(
board: BoardIdentifier | undefined
): Promise<StartDebugParams | undefined> {
if (!board || !board.fqbn) {
return undefined;
}
let debugFqbn: string | undefined = undefined;
try {
debugFqbn = await this.isDebugEnabled(board);
} catch {}
if (!debugFqbn) {
return undefined;
}
const [sketch, executables, boardsData] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
this.boardsDataStore.getData(board.fqbn),
]);
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, launchConfigsDirPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri)),
this.fileService.fsPath(new URI(ideTempFolderUri)),
]);
return {
board: { fqbn: debugFqbn, name: board.name },
cliPath,
sketchPath,
launchConfigsDirPath,
programmer: boardsData.selectedProgrammer?.id,
title: nls.localize(
'arduino/debug/getDebugInfo',
'Getting debug info...'
),
};
}
}
export namespace Debug {
export namespace Commands {
export const START_DEBUGGING = Command.toLocalizedCommand(
{
id: 'arduino-start-debug',
label: 'Start Debugging',
category: 'Arduino',
},
'vscode/debug.contribution/startDebuggingHelp'
);
export const TOGGLE_OPTIMIZE_FOR_DEBUG = Command.toLocalizedCommand(
{
id: 'arduino-toggle-optimize-for-debug',
label: 'Optimize for Debugging',
category: 'Arduino',
},
'arduino/debug/optimizeForDebugging'
);
export const IS_OPTIMIZE_FOR_DEBUG: Command = {
id: 'arduino-is-optimize-for-debug',
};
}
}
/**
* Resolves with the FQBN to use for the `debug --info --programmer p --fqbn $FQBN` command. Otherwise, rejects.
*
* (non-API)
*/
export async function isDebugEnabled(
board: BoardIdentifier | undefined,
getDetails: (fqbn: string) => MaybePromise<BoardDetails | undefined>,
getData: (fqbn: string) => MaybePromise<BoardsDataStore.Data>,
appendConfigToFqbn: (fqbn: string) => MaybePromise<string | undefined>,
checkDebugEnabled: (params: CheckDebugEnabledParams) => MaybePromise<string>
): Promise<string> {
if (!board) {
throw new Error(noBoardSelected);
}
const { fqbn } = board;
if (!fqbn) {
throw new Error(noPlatformInstalledFor(board.name));
}
const [details, data, fqbnWithConfig] = await Promise.all([
getDetails(fqbn),
getData(fqbn),
appendConfigToFqbn(fqbn),
]);
if (!details) {
throw new Error(noPlatformInstalledFor(board.name));
}
if (!fqbnWithConfig) {
throw new Error(
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
);
}
const params = {
fqbn: fqbnWithConfig,
programmer: data.selectedProgrammer?.id,
};
try {
const debugFqbn = await checkDebugEnabled(params);
return debugFqbn;
} catch (err) {
throw new Error(debuggingNotSupported(board.name));
}
}
/**
* (non-API)
*/
export function sketchIsNotCompiled(sketchName: string): string {
return nls.localize(
'arduino/debug/sketchIsNotCompiled',
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
sketchName
);
}
/**
* (non-API)
*/
export function noPlatformInstalledFor(boardName: string): string {
return nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
boardName
);
}
/**
* (non-API)
*/
export function debuggingNotSupported(boardName: string): string {
return nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
boardName
);
}