xref: /aosp_15_r20/external/cronet/base/mac/launch_application.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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