// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/common/mac/app_mode_chrome_locator.h"
#import <AppKit/AppKit.h>
#include <CoreFoundation/CoreFoundation.h>
#include <optional>
#include <set>
#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/mac/app_mode_common.h"
namespace app_mode {
namespace {
struct PathAndStructure {
NSString* __strong framework_dylib_path;
bool is_new_app_structure;
};
std::optional<PathAndStructure> GetFrameworkDylibPathAndStructure(
NSString* bundle_path,
NSString* version) {
// NEW STYLE:
// Chromium.app/Contents/Frameworks/Chromium Framework.framework/
// Versions/<version>/Chromium Framework
NSString* path = [NSString pathWithComponents:@[
bundle_path, @"Contents", @"Frameworks", @(chrome::kFrameworkName),
@"Versions", version, @(chrome::kFrameworkExecutableName)
]];
if ([NSFileManager.defaultManager fileExistsAtPath:path]) {
return PathAndStructure{path, true};
}
// OLD STYLE:
// Chromium.app/Contents/Versions/<version>/Chromium Framework.framework/
// Versions/A/Chromium Framework
path = [NSString pathWithComponents:@[
bundle_path, @"Contents", @"Versions", version, @(chrome::kFrameworkName),
@"Versions", @"A", @(chrome::kFrameworkExecutableName)
]];
if ([NSFileManager.defaultManager fileExistsAtPath:path]) {
return PathAndStructure{path, false};
}
return std::nullopt;
}
bool IsPathValidForBundle(const base::FilePath& bundle_path,
NSString* bundle_id) {
if (bundle_path.empty())
return false;
if (!base::DirectoryExists(bundle_path))
return false;
NSString* ns_bundle_path = base::SysUTF8ToNSString(bundle_path.value());
NSBundle* bundle = [NSBundle bundleWithPath:ns_bundle_path];
if (!bundle || ![bundle_id isEqualToString:bundle.bundleIdentifier]) {
return false;
}
return true;
}
} // namespace
bool FindChromeBundle(NSString* bundle_id, base::FilePath* out_bundle) {
// Retrieve the last-run Chrome bundle location.
base::FilePath last_run_bundle_path;
{
NSString* cr_bundle_path_ns = base::apple::CFToNSOwnershipCast(
base::apple::CFCastStrict<CFStringRef>(CFPreferencesCopyAppValue(
base::apple::NSToCFPtrCast(app_mode::kLastRunAppBundlePathPrefsKey),
base::apple::NSToCFPtrCast(bundle_id))));
last_run_bundle_path = base::apple::NSStringToFilePath(cr_bundle_path_ns);
}
// Look up running instances of the specified bundle ID.
{
// Note that IsPathValidForBundle is guaranteed to be true for all elements
// in `running_bundle_paths` because runningApplicationsWithBundleIdentifier
// returned them.
std::set<base::FilePath> running_bundle_paths;
NSArray<NSRunningApplication*>* running_applications = [NSRunningApplication
runningApplicationsWithBundleIdentifier:bundle_id];
for (NSRunningApplication* running_application : running_applications) {
base::FilePath bundle_path =
base::apple::NSURLToFilePath(running_application.bundleURL);
DCHECK(!bundle_path.empty());
running_bundle_paths.insert(bundle_path);
}
// If the last-run instance is still running, then use that instance.
if (running_bundle_paths.count(last_run_bundle_path)) {
*out_bundle = last_run_bundle_path;
return true;
}
// Otherwise, select a running bundle path arbitrarily.
// TODO(crbug.com/40208159): This choice should not be made
// arbitrarily.
if (!running_bundle_paths.empty()) {
*out_bundle = *running_bundle_paths.begin();
return true;
}
}
// Next, use the last run bundle path, if it is valid.
if (IsPathValidForBundle(last_run_bundle_path, bundle_id)) {
*out_bundle = last_run_bundle_path;
return true;
}
// Finally, search the filesystem for a bundle. If several copies of the
// bundle are present, this will select one arbitrarily.
{
// Note that `IsPathValidForBundle` is guaranteed to be true for
// `bundle_path` because URLForApplicationWithBundleIdentifier returned it.
NSURL* url = [NSWorkspace.sharedWorkspace
URLForApplicationWithBundleIdentifier:bundle_id];
if (url) {
*out_bundle = base::apple::NSURLToFilePath(url);
return true;
}
}
return false;
}
bool GetChromeBundleInfo(const base::FilePath& chrome_bundle,
const std::string& version_str,
base::FilePath* executable_path,
base::FilePath* framework_path,
base::FilePath* framework_dylib_path) {
NSString* cr_bundle_path = base::apple::FilePathToNSString(chrome_bundle);
NSBundle* cr_bundle = [NSBundle bundleWithPath:cr_bundle_path];
if (!cr_bundle)
return false;
// Try to get the version requested, if present.
std::optional<PathAndStructure> framework_path_and_structure;
if (!version_str.empty()) {
framework_path_and_structure = GetFrameworkDylibPathAndStructure(
cr_bundle_path, base::SysUTF8ToNSString(version_str));
}
// If the version requested is not present, or no specific version was
// requested, fall back to the "current" version. For new-style bundle
// structures, use the "Current" symlink. (This will intentionally return nil
// with the old bundle structure.)
//
// Note that the scenario where a specific version was requested but is not
// present is a "should not happen" scenario. Chromium, while it is running,
// maintains a link to the currently running version, and this function's
// caller checked to see if the Chromium was still running. However, even in
// this bizarre case, it's best to find _some_ Chromium.
if (!framework_path_and_structure) {
framework_path_and_structure =
GetFrameworkDylibPathAndStructure(cr_bundle_path, @"Current");
if (framework_path_and_structure) {
framework_path_and_structure->framework_dylib_path =
[framework_path_and_structure
->framework_dylib_path stringByResolvingSymlinksInPath];
}
}
// At this point it is known that it is an old-style bundle structure (or a
// rather broken new-style bundle). Try explicitly specifying the version of
// the framework matching the outer bundle version.
if (!framework_path_and_structure) {
NSString* cr_version = base::apple::ObjCCast<NSString>([cr_bundle
objectForInfoDictionaryKey:app_mode::kCFBundleShortVersionStringKey]);
if (cr_version) {
framework_path_and_structure =
GetFrameworkDylibPathAndStructure(cr_bundle_path, cr_version);
}
}
if (!framework_path_and_structure)
return false;
// A few sanity checks.
BOOL is_directory;
BOOL exists = [NSFileManager.defaultManager
fileExistsAtPath:framework_path_and_structure->framework_dylib_path
isDirectory:&is_directory];
if (!exists || is_directory)
return false;
NSString* cr_framework_path;
if (framework_path_and_structure->is_new_app_structure) {
// For the path to the framework version itself, remove the framework name.
cr_framework_path =
[framework_path_and_structure
->framework_dylib_path stringByDeletingLastPathComponent];
} else {
// For the path to the framework itself, remove the framework name ...
cr_framework_path =
[framework_path_and_structure
->framework_dylib_path stringByDeletingLastPathComponent];
// ... the "A" ...
cr_framework_path = [cr_framework_path stringByDeletingLastPathComponent];
// ... and the "Versions" directory.
cr_framework_path = [cr_framework_path stringByDeletingLastPathComponent];
}
// Everything is OK; copy the output parameters.
*executable_path = base::apple::NSStringToFilePath(cr_bundle.executablePath);
*framework_path = base::apple::NSStringToFilePath(cr_framework_path);
*framework_dylib_path = base::apple::NSStringToFilePath(
framework_path_and_structure->framework_dylib_path);
return true;
}
} // namespace app_mode