feat: use new debug -I -P CLI output

- Can pick a programmer if missing,
 - Can auto-select a programmer on app start,
 - Can edit the `launch.json`,
 - Adjust board discovery to new gRPC API. From now on, it's a client
 read stream, not a duplex.
 - Allow `.cxx` and `.cc` file extensions. (Closes #2265)
 - Drop `debuggingSupported` from `BoardDetails`.
 - Dedicated service endpoint for checking the debugger.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2023-11-08 12:04:44 +01:00
committed by Akos Kitta
parent 42bf1a0e99
commit 73b6dc4774
48 changed files with 4697 additions and 3041 deletions

View File

@@ -0,0 +1,123 @@
import type { MaybePromise } from '@theia/core/lib/common/types';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
BoardDetails,
Programmer,
isBoardIdentifierChangeEvent,
} from '../../common/protocol';
import {
BoardsDataStore,
findDefaultProgrammer,
isEmptyData,
} from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { Contribution } from './contribution';
/**
* Before CLI 0.35.0-rc.3, there was no `programmer#default` property in the `board details` response.
* This method does the programmer migration in the data store. If there is a programmer selected, it's a noop.
* If no programmer is selected, it forcefully reloads the details from the CLI and updates it in the local storage.
*/
@injectable()
export class AutoSelectProgrammer extends Contribution {
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
override onStart(): void {
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.ensureProgrammerIsSelected();
}
});
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() =>
this.ensureProgrammerIsSelected()
);
}
private async ensureProgrammerIsSelected(): Promise<boolean> {
return ensureProgrammerIsSelected({
fqbn: this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
getData: (fqbn) => this.boardsDataStore.getData(fqbn),
loadBoardDetails: (fqbn) => this.boardsDataStore.loadBoardDetails(fqbn),
selectProgrammer: (arg) => this.boardsDataStore.selectProgrammer(arg),
});
}
}
interface EnsureProgrammerIsSelectedParams {
fqbn: string | undefined;
getData: (fqbn: string | undefined) => MaybePromise<BoardsDataStore.Data>;
loadBoardDetails: (fqbn: string) => MaybePromise<BoardDetails | undefined>;
selectProgrammer(options: {
fqbn: string;
selectedProgrammer: Programmer;
}): MaybePromise<boolean>;
}
export async function ensureProgrammerIsSelected(
params: EnsureProgrammerIsSelectedParams
): Promise<boolean> {
const { fqbn, getData, loadBoardDetails, selectProgrammer } = params;
if (!fqbn) {
return false;
}
console.debug(`Ensuring a programmer is selected for ${fqbn}...`);
const data = await getData(fqbn);
if (isEmptyData(data)) {
// For example, the platform is not installed.
console.debug(`Skipping. No boards data is available for ${fqbn}.`);
return false;
}
if (data.selectedProgrammer) {
console.debug(
`A programmer is already selected for ${fqbn}: '${data.selectedProgrammer.id}'.`
);
return true;
}
let programmer = findDefaultProgrammer(data.programmers, data);
if (programmer) {
// select the programmer if the default info is available
const result = await selectProgrammer({
fqbn,
selectedProgrammer: programmer,
});
if (result) {
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
return result;
}
}
console.debug(`Reloading board details for ${fqbn}...`);
const reloadedData = await loadBoardDetails(fqbn);
if (!reloadedData) {
console.debug(`Skipping. No board details found for ${fqbn}.`);
return false;
}
if (!reloadedData.programmers.length) {
console.debug(`Skipping. ${fqbn} does not have programmers.`);
return false;
}
programmer = findDefaultProgrammer(reloadedData.programmers, reloadedData);
if (!programmer) {
console.debug(
`Skipping. Could not find a default programmer for ${fqbn}. Programmers were: `
);
return false;
}
const result = await selectProgrammer({
fqbn,
selectedProgrammer: programmer,
});
if (result) {
console.debug(`Selected '${programmer.id}' programmer for ${fqbn}.`);
} else {
console.debug(
`Could not select '${programmer.id}' programmer for ${fqbn}.`
);
}
return result;
}

View File

