• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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