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.activates = options.activate; 73 config.createsNewApplicationInstance = options.create_new_instance; 74 config.promptsUserIfNeeded = options.prompt_user_if_needed; 75 config.arguments = CommandLineArgsToArgsArray(command_line_args); 76 77 return config; 78} 79 80NSDictionary* GetOpenOptions(LaunchApplicationOptions options, 81 const CommandLineArgs& command_line_args) { 82 NSDictionary* dict = @{ 83 base::apple::CFToNSPtrCast(_kLSOpenOptionArgumentsKey) : 84 CommandLineArgsToArgsArray(command_line_args), 85 base::apple::CFToNSPtrCast(_kLSOpenOptionHideKey) : 86 @(options.hidden_in_background), 87 base::apple::CFToNSPtrCast(_kLSOpenOptionBackgroundLaunchKey) : 88 @(options.hidden_in_background), 89 base::apple::CFToNSPtrCast(_kLSOpenOptionAddToRecentsKey) : 90 @(!options.hidden_in_background), 91 base::apple::CFToNSPtrCast(_kLSOpenOptionActivateKey) : @(options.activate), 92 base::apple::CFToNSPtrCast(_kLSOpenOptionPreferRunningInstanceKey) : 93 @(!options.create_new_instance), 94 }; 95 return dict; 96} 97 98// Sometimes macOS 11 and 12 report an error launching even though the launch 99// succeeded anyway. This helper returns true for the error codes we have 100// observed where scanning the list of running applications appears to be a 101// usable workaround for this. 102bool ShouldScanRunningAppsForError(NSError* error) { 103 if (!error) { 104 return false; 105 } 106 if (error.domain == NSCocoaErrorDomain && 107 error.code == NSFileReadUnknownError) { 108 return true; 109 } 110 if (error.domain == NSOSStatusErrorDomain && error.code == procNotFound) { 111 return true; 112 } 113 return false; 114} 115 116void LogResultAndInvokeCallback(const base::FilePath& app_bundle_path, 117 bool create_new_instance, 118 LaunchApplicationCallback callback, 119 NSRunningApplication* app, 120 NSError* error) { 121 // Sometimes macOS 11 and 12 report an error launching even though the 122 // launch succeeded anyway. To work around such cases, check if we can 123 // find a running application matching the app we were trying to launch. 124 // Only do this if `options.create_new_instance` is false though, as 125 // otherwise we wouldn't know which instance to return. 126 if ((MacOSMajorVersion() == 11 || MacOSMajorVersion() == 12) && 127 !create_new_instance && !app && ShouldScanRunningAppsForError(error)) { 128 NSArray<NSRunningApplication*>* all_apps = 129 NSWorkspace.sharedWorkspace.runningApplications; 130 for (NSRunningApplication* running_app in all_apps) { 131 if (apple::NSURLToFilePath(running_app.bundleURL) == app_bundle_path) { 132 LOG(ERROR) << "Launch succeeded despite error: " 133 << base::SysNSStringToUTF8(error.localizedDescription); 134 app = running_app; 135 break; 136 } 137 } 138 if (app) { 139 error = nil; 140 } 141 LogLaunchResult(app ? LaunchResult::kSuccessDespiteError 142 : LaunchResult::kFailure); 143 } else { 144 LogLaunchResult(app ? LaunchResult::kSuccess : LaunchResult::kFailure); 145 } 146 147 if (error) { 148 LOG(ERROR) << base::SysNSStringToUTF8(error.localizedDescription); 149 std::move(callback).Run(nil, error); 150 } else { 151 std::move(callback).Run(app, nil); 152 } 153} 154 155} // namespace 156 157void LaunchApplication(const base::FilePath& app_bundle_path, 158 const CommandLineArgs& command_line_args, 159 const std::vector<std::string>& url_specs, 160 LaunchApplicationOptions options, 161 LaunchApplicationCallback callback) { 162 __block LaunchApplicationCallback callback_block_access = 163 base::BindOnce(&LogResultAndInvokeCallback, app_bundle_path, 164 options.create_new_instance, std::move(callback)); 165 166 NSURL* bundle_url = apple::FilePathToNSURL(app_bundle_path); 167 if (!bundle_url) { 168 dispatch_async(dispatch_get_main_queue(), ^{ 169 std::move(callback_block_access) 170 .Run(nil, [NSError errorWithDomain:NSCocoaErrorDomain 171 code:NSFileNoSuchFileError 172 userInfo:nil]); 173 }); 174 return; 175 } 176 177 NSMutableArray* ns_urls = nil; 178 if (!url_specs.empty()) { 179 ns_urls = [NSMutableArray arrayWithCapacity:url_specs.size()]; 180 for (const auto& url_spec : url_specs) { 181 [ns_urls 182 addObject:[NSURL URLWithString:base::SysUTF8ToNSString(url_spec)]]; 183 } 184 } 185 186 if (options.hidden_in_background) { 187 _LSOpenCompletionHandler action_block = 188 ^void(LSASNRef asn, Boolean success, CFErrorRef cf_error) { 189 NSRunningApplication* app = nil; 190 if (asn) { 191 app = [[NSRunningApplication alloc] 192 initWithApplicationSerialNumber:asn]; 193 } 194 NSError* error = base::apple::CFToNSPtrCast(cf_error); 195 dispatch_async(dispatch_get_main_queue(), ^{ 196 std::move(callback_block_access).Run(app, error); 197 }); 198 }; 199 200 _LSOpenURLsWithCompletionHandler( 201 base::apple::NSToCFPtrCast(ns_urls ? ns_urls : @[]), 202 apple::FilePathToCFURL(app_bundle_path).get(), 203 base::apple::NSToCFPtrCast(GetOpenOptions(options, command_line_args)), 204 action_block); 205 return; 206 } 207 208 void (^action_block)(NSRunningApplication*, NSError*) = 209 ^void(NSRunningApplication* app, NSError* error) { 210 dispatch_async(dispatch_get_main_queue(), ^{ 211 std::move(callback_block_access).Run(app, error); 212 }); 213 }; 214 215 NSWorkspaceOpenConfiguration* configuration = 216 GetOpenConfiguration(options, command_line_args); 217 218 if (ns_urls) { 219 [NSWorkspace.sharedWorkspace openURLs:ns_urls 220 withApplicationAtURL:bundle_url 221 configuration:configuration 222 completionHandler:action_block]; 223 } else { 224 [NSWorkspace.sharedWorkspace openApplicationAtURL:bundle_url 225 configuration:configuration 226 completionHandler:action_block]; 227 } 228} 229 230} // namespace base::mac 231