1// Copyright 2013 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "base/mac/launch_application.h" 6 7#include "base/apple/bridging.h" 8#include "base/apple/foundation_util.h" 9#include "base/command_line.h" 10#include "base/functional/callback.h" 11#include "base/logging.h" 12#include "base/mac/launch_services_spi.h" 13#include "base/mac/mac_util.h" 14#include "base/metrics/histogram_functions.h" 15#include "base/strings/sys_string_conversions.h" 16#include "base/types/expected.h" 17 18namespace base::mac { 19 20namespace { 21 22// These values are persisted to logs. Entries should not be renumbered and 23// numeric values should never be reused. 24enum class LaunchResult { 25 kSuccess = 0, 26 kSuccessDespiteError = 1, 27 kFailure = 2, 28 kMaxValue = kFailure, 29}; 30 31void LogLaunchResult(LaunchResult result) { 32 UmaHistogramEnumeration("Mac.LaunchApplicationResult", result); 33} 34 35NSArray* CommandLineArgsToArgsArray(const CommandLineArgs& command_line_args) { 36 if (const CommandLine* command_line = 37 absl::get_if<CommandLine>(&command_line_args)) { 38 const auto& argv = command_line->argv(); 39 size_t argc = argv.size(); 40 DCHECK_GT(argc, 0lu); 41 42 NSMutableArray* args_array = [NSMutableArray arrayWithCapacity:argc - 1]; 43 // NSWorkspace automatically adds the binary path as the first argument and 44 // thus it should not be included in the list. 45 for (size_t i = 1; i < argc; ++i) { 46 [args_array addObject:base::SysUTF8ToNSString(argv[i])]; 47 } 48 49 return args_array; 50 } 51 52 if (const std::vector<std::string>* string_vector = 53 absl::get_if<std::vector<std::string>>(&command_line_args)) { 54 NSMutableArray* args_array = 55 [NSMutableArray arrayWithCapacity:string_vector->size()]; 56 for (const auto& arg : *string_vector) { 57 [args_array addObject:base::SysUTF8ToNSString(arg)]; 58 } 59 60 return args_array; 61 } 62 63 return @[]; 64} 65 66NSWorkspaceOpenConfiguration* GetOpenConfiguration( 67 LaunchApplicationOptions options, 68 const CommandLineArgs& command_line_args) { 69 NSWorkspaceOpenConfiguration* config = 70 [NSWorkspaceOpenConfiguration configuration]; 71 72 config.arguments = CommandLineArgsToArgsArray(command_line_args); 73 74 config.activates = options.activate; 75 config.createsNewApplicationInstance = options.create_new_instance; 76 config.promptsUserIfNeeded = options.prompt_user_if_needed; 77 78 if (options.hidden_in_background) { 79 config.addsToRecentItems = NO; 80 config.hides = YES; 81 config._additionalLSOpenOptions = @{ 82 apple::CFToNSPtrCast(_kLSOpenOptionBackgroundLaunchKey) : @YES, 83 }; 84 } 85 86 return config; 87} 88 89// Sometimes macOS 11 and 12 report an error launching even though the launch 90// succeeded anyway. This helper returns true for the error codes we have 91// observed where scanning the list of running applications appears to be a 92// usable workaround for this. 93bool ShouldScanRunningAppsForError(NSError* error) { 94 if (!error) { 95 return false; 96 } 97 if (error.domain == NSCocoaErrorDomain && 98 error.code == NSFileReadUnknownError) { 99 return true; 100 } 101 if (error.domain == NSOSStatusErrorDomain && error.code == procNotFound) { 102 return true; 103 } 104 return false; 105} 106 107void LogResultAndInvokeCallback(const base::FilePath& app_bundle_path, 108 bool create_new_instance, 109 LaunchApplicationCallback callback, 110 NSRunningApplication* app, 111 NSError* error) { 112 // Sometimes macOS 11 and 12 report an error launching even though the 113 // launch succeeded anyway. To work around such cases, check if we can 114 // find a running application matching the app we were trying to launch. 115 // Only do this if `options.create_new_instance` is false though, as 116 // otherwise we wouldn't know which instance to return. 117 if ((MacOSMajorVersion() == 11 || MacOSMajorVersion() == 12) && 118 !create_new_instance && !app && ShouldScanRunningAppsForError(error)) { 119 NSArray<NSRunningApplication*>* all_apps = 120 NSWorkspace.sharedWorkspace.runningApplications; 121 for (NSRunningApplication* running_app in all_apps) { 122 if (apple::NSURLToFilePath(running_app.bundleURL) == app_bundle_path) { 123 LOG(ERROR) << "Launch succeeded despite error: " 124 << base::SysNSStringToUTF8(error.localizedDescription); 125 app = running_app; 126 break; 127 } 128 } 129 if (app) { 130 error = nil; 131 } 132 LogLaunchResult(app ? LaunchResult::kSuccessDespiteError 133 : LaunchResult::kFailure); 134 } else { 135 LogLaunchResult(app ? LaunchResult::kSuccess : LaunchResult::kFailure); 136 } 137 138 if (error) { 139 LOG(ERROR) << base::SysNSStringToUTF8(error.localizedDescription); 140 std::move(callback).Run(nil, error); 141 } else { 142 std::move(callback).Run(app, nil); 143 } 144} 145 146} // namespace 147 148void LaunchApplication(const base::FilePath& app_bundle_path, 149 const CommandLineArgs& command_line_args, 150 const std::vector<std::string>& url_specs, 151 LaunchApplicationOptions options, 152 LaunchApplicationCallback callback) { 153 __block LaunchApplicationCallback callback_block_access = 154 base::BindOnce(&LogResultAndInvokeCallback, app_bundle_path, 155 options.create_new_instance, std::move(callback)); 156 157 NSURL* bundle_url = apple::FilePathToNSURL(app_bundle_path); 158 if (!bundle_url) { 159 dispatch_async(dispatch_get_main_queue(), ^{ 160 std::move(callback_block_access) 161 .Run(nil, [NSError errorWithDomain:NSCocoaErrorDomain 162 code:NSFileNoSuchFileError 163 userInfo:nil]); 164 }); 165 return; 166 } 167 168 NSMutableArray* ns_urls = nil; 169 if (!url_specs.empty()) { 170 ns_urls = [NSMutableArray arrayWithCapacity:url_specs.size()]; 171 for (const auto& url_spec : url_specs) { 172 [ns_urls 173 addObject:[NSURL URLWithString:base::SysUTF8ToNSString(url_spec)]]; 174 } 175 } 176 177 void (^action_block)(NSRunningApplication*, NSError*) = 178 ^void(NSRunningApplication* app, NSError* error) { 179 dispatch_async(dispatch_get_main_queue(), ^{ 180 std::move(callback_block_access).Run(app, error); 181 }); 182 }; 183 184 NSWorkspaceOpenConfiguration* configuration = 185 GetOpenConfiguration(options, command_line_args); 186 187 if (ns_urls) { 188 [NSWorkspace.sharedWorkspace openURLs:ns_urls 189 withApplicationAtURL:bundle_url 190 configuration:configuration 191 completionHandler:action_block]; 192 } else { 193 [NSWorkspace.sharedWorkspace openApplicationAtURL:bundle_url 194 configuration:configuration 195 completionHandler:action_block]; 196 } 197} 198 199} // namespace base::mac 200