diff --git a/atom/common/api/atom_api_shell.cc b/atom/common/api/atom_api_shell.cc index 03e8ed32e2..39e6aea7ba 100644 --- a/atom/common/api/atom_api_shell.cc +++ b/atom/common/api/atom_api_shell.cc @@ -4,6 +4,7 @@ #include +#include "atom/common/native_mate_converters/callback.h" #include "atom/common/native_mate_converters/file_path_converter.h" #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/string16_converter.h" @@ -41,6 +42,16 @@ struct Converter { namespace { +void OnOpenExternalFinished( + v8::Isolate* isolate, + const base::Callback)>& callback, + const std::string& error) { + if (error.empty()) + callback.Run(v8::Null(isolate)); + else + callback.Run(v8::String::NewFromUtf8(isolate, error.c_str())); +} + bool OpenExternal( #if defined(OS_WIN) const base::string16& url, @@ -49,12 +60,23 @@ bool OpenExternal( #endif mate::Arguments* args) { bool activate = true; - if (args->Length() == 2) { + if (args->Length() >= 2) { mate::Dictionary options; if (args->GetNext(&options)) { options.Get("activate", &activate); } } + + if (args->Length() >= 3) { + base::Callback)> callback; + if (args->GetNext(&callback)) { + platform_util::OpenExternal( + url, activate, + base::Bind(&OnOpenExternalFinished, args->isolate(), callback)); + return true; + } + } + return platform_util::OpenExternal(url, activate); } diff --git a/atom/common/platform_util.h b/atom/common/platform_util.h index 520faa48f4..dc4b472358 100644 --- a/atom/common/platform_util.h +++ b/atom/common/platform_util.h @@ -5,6 +5,9 @@ #ifndef ATOM_COMMON_PLATFORM_UTIL_H_ #define ATOM_COMMON_PLATFORM_UTIL_H_ +#include + +#include "base/callback_forward.h" #include "build/build_config.h" #if defined(OS_WIN) @@ -19,6 +22,8 @@ class FilePath; namespace platform_util { +typedef base::Callback OpenExternalCallback; + // Show the given file in a file manager. If possible, select the file. // Must be called from the UI thread. bool ShowItemInFolder(const base::FilePath& full_path); @@ -37,6 +42,16 @@ bool OpenExternal( #endif bool activate); +// The asynchronous version of OpenExternal. +void OpenExternal( +#if defined(OS_WIN) + const base::string16& url, +#else + const GURL& url, +#endif + bool activate, + const OpenExternalCallback& callback); + // Move a file to trash. bool MoveItemToTrash(const base::FilePath& full_path); diff --git a/atom/common/platform_util_linux.cc b/atom/common/platform_util_linux.cc index 82265901f1..923adbd882 100644 --- a/atom/common/platform_util_linux.cc +++ b/atom/common/platform_util_linux.cc @@ -6,6 +6,7 @@ #include +#include "base/cancelable_callback.h" #include "base/files/file_util.h" #include "base/process/kill.h" #include "base/process/launch.h" @@ -88,6 +89,12 @@ bool OpenExternal(const GURL& url, bool activate) { return XDGOpen(url.spec(), false); } +void OpenExternal(const GURL& url, bool activate, + const OpenExternalCallback& callback) { + // TODO(gabriel): Implement async open if callback is specified + callback.Run(OpenExternal(url, activate) ? "" : "Failed to open"); +} + bool MoveItemToTrash(const base::FilePath& full_path) { std::string trash; if (getenv(ELECTRON_TRASH) != NULL) { diff --git a/atom/common/platform_util_mac.mm b/atom/common/platform_util_mac.mm index 6d253c99c8..aa64678caf 100644 --- a/atom/common/platform_util_mac.mm +++ b/atom/common/platform_util_mac.mm @@ -4,18 +4,101 @@ #include "atom/common/platform_util.h" -#include +#import #import +#include "base/callback.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/logging.h" #include "base/mac/mac_logging.h" #include "base/mac/scoped_aedesc.h" +#include "base/strings/stringprintf.h" #include "base/strings/sys_string_conversions.h" #include "net/base/mac/url_conversions.h" #include "url/gurl.h" +namespace { + +std::string MessageForOSStatus(OSStatus status, const char* default_message) { + switch (status) { + case kLSAppInTrashErr: + return "The application cannot be run because it is inside a Trash " + "folder."; + case kLSUnknownErr: + return "An unknown error has occurred."; + case kLSNotAnApplicationErr: + return "The item to be registered is not an application."; + case kLSNotInitializedErr: + return "Formerly returned by LSInit on initialization failure; " + "no longer used."; + case kLSDataUnavailableErr: + return "Data of the desired type is not available (for example, there is " + "no kind string)."; + case kLSApplicationNotFoundErr: + return "No application in the Launch Services database matches the input " + "criteria."; + case kLSDataErr: + return "Data is structured improperly (for example, an item’s " + "information property list is malformed). Not used in macOS 10.4."; + case kLSLaunchInProgressErr: + return "A launch of the application is already in progress."; + case kLSServerCommunicationErr: + return "There is a problem communicating with the server process that " + "maintains the Launch Services database."; + case kLSCannotSetInfoErr: + return "The filename extension to be hidden cannot be hidden."; + case kLSIncompatibleSystemVersionErr: + return "The application to be launched cannot run on the current Mac OS " + "version."; + case kLSNoLaunchPermissionErr: + return "The user does not have permission to launch the application (on a" + "managed network)."; + case kLSNoExecutableErr: + return "The executable file is missing or has an unusable format."; + case kLSNoClassicEnvironmentErr: + return "The Classic emulation environment was required but is not " + "available."; + case kLSMultipleSessionsNotSupportedErr: + return "The application to be launched cannot run simultaneously in two " + "different user sessions."; + default: + return base::StringPrintf("%s (%d)", default_message, status); + } +} + +// This may be called from a global dispatch queue, the methods used here are +// thread safe, including LSGetApplicationForURL (> 10.2) and +// NSWorkspace#openURLs. +std::string OpenURL(NSURL* ns_url, bool activate) { + CFURLRef openingApp = NULL; + OSStatus status = LSGetApplicationForURL((CFURLRef)ns_url, + kLSRolesAll, + NULL, + &openingApp); + if (status != noErr) + return MessageForOSStatus(status, "Failed to open"); + + CFRelease(openingApp); // NOT A BUG; LSGetApplicationForURL retains for us + + NSUInteger launchOptions = NSWorkspaceLaunchDefault; + if (!activate) + launchOptions |= NSWorkspaceLaunchWithoutActivation; + + bool opened = [[NSWorkspace sharedWorkspace] + openURLs:@[ns_url] + withAppBundleIdentifier:nil + options:launchOptions + additionalEventParamDescriptor:nil + launchIdentifiers:nil]; + if (!opened) + return "Failed to open URL"; + + return ""; +} + +} // namespace + namespace platform_util { bool ShowItemInFolder(const base::FilePath& path) { @@ -131,29 +214,26 @@ bool OpenItem(const base::FilePath& full_path) { bool OpenExternal(const GURL& url, bool activate) { DCHECK([NSThread isMainThread]); NSURL* ns_url = net::NSURLWithGURL(url); + if (ns_url) + return OpenURL(ns_url, activate).empty(); + return false; +} + +void OpenExternal(const GURL& url, bool activate, + const OpenExternalCallback& callback) { + NSURL* ns_url = net::NSURLWithGURL(url); if (!ns_url) { - return false; + callback.Run("Invalid URL"); + return; } - CFURLRef openingApp = NULL; - OSStatus status = LSGetApplicationForURL((CFURLRef)ns_url, - kLSRolesAll, - NULL, - &openingApp); - if (status != noErr) { - return false; - } - CFRelease(openingApp); // NOT A BUG; LSGetApplicationForURL retains for us - - NSUInteger launchOptions = NSWorkspaceLaunchDefault; - if (!activate) - launchOptions |= NSWorkspaceLaunchWithoutActivation; - - return [[NSWorkspace sharedWorkspace] openURLs: @[ns_url] - withAppBundleIdentifier: nil - options: launchOptions - additionalEventParamDescriptor: NULL - launchIdentifiers: NULL]; + __block OpenExternalCallback c = 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(), ^{ + c.Run(error); + }); + }); } bool MoveItemToTrash(const base::FilePath& full_path) { diff --git a/atom/common/platform_util_win.cc b/atom/common/platform_util_win.cc index 2c98115989..348868dc58 100644 --- a/atom/common/platform_util_win.cc +++ b/atom/common/platform_util_win.cc @@ -316,6 +316,12 @@ bool OpenExternal(const base::string16& url, bool activate) { return true; } +void OpenExternal(const base::string16& url, bool activate, + const OpenExternalCallback& callback) { + // TODO(gabriel): Implement async open if callback is specified + callback.Run(OpenExternal(url, activate) ? "" : "Failed to open"); +} + bool MoveItemToTrash(const base::FilePath& path) { base::win::ScopedCOMInitializer com_initializer; if (!com_initializer.succeeded()) diff --git a/docs/api/shell.md b/docs/api/shell.md index 89ae954ad6..411ef9cce5 100644 --- a/docs/api/shell.md +++ b/docs/api/shell.md @@ -34,14 +34,17 @@ Returns `Boolean` - Whether the item was successfully opened. Open the given file in the desktop's default manner. -### `shell.openExternal(url[, options])` +### `shell.openExternal(url[, options, callback])` * `url` String * `options` Object (optional) _macOS_ * `activate` Boolean - `true` to bring the opened application to the foreground. The default is `true`. +* `callback` Function (optional) - If specified will perform the open asynchronously. _macOS_ + * `error` Error Returns `Boolean` - Whether an application was available to open the URL. +If callback is specified, always returns true. Open the given external protocol URL in the desktop's default manner. (For example, mailto: URLs in the user's default mail agent).