From d24a3911f810a32dec42c29c0d0ae4664c3cf152 Mon Sep 17 00:00:00 2001
From: Akos Kitta <a.kitta@arduino.cc>
Date: Tue, 8 Nov 2022 11:40:50 +0100
Subject: [PATCH] fix: workaround for arduino/arduino-cli#1968

Do not try to parse the original `NotFound` error message, but look for
a sketch somewhere in the requested path.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
---
 .../contributions/open-sketch-files.ts        |   2 +-
 .../theia/electron-main-application.ts        |  58 ++--------
 .../src/node/sketches-service-impl.ts         | 104 +++++++++---------
 3 files changed, 67 insertions(+), 97 deletions(-)

diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts b/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts
index 7f937059..99ccbd08 100644
--- a/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts
+++ b/arduino-ide-extension/src/browser/contributions/open-sketch-files.ts
@@ -102,7 +102,7 @@ export class OpenSketchFiles extends SketchContribution {
   ): Promise<Sketch | undefined> {
     const { invalidMainSketchUri } = err.data;
     requestAnimationFrame(() => this.messageService.error(err.message));
-    await wait(10); // let IDE2 toast the error message.
+    await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
     const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
       fileService: this.fileService,
       sketchService: this.sketchService,
diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts
index 784d8fca..6cc4ad11 100644
--- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts
+++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts
@@ -9,7 +9,7 @@ import {
 import { fork } from 'child_process';
 import { AddressInfo } from 'net';
 import { join, isAbsolute, resolve } from 'path';
-import { promises as fs, Stats } from 'fs';
+import { promises as fs } from 'fs';
 import { MaybePromise } from '@theia/core/lib/common/types';
 import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
 import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
@@ -28,6 +28,7 @@ import {
   SHOW_PLOTTER_WINDOW,
 } from '../../common/ipc-communication';
 import { ErrnoException } from '../../node/utils/errors';
+import { isAccessibleSketchPath } from '../../node/sketches-service-impl';
 
 app.commandLine.appendSwitch('disable-http-cache');
 
@@ -145,7 +146,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
         event.preventDefault();
         const resolvedPath = await this.resolvePath(path, cwd);
         if (resolvedPath) {
-          const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
+          const sketchFolderPath = await isAccessibleSketchPath(
+            resolvedPath,
+            true
+          );
           if (sketchFolderPath) {
             this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
             await this.openSketch(sketchFolderPath);
@@ -158,49 +162,6 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
     }
   }
 
-  /**
-   * The `path` argument is valid, if accessible and either pointing to a `.ino` file,
-   * or it's a directory, and one of the files in the directory is an `.ino` file.
-   *
-   * If `undefined`, `path` was pointing to neither an accessible sketch file nor a sketch folder.
-   *
-   * The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
-   * The `path` must be an absolute, resolved path.
-   */
-  private async isValidSketchPath(path: string): Promise<string | undefined> {
-    let stats: Stats | undefined = undefined;
-    try {
-      stats = await fs.stat(path);
-    } catch (err) {
-      if (ErrnoException.isENOENT(err)) {
-        return undefined;
-      }
-      throw err;
-    }
-    if (!stats) {
-      return undefined;
-    }
-    if (stats.isFile()) {
-      return path.endsWith('.ino') ? path : undefined;
-    }
-    try {
-      const entries = await fs.readdir(path, { withFileTypes: true });
-      const sketchFilename = entries
-        .filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
-        .map(({ name }) => name)
-        .sort((left, right) => left.localeCompare(right))[0];
-      if (sketchFilename) {
-        return join(path, sketchFilename);
-      }
-      // If no sketches found in the folder, but the folder exists,
-      // return with the path of the empty folder and let IDE2's frontend
-      // figure out the workspace root.
-      return path;
-    } catch (err) {
-      throw err;
-    }
-  }
-
   private async resolvePath(
     maybePath: string,
     cwd: string
@@ -253,7 +214,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
         if (!resolvedPath) {
           continue;
         }
-        const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
+        const sketchFolderPath = await isAccessibleSketchPath(
+          resolvedPath,
+          true
+        );
         if (sketchFolderPath) {
           workspace.file = sketchFolderPath;
           if (this.isTempSketch.is(workspace.file)) {
@@ -284,7 +248,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
       if (!resolvedPath) {
         continue;
       }
-      const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
+      const sketchFolderPath = await isAccessibleSketchPath(resolvedPath, true);
       if (sketchFolderPath) {
         path = sketchFolderPath;
         break;
diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts
index 091bf345..f20723f0 100644
--- a/arduino-ide-extension/src/node/sketches-service-impl.ts
+++ b/arduino-ide-extension/src/node/sketches-service-impl.ts
@@ -734,62 +734,68 @@ function isNotFoundError(err: unknown): err is ServiceError {
 
 /**
  * Tries to detect whether the error was caused by an invalid main sketch file name.
- * IDE2 should handle gracefully when there is an invalid sketch folder name. See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
- * The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762), so IDE2 parses the error message and tries to guess it.
+ * IDE2 should handle gracefully when there is an invalid sketch folder name.
+ * See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
+ * The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762),
+ * IDE2 cannot parse the error message (https://github.com/arduino/arduino-cli/issues/1968#issuecomment-1306936142)
+ * so it checks if a sketch even if it's invalid can be discovered from the requested path.
  * Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move.
  */
 async function isInvalidSketchNameError(
   cliErr: unknown,
   requestSketchPath: string
 ): Promise<string | undefined> {
-  if (isNotFoundError(cliErr)) {
-    const ino = requestSketchPath.endsWith('.ino');
-    if (ino) {
-      const sketchFolderPath = path.dirname(requestSketchPath);
-      const sketchName = path.basename(sketchFolderPath);
-      const pattern = escapeRegExpCharacters(
-        `${invalidSketchNameErrorRegExpPrefix}${path.join(
-          sketchFolderPath,
-          `${sketchName}.ino`
-        )}`
-      );
-      if (new RegExp(pattern, 'i').test(cliErr.details)) {
-        try {
-          await fs.access(requestSketchPath);
-          return requestSketchPath;
-        } catch {
-          return undefined;
-        }
-      }
-    } else {
-      try {
-        const resources = await fs.readdir(requestSketchPath, {
-          withFileTypes: true,
-        });
-        return (
-          resources
-            .filter((resource) => resource.isFile())
-            .filter((resource) => resource.name.endsWith('.ino'))
-            // A folder might contain multiple sketches. It's OK to ick the first one as IDE2 cannot do much,
-            // but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
-            .sort(({ name: left }, { name: right }) =>
-              left.localeCompare(right)
-            )
-            .map(({ name }) => name)
-            .map((name) => path.join(requestSketchPath, name))[0]
-        );
-      } catch (err) {
-        if (ErrnoException.isENOENT(err) || ErrnoException.isENOTDIR(err)) {
-          return undefined;
-        }
-        throw err;
-      }
-    }
-  }
-  return undefined;
+  return isNotFoundError(cliErr)
+    ? isAccessibleSketchPath(requestSketchPath)
+    : undefined;
+}
+
+/**
+ * The `path` argument is valid, if accessible and either pointing to a `.ino` file,
+ * or it's a directory, and one of the files in the directory is an `.ino` file.
+ *
+ * `undefined` if `path` was pointing to neither an accessible sketch file nor a sketch folder.
+ *
+ * The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
+ * The `path` must be an absolute, resolved path. This method does not handle EACCES (Permission denied) errors.
+ *
+ * When `fallbackToInvalidFolderPath` is `true`, and the `path` is an accessible folder without any sketch files,
+ * this method returns with the `path` argument instead of `undefined`.
+ */
+export async function isAccessibleSketchPath(
+  path: string,
+  fallbackToInvalidFolderPath = false
+): Promise<string | undefined> {
+  let stats: Stats | undefined = undefined;
+  try {
+    stats = await fs.stat(path);
+  } catch (err) {
+    if (ErrnoException.isENOENT(err)) {
+      return undefined;
+    }
+    throw err;
+  }
+  if (!stats) {
+    return undefined;
+  }
+  if (stats.isFile()) {
+    return path.endsWith('.ino') ? path : undefined;
+  }
+  const entries = await fs.readdir(path, { withFileTypes: true });
+  const sketchFilename = entries
+    .filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
+    .map(({ name }) => name)
+    // A folder might contain multiple sketches. It's OK to pick the first one as IDE2 cannot do much,
+    // but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
+    .sort((left, right) => left.localeCompare(right))[0];
+  if (sketchFilename) {
+    return join(path, sketchFilename);
+  }
+  // If no sketches found in the folder, but the folder exists,
+  // return with the path of the empty folder and let IDE2's frontend
+  // figure out the workspace root.
+  return fallbackToInvalidFolderPath ? path : undefined;
 }
-const invalidSketchNameErrorRegExpPrefix =
-  '.*: main file missing from sketch: ';
 
 /*
  * When a new sketch is created, add a suffix to distinguish it