@@ -1,12 +1,20 @@
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 {
Board,
BoardDetails,
BoardIdentifier,
BoardsService,
CheckDebugEnabledParams,
ExecutableService,
SketchRef,
isBoardIdentifierChangeEvent,
Sketch,
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';
@@ -14,95 +22,119 @@ import { NotificationCenter } from '../notification-center';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
URI,
Command,
CommandRegistry,
SketchContribution,
TabBarToolbarRegistry,
URI,
} from './contribution';
import { MenuModelRegistry, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoMenus } from '../menu/arduino-menus';
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;
@injectable()
export class Debug extends SketchContribution {
@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 reason why it's disabled.
* If `undefined`, debugging is enabled. Otherwise, the human-readable reason why it's disabled.
*/
private _disabledMessages?: string = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
); // Initial pessimism.
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
private onDisabledMessageDidChange =
this.disabledMessageDidChangeEmitter.event;
private _message?: string = noBoardSelected; // Initial pessimism.
private didChangeMessageEmitter = new Emitter<string | undefined>();
private onDidChangeMessage = this.didChangeMessageEmitter.event;
private get disabledMessage(): string | undefined {
return this._disabledMessages;
private get message(): string | undefined {
return this._message;
}
private set disabledMessage(message: string | undefined) {
this._disabledMessages = message;
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
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.disabledMessage
this.message
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.disabledMessage
this.message
)
: Debug.Commands.START_DEBUGGING.label
}`,
priority: 3,
onDidChange: this.onDisabledMessageDidChange as Event<void>,
onDidChange: this.onDidChangeMessage as Event<void>,
};
override onStart(): void {
this.onDisabledMessageDidChange(
this.onDidChangeMessage(
() =>
(this.debugToolbarItem.tooltip = `${
this.disabledMessage
this.message
? nls.localize(
'arduino/debug/debugWithMessage',
'Debug - {0}',
this.disabledMessage
this.message
)
: Debug.Commands.START_DEBUGGING.label
}`)
);
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.refreshState(event.selectedBoard);
this.updateMessage();
}
});
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
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.refreshState();
this.updateMessage();
}
});
this.commandService.onDidExecuteCommand((event) => {
@@ -111,13 +143,13 @@ export class Debug extends SketchContribution {
commandId === 'arduino.languageserver.notifyBuildDidComplete' &&
isCompileSummary(args[0])
) {
this.refreshState();
this.updateMessage();
}
});
}
override onReady(): void {
this.boardsServiceProvider.ready.then(() => this.refreshState());
this.boardsServiceProvider.ready.then(() => this.updateMessage());
}
override registerCommands(registry: CommandRegistry): void {
@@ -125,7 +157,7 @@ export class Debug extends SketchContribution {
execute: () => this.startDebug(),
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: () => !this.disabledMessage,
isEnabled: () => !this.message,
});
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
execute: () => this.toggleCompileForDebug(),
@@ -148,94 +180,56 @@ export class Debug extends SketchContribution {
});
}
private async refreshState(
board: Board | undefined = this.boardsServiceProvider.boardsConfig
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<void> {
if (!board) {
this.disabledMessage = nls.localize(
'arduino/common/noBoardSelected',
'No board selected'
);
return;
}
const fqbn = board.fqbn;
if (!fqbn) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const details = await this.boardService.getBoardDetails({ fqbn });
if (!details) {
this.disabledMessage = nls.localize(
'arduino/debug/noPlatformInstalledFor',
"Platform is not installed for '{0}'",
board.name
);
return;
}
const { debuggingSupported } = details;
if (!debuggingSupported) {
this.disabledMessage = nls.localize(
'arduino/debug/debuggingNotSupported',
"Debugging is not supported by '{0}'",
board.name
);
} else {
this.disabledMessage = undefined;
}
): 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
): Promise<void> {
if (!board) {
return;
.selectedBoard,
sketch:
| CurrentSketch
| undefined = this.sketchServiceClient.tryGetCurrentSketch()
): Promise<StartDebugResult> {
if (!CurrentSketch.isValid(sketch)) {
return false;
}
const { name, fqbn } = board;
if (!fqbn) {
return;
const params = await this.createStartDebugParams(board);
if (!params) {
return false;
}
await this.hostedPluginSupport.didStart;
const [sketch, executables] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.executableService.list(),
]);
if (!CurrentSketch.isValid(sketch)) {
return;
}
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
sketch
);
const [cliPath, sketchPath, configPath] = await Promise.all([
this.fileService.fsPath(new URI(executables.cliUri)),
this.fileService.fsPath(new URI(sketch.uri)),
this.fileService.fsPath(new URI(ideTempFolderUri)),
]);
const config = {
cliPath,
board: {
fqbn,
name,
},
sketchPath,
configPath,
};
try {
await this.commandService.executeCommand('arduino.debug.start', config);
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(
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?",
sketch.name
),
sketchIsNotCompiled(sketch.name),
yes
);
if (answer === yes) {
@@ -247,6 +241,16 @@ export class Debug extends SketchContribution {
);
}
}
return false;
}
private async debug(
params: StartDebugParams
): Promise<StartDebugResult | undefined> {
return this.commandService.executeCommand<StartDebugResult>(
'arduino.debug.start',
params
);
}
get compileForDebug(): boolean {
@@ -254,7 +258,7 @@ export class Debug extends SketchContribution {
return value === 'true';
}
async toggleCompileForDebug(): Promise<void> {
private toggleCompileForDebug(): void {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
@@ -263,7 +267,7 @@ export class Debug extends SketchContribution {
private async isSketchNotVerifiedError(
err: unknown,
sketch: Sketch
sketch: SketchRef
): Promise<boolean> {
if (err instanceof Error) {
try {
@@ -277,6 +281,48 @@ export class Debug extends SketchContribution {
}
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 {
@@ -301,3 +347,89 @@ export namespace Debug {
};
}
}
/**
* (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}`
);
}
if (!data.selectedProgrammer) {
throw new Error(noProgrammerSelectedFor(board.name));
}
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
);
}
/**
* (non-API)
*/
export function noProgrammerSelectedFor(boardName: string): string {
return nls.localize(
'arduino/debug/noProgrammerSelectedFor',
"No programmer selected for '{0}'",
boardName
);
}

