1 /* 2 * Copyright 2007-2010, Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Stephan Aßmus, superstippi@gmx.de 7 * Axel Dörfler, axeld@pinc-software.de 8 */ 9 10 11 #include "AutoMounter.h" 12 13 #include <new> 14 15 #include <string.h> 16 #include <unistd.h> 17 18 #include <Alert.h> 19 #include <AutoLocker.h> 20 #include <Catalog.h> 21 #include <Debug.h> 22 #include <Directory.h> 23 #include <DiskDevice.h> 24 #include <DiskDeviceRoster.h> 25 #include <DiskDeviceList.h> 26 #include <DiskDeviceTypes.h> 27 #include <DiskSystem.h> 28 #include <FindDirectory.h> 29 #include <fs_info.h> 30 #include <fs_volume.h> 31 #include <LaunchRoster.h> 32 #include <Locale.h> 33 #include <Message.h> 34 #include <Node.h> 35 #include <NodeMonitor.h> 36 #include <Path.h> 37 #include <PropertyInfo.h> 38 #include <String.h> 39 #include <VolumeRoster.h> 40 41 #include "MountServer.h" 42 43 44 #undef B_TRANSLATION_CONTEXT 45 #define B_TRANSLATION_CONTEXT "AutoMounter" 46 47 48 static const char* kMountServerSettings = "mount_server"; 49 static const char* kMountFlagsKeyExtension = " mount flags"; 50 51 static const char* kInitialMountEvent = "initial_volumes_mounted"; 52 53 54 static bool 55 BootedInSafeMode() 56 { 57 const char *safeMode = getenv("SAFEMODE"); 58 return (safeMode && strcmp(safeMode, "yes") == 0); 59 } 60 61 62 // #pragma mark - 63 64 65 AutoMounter::AutoMounter() 66 : 67 BServer(kMountServerSignature, true, NULL), 68 fNormalMode(kRestorePreviousVolumes), 69 fRemovableMode(kAllVolumes), 70 fEjectWhenUnmounting(true) 71 { 72 set_thread_priority(Thread(), B_LOW_PRIORITY); 73 74 if (!BootedInSafeMode()) { 75 _ReadSettings(); 76 } else { 77 // defeat automounter in safe mode, don't even care about the settings 78 fNormalMode = kNoVolumes; 79 fRemovableMode = kNoVolumes; 80 } 81 82 BDiskDeviceRoster().StartWatching(this, 83 B_DEVICE_REQUEST_DEVICE | B_DEVICE_REQUEST_DEVICE_LIST); 84 BLaunchRoster().RegisterEvent(this, kInitialMountEvent, B_STICKY_EVENT); 85 } 86 87 88 AutoMounter::~AutoMounter() 89 { 90 BLaunchRoster().UnregisterEvent(this, kInitialMountEvent); 91 BDiskDeviceRoster().StopWatching(this); 92 } 93 94 95 void 96 AutoMounter::ReadyToRun() 97 { 98 // Do initial scan 99 _MountVolumes(fNormalMode, fRemovableMode, true); 100 BLaunchRoster().NotifyEvent(this, kInitialMountEvent); 101 } 102 103 104 void 105 AutoMounter::MessageReceived(BMessage* message) 106 { 107 switch (message->what) { 108 case kMountVolume: 109 _MountVolume(message); 110 break; 111 112 case kUnmountVolume: 113 _UnmountAndEjectVolume(message); 114 break; 115 116 case kSetAutomounterParams: 117 { 118 bool rescanNow = false; 119 message->FindBool("rescanNow", &rescanNow); 120 121 _UpdateSettingsFromMessage(message); 122 _GetSettings(&fSettings); 123 _WriteSettings(); 124 125 if (rescanNow) 126 _MountVolumes(fNormalMode, fRemovableMode); 127 break; 128 } 129 130 case kGetAutomounterParams: 131 { 132 BMessage reply; 133 _GetSettings(&reply); 134 message->SendReply(&reply); 135 break; 136 } 137 138 case kMountAllNow: 139 _MountVolumes(kAllVolumes, kAllVolumes); 140 break; 141 142 case B_DEVICE_UPDATE: 143 int32 event; 144 if (message->FindInt32("event", &event) != B_OK 145 || (event != B_DEVICE_MEDIA_CHANGED 146 && event != B_DEVICE_ADDED)) 147 break; 148 149 partition_id deviceID; 150 if (message->FindInt32("id", &deviceID) != B_OK) 151 break; 152 153 _MountVolumes(kNoVolumes, fRemovableMode, false, deviceID); 154 break; 155 156 #if 0 157 case B_NODE_MONITOR: 158 { 159 int32 opcode; 160 if (message->FindInt32("opcode", &opcode) != B_OK) 161 break; 162 163 switch (opcode) { 164 // The name of a mount point has changed 165 case B_ENTRY_MOVED: { 166 WRITELOG(("*** Received Mount Point Renamed Notification")); 167 168 const char *newName; 169 if (message->FindString("name", &newName) != B_OK) { 170 WRITELOG(("ERROR: Couldn't find name field in update " 171 "message")); 172 PRINT_OBJECT(*message); 173 break ; 174 } 175 176 // 177 // When the node monitor reports a move, it gives the 178 // parent device and inode that moved. The problem is 179 // that the inode is the inode of root *in* the filesystem, 180 // which is generally always the same number for every 181 // filesystem of a type. 182 // 183 // What we'd really like is the device that the moved 184 // volume is mounted on. Find this by using the 185 // *new* name and directory, and then stat()ing that to 186 // find the device. 187 // 188 dev_t parentDevice; 189 if (message->FindInt32("device", &parentDevice) != B_OK) { 190 WRITELOG(("ERROR: Couldn't find 'device' field in " 191 "update message")); 192 PRINT_OBJECT(*message); 193 break; 194 } 195 196 ino_t toDirectory; 197 if (message->FindInt64("to directory", &toDirectory) 198 != B_OK) { 199 WRITELOG(("ERROR: Couldn't find 'to directory' field " 200 "in update message")); 201 PRINT_OBJECT(*message); 202 break; 203 } 204 205 entry_ref root_entry(parentDevice, toDirectory, newName); 206 207 BNode entryNode(&root_entry); 208 if (entryNode.InitCheck() != B_OK) { 209 WRITELOG(("ERROR: Couldn't create mount point entry " 210 "node: %s/n", strerror(entryNode.InitCheck()))); 211 break; 212 } 213 214 node_ref mountPointNode; 215 if (entryNode.GetNodeRef(&mountPointNode) != B_OK) { 216 WRITELOG(("ERROR: Couldn't get node ref for new mount " 217 "point")); 218 break; 219 } 220 221 WRITELOG(("Attempt to rename device %li to %s", 222 mountPointNode.device, newName)); 223 224 Partition *partition = FindPartition(mountPointNode.device); 225 if (partition != NULL) { 226 WRITELOG(("Found device, changing name.")); 227 228 BVolume mountVolume(partition->VolumeDeviceID()); 229 BDirectory mountDir; 230 mountVolume.GetRootDirectory(&mountDir); 231 BPath dirPath(&mountDir, 0); 232 233 partition->SetMountedAt(dirPath.Path()); 234 partition->SetVolumeName(newName); 235 break; 236 } else { 237 WRITELOG(("ERROR: Device %li does not appear to be " 238 "present", mountPointNode.device)); 239 } 240 } 241 } 242 break; 243 } 244 #endif 245 246 default: 247 BLooper::MessageReceived(message); 248 break; 249 } 250 } 251 252 253 bool 254 AutoMounter::QuitRequested() 255 { 256 if (!BootedInSafeMode()) { 257 // Don't write out settings in safe mode - this would overwrite the 258 // normal, non-safe mode settings. 259 _WriteSettings(); 260 } 261 262 return true; 263 } 264 265 266 // #pragma mark - private methods 267 268 269 void 270 AutoMounter::_MountVolumes(mount_mode normal, mount_mode removable, 271 bool initialRescan, partition_id deviceID) 272 { 273 if (normal == kNoVolumes && removable == kNoVolumes) 274 return; 275 276 class InitialMountVisitor : public BDiskDeviceVisitor { 277 public: 278 InitialMountVisitor(mount_mode normalMode, mount_mode removableMode, 279 bool initialRescan, BMessage& previous, 280 partition_id deviceID) 281 : 282 fNormalMode(normalMode), 283 fRemovableMode(removableMode), 284 fInitialRescan(initialRescan), 285 fPrevious(previous), 286 fOnlyOnDeviceID(deviceID) 287 { 288 } 289 290 virtual 291 ~InitialMountVisitor() 292 { 293 } 294 295 virtual bool 296 Visit(BDiskDevice* device) 297 { 298 return Visit(device, 0); 299 } 300 301 virtual bool 302 Visit(BPartition* partition, int32 level) 303 { 304 if (fOnlyOnDeviceID >= 0) { 305 // only mount partitions on the given device id 306 // or if the partition ID is already matched 307 BPartition* device = partition; 308 while (device->Parent() != NULL) { 309 if (device->ID() == fOnlyOnDeviceID) { 310 // we are happy 311 break; 312 } 313 device = device->Parent(); 314 } 315 if (device->ID() != fOnlyOnDeviceID) 316 return false; 317 } 318 319 mount_mode mode = !fInitialRescan 320 && partition->Device()->IsRemovableMedia() 321 ? fRemovableMode : fNormalMode; 322 if (mode == kNoVolumes 323 || partition->IsMounted() 324 || !partition->ContainsFileSystem()) 325 return false; 326 327 BPath path; 328 if (partition->GetPath(&path) != B_OK) 329 return false; 330 331 if (mode == kRestorePreviousVolumes) { 332 // mount all volumes that were stored in the settings file 333 const char *volumeName = NULL; 334 if (partition->ContentName() == NULL 335 || fPrevious.FindString(path.Path(), &volumeName) 336 != B_OK 337 || strcmp(volumeName, partition->ContentName())) 338 return false; 339 } else if (mode == kOnlyBFSVolumes) { 340 if (partition->ContentType() == NULL 341 || strcmp(partition->ContentType(), kPartitionTypeBFS)) 342 return false; 343 } 344 345 uint32 mountFlags; 346 if (!fInitialRescan) { 347 // Ask the user about mount flags if this is not the 348 // initial scan. 349 if (!_SuggestMountFlags(partition, &mountFlags)) 350 return false; 351 } else { 352 BString mountFlagsKey(path.Path()); 353 mountFlagsKey << kMountFlagsKeyExtension; 354 if (fPrevious.FindInt32(mountFlagsKey.String(), 355 (int32*)&mountFlags) < B_OK) { 356 mountFlags = 0; 357 } 358 } 359 360 if (partition->Mount(NULL, mountFlags) != B_OK) { 361 // TODO: Error to syslog 362 } 363 return false; 364 } 365 366 private: 367 mount_mode fNormalMode; 368 mount_mode fRemovableMode; 369 bool fInitialRescan; 370 BMessage& fPrevious; 371 partition_id fOnlyOnDeviceID; 372 } visitor(normal, removable, initialRescan, fSettings, deviceID); 373 374 BDiskDeviceList devices; 375 status_t status = devices.Fetch(); 376 if (status == B_OK) 377 devices.VisitEachPartition(&visitor); 378 } 379 380 381 void 382 AutoMounter::_MountVolume(const BMessage* message) 383 { 384 int32 id; 385 if (message->FindInt32("id", &id) != B_OK) 386 return; 387 388 BDiskDeviceRoster roster; 389 BPartition *partition; 390 BDiskDevice device; 391 if (roster.GetPartitionWithID(id, &device, &partition) != B_OK) 392 return; 393 394 uint32 mountFlags; 395 if (!_SuggestMountFlags(partition, &mountFlags)) 396 return; 397 398 status_t status = partition->Mount(NULL, mountFlags); 399 if (status < B_OK) { 400 char text[512]; 401 snprintf(text, sizeof(text), 402 B_TRANSLATE("Error mounting volume:\n\n%s"), strerror(status)); 403 BAlert* alert = new BAlert(B_TRANSLATE("Mount error"), text, 404 B_TRANSLATE("OK")); 405 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 406 alert->Go(NULL); 407 } 408 } 409 410 411 bool 412 AutoMounter::_SuggestForceUnmount(const char* name, status_t error) 413 { 414 char text[1024]; 415 snprintf(text, sizeof(text), 416 B_TRANSLATE("Could not unmount disk \"%s\":\n\t%s\n\n" 417 "Should unmounting be forced?\n\n" 418 "Note: If an application is currently writing to the volume, " 419 "unmounting it now might result in loss of data.\n"), 420 name, strerror(error)); 421 422 BAlert* alert = new BAlert(B_TRANSLATE("Force unmount"), text, 423 B_TRANSLATE("Cancel"), B_TRANSLATE("Force unmount"), NULL, 424 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 425 alert->SetShortcut(0, B_ESCAPE); 426 int32 choice = alert->Go(); 427 428 return choice == 1; 429 } 430 431 432 void 433 AutoMounter::_ReportUnmountError(const char* name, status_t error) 434 { 435 char text[512]; 436 snprintf(text, sizeof(text), B_TRANSLATE("Could not unmount disk " 437 "\"%s\":\n\t%s"), name, strerror(error)); 438 439 BAlert* alert = new BAlert(B_TRANSLATE("Unmount error"), text, 440 B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT); 441 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 442 alert->Go(NULL); 443 } 444 445 446 void 447 AutoMounter::_UnmountAndEjectVolume(BPartition* partition, BPath& mountPoint, 448 const char* name) 449 { 450 BDiskDevice deviceStorage; 451 BDiskDevice* device; 452 if (partition == NULL) { 453 // Try to retrieve partition 454 BDiskDeviceRoster().FindPartitionByMountPoint(mountPoint.Path(), 455 &deviceStorage, &partition); 456 device = &deviceStorage; 457 } else { 458 device = partition->Device(); 459 } 460 461 status_t status; 462 if (partition != NULL) 463 status = partition->Unmount(); 464 else 465 status = fs_unmount_volume(mountPoint.Path(), 0); 466 467 if (status != B_OK) { 468 if (!_SuggestForceUnmount(name, status)) 469 return; 470 471 if (partition != NULL) 472 status = partition->Unmount(B_FORCE_UNMOUNT); 473 else 474 status = fs_unmount_volume(mountPoint.Path(), B_FORCE_UNMOUNT); 475 } 476 477 if (status != B_OK) { 478 _ReportUnmountError(name, status); 479 return; 480 } 481 482 if (fEjectWhenUnmounting && partition != NULL) { 483 // eject device if it doesn't have any mounted partitions left 484 class IsMountedVisitor : public BDiskDeviceVisitor { 485 public: 486 IsMountedVisitor() 487 : 488 fHasMounted(false) 489 { 490 } 491 492 virtual bool Visit(BDiskDevice* device) 493 { 494 return Visit(device, 0); 495 } 496 497 virtual bool Visit(BPartition* partition, int32 level) 498 { 499 if (partition->IsMounted()) { 500 fHasMounted = true; 501 return true; 502 } 503 504 return false; 505 } 506 507 bool HasMountedPartitions() const 508 { 509 return fHasMounted; 510 } 511 512 private: 513 bool fHasMounted; 514 } visitor; 515 516 device->VisitEachDescendant(&visitor); 517 518 if (!visitor.HasMountedPartitions()) 519 device->Eject(); 520 } 521 522 // remove the directory if it's a directory in rootfs 523 if (dev_for_path(mountPoint.Path()) == dev_for_path("/")) 524 rmdir(mountPoint.Path()); 525 } 526 527 528 void 529 AutoMounter::_UnmountAndEjectVolume(BMessage* message) 530 { 531 int32 id; 532 if (message->FindInt32("id", &id) == B_OK) { 533 BDiskDeviceRoster roster; 534 BPartition *partition; 535 BDiskDevice device; 536 if (roster.GetPartitionWithID(id, &device, &partition) != B_OK) 537 return; 538 539 BPath path; 540 if (partition->GetMountPoint(&path) == B_OK) 541 _UnmountAndEjectVolume(partition, path, partition->ContentName()); 542 } else { 543 // see if we got a dev_t 544 545 dev_t device; 546 if (message->FindInt32("device_id", &device) != B_OK) 547 return; 548 549 BVolume volume(device); 550 status_t status = volume.InitCheck(); 551 552 char name[B_FILE_NAME_LENGTH]; 553 if (status == B_OK) 554 status = volume.GetName(name); 555 if (status < B_OK) 556 snprintf(name, sizeof(name), "device:%" B_PRIdDEV, device); 557 558 BPath path; 559 if (status == B_OK) { 560 BDirectory mountPoint; 561 status = volume.GetRootDirectory(&mountPoint); 562 if (status == B_OK) 563 status = path.SetTo(&mountPoint, "."); 564 } 565 566 if (status == B_OK) 567 _UnmountAndEjectVolume(NULL, path, name); 568 } 569 } 570 571 572 void 573 AutoMounter::_FromMode(mount_mode mode, bool& all, bool& bfs, bool& restore) 574 { 575 all = bfs = restore = false; 576 577 switch (mode) { 578 case kAllVolumes: 579 all = true; 580 break; 581 case kOnlyBFSVolumes: 582 bfs = true; 583 break; 584 case kRestorePreviousVolumes: 585 restore = true; 586 break; 587 588 default: 589 break; 590 } 591 } 592 593 594 AutoMounter::mount_mode 595 AutoMounter::_ToMode(bool all, bool bfs, bool restore) 596 { 597 if (all) 598 return kAllVolumes; 599 if (bfs) 600 return kOnlyBFSVolumes; 601 if (restore) 602 return kRestorePreviousVolumes; 603 604 return kNoVolumes; 605 } 606 607 608 void 609 AutoMounter::_ReadSettings() 610 { 611 BPath directoryPath; 612 if (find_directory(B_USER_SETTINGS_DIRECTORY, &directoryPath, true) 613 != B_OK) { 614 return; 615 } 616 617 BPath path(directoryPath); 618 path.Append(kMountServerSettings); 619 fPrefsFile.SetTo(path.Path(), O_RDWR); 620 621 if (fPrefsFile.InitCheck() != B_OK) { 622 // no prefs file yet, create a new one 623 624 BDirectory dir(directoryPath.Path()); 625 dir.CreateFile(kMountServerSettings, &fPrefsFile); 626 return; 627 } 628 629 ssize_t settingsSize = (ssize_t)fPrefsFile.Seek(0, SEEK_END); 630 if (settingsSize == 0) 631 return; 632 633 ASSERT(settingsSize != 0); 634 char *buffer = new(std::nothrow) char[settingsSize]; 635 if (buffer == NULL) { 636 PRINT(("error writing automounter settings, out of memory\n")); 637 return; 638 } 639 640 fPrefsFile.Seek(0, 0); 641 if (fPrefsFile.Read(buffer, (size_t)settingsSize) != settingsSize) { 642 PRINT(("error reading automounter settings\n")); 643 delete [] buffer; 644 return; 645 } 646 647 BMessage message('stng'); 648 status_t result = message.Unflatten(buffer); 649 if (result != B_OK) { 650 PRINT(("error %s unflattening automounter settings, size %" B_PRIdSSIZE "\n", 651 strerror(result), settingsSize)); 652 delete [] buffer; 653 return; 654 } 655 656 delete [] buffer; 657 658 // update flags and modes from the message 659 _UpdateSettingsFromMessage(&message); 660 // copy the previously mounted partitions 661 fSettings = message; 662 } 663 664 665 void 666 AutoMounter::_WriteSettings() 667 { 668 if (fPrefsFile.InitCheck() != B_OK) 669 return; 670 671 BMessage message('stng'); 672 _GetSettings(&message); 673 674 ssize_t settingsSize = message.FlattenedSize(); 675 676 char *buffer = new(std::nothrow) char[settingsSize]; 677 if (buffer == NULL) { 678 PRINT(("error writing automounter settings, out of memory\n")); 679 return; 680 } 681 682 status_t result = message.Flatten(buffer, settingsSize); 683 684 fPrefsFile.Seek(0, SEEK_SET); 685 result = fPrefsFile.Write(buffer, (size_t)settingsSize); 686 if (result != settingsSize) 687 PRINT(("error writing automounter settings, %s\n", strerror(result))); 688 689 delete [] buffer; 690 } 691 692 693 void 694 AutoMounter::_UpdateSettingsFromMessage(BMessage* message) 695 { 696 // auto mounter settings 697 698 bool all, bfs, restore; 699 if (message->FindBool("autoMountAll", &all) != B_OK) 700 all = true; 701 if (message->FindBool("autoMountAllBFS", &bfs) != B_OK) 702 bfs = false; 703 704 fRemovableMode = _ToMode(all, bfs, false); 705 706 // initial mount settings 707 708 if (message->FindBool("initialMountAll", &all) != B_OK) 709 all = false; 710 if (message->FindBool("initialMountAllBFS", &bfs) != B_OK) 711 bfs = false; 712 if (message->FindBool("initialMountRestore", &restore) != B_OK) 713 restore = true; 714 715 fNormalMode = _ToMode(all, bfs, restore); 716 717 // eject settings 718 bool eject; 719 if (message->FindBool("ejectWhenUnmounting", &eject) == B_OK) 720 fEjectWhenUnmounting = eject; 721 } 722 723 724 void 725 AutoMounter::_GetSettings(BMessage *message) 726 { 727 message->MakeEmpty(); 728 729 bool all, bfs, restore; 730 731 _FromMode(fNormalMode, all, bfs, restore); 732 message->AddBool("initialMountAll", all); 733 message->AddBool("initialMountAllBFS", bfs); 734 message->AddBool("initialMountRestore", restore); 735 736 _FromMode(fRemovableMode, all, bfs, restore); 737 message->AddBool("autoMountAll", all); 738 message->AddBool("autoMountAllBFS", bfs); 739 740 message->AddBool("ejectWhenUnmounting", fEjectWhenUnmounting); 741 742 // Save mounted volumes so we can optionally mount them on next 743 // startup 744 BVolumeRoster volumeRoster; 745 BVolume volume; 746 while (volumeRoster.GetNextVolume(&volume) == B_OK) { 747 fs_info info; 748 if (fs_stat_dev(volume.Device(), &info) == 0 749 && (info.flags & (B_FS_IS_REMOVABLE | B_FS_IS_PERSISTENT)) != 0) { 750 message->AddString(info.device_name, info.volume_name); 751 752 BString mountFlagsKey(info.device_name); 753 mountFlagsKey << kMountFlagsKeyExtension; 754 uint32 mountFlags = 0; 755 if (volume.IsReadOnly()) 756 mountFlags |= B_MOUNT_READ_ONLY; 757 message->AddInt32(mountFlagsKey.String(), mountFlags); 758 } 759 } 760 } 761 762 763 /*static*/ bool 764 AutoMounter::_SuggestMountFlags(const BPartition* partition, uint32* _flags) 765 { 766 uint32 mountFlags = 0; 767 768 bool askReadOnly = true; 769 bool isBFS = false; 770 771 if (partition->ContentType() != NULL 772 && strcmp(partition->ContentType(), kPartitionTypeBFS) == 0) { 773 #if 0 774 askReadOnly = false; 775 #endif 776 isBFS = true; 777 } 778 779 BDiskSystem diskSystem; 780 status_t status = partition->GetDiskSystem(&diskSystem); 781 if (status == B_OK && !diskSystem.SupportsWriting()) 782 askReadOnly = false; 783 784 if (partition->IsReadOnly()) 785 askReadOnly = false; 786 787 if (askReadOnly) { 788 // Suggest to the user to mount read-only until Haiku is more mature. 789 BString string; 790 if (partition->ContentName() != NULL) { 791 char buffer[512]; 792 snprintf(buffer, sizeof(buffer), 793 B_TRANSLATE("Mounting volume '%s'\n\n"), 794 partition->ContentName()); 795 string << buffer; 796 } else 797 string << B_TRANSLATE("Mounting volume <unnamed volume>\n\n"); 798 799 // TODO: Use distro name instead of "Haiku"... 800 if (!isBFS) { 801 string << B_TRANSLATE("The file system on this volume is not the " 802 "Haiku file system. It is strongly suggested to mount it in " 803 "read-only mode. This will prevent unintentional data loss " 804 "because of errors in Haiku."); 805 } else { 806 string << B_TRANSLATE("It is suggested to mount all additional " 807 "Haiku volumes in read-only mode. This will prevent " 808 "unintentional data loss because of errors in Haiku."); 809 } 810 811 BAlert* alert = new BAlert(B_TRANSLATE("Mount warning"), 812 string.String(), B_TRANSLATE("Mount read/write"), 813 B_TRANSLATE("Cancel"), B_TRANSLATE("Mount read-only"), 814 B_WIDTH_FROM_WIDEST, B_WARNING_ALERT); 815 alert->SetShortcut(1, B_ESCAPE); 816 int32 choice = alert->Go(); 817 switch (choice) { 818 case 0: 819 break; 820 case 1: 821 return false; 822 case 2: 823 mountFlags |= B_MOUNT_READ_ONLY; 824 break; 825 } 826 } 827 828 *_flags = mountFlags; 829 return true; 830 } 831 832 833 // #pragma mark - 834 835 836 int 837 main(int argc, char* argv[]) 838 { 839 AutoMounter app; 840 841 app.Run(); 842 return 0; 843 } 844 845 846