1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/mac/install_from_dmg.h"
7 #import <AppKit/AppKit.h>
8 #include <ApplicationServices/ApplicationServices.h>
9 #include <CoreFoundation/CoreFoundation.h>
10 #include <CoreServices/CoreServices.h>
11 #include <DiskArbitration/DiskArbitration.h>
12 #include <IOKit/IOKitLib.h>
16 #include <sys/mount.h>
17 #include <sys/param.h>
19 #include "base/auto_reset.h"
20 #include "base/basictypes.h"
21 #include "base/command_line.h"
22 #include "base/files/file_path.h"
23 #include "base/logging.h"
24 #include "base/mac/authorization_util.h"
25 #include "base/mac/bundle_locations.h"
26 #include "base/mac/foundation_util.h"
27 #include "base/mac/mac_logging.h"
28 #include "base/mac/mach_logging.h"
29 #include "base/mac/scoped_authorizationref.h"
30 #include "base/mac/scoped_cftyperef.h"
31 #include "base/mac/scoped_ioobject.h"
32 #include "base/mac/scoped_nsautorelease_pool.h"
33 #include "base/strings/string_util.h"
34 #include "base/strings/sys_string_conversions.h"
35 #include "chrome/browser/mac/dock.h"
36 #import "chrome/browser/mac/keystone_glue.h"
37 #include "chrome/browser/mac/relauncher.h"
38 #include "chrome/common/chrome_constants.h"
39 #include "chrome/grit/chromium_strings.h"
40 #include "chrome/grit/generated_resources.h"
41 #include "ui/base/l10n/l10n_util.h"
42 #include "ui/base/l10n/l10n_util_mac.h"
44 // When C++ exceptions are disabled, the C++ library defines |try| and
45 // |catch| so as to allow exception-expecting C++ code to build properly when
46 // language support for exceptions is not present. These macros interfere
47 // with the use of |@try| and |@catch| in Objective-C files such as this one.
48 // Undefine these macros here, after everything has been #included, since
49 // there will be no C++ uses and only Objective-C uses from this point on.
55 // Given an io_service_t (expected to be of class IOMedia), walks the ancestor
56 // chain, returning the closest ancestor that implements class IOHDIXHDDrive,
57 // if any. If no such ancestor is found, returns NULL. Following the "copy"
58 // rule, the caller assumes ownership of the returned value.
60 // Note that this looks for a class that inherits from IOHDIXHDDrive, but it
61 // will not likely find a concrete IOHDIXHDDrive. It will be
62 // IOHDIXHDDriveOutKernel for disk images mounted "out-of-kernel" or
63 // IOHDIXHDDriveInKernel for disk images mounted "in-kernel." Out-of-kernel is
64 // the default as of Mac OS X 10.5. See the documentation for "hdiutil attach
65 // -kernel" for more information.
66 io_service_t CopyHDIXDriveServiceForMedia(io_service_t media) {
67 const char disk_image_class[] = "IOHDIXHDDrive";
69 // This is highly unlikely. media as passed in is expected to be of class
70 // IOMedia. Since the media service's entire ancestor chain will be checked,
71 // though, check it as well.
72 if (IOObjectConformsTo(media, disk_image_class)) {
73 IOObjectRetain(media);
77 io_iterator_t iterator_ref;
79 IORegistryEntryCreateIterator(media,
81 kIORegistryIterateRecursively |
82 kIORegistryIterateParents,
84 if (kr != KERN_SUCCESS) {
85 MACH_LOG(ERROR, kr) << "IORegistryEntryCreateIterator";
86 return IO_OBJECT_NULL;
88 base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
89 iterator_ref = IO_OBJECT_NULL;
91 // Look at each of the ancestor services, beginning with the parent,
92 // iterating all the way up to the device tree's root. If any ancestor
93 // service matches the class used for disk images, the media resides on a
94 // disk image, and the disk image file's path can be determined by examining
95 // the image-path property.
96 for (base::mac::ScopedIOObject<io_service_t> ancestor(
97 IOIteratorNext(iterator));
99 ancestor.reset(IOIteratorNext(iterator))) {
100 if (IOObjectConformsTo(ancestor, disk_image_class)) {
101 return ancestor.release();
105 // The media does not reside on a disk image.
106 return IO_OBJECT_NULL;
109 // Given an io_service_t (expected to be of class IOMedia), determines whether
110 // that service is on a disk image. If it is, returns true. If image_path is
111 // present, it will be set to the pathname of the disk image file, encoded in
112 // filesystem encoding.
113 bool MediaResidesOnDiskImage(io_service_t media, std::string* image_path) {
118 base::mac::ScopedIOObject<io_service_t> hdix_drive(
119 CopyHDIXDriveServiceForMedia(media));
125 base::ScopedCFTypeRef<CFTypeRef> image_path_cftyperef(
126 IORegistryEntryCreateCFProperty(
127 hdix_drive, CFSTR("image-path"), NULL, 0));
128 if (!image_path_cftyperef) {
129 LOG(ERROR) << "IORegistryEntryCreateCFProperty";
132 if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) {
133 base::ScopedCFTypeRef<CFStringRef> observed_type_cf(
134 CFCopyTypeIDDescription(CFGetTypeID(image_path_cftyperef)));
135 std::string observed_type;
136 if (observed_type_cf) {
137 observed_type.assign(", observed ");
138 observed_type.append(base::SysCFStringRefToUTF8(observed_type_cf));
140 LOG(ERROR) << "image-path: expected CFData, observed " << observed_type;
144 CFDataRef image_path_data = static_cast<CFDataRef>(
145 image_path_cftyperef.get());
146 CFIndex length = CFDataGetLength(image_path_data);
148 LOG(ERROR) << "image_path_data is unexpectedly empty";
151 char* image_path_c = WriteInto(image_path, length + 1);
152 CFDataGetBytes(image_path_data,
153 CFRangeMake(0, length),
154 reinterpret_cast<UInt8*>(image_path_c));
160 // Returns true if |path| is located on a read-only filesystem of a disk
161 // image. Returns false if not, or in the event of an error. If
162 // out_dmg_bsd_device_name is present, it will be set to the BSD device name
163 // for the disk image's device, in "diskNsM" form.
164 bool IsPathOnReadOnlyDiskImage(const char path[],
165 std::string* out_dmg_bsd_device_name) {
166 if (out_dmg_bsd_device_name) {
167 out_dmg_bsd_device_name->clear();
170 struct statfs statfs_buf;
171 if (statfs(path, &statfs_buf) != 0) {
172 PLOG(ERROR) << "statfs " << path;
176 if (!(statfs_buf.f_flags & MNT_RDONLY)) {
177 // Not on a read-only filesystem.
181 const char dev_root[] = "/dev/";
182 const int dev_root_length = arraysize(dev_root) - 1;
183 if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
184 // Not rooted at dev_root, no BSD name to search on.
188 // BSD names in IOKit don't include dev_root.
189 const char* dmg_bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
190 if (out_dmg_bsd_device_name) {
191 out_dmg_bsd_device_name->assign(dmg_bsd_device_name);
194 const mach_port_t master_port = kIOMasterPortDefault;
196 // IOBSDNameMatching gives ownership of match_dict to the caller, but
197 // IOServiceGetMatchingServices will assume that reference.
198 CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
200 dmg_bsd_device_name);
202 LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name;
206 io_iterator_t iterator_ref;
207 kern_return_t kr = IOServiceGetMatchingServices(master_port,
210 if (kr != KERN_SUCCESS) {
211 MACH_LOG(ERROR, kr) << "IOServiceGetMatchingServices";
214 base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
215 iterator_ref = IO_OBJECT_NULL;
217 // There needs to be exactly one matching service.
218 base::mac::ScopedIOObject<io_service_t> media(IOIteratorNext(iterator));
220 LOG(ERROR) << "IOIteratorNext: no service";
223 base::mac::ScopedIOObject<io_service_t> unexpected_service(
224 IOIteratorNext(iterator));
225 if (unexpected_service) {
226 LOG(ERROR) << "IOIteratorNext: too many services";
232 return MediaResidesOnDiskImage(media, NULL);
235 // Returns true if the application is located on a read-only filesystem of a
236 // disk image. Returns false if not, or in the event of an error. If
237 // dmg_bsd_device_name is present, it will be set to the BSD device name for
238 // the disk image's device, in "diskNsM" form.
239 bool IsAppRunningFromReadOnlyDiskImage(std::string* dmg_bsd_device_name) {
240 return IsPathOnReadOnlyDiskImage(
241 [[base::mac::OuterBundle() bundlePath] fileSystemRepresentation],
242 dmg_bsd_device_name);
245 // Shows a dialog asking the user whether or not to install from the disk
246 // image. Returns true if the user approves installation.
247 bool ShouldInstallDialog() {
248 NSString* title = l10n_util::GetNSStringFWithFixup(
249 IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
250 NSString* prompt = l10n_util::GetNSStringFWithFixup(
251 IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
252 NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
253 NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
255 NSAlert* alert = [[[NSAlert alloc] init] autorelease];
257 [alert setAlertStyle:NSInformationalAlertStyle];
258 [alert setMessageText:title];
259 [alert setInformativeText:prompt];
260 [alert addButtonWithTitle:yes];
261 NSButton* cancel_button = [alert addButtonWithTitle:no];
262 [cancel_button setKeyEquivalent:@"\e"];
264 NSInteger result = [alert runModal];
266 return result == NSAlertFirstButtonReturn;
269 // Potentially shows an authorization dialog to request authentication to
270 // copy. If application_directory appears to be unwritable, attempts to
271 // obtain authorization, which may result in the display of the dialog.
272 // Returns NULL if authorization is not performed because it does not appear
273 // to be necessary because the user has permission to write to
274 // application_directory. Returns NULL if authorization fails.
275 AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
276 NSFileManager* file_manager = [NSFileManager defaultManager];
277 if ([file_manager isWritableFileAtPath:application_directory]) {
281 NSString* prompt = l10n_util::GetNSStringFWithFixup(
282 IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
283 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
284 return base::mac::AuthorizationCreateToRunAsRoot(
285 base::mac::NSToCFCast(prompt));
288 // Invokes the installer program at installer_path to copy source_path to
289 // target_path and perform any additional on-disk bookkeeping needed to be
290 // able to launch target_path properly. If authorization_arg is non-NULL,
291 // function will assume ownership of it, will invoke the installer with that
292 // authorization reference, and will attempt Keystone ticket promotion.
293 bool InstallFromDiskImage(AuthorizationRef authorization_arg,
294 NSString* installer_path,
295 NSString* source_path,
296 NSString* target_path) {
297 base::mac::ScopedAuthorizationRef authorization(authorization_arg);
298 authorization_arg = NULL;
301 const char* installer_path_c = [installer_path fileSystemRepresentation];
302 const char* source_path_c = [source_path fileSystemRepresentation];
303 const char* target_path_c = [target_path fileSystemRepresentation];
304 const char* arguments[] = {source_path_c, target_path_c, NULL};
306 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
309 kAuthorizationFlagDefaults,
313 if (status != errAuthorizationSuccess) {
314 OSSTATUS_LOG(ERROR, status)
315 << "AuthorizationExecuteWithPrivileges install";
319 NSArray* arguments = [NSArray arrayWithObjects:source_path,
325 task = [NSTask launchedTaskWithLaunchPath:installer_path
326 arguments:arguments];
327 } @catch(NSException* exception) {
328 LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
329 << [[exception description] UTF8String];
333 [task waitUntilExit];
334 exit_status = [task terminationStatus];
337 if (exit_status != 0) {
338 LOG(ERROR) << "install.sh: exit status " << exit_status;
343 // As long as an AuthorizationRef is available, promote the Keystone
344 // ticket. Inform KeystoneGlue of the new path to use.
345 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
346 [keystone_glue setAppPath:target_path];
347 [keystone_glue promoteTicketWithAuthorization:authorization.release()
354 // Launches the application at installed_path. The helper application
355 // contained within install_path will be used for the relauncher process. This
356 // keeps Launch Services from ever having to see or think about the helper
357 // application on the disk image. The relauncher process will be asked to
358 // call EjectAndTrashDiskImage on dmg_bsd_device_name.
359 bool LaunchInstalledApp(NSString* installed_path,
360 const std::string& dmg_bsd_device_name) {
361 base::FilePath browser_path([installed_path fileSystemRepresentation]);
363 base::FilePath helper_path = browser_path.Append("Contents/Versions");
364 helper_path = helper_path.Append(chrome::kChromeVersion);
365 helper_path = helper_path.Append(chrome::kHelperProcessExecutablePath);
367 std::vector<std::string> args =
368 base::CommandLine::ForCurrentProcess()->argv();
369 args[0] = browser_path.value();
371 std::vector<std::string> relauncher_args;
372 if (!dmg_bsd_device_name.empty()) {
373 std::string dmg_arg(mac_relauncher::kRelauncherDMGDeviceArg);
374 dmg_arg.append(dmg_bsd_device_name);
375 relauncher_args.push_back(dmg_arg);
378 return mac_relauncher::RelaunchAppWithHelper(helper_path.value(),
383 void ShowErrorDialog() {
384 NSString* title = l10n_util::GetNSStringWithFixup(
385 IDS_INSTALL_FROM_DMG_ERROR_TITLE);
386 NSString* error = l10n_util::GetNSStringFWithFixup(
387 IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
388 NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
390 NSAlert* alert = [[[NSAlert alloc] init] autorelease];
392 [alert setAlertStyle:NSWarningAlertStyle];
393 [alert setMessageText:title];
394 [alert setInformativeText:error];
395 [alert addButtonWithTitle:ok];
402 bool MaybeInstallFromDiskImage() {
403 base::mac::ScopedNSAutoreleasePool autorelease_pool;
405 std::string dmg_bsd_device_name;
406 if (!IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name)) {
410 NSArray* application_directories =
411 NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
414 if ([application_directories count] == 0) {
415 LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
416 << "no local application directories";
419 NSString* application_directory = [application_directories objectAtIndex:0];
421 NSFileManager* file_manager = [NSFileManager defaultManager];
424 if (![file_manager fileExistsAtPath:application_directory
425 isDirectory:&is_directory] ||
427 VLOG(1) << "No application directory at "
428 << [application_directory UTF8String];
432 NSString* source_path = [base::mac::OuterBundle() bundlePath];
433 NSString* application_name = [source_path lastPathComponent];
434 NSString* target_path =
435 [application_directory stringByAppendingPathComponent:application_name];
437 if ([file_manager fileExistsAtPath:target_path]) {
438 VLOG(1) << "Something already exists at " << [target_path UTF8String];
442 NSString* installer_path =
443 [base::mac::FrameworkBundle() pathForResource:@"install" ofType:@"sh"];
444 if (!installer_path) {
445 VLOG(1) << "Could not locate install.sh";
449 if (!ShouldInstallDialog()) {
453 base::mac::ScopedAuthorizationRef authorization(
454 MaybeShowAuthorizationDialog(application_directory));
455 // authorization will be NULL if it's deemed unnecessary or if
456 // authentication fails. In either case, try to install without privilege
459 if (!InstallFromDiskImage(authorization.release(),
467 dock::AddIcon(target_path, source_path);
469 if (dmg_bsd_device_name.empty()) {
470 // Not fatal, just diagnostic.
471 LOG(ERROR) << "Could not determine disk image BSD device name";
474 if (!LaunchInstalledApp(target_path, dmg_bsd_device_name)) {
484 // A simple scoper that calls DASessionScheduleWithRunLoop when created and
485 // DASessionUnscheduleFromRunLoop when destroyed.
486 class ScopedDASessionScheduleWithRunLoop {
488 ScopedDASessionScheduleWithRunLoop(DASessionRef session,
489 CFRunLoopRef run_loop,
490 CFStringRef run_loop_mode)
493 run_loop_mode_(run_loop_mode) {
494 DASessionScheduleWithRunLoop(session_, run_loop_, run_loop_mode_);
497 ~ScopedDASessionScheduleWithRunLoop() {
498 DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_);
502 DASessionRef session_;
503 CFRunLoopRef run_loop_;
504 CFStringRef run_loop_mode_;
506 DISALLOW_COPY_AND_ASSIGN(ScopedDASessionScheduleWithRunLoop);
509 // A small structure used to ferry data between SynchronousDAOperation and
510 // SynchronousDACallbackAdapter.
511 struct SynchronousDACallbackData {
513 SynchronousDACallbackData()
514 : callback_called(false),
515 run_loop_running(false) {
518 base::ScopedCFTypeRef<DADissenterRef> dissenter;
519 bool callback_called;
520 bool run_loop_running;
523 DISALLOW_COPY_AND_ASSIGN(SynchronousDACallbackData);
526 // The callback target for SynchronousDAOperation. Set the fields in
527 // SynchronousDACallbackData properly and then stops the run loop so that
528 // SynchronousDAOperation may proceed.
529 void SynchronousDACallbackAdapter(DADiskRef disk,
530 DADissenterRef dissenter,
532 SynchronousDACallbackData* callback_data =
533 static_cast<SynchronousDACallbackData*>(context);
534 callback_data->callback_called = true;
538 callback_data->dissenter.reset(dissenter);
541 // Only stop the run loop if SynchronousDAOperation started it. Don't stop
542 // anything if this callback was reached synchronously from DADiskUnmount or
544 if (callback_data->run_loop_running) {
545 CFRunLoopStop(CFRunLoopGetCurrent());
549 // Performs a DiskArbitration operation synchronously. After the operation is
550 // requested by SynchronousDADiskUnmount or SynchronousDADiskEject, those
551 // functions will call this one to run a run loop for a period of time,
552 // waiting for the callback to be called. When the callback is called, the
553 // run loop will be stopped, and this function will examine the result. If
554 // a dissenter prevented the operation from completing, or if the run loop
555 // timed out without the callback being called, this function will return
556 // false. When the callback completes successfully with no dissenters within
557 // the time allotted, this function returns true. This function requires that
558 // the DASession being used for the operation being performed has been added
559 // to the current run loop with DASessionScheduleWithRunLoop.
560 bool SynchronousDAOperation(const char* name,
561 SynchronousDACallbackData* callback_data) {
562 // The callback may already have been called synchronously. In that case,
563 // avoid spinning the run loop at all.
564 if (!callback_data->callback_called) {
565 const CFTimeInterval kOperationTimeoutSeconds = 15;
566 base::AutoReset<bool> running_reset(&callback_data->run_loop_running, true);
567 CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE);
570 if (!callback_data->callback_called) {
571 LOG(ERROR) << name << ": timed out";
573 } else if (callback_data->dissenter) {
574 CFStringRef status_string_cf =
575 DADissenterGetStatusString(callback_data->dissenter);
576 std::string status_string;
577 if (status_string_cf) {
578 status_string.assign(" ");
579 status_string.append(base::SysCFStringRefToUTF8(status_string_cf));
581 LOG(ERROR) << name << ": dissenter: "
582 << DADissenterGetStatus(callback_data->dissenter)
590 // Calls DADiskUnmount synchronously, returning the result.
591 bool SynchronousDADiskUnmount(DADiskRef disk, DADiskUnmountOptions options) {
592 SynchronousDACallbackData callback_data;
593 DADiskUnmount(disk, options, SynchronousDACallbackAdapter, &callback_data);
594 return SynchronousDAOperation("DADiskUnmount", &callback_data);
597 // Calls DADiskEject synchronously, returning the result.
598 bool SynchronousDADiskEject(DADiskRef disk, DADiskEjectOptions options) {
599 SynchronousDACallbackData callback_data;
600 DADiskEject(disk, options, SynchronousDACallbackAdapter, &callback_data);
601 return SynchronousDAOperation("DADiskEject", &callback_data);
606 void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) {
607 base::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL));
608 if (!session.get()) {
609 LOG(ERROR) << "DASessionCreate";
613 base::ScopedCFTypeRef<DADiskRef> disk(
614 DADiskCreateFromBSDName(NULL, session, dmg_bsd_device_name.c_str()));
616 LOG(ERROR) << "DADiskCreateFromBSDName";
620 // dmg_bsd_device_name may only refer to part of the disk: it may be a
621 // single filesystem on a larger disk. Use the "whole disk" object to
622 // be able to unmount all mounted filesystems from the disk image, and eject
623 // the image. This is harmless if dmg_bsd_device_name already referred to a
625 disk.reset(DADiskCopyWholeDisk(disk));
627 LOG(ERROR) << "DADiskCopyWholeDisk";
631 base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk));
633 LOG(ERROR) << "DADiskCopyIOMedia";
637 // Make sure the device is a disk image, and get the path to its disk image
639 std::string disk_image_path;
640 if (!MediaResidesOnDiskImage(media, &disk_image_path)) {
641 LOG(ERROR) << "MediaResidesOnDiskImage";
645 // SynchronousDADiskUnmount and SynchronousDADiskEject require that the
646 // session be scheduled with the current run loop.
647 ScopedDASessionScheduleWithRunLoop session_run_loop(session,
648 CFRunLoopGetCurrent(),
649 kCFRunLoopCommonModes);
651 if (!SynchronousDADiskUnmount(disk, kDADiskUnmountOptionWhole)) {
652 LOG(ERROR) << "SynchronousDADiskUnmount";
656 if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) {
657 LOG(ERROR) << "SynchronousDADiskEject";
661 char* disk_image_path_in_trash_c;
662 OSStatus status = FSPathMoveObjectToTrashSync(disk_image_path.c_str(),
663 &disk_image_path_in_trash_c,
664 kFSFileOperationDefaultOptions);
665 if (status != noErr) {
666 OSSTATUS_LOG(ERROR, status) << "FSPathMoveObjectToTrashSync";
670 // FSPathMoveObjectToTrashSync alone doesn't result in the Trash icon in the
671 // Dock indicating that any garbage has been placed within it. Using the
672 // trash path that FSPathMoveObjectToTrashSync claims to have used, call
673 // FNNotifyByPath to fatten up the icon.
674 base::FilePath disk_image_path_in_trash(disk_image_path_in_trash_c);
675 free(disk_image_path_in_trash_c);
677 base::FilePath trash_path = disk_image_path_in_trash.DirName();
678 const UInt8* trash_path_u8 = reinterpret_cast<const UInt8*>(
679 trash_path.value().c_str());
680 status = FNNotifyByPath(trash_path_u8,
681 kFNDirectoryModifiedMessage,
683 if (status != noErr) {
684 OSSTATUS_LOG(ERROR, status) << "FNNotifyByPath";