View File

@@ -20,26 +20,83 @@ import { NotificationCenter } from '../notification-center';
import { SketchContribution, URI } from './contribution';
import { BoardsDataStore } from '../boards/boards-data-store';
interface DaemonAddress {
/**
* The host where the Arduino CLI daemon is available.
*/
readonly hostname: string;
/**
* The port where the Arduino CLI daemon is listening.
*/
readonly port: number;
/**
* The [id](https://arduino.github.io/arduino-cli/latest/rpc/commands/#instance) of the initialized core Arduino client instance.
*/
readonly instance: number;
}
interface StartLanguageServerParams {
/**
* Absolute filesystem path to the Arduino Language Server executable.
*/
readonly lsPath: string;
/**
* The hostname and the port for the gRPC channel connecting to the Arduino CLI daemon.
* The `instance` number is for the initialized core Arduino client.
*/
readonly daemonAddress: DaemonAddress;
/**
* Absolute filesystem path to [`clangd`](https://clangd.llvm.org/).
*/
readonly clangdPath: string;
/**
* The board is relevant to start a specific "flavor" of the language.
*/
readonly board: { fqbn: string; name?: string };
/**
* `true` if the LS should generate the log files into the default location. The default location is the `cwd` of the process.
* It's very often the same as the workspace root of the IDE, aka the sketch folder.
* When it is a string, it is the absolute filesystem path to the folder to generate the log files.
* If `string`, but the path is inaccessible, the log files will be generated into the default location.
*/
readonly log?: boolean | string;
/**
* Optional `env` for the language server process.
*/
readonly env?: NodeJS.ProcessEnv;
/**
* Additional flags for the Arduino Language server process.
*/
readonly flags?: readonly string[];
/**
* Set to `true`, to enable `Diagnostics`.
*/
readonly realTimeDiagnostics?: boolean;
/**
* If `true`, the logging is not forwarded to the _Output_ view via the language client.
*/
readonly silentOutput?: boolean;
}
/**
* The FQBN the language server runs with or `undefined` if it could not start.
*/
type StartLanguageServerResult = string | undefined;
@injectable()
export class InoLanguage extends SketchContribution {
@inject(HostedPluginEvents)
private readonly hostedPluginEvents: HostedPluginEvents;
@inject(ExecutableService)
private readonly executableService: ExecutableService;
@inject(ArduinoDaemon)
private readonly daemon: ArduinoDaemon;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardDataStore: BoardsDataStore;
@@ -129,6 +186,10 @@ export class InoLanguage extends SketchContribution {
if (!port) {
return;
}
const portNumber = Number.parseInt(port, 10); // TODO: IDE2 APIs should provide a number and not string
if (Number.isNaN(portNumber)) {
return;
}
const release = await this.languageServerStartMutex.acquire();
const toDisposeOnRelease = new DisposableCollection();
try {
@@ -197,22 +258,22 @@ export class InoLanguage extends SketchContribution {
);
toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
}),
this.commandService.executeCommand<string>(
'arduino.languageserver.start',
{
lsPath,
cliDaemonAddr: `localhost:${port}`,
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1',
board: {
fqbn: fqbnWithConfig,
name: name ? `"${name}"` : undefined,
},
realTimeDiagnostics,
silentOutput: true,
}
),
this.start({
lsPath,
daemonAddress: {
hostname: 'localhost',
port: portNumber,
instance: 1, // TODO: get it from the backend
},
clangdPath,
log: currentSketchPath ? currentSketchPath : log,
board: {
fqbn: fqbnWithConfig,
name,
},
realTimeDiagnostics,
silentOutput: true,
}),
]);
} catch (e) {
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
@@ -222,4 +283,13 @@ export class InoLanguage extends SketchContribution {
release();
}
}
private async start(
params: StartLanguageServerParams
): Promise<StartLanguageServerResult | undefined> {
return this.commandService.executeCommand<StartLanguageServerResult>(
'arduino.languageserver.start',
params
);
}
}