From be88074c0e00c19659f6ae8177a1bd69adbadd67 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 9 Jan 2023 15:03:28 +0100 Subject: [PATCH 1/2] feat: handle when starting debug session failed If the sketch has not been verified, IDE2 offers user a verify action. Closes #808 Signed-off-by: Akos Kitta --- .../src/browser/contributions/debug.ts | 53 ++++++++++++++++--- .../src/common/protocol/sketches-service.ts | 6 +++ .../src/node/is-temp-sketch.ts | 4 +- .../src/node/sketches-service-impl.ts | 17 +++++- i18n/en.json | 3 +- package.json | 2 +- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index b577d4167..0a8af26fc 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -3,7 +3,12 @@ import { Event, Emitter } from '@theia/core/lib/common/event'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { NotificationCenter } from '../notification-center'; -import { Board, BoardsService, ExecutableService } from '../../common/protocol'; +import { + Board, + BoardsService, + ExecutableService, + Sketch, +} from '../../common/protocol'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { URI, @@ -16,9 +21,8 @@ import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; import { ArduinoMenus } from '../menu/arduino-menus'; -import { MainMenuManager } from '../../common/main-menu-manager'; - const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug'; + @injectable() export class Debug extends SketchContribution { @inject(HostedPluginSupport) @@ -36,9 +40,6 @@ export class Debug extends SketchContribution { @inject(BoardsServiceProvider) private readonly boardsServiceProvider: BoardsServiceProvider; - @inject(MainMenuManager) - private readonly mainMenuManager: MainMenuManager; - /** * If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled. */ @@ -203,7 +204,28 @@ export class Debug extends SketchContribution { sketchPath, configPath, }; - return this.commandService.executeCommand('arduino.debug.start', config); + try { + await this.commandService.executeCommand('arduino.debug.start', config); + } 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 + ), + yes + ); + if (answer === yes) { + this.commandService.executeCommand('arduino-verify-sketch'); + } + } else { + this.messageService.error( + err instanceof Error ? err.message : String(err) + ); + } + } } get compileForDebug(): boolean { @@ -215,7 +237,22 @@ export class Debug extends SketchContribution { const oldState = this.compileForDebug; const newState = !oldState; window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState)); - this.mainMenuManager.update(); + this.menuManager.update(); + } + + private async isSketchNotVerifiedError( + err: unknown, + sketch: Sketch + ): Promise { + if (err instanceof Error) { + try { + const tempBuildPath = await this.sketchService.tempBuildPath(sketch); + return err.message.includes(tempBuildPath); + } catch { + return false; + } + } + return false; } } export namespace Debug { diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 6a3fd370e..1f473b6cb 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -105,6 +105,12 @@ export interface SketchesService { * Recursively deletes the sketch folder with all its content. */ deleteSketch(sketch: Sketch): Promise; + + /** + * This is the JS/TS re-implementation of [`GenBuildPath`](https://github.com/arduino/arduino-cli/blob/c0d4e4407d80aabad81142693513b3306759cfa6/arduino/sketch/sketch.go#L296-L306) of the CLI. + * Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. This method does not check the existence of the sketch. + */ + tempBuildPath(sketch: Sketch): Promise; } export interface SketchRef { diff --git a/arduino-ide-extension/src/node/is-temp-sketch.ts b/arduino-ide-extension/src/node/is-temp-sketch.ts index 5c62716e9..d0a74b8a3 100644 --- a/arduino-ide-extension/src/node/is-temp-sketch.ts +++ b/arduino-ide-extension/src/node/is-temp-sketch.ts @@ -10,10 +10,10 @@ export const TempSketchPrefix = '.arduinoIDE-unsaved'; @injectable() export class IsTempSketch { // If on macOS, the `temp-dir` lib will make sure there is resolved realpath. - // If on Windows, the `C:\Users\KITTAA~1\AppData\Local\Temp` path will be resolved and normalized to `C:\Users\kittaakos\AppData\Local\Temp`. + // If on Windows, the `C:\Users\KITTAA~1\AppData\Local\Temp` path will be resolved and normalized to `c:\Users\kittaakos\AppData\Local\Temp`. // Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`. // https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992 - private readonly tempDirRealpath = isOSX + readonly tempDirRealpath = isOSX ? tempDir : maybeNormalizeDrive(fs.realpathSync.native(tempDir)); diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index f188b62be..cbf22671a 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -566,11 +566,24 @@ export class SketchesServiceImpl return FileUri.create(genBuildPath).toString(); } - async getIdeTempFolderPath(sketch: Sketch): Promise { + private async getIdeTempFolderPath(sketch: Sketch): Promise { const sketchPath = FileUri.fsPath(sketch.uri); await fs.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible. const suffix = crypto.createHash('md5').update(sketchPath).digest('hex'); - return path.join(os.tmpdir(), `arduino-ide2-${suffix}`); + return path.join( + this.isTempSketch.tempDirRealpath, + `arduino-ide2-${suffix}` + ); + } + + async tempBuildPath(sketch: Sketch): Promise { + const sketchPath = FileUri.fsPath(sketch.uri); + const hash = crypto + .createHash('md5') + .update(sketchPath) + .digest('hex') + .toUpperCase(); + return join(this.isTempSketch.tempDirRealpath, `arduino-sketch-${hash}`); } async deleteSketch(sketch: Sketch): Promise { diff --git a/i18n/en.json b/i18n/en.json index 84b4b956d..e6cb59679 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -191,7 +191,8 @@ "debugWithMessage": "Debug - {0}", "debuggingNotSupported": "Debugging is not supported by '{0}'", "noPlatformInstalledFor": "Platform is not installed for '{0}'", - "optimizeForDebugging": "Optimize for Debugging" + "optimizeForDebugging": "Optimize for Debugging", + "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?" }, "dialog": { "dontAskAgain": "Don't ask again" diff --git a/package.json b/package.json index edeacbf12..ad63a5e12 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "theiaPluginsDir": "plugins", "theiaPlugins": { "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix", - "vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.5.vsix", + "vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.7.vsix", "vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix", "vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix", "cortex-debug": "https://downloads.arduino.cc/marus25.cortex-debug/marus25.cortex-debug-1.5.1.vsix", From 9228dfb83a88abe178d9274c33a5c337fab8989f Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 16 Jan 2023 17:22:18 +0100 Subject: [PATCH 2/2] fix: provide multiple build path guesses IDE2 does not know what casing the CLI uses Signed-off-by: Akos Kitta --- .../src/browser/contributions/debug.ts | 6 ++- .../src/common/protocol/sketches-service.ts | 9 +++- arduino-ide-extension/src/common/utils.ts | 6 ++- .../src/node/is-temp-sketch.ts | 2 +- .../src/node/sketches-service-impl.ts | 48 +++++++++++++++++-- 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index 0a8af26fc..15881b98d 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -246,8 +246,10 @@ export class Debug extends SketchContribution { ): Promise { if (err instanceof Error) { try { - const tempBuildPath = await this.sketchService.tempBuildPath(sketch); - return err.message.includes(tempBuildPath); + const tempBuildPaths = await this.sketchService.tempBuildPath(sketch); + return tempBuildPaths.some((tempBuildPath) => + err.message.includes(tempBuildPath) + ); } catch { return false; } diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 1f473b6cb..9334c20ce 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -108,9 +108,14 @@ export interface SketchesService { /** * This is the JS/TS re-implementation of [`GenBuildPath`](https://github.com/arduino/arduino-cli/blob/c0d4e4407d80aabad81142693513b3306759cfa6/arduino/sketch/sketch.go#L296-L306) of the CLI. - * Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. This method does not check the existence of the sketch. + * Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. Can be multiple ones. This method does not check the existence of the sketch. + * + * The case sensitivity of the drive letter on Windows matters when the CLI calculates the MD5 hash of the temporary build folder. + * IDE2 does not know and does not want to rely on how the CLI treats the paths: with lowercase or uppercase drive letters. + * Hence, IDE2 has to provide multiple build paths on Windows. This hack will be obsolete when the CLI can provide error codes: + * https://github.com/arduino/arduino-cli/issues/1762. */ - tempBuildPath(sketch: Sketch): Promise; + tempBuildPath(sketch: Sketch): Promise; } export interface SketchRef { diff --git a/arduino-ide-extension/src/common/utils.ts b/arduino-ide-extension/src/common/utils.ts index 8ffa1fd9a..3fbf98be9 100644 --- a/arduino-ide-extension/src/common/utils.ts +++ b/arduino-ide-extension/src/common/utils.ts @@ -13,6 +13,10 @@ export function firstToUpperCase(what: string): string { return what.charAt(0).toUpperCase() + what.slice(1); } -export function isNullOrUndefined(what: any): what is undefined | null { +export function startsWithUpperCase(what: string): boolean { + return !!what && what.charAt(0) === firstToUpperCase(what.charAt(0)); +} + +export function isNullOrUndefined(what: unknown): what is undefined | null { return what === undefined || what === null; } diff --git a/arduino-ide-extension/src/node/is-temp-sketch.ts b/arduino-ide-extension/src/node/is-temp-sketch.ts index d0a74b8a3..3bb3c4bda 100644 --- a/arduino-ide-extension/src/node/is-temp-sketch.ts +++ b/arduino-ide-extension/src/node/is-temp-sketch.ts @@ -4,7 +4,7 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { injectable } from '@theia/core/shared/inversify'; import { firstToLowerCase } from '../common/utils'; -const Win32DriveRegex = /^[a-zA-Z]:\\/; +export const Win32DriveRegex = /^[a-zA-Z]:\\/; export const TempSketchPrefix = '.arduinoIDE-unsaved'; @injectable() diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index cbf22671a..41a7837b6 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -33,9 +33,16 @@ import { IsTempSketch, maybeNormalizeDrive, TempSketchPrefix, + Win32DriveRegex, } from './is-temp-sketch'; import { join } from 'path'; import { ErrnoException } from './utils/errors'; +import { isWindows } from '@theia/core/lib/common/os'; +import { + firstToLowerCase, + firstToUpperCase, + startsWithUpperCase, +} from '../common/utils'; const RecentSketches = 'recent-sketches.json'; const DefaultIno = `void setup() { @@ -576,14 +583,49 @@ export class SketchesServiceImpl ); } - async tempBuildPath(sketch: Sketch): Promise { + async tempBuildPath(sketch: Sketch): Promise { const sketchPath = FileUri.fsPath(sketch.uri); + const { tempDirRealpath } = this.isTempSketch; + const tempBuildPaths = [ + this.tempBuildPathMD5Hash(tempDirRealpath, sketchPath), + ]; + + // If on Windows, provide both the upper and the lowercase drive letter MD5 hashes. All together four paths are expected: + // One of them should match if the sketch is not yet compiled. + // https://github.com/arduino/arduino-ide/pull/1809#discussion_r1071031040 + if (isWindows && Win32DriveRegex.test(tempDirRealpath)) { + const toggleFirstCharCasing = (s: string) => + startsWithUpperCase(s) ? firstToLowerCase(s) : firstToUpperCase(s); + const otherCaseTempDirRealPath = toggleFirstCharCasing(tempDirRealpath); + tempBuildPaths.push( + this.tempBuildPathMD5Hash(otherCaseTempDirRealPath, sketchPath) + ); + if (Win32DriveRegex.test(sketchPath)) { + const otherCaseSketchPath = toggleFirstCharCasing(sketchPath); + tempBuildPaths.push( + this.tempBuildPathMD5Hash(tempDirRealpath, otherCaseSketchPath), + this.tempBuildPathMD5Hash( + otherCaseTempDirRealPath, + otherCaseSketchPath + ) + ); + } + } + return tempBuildPaths; + } + + private tempBuildPathMD5Hash(tempFolderPath: string, path: string): string { + return join(tempFolderPath, this.tempBuildFolderMD5Hash(path)); + } + + private tempBuildFolderMD5Hash(path: string): string { const hash = crypto .createHash('md5') - .update(sketchPath) + .update(path) .digest('hex') .toUpperCase(); - return join(this.isTempSketch.tempDirRealpath, `arduino-sketch-${hash}`); + const folderName = `arduino-sketch-${hash}`; + return folderName; } async deleteSketch(sketch: Sketch): Promise {