Repository: electron/electron (language: cpp)

Issue:
[Bug]: nativeImage.createThumbnailFromPath fails to resolve in renderer process from version 25.0.0
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a bug report that matches the one I want to file, without success.

### Electron Version

29.2.0

### What operating system are you using?

macOS

### Operating System Version

13.6 (22G116)

### What arch are you using?

x64

### Last Known Working Electron version

24.8.8

### Expected Behavior

The function should resolve as it did in version 24.8.8, returning a thumbnail image or throwing an error if the operation fails.

### Actual Behavior

The function call does not resolve or throw errors, leaving the operation in an unresolved state.

### Testcase Gist URL

https://gist.github.com/gaodeng/05a4fefb9cde972336a1285821953df6

### Additional Information

_No response_


Discussion / hints:
<!-- blocked/need-repro -->

Hello @gaodeng. Thanks for reporting this and helping to make Electron better!

Would it be possible for you to make a standalone testcase with only the code necessary to reproduce the issue? For example, [Electron Fiddle](https://www.electronjs.org/fiddle) is a great tool for making small test cases and makes it easy to publish your test case to a [gist](https://gist.github.com) that Electron maintainers can use.

Stand-alone test cases make fixing issues go more smoothly: it ensure everyone's looking at the same issue, it removes all unnecessary variables from the equation, and it can also provide the basis for automated regression tests.

Now adding the https://github.com/electron/electron/labels/blocked%2Fneed-repro label for this reason. After you make a test case, please link to it in a followup comment. This issue will be closed in 10 days if the above is not addressed.
https://gist.github.com/gaodeng/05a4fefb9cde972336a1285821953df6
Bisected to https://github.com/electron/electron/compare/v25.0.0-nightly.20230330...v25.0.0-nightly.20230331, likely caused by https://github.com/electron/electron/pull/37701. Haven't narrowed down the specific cause yet but that's where it broke.

Fix patch:
diff --git a/shell/common/api/electron_api_native_image_mac.mm b/shell/common/api/electron_api_native_image_mac.mm
index cf9c6cdcfa37f..e45eaae9a9387 100644
--- a/shell/common/api/electron_api_native_image_mac.mm
+++ b/shell/common/api/electron_api_native_image_mac.mm
@@ -14,6 +14,7 @@
 
 #include "base/apple/foundation_util.h"
 #include "base/strings/sys_string_conversions.h"
+#include "base/task/bind_post_task.h"
 #include "gin/arguments.h"
 #include "shell/common/gin_converters/image_converter.h"
 #include "shell/common/gin_helper/promise.h"
@@ -38,6 +39,23 @@
   return def;
 }
 
+void ReceivedThumbnailResult(CGSize size,
+                             gin_helper::Promise<gfx::Image> p,
+                             QLThumbnailRepresentation* thumbnail,
+                             NSError* error) {
+  if (error || !thumbnail) {
+    std::string err_msg([error.localizedDescription UTF8String]);
+    p.RejectWithErrorMessage("unable to retrieve thumbnail preview "
+                             "image for the given path: " +
+                             err_msg);
+  } else {
+    NSImage* result = [[NSImage alloc] initWithCGImage:[thumbnail CGImage]
+                                                  size:size];
+    gfx::Image image(result);
+    p.Resolve(image);
+  }
+}
+
 // static
 v8::Local<v8::Promise> NativeImage::CreateThumbnailFromPath(
     v8::Isolate* isolate,
@@ -70,31 +88,15 @@
                      size:cg_size
                     scale:[screen backingScaleFactor]
       representationTypes:QLThumbnailGenerationRequestRepresentationTypeAll]);
-  __block gin_helper::Promise<gfx::Image> p = std::move(promise);
+  __block auto block_callback = base::BindPostTaskToCurrentDefault(
+      base::BindOnce(&ReceivedThumbnailResult, cg_size, std::move(promise)));
+  auto completionHandler =
+      ^(QLThumbnailRepresentation* thumbnail, NSError* error) {
+        std::move(block_callback).Run(thumbnail, error);
+      };
   [[QLThumbnailGenerator sharedGenerator]
       generateBestRepresentationForRequest:request
-                         completionHandler:^(
-                             QLThumbnailRepresentation* thumbnail,
-                             NSError* error) {
-                           if (error || !thumbnail) {
-                             std::string err_msg(
-                                 [error.localizedDescription UTF8String]);
-                             dispatch_async(dispatch_get_main_queue(), ^{
-                               p.RejectWithErrorMessage(
-                                   "unable to retrieve thumbnail preview "
-                                   "image for the given path: " +
-                                   err_msg);
-                             });
-                           } else {
-                             NSImage* result = [[NSImage alloc]
-                                 initWithCGImage:[thumbnail CGImage]
-                                            size:cg_size];
-                             gfx::Image image(result);
-                             dispatch_async(dispatch_get_main_queue(), ^{
-                               p.Resolve(image);
-                             });
-                           }
-                         }];
+                         completionHandler:completionHandler];
 
   return handle;
 }
