// 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(&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* string_vector = absl::get_if>(&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* 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& 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