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.
4
5#include "chrome/browser/mac/install_from_dmg.h"
6
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>
13#include <signal.h>
14#include <stdlib.h>
15#include <string.h>
16#include <sys/mount.h>
17#include <sys/param.h>
18#include <unistd.h>
19
20#include "base/auto_reset.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/macros.h"
33#include "base/stl_util.h"
34#include "base/strings/string_util.h"
35#include "base/strings/stringprintf.h"
36#include "base/strings/sys_string_conversions.h"
37#import "chrome/browser/mac/dock.h"
38#import "chrome/browser/mac/keystone_glue.h"
39#include "chrome/browser/mac/relauncher.h"
40#include "chrome/common/chrome_constants.h"
41#include "chrome/common/chrome_switches.h"
42#include "chrome/grit/chromium_strings.h"
43#include "chrome/grit/generated_resources.h"
44#include "components/strings/grit/components_strings.h"
45#include "ui/base/l10n/l10n_util.h"
46#include "ui/base/l10n/l10n_util_mac.h"
47
48// When C++ exceptions are disabled, the C++ library defines |try| and
49// |catch| so as to allow exception-expecting C++ code to build properly when
50// language support for exceptions is not present.  These macros interfere
51// with the use of |@try| and |@catch| in Objective-C files such as this one.
52// Undefine these macros here, after everything has been #included, since
53// there will be no C++ uses and only Objective-C uses from this point on.
54#undef try
55#undef catch
56
57namespace {
58
59// Given an io_service_t (expected to be of class IOMedia), walks the ancestor
60// chain, returning the closest ancestor that implements class IOHDIXHDDrive,
61// if any. If no such ancestor is found, returns NULL. Following the "copy"
62// rule, the caller assumes ownership of the returned value.
63//
64// Note that this looks for a class that inherits from IOHDIXHDDrive, but it
65// will not likely find a concrete IOHDIXHDDrive. It will be
66// IOHDIXHDDriveOutKernel for disk images mounted "out-of-kernel" or
67// IOHDIXHDDriveInKernel for disk images mounted "in-kernel." Out-of-kernel is
68// the default as of Mac OS X 10.5. See the documentation for "hdiutil attach
69// -kernel" for more information.
70io_service_t CopyHDIXDriveServiceForMedia(io_service_t media) {
71  const char disk_image_class[] = "IOHDIXHDDrive";
72
73  // This is highly unlikely. media as passed in is expected to be of class
74  // IOMedia. Since the media service's entire ancestor chain will be checked,
75  // though, check it as well.
76  if (IOObjectConformsTo(media, disk_image_class)) {
77    IOObjectRetain(media);
78    return media;
79  }
80
81  io_iterator_t iterator_ref;
82  kern_return_t kr =
83      IORegistryEntryCreateIterator(media,
84                                    kIOServicePlane,
85                                    kIORegistryIterateRecursively |
86                                        kIORegistryIterateParents,
87                                    &iterator_ref);
88  if (kr != KERN_SUCCESS) {
89    MACH_LOG(ERROR, kr) << "IORegistryEntryCreateIterator";
90    return IO_OBJECT_NULL;
91  }
92  base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
93  iterator_ref = IO_OBJECT_NULL;
94
95  // Look at each of the ancestor services, beginning with the parent,
96  // iterating all the way up to the device tree's root. If any ancestor
97  // service matches the class used for disk images, the media resides on a
98  // disk image, and the disk image file's path can be determined by examining
99  // the image-path property.
100  for (base::mac::ScopedIOObject<io_service_t> ancestor(
101           IOIteratorNext(iterator));
102       ancestor;
103       ancestor.reset(IOIteratorNext(iterator))) {
104    if (IOObjectConformsTo(ancestor, disk_image_class)) {
105      return ancestor.release();
106    }
107  }
108
109  // The media does not reside on a disk image.
110  return IO_OBJECT_NULL;
111}
112
113// Given an io_service_t (expected to be of class IOMedia), determines whether
114// that service is on a disk image. If it is, returns true. If image_path is
115// present, it will be set to the pathname of the disk image file, encoded in
116// filesystem encoding.
117bool MediaResidesOnDiskImage(io_service_t media, std::string* image_path) {
118  if (image_path) {
119    image_path->clear();
120  }
121
122  base::mac::ScopedIOObject<io_service_t> hdix_drive(
123      CopyHDIXDriveServiceForMedia(media));
124  if (!hdix_drive) {
125    return false;
126  }
127
128  if (image_path) {
129    base::ScopedCFTypeRef<CFTypeRef> image_path_cftyperef(
130        IORegistryEntryCreateCFProperty(
131            hdix_drive, CFSTR("image-path"), NULL, 0));
132    if (!image_path_cftyperef) {
133      LOG(ERROR) << "IORegistryEntryCreateCFProperty";
134      return true;
135    }
136    if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) {
137      base::ScopedCFTypeRef<CFStringRef> observed_type_cf(
138          CFCopyTypeIDDescription(CFGetTypeID(image_path_cftyperef)));
139      std::string observed_type;
140      if (observed_type_cf) {
141        observed_type.assign(", observed ");
142        observed_type.append(base::SysCFStringRefToUTF8(observed_type_cf));
143      }
144      LOG(ERROR) << "image-path: expected CFData, observed " << observed_type;
145      return true;
146    }
147
148    CFDataRef image_path_data = static_cast<CFDataRef>(
149        image_path_cftyperef.get());
150    CFIndex length = CFDataGetLength(image_path_data);
151    if (length <= 0) {
152      LOG(ERROR) << "image_path_data is unexpectedly empty";
153      return true;
154    }
155    char* image_path_c = base::WriteInto(image_path, length + 1);
156    CFDataGetBytes(image_path_data,
157                   CFRangeMake(0, length),
158                   reinterpret_cast<UInt8*>(image_path_c));
159  }
160
161  return true;
162}
163
164// Returns true if |path| is located on a read-only filesystem of a disk
165// image. Returns false if not, or in the event of an error. If
166// out_dmg_bsd_device_name is present, it will be set to the BSD device name
167// for the disk image's device, in "diskNsM" form.
168DiskImageStatus IsPathOnReadOnlyDiskImage(
169    const char path[],
170    std::string* out_dmg_bsd_device_name) {
171  if (out_dmg_bsd_device_name) {
172    out_dmg_bsd_device_name->clear();
173  }
174
175  struct statfs statfs_buf;
176  if (statfs(path, &statfs_buf) != 0) {
177    PLOG(ERROR) << "statfs " << path;
178    return DiskImageStatusFailure;
179  }
180
181  if (!(statfs_buf.f_flags & MNT_RDONLY)) {
182    // Not on a read-only filesystem.
183    return DiskImageStatusFalse;
184  }
185
186  const char dev_root[] = "/dev/";
187  const int dev_root_length = base::size(dev_root) - 1;
188  if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
189    // Not rooted at dev_root, no BSD name to search on.
190    return DiskImageStatusFalse;
191  }
192
193  // BSD names in IOKit don't include dev_root.
194  const char* dmg_bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
195  if (out_dmg_bsd_device_name) {
196    out_dmg_bsd_device_name->assign(dmg_bsd_device_name);
197  }
198
199  const mach_port_t master_port = kIOMasterPortDefault;
200
201  // IOBSDNameMatching gives ownership of match_dict to the caller, but
202  // IOServiceGetMatchingServices will assume that reference.
203  CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
204                                                        0,
205                                                        dmg_bsd_device_name);
206  if (!match_dict) {
207    LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name;
208    return DiskImageStatusFailure;
209  }
210
211  io_iterator_t iterator_ref;
212  kern_return_t kr = IOServiceGetMatchingServices(master_port,
213                                                  match_dict,
214                                                  &iterator_ref);
215  if (kr != KERN_SUCCESS) {
216    MACH_LOG(ERROR, kr) << "IOServiceGetMatchingServices";
217    return DiskImageStatusFailure;
218  }
219  base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
220  iterator_ref = IO_OBJECT_NULL;
221
222  // There needs to be exactly one matching service.
223  base::mac::ScopedIOObject<io_service_t> media(IOIteratorNext(iterator));
224  if (!media) {
225    LOG(ERROR) << "IOIteratorNext: no service";
226    return DiskImageStatusFailure;
227  }
228  base::mac::ScopedIOObject<io_service_t> unexpected_service(
229      IOIteratorNext(iterator));
230  if (unexpected_service) {
231    LOG(ERROR) << "IOIteratorNext: too many services";
232    return DiskImageStatusFailure;
233  }
234
235  iterator.reset();
236
237  return MediaResidesOnDiskImage(media, NULL) ? DiskImageStatusTrue
238                                              : DiskImageStatusFalse;
239}
240
241// Shows a dialog asking the user whether or not to install from the disk
242// image.  Returns true if the user approves installation.
243bool ShouldInstallDialog() {
244  NSString* title = l10n_util::GetNSStringFWithFixup(
245      IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
246  NSString* prompt = l10n_util::GetNSStringFWithFixup(
247      IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
248  NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
249  NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
250
251  NSAlert* alert = [[[NSAlert alloc] init] autorelease];
252
253  [alert setAlertStyle:NSInformationalAlertStyle];
254  [alert setMessageText:title];
255  [alert setInformativeText:prompt];
256  [alert addButtonWithTitle:yes];
257  NSButton* cancel_button = [alert addButtonWithTitle:no];
258  [cancel_button setKeyEquivalent:@"\e"];
259
260  NSInteger result = [alert runModal];
261
262  return result == NSAlertFirstButtonReturn;
263}
264
265// Potentially shows an authorization dialog to request authentication to
266// copy.  If application_directory appears to be unwritable, attempts to
267// obtain authorization, which may result in the display of the dialog.
268// Returns NULL if authorization is not performed because it does not appear
269// to be necessary because the user has permission to write to
270// application_directory.  Returns NULL if authorization fails.
271AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
272  NSFileManager* file_manager = [NSFileManager defaultManager];
273  if ([file_manager isWritableFileAtPath:application_directory]) {
274    return NULL;
275  }
276
277  NSString* prompt = l10n_util::GetNSStringFWithFixup(
278      IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
279      l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
280  return base::mac::AuthorizationCreateToRunAsRoot(
281      base::mac::NSToCFCast(prompt));
282}
283
284// Invokes the installer program at installer_path to copy source_path to
285// target_path and perform any additional on-disk bookkeeping needed to be
286// able to launch target_path properly.  If authorization_arg is non-NULL,
287// function will assume ownership of it, will invoke the installer with that
288// authorization reference, and will attempt Keystone ticket promotion.
289bool InstallFromDiskImage(AuthorizationRef authorization_arg,
290                          NSString* installer_path,
291                          NSString* source_path,
292                          NSString* target_path) {
293  base::mac::ScopedAuthorizationRef authorization(authorization_arg);
294  authorization_arg = NULL;
295  int exit_status;
296  if (authorization) {
297    const char* installer_path_c = [installer_path fileSystemRepresentation];
298    const char* source_path_c = [source_path fileSystemRepresentation];
299    const char* target_path_c = [target_path fileSystemRepresentation];
300    const char* arguments[] = {source_path_c, target_path_c, NULL};
301
302    OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
303        authorization,
304        installer_path_c,
305        kAuthorizationFlagDefaults,
306        arguments,
307        NULL,  // pipe
308        &exit_status);
309    if (status != errAuthorizationSuccess) {
310      OSSTATUS_LOG(ERROR, status)
311          << "AuthorizationExecuteWithPrivileges install";
312      return false;
313    }
314  } else {
315    NSArray* arguments = @[ source_path, target_path ];
316
317    NSTask* task;
318    @try {
319      task = [NSTask launchedTaskWithLaunchPath:installer_path
320                                      arguments:arguments];
321    } @catch(NSException* exception) {
322      LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
323                 << [[exception description] UTF8String];
324      return false;
325    }
326
327    [task waitUntilExit];
328    exit_status = [task terminationStatus];
329  }
330
331  if (exit_status != 0) {
332    LOG(ERROR) << "install.sh: exit status " << exit_status;
333    return false;
334  }
335
336  if (authorization) {
337    // As long as an AuthorizationRef is available, promote the Keystone
338    // ticket.  Inform KeystoneGlue of the new path to use.
339    KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
340    [keystone_glue setAppPath:target_path];
341    [keystone_glue promoteTicketWithAuthorization:authorization.release()
342                                      synchronous:YES];
343  }
344
345  return true;
346}
347
348// Launches the application at installed_path. The helper application
349// contained within install_path will be used for the relauncher process. This
350// keeps Launch Services from ever having to see or think about the helper
351// application on the disk image. The relauncher process will be asked to
352// call EjectAndTrashDiskImage on dmg_bsd_device_name.
353bool LaunchInstalledApp(NSString* installed_path,
354                        const std::string& dmg_bsd_device_name) {
355  base::FilePath browser_path([installed_path fileSystemRepresentation]);
356
357  base::FilePath helper_path = browser_path.Append("Contents/Frameworks");
358  helper_path = helper_path.Append(chrome::kFrameworkName);
359  helper_path = helper_path.Append("Versions");
360  helper_path = helper_path.Append(chrome::kChromeVersion);
361  helper_path = helper_path.Append("Helpers");
362  helper_path = helper_path.Append(chrome::kHelperProcessExecutablePath);
363
364  std::vector<std::string> args =
365      base::CommandLine::ForCurrentProcess()->argv();
366  args[0] = browser_path.value();
367
368  std::vector<std::string> relauncher_args;
369  if (!dmg_bsd_device_name.empty()) {
370    std::string dmg_arg =
371        base::StringPrintf("--%s=%s",
372                           switches::kRelauncherProcessDMGDevice,
373                           dmg_bsd_device_name.c_str());
374    relauncher_args.push_back(dmg_arg);
375  }
376
377  return mac_relauncher::RelaunchAppWithHelper(helper_path.value(),
378                                               relauncher_args,
379                                               args);
380}
381
382void 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);
388
389  NSAlert* alert = [[[NSAlert alloc] init] autorelease];
390
391  [alert setAlertStyle:NSWarningAlertStyle];
392  [alert setMessageText:title];
393  [alert setInformativeText:error];
394  [alert addButtonWithTitle:ok];
395
396  [alert runModal];
397}
398
399}  // namespace
400
401DiskImageStatus IsAppRunningFromReadOnlyDiskImage(
402    std::string* dmg_bsd_device_name) {
403  return IsPathOnReadOnlyDiskImage(
404      [[base::mac::OuterBundle() bundlePath] fileSystemRepresentation],
405      dmg_bsd_device_name);
406}
407
408bool MaybeInstallFromDiskImage() {
409  @autoreleasepool {
410    std::string dmg_bsd_device_name;
411    if (IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name) !=
412        DiskImageStatusTrue) {
413      return false;
414    }
415
416    NSArray* application_directories = NSSearchPathForDirectoriesInDomains(
417        NSApplicationDirectory, NSLocalDomainMask, YES);
418    if ([application_directories count] == 0) {
419      LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
420                 << "no local application directories";
421      return false;
422    }
423    NSString* application_directory = application_directories[0];
424
425    NSFileManager* file_manager = [NSFileManager defaultManager];
426
427    BOOL is_directory;
428    if (![file_manager fileExistsAtPath:application_directory
429                            isDirectory:&is_directory] ||
430        !is_directory) {
431      VLOG(1) << "No application directory at "
432              << [application_directory UTF8String];
433      return false;
434    }
435
436    NSString* source_path = [base::mac::OuterBundle() bundlePath];
437    NSString* application_name = [source_path lastPathComponent];
438    NSString* target_path =
439        [application_directory stringByAppendingPathComponent:application_name];
440
441    if ([file_manager fileExistsAtPath:target_path]) {
442      VLOG(1) << "Something already exists at " << [target_path UTF8String];
443      return false;
444    }
445
446    NSString* installer_path =
447        [base::mac::FrameworkBundle() pathForResource:@"install" ofType:@"sh"];
448    if (!installer_path) {
449      VLOG(1) << "Could not locate install.sh";
450      return false;
451    }
452
453    if (!ShouldInstallDialog()) {
454      return false;
455    }
456
457    base::mac::ScopedAuthorizationRef authorization(
458        MaybeShowAuthorizationDialog(application_directory));
459    // authorization will be NULL if it's deemed unnecessary or if
460    // authentication fails.  In either case, try to install without privilege
461    // escalation.
462
463    if (!InstallFromDiskImage(authorization.release(), installer_path,
464                              source_path, target_path)) {
465      ShowErrorDialog();
466      return false;
467    }
468
469    dock::AddIcon(target_path, source_path);
470
471    if (dmg_bsd_device_name.empty()) {
472      // Not fatal, just diagnostic.
473      LOG(ERROR) << "Could not determine disk image BSD device name";
474    }
475
476    if (!LaunchInstalledApp(target_path, dmg_bsd_device_name)) {
477      ShowErrorDialog();
478      return false;
479    }
480
481    return true;
482  }
483}
484
485namespace {
486
487// A simple scoper that calls DASessionScheduleWithRunLoop when created and
488// DASessionUnscheduleFromRunLoop when destroyed.
489class ScopedDASessionScheduleWithRunLoop {
490 public:
491  ScopedDASessionScheduleWithRunLoop(DASessionRef session,
492                                     CFRunLoopRef run_loop,
493                                     CFStringRef run_loop_mode)
494      : session_(session),
495        run_loop_(run_loop),
496        run_loop_mode_(run_loop_mode) {
497    DASessionScheduleWithRunLoop(session_, run_loop_, run_loop_mode_);
498  }
499
500  ~ScopedDASessionScheduleWithRunLoop() {
501    DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_);
502  }
503
504 private:
505  DASessionRef session_;
506  CFRunLoopRef run_loop_;
507  CFStringRef run_loop_mode_;
508
509  DISALLOW_COPY_AND_ASSIGN(ScopedDASessionScheduleWithRunLoop);
510};
511
512// A small structure used to ferry data between SynchronousDAOperation and
513// SynchronousDACallbackAdapter.
514struct SynchronousDACallbackData {
515 public:
516  SynchronousDACallbackData()
517      : callback_called(false),
518        run_loop_running(false),
519        can_log(true) {
520  }
521
522  base::ScopedCFTypeRef<DADissenterRef> dissenter;
523  bool callback_called;
524  bool run_loop_running;
525  bool can_log;
526
527 private:
528  DISALLOW_COPY_AND_ASSIGN(SynchronousDACallbackData);
529};
530
531// The callback target for SynchronousDAOperation. Set the fields in
532// SynchronousDACallbackData properly and then stops the run loop so that
533// SynchronousDAOperation may proceed.
534void SynchronousDACallbackAdapter(DADiskRef disk,
535                                  DADissenterRef dissenter,
536                                  void* context) {
537  SynchronousDACallbackData* callback_data =
538      static_cast<SynchronousDACallbackData*>(context);
539  callback_data->callback_called = true;
540
541  if (dissenter) {
542    CFRetain(dissenter);
543    callback_data->dissenter.reset(dissenter);
544  }
545
546  // Only stop the run loop if SynchronousDAOperation started it. Don't stop
547  // anything if this callback was reached synchronously from DADiskUnmount or
548  // DADiskEject.
549  if (callback_data->run_loop_running) {
550    CFRunLoopStop(CFRunLoopGetCurrent());
551  }
552}
553
554// Performs a DiskArbitration operation synchronously. After the operation is
555// requested by SynchronousDADiskUnmount or SynchronousDADiskEject, those
556// functions will call this one to run a run loop for a period of time,
557// waiting for the callback to be called. When the callback is called, the
558// run loop will be stopped, and this function will examine the result. If
559// a dissenter prevented the operation from completing, or if the run loop
560// timed out without the callback being called, this function will return
561// false. When the callback completes successfully with no dissenters within
562// the time allotted, this function returns true. This function requires that
563// the DASession being used for the operation being performed has been added
564// to the current run loop with DASessionScheduleWithRunLoop.
565bool SynchronousDAOperation(const char* name,
566                            SynchronousDACallbackData* callback_data) {
567  // The callback may already have been called synchronously. In that case,
568  // avoid spinning the run loop at all.
569  if (!callback_data->callback_called) {
570    const CFTimeInterval kOperationTimeoutSeconds = 15;
571    base::AutoReset<bool> running_reset(&callback_data->run_loop_running, true);
572    CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE);
573  }
574
575  if (!callback_data->callback_called) {
576    LOG_IF(ERROR, callback_data->can_log) << name << ": timed out";
577    return false;
578  } else if (callback_data->dissenter) {
579    if (callback_data->can_log) {
580      CFStringRef status_string_cf =
581          DADissenterGetStatusString(callback_data->dissenter);
582      std::string status_string;
583      if (status_string_cf) {
584        status_string.assign(" ");
585        status_string.append(base::SysCFStringRefToUTF8(status_string_cf));
586      }
587      LOG(ERROR) << name << ": dissenter: "
588                 << DADissenterGetStatus(callback_data->dissenter)
589                 << status_string;
590    }
591    return false;
592  }
593
594  return true;
595}
596
597// Calls DADiskUnmount synchronously, returning the result.
598bool SynchronousDADiskUnmount(DADiskRef disk,
599                              DADiskUnmountOptions options,
600                              bool can_log) {
601  SynchronousDACallbackData callback_data;
602  callback_data.can_log = can_log;
603  DADiskUnmount(disk, options, SynchronousDACallbackAdapter, &callback_data);
604  return SynchronousDAOperation("DADiskUnmount", &callback_data);
605}
606
607// Calls DADiskEject synchronously, returning the result.
608bool SynchronousDADiskEject(DADiskRef disk, DADiskEjectOptions options) {
609  SynchronousDACallbackData callback_data;
610  DADiskEject(disk, options, SynchronousDACallbackAdapter, &callback_data);
611  return SynchronousDAOperation("DADiskEject", &callback_data);
612}
613
614}  // namespace
615
616void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) {
617  base::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL));
618  if (!session.get()) {
619    LOG(ERROR) << "DASessionCreate";
620    return;
621  }
622
623  base::ScopedCFTypeRef<DADiskRef> disk(
624      DADiskCreateFromBSDName(NULL, session, dmg_bsd_device_name.c_str()));
625  if (!disk.get()) {
626    LOG(ERROR) << "DADiskCreateFromBSDName";
627    return;
628  }
629
630  // dmg_bsd_device_name may only refer to part of the disk: it may be a
631  // single filesystem on a larger disk. Use the "whole disk" object to
632  // be able to unmount all mounted filesystems from the disk image, and eject
633  // the image. This is harmless if dmg_bsd_device_name already referred to a
634  // "whole disk."
635  disk.reset(DADiskCopyWholeDisk(disk));
636  if (!disk.get()) {
637    LOG(ERROR) << "DADiskCopyWholeDisk";
638    return;
639  }
640
641  base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk));
642  if (!media.get()) {
643    LOG(ERROR) << "DADiskCopyIOMedia";
644    return;
645  }
646
647  // Make sure the device is a disk image, and get the path to its disk image
648  // file.
649  std::string disk_image_path;
650  if (!MediaResidesOnDiskImage(media, &disk_image_path)) {
651    LOG(ERROR) << "MediaResidesOnDiskImage";
652    return;
653  }
654
655  // SynchronousDADiskUnmount and SynchronousDADiskEject require that the
656  // session be scheduled with the current run loop.
657  ScopedDASessionScheduleWithRunLoop session_run_loop(session,
658                                                      CFRunLoopGetCurrent(),
659                                                      kCFRunLoopCommonModes);
660
661  // Retry the unmount in a loop to give anything that may have been in use on
662  // the disk image (such as crashpad_handler) a chance to exit.
663  int tries = 15;
664  while (!SynchronousDADiskUnmount(disk,
665                                   kDADiskUnmountOptionWhole,
666                                   --tries == 0)) {
667    if (tries == 0) {
668      LOG(ERROR) << "SynchronousDADiskUnmount";
669      return;
670    }
671    sleep(1);
672  }
673
674  if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) {
675    LOG(ERROR) << "SynchronousDADiskEject";
676    return;
677  }
678
679  NSURL* disk_image_path_nsurl =
680      [NSURL fileURLWithPath:base::SysUTF8ToNSString(disk_image_path)];
681  NSError* ns_error = nil;
682  if (![[NSFileManager defaultManager] trashItemAtURL:disk_image_path_nsurl
683                                     resultingItemURL:nil
684                                                error:&ns_error]) {
685    LOG(ERROR) << base::SysNSStringToUTF8([ns_error localizedDescription]);
686    return;
687  }
688
689}
690