summaryrefslogtreecommitdiffstats
path: root/chrome/app_shim/app_mode_loader_mac.mm
blob: 16e7f7ec6228783d5d073fe96f566c6d39f2db07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// On Mac, shortcuts can't have command-line arguments. Instead, produce small
// app bundles which locate the Chromium framework and load it, passing the
// appropriate data. This is the code for such an app bundle. It should be kept
// minimal and do as little work as possible (with as much work done on
// framework side as possible).

#include <dlfcn.h>

#import <Cocoa/Cocoa.h>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launch_services_util.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#import "chrome/common/mac/app_mode_chrome_locator.h"
#include "chrome/common/mac/app_mode_common.h"

namespace {

typedef int (*StartFun)(const app_mode::ChromeAppModeInfo*);

int LoadFrameworkAndStart(app_mode::ChromeAppModeInfo* info) {
  using base::SysNSStringToUTF8;
  using base::SysNSStringToUTF16;
  using base::mac::CFToNSCast;
  using base::mac::CFCastStrict;
  using base::mac::NSToCFCast;

  base::mac::ScopedNSAutoreleasePool scoped_pool;

  // Get the current main bundle, i.e., that of the app loader that's running.
  NSBundle* app_bundle = [NSBundle mainBundle];
  CHECK(app_bundle) << "couldn't get loader bundle";

  // ** 1: Get path to outer Chrome bundle.
  // Get the bundle ID of the browser that created this app bundle.
  NSString* cr_bundle_id = base::mac::ObjCCast<NSString>(
      [app_bundle objectForInfoDictionaryKey:app_mode::kBrowserBundleIDKey]);
  CHECK(cr_bundle_id) << "couldn't get browser bundle ID";

  // First check if Chrome exists at the last known location.
  base::FilePath cr_bundle_path;
  NSString* cr_bundle_path_ns =
      [CFToNSCast(CFCastStrict<CFStringRef>(CFPreferencesCopyAppValue(
          NSToCFCast(app_mode::kLastRunAppBundlePathPrefsKey),
          NSToCFCast(cr_bundle_id)))) autorelease];
  cr_bundle_path = base::mac::NSStringToFilePath(cr_bundle_path_ns);
  bool found_bundle =
      !cr_bundle_path.empty() && base::DirectoryExists(cr_bundle_path);

  if (!found_bundle) {
    // If no such bundle path exists, try to search by bundle ID.
    if (!app_mode::FindBundleById(cr_bundle_id, &cr_bundle_path)) {
      // TODO(jeremy): Display UI to allow user to manually locate the Chrome
      // bundle.
      LOG(FATAL) << "Failed to locate bundle by identifier";
    }
  }

  // ** 2: Read the running Chrome version.
  // The user_data_dir for shims actually contains the app_data_path.
  // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
  base::FilePath app_data_dir = base::mac::NSStringToFilePath([app_bundle
      objectForInfoDictionaryKey:app_mode::kCrAppModeUserDataDirKey]);
  base::FilePath user_data_dir = app_data_dir.DirName().DirName().DirName();
  CHECK(!user_data_dir.empty());

  // If the version file does not exist, |cr_version_str| will be empty and
  // app_mode::GetChromeBundleInfo will default to the latest version.
  base::FilePath cr_version_str;
  base::ReadSymbolicLink(
      user_data_dir.Append(app_mode::kRunningChromeVersionSymlinkName),
      &cr_version_str);

  // If the version file does exist, it may have been left by a crashed Chrome
  // process. Ensure the process is still running.
  if (!cr_version_str.empty()) {
    NSArray* existing_chrome = [NSRunningApplication
        runningApplicationsWithBundleIdentifier:cr_bundle_id];
    if ([existing_chrome count] == 0)
      cr_version_str.clear();
  }

  // ** 3: Read information from the Chrome bundle.
  base::FilePath executable_path;
  base::FilePath version_path;
  base::FilePath framework_shlib_path;
  if (!app_mode::GetChromeBundleInfo(cr_bundle_path,
                                     cr_version_str.value(),
                                     &executable_path,
                                     &version_path,
                                     &framework_shlib_path)) {
    LOG(FATAL) << "Couldn't ready Chrome bundle info";
  }
  base::FilePath app_mode_bundle_path =
      base::mac::NSStringToFilePath([app_bundle bundlePath]);

  // ** 4: Fill in ChromeAppModeInfo.
  info->chrome_outer_bundle_path = cr_bundle_path;
  info->chrome_versioned_path = version_path;
  info->app_mode_bundle_path = app_mode_bundle_path;

  // Read information about the this app shortcut from the Info.plist.
  // Don't check for null-ness on optional items.
  NSDictionary* info_plist = [app_bundle infoDictionary];
  CHECK(info_plist) << "couldn't get loader Info.plist";

  info->app_mode_id = SysNSStringToUTF8(
      [info_plist objectForKey:app_mode::kCrAppModeShortcutIDKey]);
  CHECK(info->app_mode_id.size()) << "couldn't get app shortcut ID";

  info->app_mode_name = SysNSStringToUTF16(
      [info_plist objectForKey:app_mode::kCrAppModeShortcutNameKey]);

  info->app_mode_url = SysNSStringToUTF8(
      [info_plist objectForKey:app_mode::kCrAppModeShortcutURLKey]);

  info->user_data_dir = base::mac::NSStringToFilePath(
      [info_plist objectForKey:app_mode::kCrAppModeUserDataDirKey]);

  info->profile_dir = base::mac::NSStringToFilePath(
      [info_plist objectForKey:app_mode::kCrAppModeProfileDirKey]);

  // ** 5: Open the framework.
  StartFun ChromeAppModeStart = NULL;
  void* cr_dylib = dlopen(framework_shlib_path.value().c_str(), RTLD_LAZY);
  if (cr_dylib) {
    // Find the entry point.
    ChromeAppModeStart = (StartFun)dlsym(cr_dylib, "ChromeAppModeStart");
    if (!ChromeAppModeStart)
      LOG(ERROR) << "Couldn't get entry point: " << dlerror();
  } else {
    LOG(ERROR) << "Couldn't load framework: " << dlerror();
  }

  if (ChromeAppModeStart)
    return ChromeAppModeStart(info);

  LOG(ERROR) << "Loading Chrome failed, launching Chrome with command line";
  base::CommandLine command_line(executable_path);
  // The user_data_dir from the plist is actually the app data dir.
  command_line.AppendSwitchPath(
      switches::kUserDataDir,
      info->user_data_dir.DirName().DirName().DirName());
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          app_mode::kLaunchedByChromeProcessId) ||
      info->app_mode_id == app_mode::kAppListModeId) {
    // Pass --app-shim-error to have Chrome rebuild this shim.
    // If Chrome has rebuilt this shim once already, then rebuilding doesn't fix
    // the problem, so don't try again.
    if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
            app_mode::kLaunchedAfterRebuild)) {
      command_line.AppendSwitchPath(app_mode::kAppShimError,
                                    app_mode_bundle_path);
    }
  } else {
    // If the shim was launched directly (instead of by Chrome), first ask
    // Chrome to launch the app. Chrome will launch the shim again, the same
    // error will occur and be handled above. This approach allows the app to be
    // started without blocking on fixing the shim and guarantees that the
    // profile is loaded when Chrome receives --app-shim-error.
    command_line.AppendSwitchPath(switches::kProfileDirectory,
                                  info->profile_dir);
    command_line.AppendSwitchASCII(switches::kAppId, info->app_mode_id);
  }
  // Launch the executable directly since base::mac::OpenApplicationWithPath
  // uses LSOpenApplication which doesn't pass command line arguments if the
  // application is already running.
  if (!base::LaunchProcess(command_line, base::LaunchOptions()).IsValid()) {
    LOG(ERROR) << "Could not launch Chrome: "
               << command_line.GetCommandLineString();
    return 1;
  }

  return 0;
}

} // namespace

__attribute__((visibility("default")))
int main(int argc, char** argv) {
  base::CommandLine::Init(argc, argv);
  app_mode::ChromeAppModeInfo info;

  // Hard coded info parameters.
  info.major_version = app_mode::kCurrentChromeAppModeInfoMajorVersion;
  info.minor_version = app_mode::kCurrentChromeAppModeInfoMinorVersion;
  info.argc = argc;
  info.argv = argv;

  // Exit instead of returning to avoid the the removal of |main()| from stack
  // backtraces under tail call optimization.
  exit(LoadFrameworkAndStart(&info));
}