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/param.h>
17 #include <sys/mount.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/mac_logging.h"
27 #import "base/mac/mac_util.h"
28 #include "base/mac/scoped_authorizationref.h"
29 #include "base/mac/scoped_cftyperef.h"
30 #include "base/mac/scoped_ioobject.h"
31 #include "base/mac/scoped_nsautorelease_pool.h"
32 #include "base/strings/string_util.h"
33 #include "base/strings/sys_string_conversions.h"
34 #include "chrome/browser/mac/dock.h"
35 #import "chrome/browser/mac/keystone_glue.h"
36 #include "chrome/browser/mac/relauncher.h"
37 #include "chrome/common/chrome_constants.h"
38 #include "grit/chromium_strings.h"
39 #include "grit/generated_resources.h"
40 #include "ui/base/l10n/l10n_util.h"
41 #include "ui/base/l10n/l10n_util_mac.h"
43 // When C++ exceptions are disabled, the C++ library defines |try| and
44 // |catch| so as to allow exception-expecting C++ code to build properly when
45 // language support for exceptions is not present. These macros interfere
46 // with the use of |@try| and |@catch| in Objective-C files such as this one.
47 // Undefine these macros here, after everything has been #included, since
48 // there will be no C++ uses and only Objective-C uses from this point on.
54 // Given an io_service_t (expected to be of class IOMedia), walks the ancestor
55 // chain, returning the closest ancestor that implements class IOHDIXHDDrive,
56 // if any. If no such ancestor is found, returns NULL. Following the "copy"
57 // rule, the caller assumes ownership of the returned value.
59 // Note that this looks for a class that inherits from IOHDIXHDDrive, but it
60 // will not likely find a concrete IOHDIXHDDrive. It will be
61 // IOHDIXHDDriveOutKernel for disk images mounted "out-of-kernel" or
62 // IOHDIXHDDriveInKernel for disk images mounted "in-kernel." Out-of-kernel is
63 // the default as of Mac OS X 10.5. See the documentation for "hdiutil attach
64 // -kernel" for more information.
65 io_service_t CopyHDIXDriveServiceForMedia(io_service_t media) {
66 const char disk_image_class[] = "IOHDIXHDDrive";
68 // This is highly unlikely. media as passed in is expected to be of class
69 // IOMedia. Since the media service's entire ancestor chain will be checked,
70 // though, check it as well.
71 if (IOObjectConformsTo(media, disk_image_class)) {
72 IOObjectRetain(media);
76 io_iterator_t iterator_ref;
78 IORegistryEntryCreateIterator(media,
80 kIORegistryIterateRecursively |
81 kIORegistryIterateParents,
83 if (kr != KERN_SUCCESS) {
84 LOG(ERROR) << "IORegistryEntryCreateIterator: " << kr;
85 return IO_OBJECT_NULL;
87 base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
88 iterator_ref = IO_OBJECT_NULL;
90 // Look at each of the ancestor services, beginning with the parent,
91 // iterating all the way up to the device tree's root. If any ancestor
92 // service matches the class used for disk images, the media resides on a
93 // disk image, and the disk image file's path can be determined by examining
94 // the image-path property.
95 for (base::mac::ScopedIOObject<io_service_t> ancestor(
96 IOIteratorNext(iterator));
98 ancestor.reset(IOIteratorNext(iterator))) {
99 if (IOObjectConformsTo(ancestor, disk_image_class)) {
100 return ancestor.release();
104 // The media does not reside on a disk image.
105 return IO_OBJECT_NULL;
108 // Given an io_service_t (expected to be of class IOMedia), determines whether
109 // that service is on a disk image. If it is, returns true. If image_path is
110 // present, it will be set to the pathname of the disk image file, encoded in
111 // filesystem encoding.
112 bool MediaResidesOnDiskImage(io_service_t media, std::string* image_path) {
117 base::mac::ScopedIOObject<io_service_t> hdix_drive(
118 CopyHDIXDriveServiceForMedia(media));
124 base::ScopedCFTypeRef<CFTypeRef> image_path_cftyperef(
125 IORegistryEntryCreateCFProperty(
126 hdix_drive, CFSTR("image-path"), NULL, 0));
127 if (!image_path_cftyperef) {
128 LOG(ERROR) << "IORegistryEntryCreateCFProperty";
131 if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) {
132 base::ScopedCFTypeRef<CFStringRef> observed_type_cf(
133 CFCopyTypeIDDescription(CFGetTypeID(image_path_cftyperef)));
134 std::string observed_type;
135 if (observed_type_cf) {
136 observed_type.assign(", observed ");
137 observed_type.append(base::SysCFStringRefToUTF8(observed_type_cf));
139 LOG(ERROR) << "image-path: expected CFData, observed " << observed_type;
143 CFDataRef image_path_data = static_cast<CFDataRef>(
144 image_path_cftyperef.get());
145 CFIndex length = CFDataGetLength(image_path_data);
147 LOG(ERROR) << "image_path_data is unexpectedly empty";
150 char* image_path_c = WriteInto(image_path, length + 1);
151 CFDataGetBytes(image_path_data,
152 CFRangeMake(0, length),
153 reinterpret_cast<UInt8*>(image_path_c));
159 // Returns true if |path| is located on a read-only filesystem of a disk
160 // image. Returns false if not, or in the event of an error. If
161 // out_dmg_bsd_device_name is present, it will be set to the BSD device name
162 // for the disk image's device, in "diskNsM" form.
163 bool IsPathOnReadOnlyDiskImage(const char path[],
164 std::string* out_dmg_bsd_device_name) {
165 if (out_dmg_bsd_device_name) {
166 out_dmg_bsd_device_name->clear();
169 struct statfs statfs_buf;
170 if (statfs(path, &statfs_buf) != 0) {
171 PLOG(ERROR) << "statfs " << path;
175 if (!(statfs_buf.f_flags & MNT_RDONLY)) {
176 // Not on a read-only filesystem.
180 const char dev_root[] = "/dev/";
181 const int dev_root_length = arraysize(dev_root) - 1;
182 if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
183 // Not rooted at dev_root, no BSD name to search on.
187 // BSD names in IOKit don't include dev_root.
188 const char* dmg_bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
189 if (out_dmg_bsd_device_name) {
190 out_dmg_bsd_device_name->assign(dmg_bsd_device_name);
193 const mach_port_t master_port = kIOMasterPortDefault;
195 // IOBSDNameMatching gives ownership of match_dict to the caller, but
196 // IOServiceGetMatchingServices will assume that reference.
197 CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
199 dmg_bsd_device_name);
201 LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name;
205 io_iterator_t iterator_ref;
206 kern_return_t kr = IOServiceGetMatchingServices(master_port,
209 if (kr != KERN_SUCCESS) {
210 LOG(ERROR) << "IOServiceGetMatchingServices: " << kr;
213 base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
214 iterator_ref = IO_OBJECT_NULL;
216 // There needs to be exactly one matching service.
217 base::mac::ScopedIOObject<io_service_t> media(IOIteratorNext(iterator));
219 LOG(ERROR) << "IOIteratorNext: no service";
222 base::mac::ScopedIOObject<io_service_t> unexpected_service(
223 IOIteratorNext(iterator));
224 if (unexpected_service) {
225 LOG(ERROR) << "IOIteratorNext: too many services";
231 return MediaResidesOnDiskImage(media, NULL);
234 // Returns true if the application is located on a read-only filesystem of a
235 // disk image. Returns false if not, or in the event of an error. If
236 // dmg_bsd_device_name is present, it will be set to the BSD device name for
237 // the disk image's device, in "diskNsM" form.
238 bool IsAppRunningFromReadOnlyDiskImage(std::string* dmg_bsd_device_name) {
239 return IsPathOnReadOnlyDiskImage(
240 [[base::mac::OuterBundle() bundlePath] fileSystemRepresentation],
241 dmg_bsd_device_name);
244 // Shows a dialog asking the user whether or not to install from the disk
245 // image. Returns true if the user approves installation.
246 bool ShouldInstallDialog() {
247 NSString* title = l10n_util::GetNSStringFWithFixup(
248 IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
249 NSString* prompt = l10n_util::GetNSStringFWithFixup(
250 IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
251 NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
252 NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
254 NSAlert* alert = [[[NSAlert alloc] init] autorelease];
256 [alert setAlertStyle:NSInformationalAlertStyle];
257 [alert setMessageText:title];
258 [alert setInformativeText:prompt];
259 [alert addButtonWithTitle:yes];
260 NSButton* cancel_button = [alert addButtonWithTitle:no];
261 [cancel_button setKeyEquivalent:@"\e"];
263 NSInteger result = [alert runModal];
265 return result == NSAlertFirstButtonReturn;
268 // Potentially shows an authorization dialog to request authentication to
269 // copy. If application_directory appears to be unwritable, attempts to
270 // obtain authorization, which may result in the display of the dialog.
271 // Returns NULL if authorization is not performed because it does not appear
272 // to be necessary because the user has permission to write to
273 // application_directory. Returns NULL if authorization fails.
274 AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
275 NSFileManager* file_manager = [NSFileManager defaultManager];
276 if ([file_manager isWritableFileAtPath:application_directory]) {
280 NSString* prompt = l10n_util::GetNSStringFWithFixup(
281 IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
282 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
283 return base::mac::AuthorizationCreateToRunAsRoot(
284 base::mac::NSToCFCast(prompt));
287 // Invokes the installer program at installer_path to copy source_path to
288 // target_path and perform any additional on-disk bookkeeping needed to be
289 // able to launch target_path properly. If authorization_arg is non-NULL,
290 // function will assume ownership of it, will invoke the installer with that
291 // authorization reference, and will attempt Keystone ticket promotion.
292 bool InstallFromDiskImage(AuthorizationRef authorization_arg,
293 NSString* installer_path,
294 NSString* source_path,
295 NSString* target_path) {
296 base::mac::ScopedAuthorizationRef authorization(authorization_arg);
297 authorization_arg = NULL;
300 const char* installer_path_c = [installer_path fileSystemRepresentation];
301 const char* source_path_c = [source_path fileSystemRepresentation];
302 const char* target_path_c = [target_path fileSystemRepresentation];
303 const char* arguments[] = {source_path_c, target_path_c, NULL};
305 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
308 kAuthorizationFlagDefaults,
312 if (status != errAuthorizationSuccess) {
313 OSSTATUS_LOG(ERROR, status)
314 << "AuthorizationExecuteWithPrivileges install";
318 NSArray* arguments = [NSArray arrayWithObjects:source_path,
324 task = [NSTask launchedTaskWithLaunchPath:installer_path
325 arguments:arguments];
326 } @catch(NSException* exception) {
327 LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
328 << [[exception description] UTF8String];
332 [task waitUntilExit];
333 exit_status = [task terminationStatus];
336 if (exit_status != 0) {
337 LOG(ERROR) << "install.sh: exit status " << exit_status;
342 // As long as an AuthorizationRef is available, promote the Keystone
343 // ticket. Inform KeystoneGlue of the new path to use.
344 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
345 [keystone_glue setAppPath:target_path];
346 [keystone_glue promoteTicketWithAuthorization:authorization.release()
353 // Launches the application at installed_path. The helper application
354 // contained within install_path will be used for the relauncher process. This
355 // keeps Launch Services from ever having to see or think about the helper
356 // application on the disk image. The relauncher process will be asked to
357 // call EjectAndTrashDiskImage on dmg_bsd_device_name.
358 bool LaunchInstalledApp(NSString* installed_path,
359 const std::string& dmg_bsd_device_name) {
360 base::FilePath browser_path([installed_path fileSystemRepresentation]);
362 base::FilePath helper_path = browser_path.Append("Contents/Versions");
363 helper_path = helper_path.Append(chrome::kChromeVersion);
364 helper_path = helper_path.Append(chrome::kHelperProcessExecutablePath);
366 std::vector<std::string> args =
367 CommandLine::ForCurrentProcess()->argv();
368 args[0] = browser_path.value();
370 std::vector<std::string> relauncher_args;
371 if (!dmg_bsd_device_name.empty()) {
372 std::string dmg_arg(mac_relauncher::kRelauncherDMGDeviceArg);
373 dmg_arg.append(dmg_bsd_device_name);
374 relauncher_args.push_back(dmg_arg);
377 return mac_relauncher::RelaunchAppWithHelper(helper_path.value(),
382 void ShowErrorDialog() {
383 NSString* title = l10n_util::GetNSStringWithFixup(
384 IDS_INSTALL_FROM_DMG_ERROR_TITLE);
385 NSString* error = l10n_util::GetNSStringFWithFixup(
386 IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
387 NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
389 NSAlert* alert = [[[NSAlert alloc] init] autorelease];
391 [alert setAlertStyle:NSWarningAlertStyle];
392 [alert setMessageText:title];
393 [alert setInformativeText:error];
394 [alert addButtonWithTitle:ok];
401 bool MaybeInstallFromDiskImage() {
402 base::mac::ScopedNSAutoreleasePool autorelease_pool;
404 std::string dmg_bsd_device_name;
405 if (!IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name)) {
409 NSArray* application_directories =
410 NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
413 if ([application_directories count] == 0) {
414 LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
415 << "no local application directories";
418 NSString* application_directory = [application_directories objectAtIndex:0];
420 NSFileManager* file_manager = [NSFileManager defaultManager];
423 if (![file_manager fileExistsAtPath:application_directory
424 isDirectory:&is_directory] ||
426 VLOG(1) << "No application directory at "
427 << [application_directory UTF8String];
431 NSString* source_path = [base::mac::OuterBundle() bundlePath];
432 NSString* application_name = [source_path lastPathComponent];
433 NSString* target_path =
434 [application_directory stringByAppendingPathComponent:application_name];
436 if ([file_manager fileExistsAtPath:target_path]) {
437 VLOG(1) << "Something already exists at " << [target_path UTF8String];
441 NSString* installer_path =
442 [base::mac::FrameworkBundle() pathForResource:@"install" ofType:@"sh"];
443 if (!installer_path) {
444 VLOG(1) << "Could not locate install.sh";
448 if (!ShouldInstallDialog()) {
452 base::mac::ScopedAuthorizationRef authorization(
453 MaybeShowAuthorizationDialog(application_directory));
454 // authorization will be NULL if it's deemed unnecessary or if
455 // authentication fails. In either case, try to install without privilege
458 if (!InstallFromDiskImage(authorization.release(),
466 dock::AddIcon(target_path, source_path);
468 if (dmg_bsd_device_name.empty()) {
469 // Not fatal, just diagnostic.
470 LOG(ERROR) << "Could not determine disk image BSD device name";
473 if (!LaunchInstalledApp(target_path, dmg_bsd_device_name)) {
483 // A simple scoper that calls DASessionScheduleWithRunLoop when created and
484 // DASessionUnscheduleFromRunLoop when destroyed.
485 class ScopedDASessionScheduleWithRunLoop {
487 ScopedDASessionScheduleWithRunLoop(DASessionRef session,
488 CFRunLoopRef run_loop,
489 CFStringRef run_loop_mode)
492 run_loop_mode_(run_loop_mode) {
493 DASessionScheduleWithRunLoop(session_, run_loop_, run_loop_mode_);
496 ~ScopedDASessionScheduleWithRunLoop() {
497 DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_);
501 DASessionRef session_;
502 CFRunLoopRef run_loop_;
503 CFStringRef run_loop_mode_;
505 DISALLOW_COPY_AND_ASSIGN(ScopedDASessionScheduleWithRunLoop);
508 // A small structure used to ferry data between SynchronousDAOperation and
509 // SynchronousDACallbackAdapter.
510 struct SynchronousDACallbackData {
512 SynchronousDACallbackData()
513 : callback_called(false),
514 run_loop_running(false) {
517 base::ScopedCFTypeRef<DADissenterRef> dissenter;
518 bool callback_called;
519 bool run_loop_running;
522 DISALLOW_COPY_AND_ASSIGN(SynchronousDACallbackData);
525 // The callback target for SynchronousDAOperation. Set the fields in
526 // SynchronousDACallbackData properly and then stops the run loop so that
527 // SynchronousDAOperation may proceed.
528 void SynchronousDACallbackAdapter(DADiskRef disk,
529 DADissenterRef dissenter,
531 SynchronousDACallbackData* callback_data =
532 static_cast<SynchronousDACallbackData*>(context);
533 callback_data->callback_called = true;
537 callback_data->dissenter.reset(dissenter);
540 // Only stop the run loop if SynchronousDAOperation started it. Don't stop
541 // anything if this callback was reached synchronously from DADiskUnmount or
543 if (callback_data->run_loop_running) {
544 CFRunLoopStop(CFRunLoopGetCurrent());
548 // Performs a DiskArbitration operation synchronously. After the operation is
549 // requested by SynchronousDADiskUnmount or SynchronousDADiskEject, those
550 // functions will call this one to run a run loop for a period of time,
551 // waiting for the callback to be called. When the callback is called, the
552 // run loop will be stopped, and this function will examine the result. If
553 // a dissenter prevented the operation from completing, or if the run loop
554 // timed out without the callback being called, this function will return
555 // false. When the callback completes successfully with no dissenters within
556 // the time allotted, this function returns true. This function requires that
557 // the DASession being used for the operation being performed has been added
558 // to the current run loop with DASessionScheduleWithRunLoop.
559 bool SynchronousDAOperation(const char* name,
560 SynchronousDACallbackData* callback_data) {
561 // The callback may already have been called synchronously. In that case,
562 // avoid spinning the run loop at all.
563 if (!callback_data->callback_called) {
564 const CFTimeInterval kOperationTimeoutSeconds = 15;
565 base::AutoReset<bool> running_reset(&callback_data->run_loop_running, true);
566 CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE);
569 if (!callback_data->callback_called) {
570 LOG(ERROR) << name << ": timed out";
572 } else if (callback_data->dissenter) {
573 CFStringRef status_string_cf =
574 DADissenterGetStatusString(callback_data->dissenter);
575 std::string status_string;
576 if (status_string_cf) {
577 status_string.assign(" ");
578 status_string.append(base::SysCFStringRefToUTF8(status_string_cf));
580 LOG(ERROR) << name << ": dissenter: "
581 << DADissenterGetStatus(callback_data->dissenter)
589 // Calls DADiskUnmount synchronously, returning the result.
590 bool SynchronousDADiskUnmount(DADiskRef disk, DADiskUnmountOptions options) {
591 SynchronousDACallbackData callback_data;
592 DADiskUnmount(disk, options, SynchronousDACallbackAdapter, &callback_data);
593 return SynchronousDAOperation("DADiskUnmount", &callback_data);
596 // Calls DADiskEject synchronously, returning the result.
597 bool SynchronousDADiskEject(DADiskRef disk, DADiskEjectOptions options) {
598 SynchronousDACallbackData callback_data;
599 DADiskEject(disk, options, SynchronousDACallbackAdapter, &callback_data);
600 return SynchronousDAOperation("DADiskEject", &callback_data);
605 void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) {
606 base::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL));
607 if (!session.get()) {
608 LOG(ERROR) << "DASessionCreate";
612 base::ScopedCFTypeRef<DADiskRef> disk(
613 DADiskCreateFromBSDName(NULL, session, dmg_bsd_device_name.c_str()));
615 LOG(ERROR) << "DADiskCreateFromBSDName";
619 // dmg_bsd_device_name may only refer to part of the disk: it may be a
620 // single filesystem on a larger disk. Use the "whole disk" object to
621 // be able to unmount all mounted filesystems from the disk image, and eject
622 // the image. This is harmless if dmg_bsd_device_name already referred to a
624 disk.reset(DADiskCopyWholeDisk(disk));
626 LOG(ERROR) << "DADiskCopyWholeDisk";
630 base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk));
632 LOG(ERROR) << "DADiskCopyIOMedia";
636 // Make sure the device is a disk image, and get the path to its disk image
638 std::string disk_image_path;
639 if (!MediaResidesOnDiskImage(media, &disk_image_path)) {
640 LOG(ERROR) << "MediaResidesOnDiskImage";
644 // SynchronousDADiskUnmount and SynchronousDADiskEject require that the
645 // session be scheduled with the current run loop.
646 ScopedDASessionScheduleWithRunLoop session_run_loop(session,
647 CFRunLoopGetCurrent(),
648 kCFRunLoopCommonModes);
650 if (!SynchronousDADiskUnmount(disk, kDADiskUnmountOptionWhole)) {
651 LOG(ERROR) << "SynchronousDADiskUnmount";
655 if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) {
656 LOG(ERROR) << "SynchronousDADiskEject";
660 char* disk_image_path_in_trash_c;
661 OSStatus status = FSPathMoveObjectToTrashSync(disk_image_path.c_str(),
662 &disk_image_path_in_trash_c,
663 kFSFileOperationDefaultOptions);
664 if (status != noErr) {
665 OSSTATUS_LOG(ERROR, status) << "FSPathMoveObjectToTrashSync";
669 // FSPathMoveObjectToTrashSync alone doesn't result in the Trash icon in the
670 // Dock indicating that any garbage has been placed within it. Using the
671 // trash path that FSPathMoveObjectToTrashSync claims to have used, call
672 // FNNotifyByPath to fatten up the icon.
673 base::FilePath disk_image_path_in_trash(disk_image_path_in_trash_c);
674 free(disk_image_path_in_trash_c);
676 base::FilePath trash_path = disk_image_path_in_trash.DirName();
677 const UInt8* trash_path_u8 = reinterpret_cast<const UInt8*>(
678 trash_path.value().c_str());
679 status = FNNotifyByPath(trash_path_u8,
680 kFNDirectoryModifiedMessage,
682 if (status != noErr) {
683 OSSTATUS_LOG(ERROR, status) << "FNNotifyByPath";