diff --git a/shell/common/platform_util_mac.mm b/shell/common/platform_util_mac.mm
index 60419c7b28daa..32972a5dfe44f 100644
--- a/shell/common/platform_util_mac.mm
+++ b/shell/common/platform_util_mac.mm
@@ -15,11 +15,15 @@
 #include "base/apple/osstatus_logging.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
+#include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/logging.h"
 #include "base/mac/scoped_aedesc.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/sys_string_conversions.h"
+#include "base/task/thread_pool.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
 #include "net/base/apple/url_conversions.h"
 #include "ui/views/widget/widget.h"
 #include "url/gurl.h"
@@ -183,15 +187,12 @@ void OpenExternal(const GURL& url,
     return;
   }
 
-  bool activate = options.activate;
-  __block OpenCallback c = std::move(callback);
-  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
-                 ^{
-                   __block std::string error = OpenURL(ns_url, activate);
-                   dispatch_async(dispatch_get_main_queue(), ^{
-                     std::move(c).Run(error);
-                   });
-                 });
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE,
+      {base::MayBlock(), base::WithBaseSyncPrimitives(),
+       base::TaskPriority::USER_BLOCKING,
+       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
+      base::BindOnce(&OpenURL, ns_url, options.activate), std::move(callback));
 }
 
 bool MoveItemToTrashWithError(const base::FilePath& full_path,
diff --git a/spec/api-native-image-spec.ts b/spec/api-native-image-spec.ts
index 7e055dd6a169f..d498214942af9 100644
--- a/spec/api-native-image-spec.ts
+++ b/spec/api-native-image-spec.ts
@@ -1,6 +1,6 @@
 import { expect } from 'chai';
 import { nativeImage } from 'electron/common';
-import { ifdescribe, ifit } from './lib/spec-helpers';
+import { ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
 import * as path from 'node:path';
 
 describe('nativeImage module', () => {
@@ -426,6 +426,8 @@ describe('nativeImage module', () => {
   });
 
   ifdescribe(process.platform !== 'linux')('createThumbnailFromPath(path, size)', () => {
+    useRemoteContext({ webPreferences: { contextIsolation: false, nodeIntegration: true } });
+
     it('throws when invalid size is passed', async () => {
       const badSize = { width: -1, height: -1 };
 
@@ -473,6 +475,13 @@ describe('nativeImage module', () => {
       const result = await nativeImage.createThumbnailFromPath(imgPath, maxSize);
       expect(result.getSize()).to.deep.equal(maxSize);
     });
+
+    itremote('works in the renderer', async (path: string) => {
+      const { nativeImage } = require('electron');
+      const goodSize = { width: 100, height: 100 };
+      const result = await nativeImage.createThumbnailFromPath(path, goodSize);
+      expect(result.isEmpty()).to.equal(false);
+    }, [path.join(fixturesPath, 'assets', 'logo.png')]);
   });
 
   describe('addRepresentation()', () => {
diff --git a/spec/api-shell-spec.ts b/spec/api-shell-spec.ts
index 749677311bd57..a2e9cef96ff8c 100644
--- a/spec/api-shell-spec.ts
+++ b/spec/api-shell-spec.ts
@@ -31,7 +31,7 @@ describe('shell module', () => {
     });
     afterEach(closeAllWindows);
 
-    it('opens an external link', async () => {
+    async function urlOpened () {
       let url = 'http://127.0.0.1';
       let requestReceived: Promise<any>;
       if (process.platform === 'linux') {
@@ -53,12 +53,26 @@ describe('shell module', () => {
         url = (await listen(server)).url;
         requestReceived = new Promise<void>(resolve => server.on('connection', () => resolve()));
       }
+      return { url, requestReceived };
+    }
 
+    it('opens an external link', async () => {
+      const { url, requestReceived } = await urlOpened();
       await Promise.all<void>([
         shell.openExternal(url),
         requestReceived
       ]);
     });
+
+    it('opens an external link in the renderer', async () => {
+      const { url, requestReceived } = await urlOpened();
+      const w = new BrowserWindow({ show: false, webPreferences: { sandbox: false, contextIsolation: false, nodeIntegration: true } });
+      await w.loadURL('about:blank');
+      await Promise.all<void>([
+        w.webContents.executeJavaScript(`require("electron").shell.openExternal(${JSON.stringify(url)})`),
+        requestReceived
+      ]);
+    });
   });
 
   describe('shell.trashItem()', () => {


Test patch: