| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "base/mac/launch_application.h" |
| |
| #include "base/apple/bridging.h" |
| #include "base/apple/foundation_util.h" |
| #include "base/command_line.h" |
| #include "base/functional/callback.h" |
| #include "base/logging.h" |
| #include "base/mac/launch_services_spi.h" |
| #include "base/mac/mac_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/types/expected.h" |
| |
| namespace base::mac { |
| |
| namespace { |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class LaunchResult { |
| kSuccess = 0, |
| kSuccessDespiteError = 1, |
| kFailure = 2, |
| kMaxValue = kFailure, |
| }; |
| |
| void LogLaunchResult(LaunchResult result) { |
| UmaHistogramEnumeration("Mac.LaunchApplicationResult", result); |
| } |
| |
| NSArray* CommandLineArgsToArgsArray(const CommandLineArgs& command_line_args) { |
| if (const CommandLine* command_line = |
| absl::get_if<CommandLine>(&command_line_args)) { |
| const auto& argv = command_line->argv(); |
| size_t argc = argv.size(); |
| DCHECK_GT(argc, 0lu); |
| |
| NSMutableArray* args_array = [NSMutableArray arrayWithCapacity:argc - 1]; |
| // NSWorkspace automatically adds the binary path as the first argument and |
| // thus it should not be included in the list. |
| for (size_t i = 1; i < argc; ++i) { |
| [args_array addObject:base::SysUTF8ToNSString(argv[i])]; |
| } |
| |
| return args_array; |
| } |
| |
| if (const std::vector<std::string>* string_vector = |
| absl::get_if<std::vector<std::string>>(&command_line_args)) { |
| NSMutableArray* args_array = |
| [NSMutableArray arrayWithCapacity:string_vector->size()]; |
| for (const auto& arg : *string_vector) { |
| [args_array addObject:base::SysUTF8ToNSString(arg)]; |
| } |
| |
| return args_array; |
| } |
| |
| return @[]; |
| } |
| |
| NSWorkspaceOpenConfiguration* GetOpenConfiguration( |
| LaunchApplicationOptions options, |
| const CommandLineArgs& command_line_args) { |
| NSWorkspaceOpenConfiguration* config = |
| [NSWorkspaceOpenConfiguration configuration]; |
| |
| config.arguments = CommandLineArgsToArgsArray(command_line_args); |
| |
| config.activates = options.activate; |
| config.createsNewApplicationInstance = options.create_new_instance; |
| config.promptsUserIfNeeded = options.prompt_user_if_needed; |
| |
| if (options.hidden_in_background) { |
| config.addsToRecentItems = NO; |
| config.hides = YES; |
| config._additionalLSOpenOptions = @{ |
| apple::CFToNSPtrCast(_kLSOpenOptionBackgroundLaunchKey) : @YES, |
| }; |
| } |
| |
| return config; |
| } |
| |
| // Sometimes macOS 11 and 12 report an error launching even though the launch |
| // succeeded anyway. This helper returns true for the error codes we have |
| // observed where scanning the list of running applications appears to be a |
| // usable workaround for this. |
| bool ShouldScanRunningAppsForError(NSError* error) { |
| if (!error) { |
| return false; |
| } |
| if (error.domain == NSCocoaErrorDomain && |
| error.code == NSFileReadUnknownError) { |
| return true; |
| } |
| if (error.domain == NSOSStatusErrorDomain && error.code == procNotFound) { |
| return true; |
| } |
| return false; |
| } |
| |
| void LogResultAndInvokeCallback(const base::FilePath& app_bundle_path, |
| bool create_new_instance, |
| LaunchApplicationCallback callback, |
| NSRunningApplication* app, |
| NSError* error) { |
| // Sometimes macOS 11 and 12 report an error launching even though the |
| // launch succeeded anyway. To work around such cases, check if we can |
| // find a running application matching the app we were trying to launch. |
| // Only do this if `options.create_new_instance` is false though, as |
| // otherwise we wouldn't know which instance to return. |
| if ((MacOSMajorVersion() == 11 || MacOSMajorVersion() == 12) && |
| !create_new_instance && !app && ShouldScanRunningAppsForError(error)) { |
| NSArray<NSRunningApplication*>* all_apps = |
| NSWorkspace.sharedWorkspace.runningApplications; |
| for (NSRunningApplication* running_app in all_apps) { |
| if (apple::NSURLToFilePath(running_app.bundleURL) == app_bundle_path) { |
| LOG(ERROR) << "Launch succeeded despite error: " |
| << base::SysNSStringToUTF8(error.localizedDescription); |
| app = running_app; |
| break; |
| } |
| } |
| if (app) { |
| error = nil; |
| } |
| LogLaunchResult(app ? LaunchResult::kSuccessDespiteError |
| : LaunchResult::kFailure); |
| } else { |
| LogLaunchResult(app ? LaunchResult::kSuccess : LaunchResult::kFailure); |
| } |
| |
| if (error) { |
| LOG(ERROR) << base::SysNSStringToUTF8(error.localizedDescription); |
| std::move(callback).Run(nil, error); |
| } else { |
| std::move(callback).Run(app, nil); |
| } |
| } |
| |
| } // namespace |
| |
| void LaunchApplication(const base::FilePath& app_bundle_path, |
| const CommandLineArgs& command_line_args, |
| const std::vector<std::string>& url_specs, |
| LaunchApplicationOptions options, |
| LaunchApplicationCallback callback) { |
| __block LaunchApplicationCallback callback_block_access = |
| base::BindOnce(&LogResultAndInvokeCallback, app_bundle_path, |
| options.create_new_instance, std::move(callback)); |
| |
| NSURL* bundle_url = apple::FilePathToNSURL(app_bundle_path); |
| if (!bundle_url) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| std::move(callback_block_access) |
| .Run(nil, [NSError errorWithDomain:NSCocoaErrorDomain |
| code:NSFileNoSuchFileError |
| userInfo:nil]); |
| }); |
| return; |
| } |
| |
| NSMutableArray* ns_urls = nil; |
| if (!url_specs.empty()) { |
| ns_urls = [NSMutableArray arrayWithCapacity:url_specs.size()]; |
| for (const auto& url_spec : url_specs) { |
| [ns_urls |
| addObject:[NSURL URLWithString:base::SysUTF8ToNSString(url_spec)]]; |
| } |
| } |
| |
| void (^action_block)(NSRunningApplication*, NSError*) = |
| ^void(NSRunningApplication* app, NSError* error) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| std::move(callback_block_access).Run(app, error); |
| }); |
| }; |
| |
| NSWorkspaceOpenConfiguration* configuration = |
| GetOpenConfiguration(options, command_line_args); |
| |
| if (ns_urls) { |
| [NSWorkspace.sharedWorkspace openURLs:ns_urls |
| withApplicationAtURL:bundle_url |
| configuration:configuration |
| completionHandler:action_block]; |
| } else { |
| [NSWorkspace.sharedWorkspace openApplicationAtURL:bundle_url |
| configuration:configuration |
| completionHandler:action_block]; |
| } |
| } |
| |
| } // namespace base::mac |