├── .gitignore ├── Base.lproj ├── AttachDiskImageAccessory.xib ├── DiskInfo.xib ├── MainMenu.xib ├── MountOptions.xib └── Preferences.xib ├── DiskArbitrator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Build Disk Arbitrator DMG.xcscheme │ └── DiskArbitrator.xcscheme ├── Documents ├── App Icon │ ├── App Icon 16.png │ ├── App Icon 32.png │ ├── App Icon.icns │ └── App Icon.png ├── Disk Image Toolbar │ └── Disk Image Document.graffle ├── SecPasswordAction │ ├── AuthorizationTagsPriv.h │ ├── Readme.rtf │ ├── SecPassword.cpp │ └── SecPassword.h ├── Status Item Icons.graffle └── Toolbar Item Icons.graffle ├── LICENSE ├── README.markdown ├── ReleaseNotes.markdown ├── Resources ├── DiskArbitrator-Info.plist ├── StatusItem Disabled 1.png ├── StatusItem Disabled 2.png ├── StatusItem Green.png ├── StatusItem Orange.png ├── ToolbarItem Attach Disk Image.png ├── ToolbarItem Attach Disk Plug.png ├── ToolbarItem Eject.png ├── ToolbarItem Info.png └── ToolbarItem Mount.png ├── Source ├── AppError.h ├── AppError.m ├── Arbitrator.h ├── Arbitrator.m ├── AttachDiskImageController.h ├── AttachDiskImageController.m ├── Disk.h ├── Disk.m ├── DiskArbitrationPrivateFunctions.h ├── DiskArbitrationPrivateFunctions.m ├── DiskArbitratorAppController+Toolbar.h ├── DiskArbitratorAppController+Toolbar.m ├── DiskArbitratorAppController.h ├── DiskArbitratorAppController.m ├── DiskArbitrator_Prefix.pch ├── DiskCell.h ├── DiskCell.m ├── DiskInfoController.h ├── DiskInfoController.m ├── SheetController.h ├── SheetController.m ├── build_dmg.sh └── main.m └── scripts └── markdown.pl /.gitignore: -------------------------------------------------------------------------------- 1 | *.pbxuser 2 | *.perspectivev3 3 | *.mode1v3 4 | .DS_Store 5 | *.log 6 | *.out 7 | *.aux 8 | xcuserdata 9 | -------------------------------------------------------------------------------- /Base.lproj/AttachDiskImageAccessory.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | readOnly 20 | noOwners 21 | noBrowse 22 | rootPath 23 | password 24 | needPassword 25 | noVerify 26 | attemptMount 27 | filePath 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 69 | 81 | 93 | 104 | 115 | 116 | 117 | 118 | If Path is empty, the OS will create a folder in /Volumes and use that for the mount point for the filesystem, which is the normal action when a disk is connected. 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | NSAllRomanInputSourcesLocaleIdentifier 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | NSNegateBoolean 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | NSNegateBoolean 205 | 206 | 207 | 208 | 209 | 210 | NSIsNotNil 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 253 | 264 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /Base.lproj/DiskInfo.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Cg 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | icon 150 | BSDName 151 | diskDescription 152 | mountable 153 | mounted 154 | isWholeDisk 155 | diskDescription.DAMediaName 156 | DAMediaName 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | DAMediaName 165 | DAVolumeName 166 | diskDescription.DAVolumeName 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /Base.lproj/MountOptions.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 45 | 56 | 67 | 82 | 83 | 84 | 85 | If Path is empty, the OS will create a folder in /Volumes and use that for the mount point for the filesystem, which is the normal action when a disk is connected. 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 109 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | readOnly 148 | noOwners 149 | noBrowse 150 | ignoreJournal 151 | path 152 | isHFS 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /Base.lproj/Preferences.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 36 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /DiskArbitrator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DiskArbitrator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DiskArbitrator.xcodeproj/xcshareddata/xcschemes/Build Disk Arbitrator DMG.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 45 | 46 | 52 | 53 | 54 | 55 | 63 | 64 | 66 | 67 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /DiskArbitrator.xcodeproj/xcshareddata/xcschemes/DiskArbitrator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Documents/App Icon/App Icon 16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Documents/App Icon/App Icon 16.png -------------------------------------------------------------------------------- /Documents/App Icon/App Icon 32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Documents/App Icon/App Icon 32.png -------------------------------------------------------------------------------- /Documents/App Icon/App Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Documents/App Icon/App Icon.icns -------------------------------------------------------------------------------- /Documents/App Icon/App Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Documents/App Icon/App Icon.png -------------------------------------------------------------------------------- /Documents/SecPasswordAction/AuthorizationTagsPriv.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2003-2004 Apple Computer, Inc. All Rights Reserved. 3 | * 4 | * @APPLE_LICENSE_HEADER_START@ 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apple Public Source License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://www.opensource.apple.com/apsl/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | * 21 | * @APPLE_LICENSE_HEADER_END@ 22 | */ 23 | 24 | 25 | /* 26 | * AuthorizationTagsPriv.h -- private Authorization tags 27 | * 28 | */ 29 | 30 | #ifndef _SECURITY_AUTHORIZATIONTAGSPRIV_H_ 31 | #define _SECURITY_AUTHORIZATIONTAGSPRIV_H_ 32 | 33 | /*! 34 | @header AuthorizationTagsPriv 35 | Version 1.0 10/2003 36 | 37 | This header contains private details for authorization services. 38 | */ 39 | 40 | 41 | /* meta-rightname prefixes that configure authorization for policy changes */ 42 | 43 | /*! 44 | @defined kConfigRightAdd 45 | meta-rightname for prefix adding rights. 46 | */ 47 | #define kAuthorizationConfigRightAdd "config.add." 48 | /*! 49 | @defined kConfigRightModify 50 | meta-rightname prefix for modifying rights. 51 | */ 52 | #define kAuthorizationConfigRightModify "config.modify." 53 | /*! 54 | @defined kConfigRightRemove 55 | meta-rightname prefix for removing rights. 56 | */ 57 | #define kAuthorizationConfigRightRemove "config.remove." 58 | /*! 59 | @defined kConfigRight 60 | meta-rightname prefix. 61 | */ 62 | #define kConfigRight "config." 63 | 64 | /*! 65 | @defined kRuleIsRoot 66 | canned rule for daemon to daemon convincing (see AuthorizationDB.h for public ones) 67 | */ 68 | #define kAuthorizationRuleIsRoot "is-root" 69 | 70 | /* rule classes the specify behavior */ 71 | 72 | /*! @defined kAuthorizationRuleClass 73 | Specifying rule class 74 | */ 75 | #define kAuthorizationRuleClass "class" 76 | 77 | /*! @defined kAuthorizationRuleClassUser 78 | Specifying user class 79 | */ 80 | #define kAuthorizationRuleClassUser "user" 81 | 82 | /*! @defined kAuthorizationRuleClassMechanisms 83 | Specifying evaluate-mechanisms class 84 | */ 85 | #define kAuthorizationRuleClassMechanisms "evaluate-mechanisms" 86 | 87 | /* rule attributes to specify above classes */ 88 | 89 | /*! @defined kAuthorizationRuleParameterGroup 90 | string, group specification for user rules. 91 | */ 92 | #define kAuthorizationRuleParameterGroup "group" 93 | 94 | /*! @defined kAuthorizationRuleParameterKofN 95 | number, k specification for k-of-n 96 | */ 97 | #define kAuthorizationRuleParameterKofN "k-of-n" 98 | 99 | /*! @defined kAuthorizationRuleParameterRules 100 | rules specification for rule delegation (incl. k-of-n) 101 | */ 102 | #define kAuthorizationRuleParameterRules "rules" 103 | 104 | /*! @defined kAuthorizationRuleParameterMechanisms 105 | mechanism specification, a sequence of mechanisms to be evaluated */ 106 | #define kAuthorizationRuleParameterMechanisms "mechanisms" 107 | 108 | /*! @defined kAuthorizationRightParameterTimeout 109 | timeout if any when a remembered right expires. 110 | special values: 111 | - not specified retains previous behavior: most privileged, credential based. 112 | - zero grants the right once 113 | (can be achieved with zero credential timeout, needed?) 114 | - all other values are interpreted as number of seconds since granted. 115 | */ 116 | #define kAuthorizationRightParameterTimeout "timeout-right" 117 | 118 | /*! @defined kAuthorizationRuleParameterCredentialTimeout 119 | timeout if any for the use of cached credentials when authorizing rights. 120 | - not specified allows for any credentials regardless of age; rights will be remembered in authorizations, removing a credential does not stop it from granting this right, specifying a zero timeout for the right will delegate it back to requiring a credential. 121 | - all other values are interpreted as number of seconds since the credential was created 122 | - zero only allows for the use of credentials created "now" // This is deprecated by means of specifying zero for kRightTimeout 123 | */ 124 | #define kAuthorizationRuleParameterCredentialTimeout "timeout" 125 | 126 | /*! @defined kAuthorizationRuleParameterCredentialShared 127 | boolean that indicates whether credentials acquired during authorization are added to the shared pool. 128 | */ 129 | #define kAuthorizationRuleParameterCredentialShared "shared" 130 | 131 | /*! @defined kAuthorizationRuleParameterAllowRoot 132 | boolean that indicates whether to grant a right purely because the caller is root */ 133 | #define kAuthorizationRuleParameterAllowRoot "allow-root" 134 | 135 | /*! @defined kAuthorizationRuleParameterCredentialSessionOwner 136 | boolean that indicates whether to grant a right based on a valid session-owner credential */ 137 | #define kAuthorizationRuleParameterCredentialSessionOwner "session-owner" 138 | 139 | /*! @defined kRuleDefaultPrompt 140 | dictionary of localization-name and localized prompt pairs */ 141 | #define kAuthorizationRuleParameterDefaultPrompt "default-prompt" 142 | 143 | /*! @defined kAuthorizationRuleParameterDescription 144 | string, default description of right. Usually localized versions are added using the 145 | AuthorizationDBSet call (@see AuthorizationDB.h). */ 146 | #define kAuthorizationRuleParameterDescription "description" 147 | 148 | /*! @defined kAuthorizationRuleParameterAuthenticateUser 149 | boolean that indicates whether to authenticate the user requesting authorization */ 150 | #define kAuthorizationRuleParameterAuthenticateUser "authenticate-user" 151 | 152 | /* authorization hints passed between securityd and agent */ 153 | #define AGENT_HINT_SUGGESTED_USER "suggested-user" 154 | #define AGENT_HINT_SUGGESTED_USER_LONG "suggested-realname" 155 | #define AGENT_HINT_REQUIRE_USER_IN_GROUP "require-user-in-group" 156 | #define AGENT_HINT_CUSTOM_PROMPT "prompt" 157 | #define AGENT_HINT_AUTHORIZE_RIGHT "authorize-right" 158 | #define AGENT_HINT_CLIENT_PID "client-pid" 159 | #define AGENT_HINT_CLIENT_UID "client-uid" 160 | #define AGENT_HINT_CLIENT_VALIDITY "client-signature-validity" 161 | #define AGENT_HINT_CREATOR_PID "creator-pid" 162 | #define AGENT_HINT_CLIENT_TYPE "client-type" 163 | #define AGENT_HINT_CLIENT_PATH "client-path" 164 | #define AGENT_HINT_CLIENT_NAME "client-name" 165 | #define AGENT_HINT_TRIES "tries" 166 | #define AGENT_HINT_RETRY_REASON "reason" 167 | #define AGENT_HINT_AUTHORIZE_RULE "authorize-rule" 168 | #define AGENT_HINT_TOKEN_NAME "token-name" 169 | 170 | /* passed from mechanisms to loginwindow */ 171 | #define kAuthorizationEnvironmentTokenSubserviceID "token-subservice-uid" 172 | 173 | // remote home directory specification 174 | #define AGENT_CONTEXT_AFP_DIR "afp_dir" 175 | // home directory (where it's locally mounted) 176 | #define AGENT_CONTEXT_HOME "home" 177 | #define AGENT_CONTEXT_UID "uid" 178 | #define AGENT_CONTEXT_GID "gid" 179 | // kerberos principal; decoded from auth-authority specification 180 | #define AGENT_CONTEXT_KERBEROSPRINCIPAL "kerberos-principal" 181 | // tell loginwindow where we're mounted 182 | // (this should really be equal to our homedirectory according to HOME 183 | #define AGENT_CONTEXT_MOUNTPOINT "mountpoint" 184 | 185 | /* authorization context passed from agent to securityd */ 186 | #define AGENT_USERNAME "username" 187 | #define AGENT_PASSWORD "password" 188 | #define AGENT_CONTEXT_NEW_PASSWORD "new-password" 189 | 190 | #define AGENT_HINT_SHOW_ADD_TO_KEYCHAIN "show-add-to-keychain" 191 | #define AGENT_ADD_TO_KEYCHAIN "add-to-keychain" 192 | 193 | #define AGENT_CONTEXT_AUTHENTICATION_FAILURE "authentication-failure" 194 | 195 | /* keychain panels */ 196 | // ACLowner etc. code identity panel 197 | 198 | // Application Path is needed at this stage for identifying the application 199 | // for which the ACL entry is about to be updated 200 | #define AGENT_HINT_APPLICATION_PATH "application-path" 201 | #define AGENT_HINT_ACL_TAG "acl-tag" 202 | #define AGENT_HINT_GROUPKEY "group-key" 203 | #define AGENT_HINT_ACL_MISMATCH "acl-mismatch" 204 | #define AGENT_HINT_KEYCHAIN_ITEM_NAME "keychain-item-name" 205 | #define AGENT_HINT_KEYCHAIN_PATH "keychain-path" 206 | #define AGENT_HINT_WINDOW_LEVEL "window-level" 207 | 208 | #define AGENT_CONTEXT_REMEMBER_ACTION "remember-action" 209 | #define AGENT_CONTEXT_ALLOW "allow" 210 | 211 | /* Login Keychain Creation hint and context keys */ 212 | 213 | #define AGENT_HINT_ATTR_NAME "loginKCCreate:attributeName" 214 | #define AGENT_HINT_LOGIN_KC_NAME "loginKCCreate:pathName" 215 | #define AGENT_HINT_LOGIN_KC_EXISTS_IN_KC_FOLDER "loginKCCreate:exists" 216 | #define AGENT_HINT_LOGIN_KC_USER_NAME "loginKCCreate:userName" 217 | #define AGENT_HINT_LOGIN_KC_CUST_STR1 "loginKCCreate:customStr1" 218 | #define AGENT_HINT_LOGIN_KC_CUST_STR2 "loginKCCreate:customStr2" 219 | #define AGENT_HINT_LOGIN_KC_USER_HAS_OTHER_KCS_STR "loginKCCreate:moreThanOneKeychainExists" 220 | 221 | /*! @defined LOGIN_KC_CREATION_RIGHT 222 | the right used to invoke the right mechanisms to (re)create a login 223 | keychain */ 224 | #define LOGIN_KC_CREATION_RIGHT "system.keychain.create.loginkc" 225 | 226 | /* Keychain synchronization */ 227 | // iDisk keychain blob metainfo dictionary; follows "defaults" naming 228 | #define AGENT_HINT_KCSYNC_DICT "com.apple.keychainsync.dictionary" 229 | 230 | #endif /* !_SECURITY_AUTHORIZATIONTAGSPRIV_H_ */ 231 | -------------------------------------------------------------------------------- /Documents/SecPasswordAction/Readme.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf250 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\info 5 | {\author Aaron Burghardt}}\margl1440\margr1440\vieww16860\viewh14820\viewkind0 6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 7 | 8 | \f0\fs24 \cf0 SecPasswordAction() is an private function in Security.framework. The source code was downloaded from:\ 9 | \ 10 | http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-26098/lib/SecPassword.cpp\ 11 | http://www.opensource.apple.com/source/libsecurity_authorization/libsecurity_authorization-36329/lib/AuthorizationTagsPriv.h\ 12 | \ 13 | It is used by the DiskImages framework to prompt for the password of an encrypted disk image and is saved here as an example.\ 14 | } -------------------------------------------------------------------------------- /Documents/SecPasswordAction/SecPassword.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2004 Apple Computer, Inc. All Rights Reserved. 3 | * 4 | * @APPLE_LICENSE_HEADER_START@ 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apple Public Source License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://www.opensource.apple.com/apsl/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | * 21 | * @APPLE_LICENSE_HEADER_END@ 22 | */ 23 | 24 | #include "SecPassword.h" 25 | #include "Password.h" 26 | 27 | #include "SecBridge.h" 28 | 29 | #include "KCExceptions.h" 30 | #include 31 | #include 32 | 33 | CFTypeID 34 | SecPasswordGetTypeID(void) 35 | { 36 | BEGIN_SECAPI 37 | 38 | secdebug("passwditem", "SecPasswordGetTypeID()"); 39 | return gTypes().PasswordImpl.typeID; 40 | 41 | END_SECAPI1(_kCFRuntimeNotATypeID) 42 | } 43 | 44 | OSStatus 45 | SecGenericPasswordCreate(SecKeychainAttributeList *searchAttrList, SecKeychainAttributeList *itemAttrList, SecPasswordRef *itemRef) 46 | { 47 | BEGIN_SECAPI 48 | KCThrowParamErrIf_( (itemRef == NULL) ); 49 | KCThrowParamErrIf_( (searchAttrList == NULL) ^ (itemAttrList == NULL) ); // Both or neither 50 | secdebug("passworditem", "SecPasswordCreate(%p, %p)", searchAttrList, itemAttrList); 51 | 52 | Password passwordItem(kSecGenericPasswordItemClass, searchAttrList, itemAttrList); 53 | if (itemRef) 54 | *itemRef = passwordItem->handle(); 55 | 56 | END_SECAPI 57 | } 58 | 59 | 60 | OSStatus 61 | SecPasswordAction(SecPasswordRef itemRef, CFTypeRef message, UInt32 flags, UInt32 *length, const void **data) 62 | { 63 | BEGIN_SECAPI 64 | 65 | Password passwordRef = PasswordImpl::required(itemRef); 66 | 67 | void *passwordData = NULL; 68 | uint32_t passwordLength = 0; 69 | 70 | // no flags has no meaning, and there is no apparent default 71 | assert( flags ); 72 | 73 | // fail can only be combined with get or new 74 | assert( (flags & kSecPasswordFail) ? ((flags & kSecPasswordGet) || (flags & kSecPasswordNew)) : true ); 75 | 76 | // XXX/cs replace this with our CFString->UTF8 conversion 77 | const char *messageData = NULL; 78 | auto_array messageBuffer; 79 | 80 | if (message && (CFStringGetTypeID() == CFGetTypeID(message))) 81 | { 82 | messageData = CFStringGetCStringPtr(static_cast(message), kCFStringEncodingUTF8); 83 | 84 | if (messageData == NULL) 85 | { 86 | CFIndex maxLen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(static_cast(message)), kCFStringEncodingUTF8) + 1; 87 | 88 | messageBuffer.allocate(maxLen); 89 | if (CFStringGetCString(static_cast(message), messageBuffer.get(), maxLen, kCFStringEncodingUTF8)) 90 | messageData = messageBuffer.get(); 91 | } 92 | } 93 | 94 | secdebug("SecPassword", "SecPasswordGet(%p, %p)", length, data); 95 | 96 | if (passwordRef->useKeychain() && !(flags & kSecPasswordNew) && !(flags & kSecPasswordFail)) 97 | { 98 | // Pull out data and if it's successful return it 99 | if (flags & kSecPasswordGet) 100 | { 101 | 102 | // XXX/cs if there are unsaved changes this doesn't work 103 | // so doing a Get followed by a Get|Set will do the wrong thing 104 | 105 | // check mItem whether it's got data 106 | if (passwordRef->getData(length, data)) 107 | return noErr; 108 | } 109 | 110 | // User might cancel here, immediately return that too (it will be thrown) 111 | } 112 | 113 | // If we're still here we're not using the keychain or it wasn't there yet 114 | 115 | // Do the authorization call to get the password, unless only kSecPasswordSet is specified) 116 | if ((flags & kSecPasswordNew) || (flags & kSecPasswordGet)) 117 | { 118 | AuthorizationRef authRef; 119 | OSStatus status = AuthorizationCreate(NULL,NULL,0,&authRef); 120 | AuthorizationItem right = { NULL, 0, NULL, 0 }; 121 | AuthorizationItemSet rightSet = { 1, &right }; 122 | uint32_t reason, tries; 123 | bool keychain, addToKeychain; 124 | 125 | if (passwordRef->useKeychain()) 126 | { 127 | keychain = 1; 128 | addToKeychain = 1; 129 | } 130 | 131 | // Get|Fail conceivable would have it enabled, but since the effect is that it will get overwritten 132 | // we'll make the user explicitly do it 133 | if (flags & kSecPasswordNew) 134 | addToKeychain = 1; // turn it on for new items 135 | else 136 | if (flags & kSecPasswordGet) 137 | addToKeychain = 0; // turn it off for old items that weren't successfully retrieved from the keychain 138 | 139 | if (flags & kSecPasswordFail) // set up retry to reflect failure 140 | { 141 | if (flags & kSecPasswordNew) 142 | reason = 34; // passphraseUnacceptable = 34 passphrase unacceptable for some other reason 143 | else 144 | reason = 21; // invalidPassphrase = 21 passphrase was wrong 145 | } 146 | else 147 | reason = 0; 148 | 149 | if (flags & kSecPasswordNew) // pick new passphrase 150 | right.name = "com.apple.builtin.generic-new-passphrase"; 151 | else 152 | right.name = "com.apple.builtin.generic-unlock"; 153 | 154 | AuthorizationItem envRights[5] = { { AGENT_HINT_RETRY_REASON, sizeof(reason), &reason, 0 }, 155 | { AGENT_HINT_TRIES, sizeof(tries), &tries, 0 }, 156 | { AGENT_HINT_CUSTOM_PROMPT, messageData ? strlen(messageData) : 0, const_cast(messageData), 0 }, 157 | { AGENT_HINT_SHOW_ADD_TO_KEYCHAIN, keychain ? strlen("YES") : strlen("NO"), const_cast(keychain ? "YES" : "NO"), 0 }, 158 | { AGENT_ADD_TO_KEYCHAIN, addToKeychain ? strlen("YES") : strlen("NO"), const_cast(addToKeychain ? "YES" : "NO"), 0 } }; 159 | 160 | AuthorizationItemSet envSet = { sizeof(envRights) / sizeof(*envRights), envRights }; 161 | 162 | status = AuthorizationCopyRights(authRef, &rightSet, &envSet, kAuthorizationFlagDefaults|kAuthorizationFlagInteractionAllowed|kAuthorizationFlagExtendRights, NULL); 163 | 164 | if (status) 165 | { 166 | AuthorizationFree(authRef, 0); 167 | return errAuthorizationCanceled; 168 | } 169 | 170 | // if success pull the data 171 | AuthorizationItemSet *returnedInfo; 172 | status = AuthorizationCopyInfo(authRef, NULL, &returnedInfo); 173 | 174 | if (status) 175 | { 176 | AuthorizationFree(authRef, 0); 177 | 178 | return status; 179 | } 180 | 181 | if (returnedInfo && (returnedInfo->count > 0)) 182 | { 183 | for (uint32_t index = 0; index < returnedInfo->count; index++) 184 | { 185 | AuthorizationItem &item = returnedInfo->items[index]; 186 | 187 | if (!strcmp(AGENT_PASSWORD, item.name)) 188 | { 189 | passwordLength = item.valueLength; 190 | 191 | if (passwordLength) 192 | { 193 | Allocator &allocator = Allocator::standard(); 194 | passwordData = allocator.malloc(passwordLength); 195 | if (passwordData) 196 | memcpy(passwordData, item.value, passwordLength); 197 | } 198 | 199 | if (length) 200 | *length = passwordLength; 201 | if (data) 202 | *data = passwordData; 203 | } 204 | else if (!strcmp(AGENT_ADD_TO_KEYCHAIN, item.name)) 205 | { 206 | if (item.value && item.valueLength == strlen("YES") && !memcmp("YES", static_cast(item.value), item.valueLength)) 207 | passwordRef->setRememberInKeychain(true); 208 | else 209 | passwordRef->setRememberInKeychain(false); 210 | } 211 | } 212 | } 213 | 214 | AuthorizationFreeItemSet(returnedInfo); 215 | AuthorizationFree(authRef, 0); 216 | 217 | } 218 | 219 | // If we're still here the use gave us his password, store it if keychain is in use 220 | if (passwordRef->useKeychain()) 221 | { 222 | if (passwordRef->rememberInKeychain() && passwordLength && passwordData) 223 | passwordRef->setData(passwordLength, passwordData); 224 | 225 | if ((flags & kSecPasswordSet) && passwordRef->rememberInKeychain()) 226 | passwordRef->save(); 227 | } 228 | 229 | END_SECAPI 230 | } 231 | -------------------------------------------------------------------------------- /Documents/SecPasswordAction/SecPassword.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2004 Apple Computer, Inc. All Rights Reserved. 3 | * 4 | * @APPLE_LICENSE_HEADER_START@ 5 | * 6 | * This file contains Original Code and/or Modifications of Original Code 7 | * as defined in and that are subject to the Apple Public Source License 8 | * Version 2.0 (the 'License'). You may not use this file except in 9 | * compliance with the License. Please obtain a copy of the License at 10 | * http://www.opensource.apple.com/apsl/ and read it before using this 11 | * file. 12 | * 13 | * The Original Code and all software distributed under the License are 14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 | * Please see the License for the specific language governing rights and 19 | * limitations under the License. 20 | * 21 | * @APPLE_LICENSE_HEADER_END@ 22 | */ 23 | 24 | /*! 25 | @header SecPassword 26 | SecPassword implements logic to use the system facilities for acquiring a password, 27 | optionally stored and retrieved from the user's keychain. 28 | */ 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | #ifndef _SECURITY_SECPASSWORD_H_ 35 | #define _SECURITY_SECPASSWORD_H_ 36 | 37 | #if defined(__cplusplus) 38 | extern "C" { 39 | #endif 40 | 41 | /*! 42 | @abstract Flags to specify SecPasswordAction behavior, as the application steps through the options 43 | Get, just get it. 44 | Get|Set, get it and set it if it wasn't in the keychain; client doesn't verify it before it's stored 45 | Get|Fail, get it and flag that the previously given or stored password is busted. 46 | Get|Set|Fail, same as above but also store it. 47 | New instead of Get toggles between asking for a new passphrase and an existing one. 48 | */ 49 | enum { 50 | kSecPasswordGet = 1<<0, // Get password from keychain or user 51 | kSecPasswordSet = 1<<1, // Set password (passed in if kSecPasswordGet not set, otherwise from user) 52 | kSecPasswordFail = 1<<2, // Wrong password (ignore item in keychain and flag error) 53 | kSecPasswordNew = 1<<3 // Explicitly get a new passphrase 54 | }; 55 | 56 | /*! 57 | @abstract Create an SecPassword object used to query and set a password used in the client. 58 | Keychain item attributes and class are optional, pass NULL if the password shouldn't be searched 59 | for or stored in the keychain. Use CFRelease on 60 | @param itemClass (in/opt) class of item to search for or store password as. 61 | @param attrList (in/opt) the list of attributes of the item to search or create. 62 | @param passwordRef (out) On return, a pointer to a password reference. Release this by calling CFRelease function. 63 | */ 64 | OSStatus SecGenericPasswordCreate(SecKeychainAttributeList *searchAttrList, SecKeychainAttributeList *itemAttrList, SecPasswordRef *itemRef); 65 | 66 | /*! 67 | @abstract Get the password for a SecPassword, either from the user or the keychain and return it. 68 | Use SecKeychainItemFreeContent to free the data. 69 | 70 | @param message Message to display to the user as a CFString or nil for a default message. 71 | (future extension accepts CFDictionary for other hints, icon, secaccess) 72 | @param flags (in) The mode of operation. See 73 | @param length (out) The length of the buffer pointed to by data. 74 | @param data A pointer to a buffer containing the data to store. 75 | 76 | */ 77 | OSStatus SecPasswordAction(SecPasswordRef itemRef, CFTypeRef message, UInt32 flags, UInt32 *length, const void **data); 78 | 79 | #if defined(__cplusplus) 80 | } 81 | #endif 82 | 83 | #endif /* !_SECURITY_SECPASSWORD_H_ */ 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Aaron Burghardt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name Aaron Burghardt nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Disk Arbitrator is a Mac OS X forensic utility designed to help the user ensure correct forensic procedures are followed during imaging of a disk device. 4 | Disk Arbitrator is essentially a user interface to the Disk Arbitration framework, which enables a program to participate in the management of block 5 | storage devices, including the automatic mounting of file systems. When enabled, Disk Arbitrator will block the mounting of file systems to avoid mounting as read-write and violating the integrity of the evidence. 6 | 7 | It is important to note that Disk Arbitrator is *not* a software write blocker---it does not change the state of currently attached devices nor does it affect newly attached devices to force a device to be read-only. The user still must be careful to not accidentally write to a disk with a command such as "dd". Owing to this fact, a hardware or software write-blocker may still be desirable for the most sound procedure. Disk Arbitrator compliments a write-blocker with additional useful features and eliminates the typical forensic recommendation to "disable disk arbitration." 8 | 9 | ## System Requirements 10 | 11 | * Intel Mac 12 | * OS X 10.5 or later 13 | 14 | ## Downloads 15 | 16 | You can find links to compiled executables on the [releases](https://github.com/aburgh/Disk-Arbitrator/releases) page. 17 | 18 | ## Quick Start 19 | 20 | ### Installation 21 | 22 | To install, drag the Disk Arbitrator application to the desired location, for example /Applications. 23 | 24 | You may optionally want to have Disk Arbitrator automatically running every time you log in. There are two ways to do this: 25 | 26 | * Add Disk Arbitrator to your Login Items in the User & Groups (or Accounts on older OS X versions) preference panel in System Preferences. 27 | 28 | * Use the included "Install User Launch Agent" feature (accessible from the menu). When installed, the system's launchd will automatically launch Disk Arbitrator when you log in, just like a Login Item. In addition, the plist contains a setting which instructs launchd to monitor the application and automatically relaunch it in the event of a crash or if otherwise quit. This offers the most assurance that Disk Arbitrator will be running whenever you are logged in. 29 | 30 | ### Usage 31 | 32 | When launched, it adds its icon to the status bar on the right side of the menu bar. The status bar icon indicates one of three states: 33 | 34 | * Green: the utility is activated and in Block Mounts mode. 35 | 36 | * Orange: the utility is activated and in Read-only mode. 37 | 38 | * Gray: the utility is deactivated and attached disks will be automatically mounted by the system. 39 | 40 | Disk Arbitrator continuously monitors for disks to appear and disappear and tracks the disks in the main window. When a new disk is attached, the system notifies Disk Arbitrator and gives it a chance to reject mounting of a disk volume. Disk Arbitrator responds as such: 41 | 42 | * When deactivated, it just observes the disk changes 43 | 44 | * When activated and in Block Mounts mode, it simply rejects every new system attempt to mount a volume. 45 | 46 | * When activated and in Read-only mode, it rejects the original mount action and automatically sends its own request to mount the volume, but it ensures the mount includes the option to make the file system read-only. It also checks the file system type and, if it is HFS, it includes the flag to ignore the journal. 47 | 48 | **Reminder:** Disk Arbitrator does its work by actively participating in the mounting process. If the utility is deactivated or is quit and not running, there is no protection from auto-mounting attached disks. However, once a disk appears and the process of either mounting or rejecting the mount is finished, then Disk Arbitrator may be quit without affecting the state of the disk. 49 | 50 | ### Working With Disk Images 51 | 52 | As of version 0.3.0, Disk Arbitrator has support for disk images. 53 | 54 | Using Disk Arbitrator's Attach Disk Image feature, attaching the disk image is effectively coordinated. When "Attach Disk Image..." is selected from the menu, an open panel appears which includes additional options for attaching and mounting the disk image. The default is to only attach the disk image. When the "Open" button is clicked, Disk Arbitrator attaches the disk using hdiutil. Once it is attached, Disk Arbitrator's normal behavior applies, so if it is activated and the mode is set to Read-Only, the volumes on the disk image will be mounted read-only. If the mode is set to Block Mounts, the volumes will be ignored. 55 | 56 | The disk image open panel includes an option to attempt to mount the disk image for when Disk Arbitrator isn't activated and is being used as a convenient means to attach a disk image. When the mount option is used, Disk Arbitrator passes "-mount optional" to hdiutil, which attempts to attach and mount the disk image, but with the benefit that if the mount fails, the disk image is not detached. 57 | 58 | Disk Arbitrator also supports drag and drop to attach a disk image. Simply drag one or more disk images from a Finder window to Disk Arbitrator's list of disks to initiate attaching the disk images. When using drag and drop, the default options include "-mount optional", so mounting is also attempted. If the mount fails, the disk image remains attached and can be mounted manually. 59 | 60 | Notes: 61 | 62 | * Attempting to attach and mount a disk image outside of Disk Arbitrator, either by double-clicking it in the Finder or using hdiutil attach, will behave as before: the disk image will be attached, then the system will attempt to mount it, Disk Arbitrator will reject the mount, and the disk image will be unattached because the mount failed. 63 | 64 | * If a disk image with a Software License Agreement is attached, Disk Arbitrator automatically replies "Yes" to the agreement. Caveat Emptor. 65 | 66 | ### A Note On Dirty Journals 67 | 68 | When set to Read-only mode, the mount request that Disk Arbitrator sends includes the option to ignore the journal, which is useful when the HFS file system was last detached without unmounting (e.g., the system crashed, or an external drive was unplugged without ejecting it). If you are working with a disk that was not cleanly ejected, then attempts to attach it read-only will normally fail because HFS knows the journal needs to be replayed and it is not allowed to make the changes, so it fails to mount. 69 | 70 | Mounting a disk image with a dirty file system can be achieved by using a shadow file, but this is less than ideal. The shadow file protects the original disk image from changes, but the file system is mounted read-write. A better option is to execute the two steps manually, using Terminal: 71 | 72 | 1. `hdiutil attach -nomount disk_image.dmg` 73 | 2. `mount_hfs -j -o rdonly /dev/diskx /mount/path` 74 | 75 | Disk Arbitrator provides a convenient way to execute the second step, and a future version will perform both steps in one operation. 76 | -------------------------------------------------------------------------------- /ReleaseNotes.markdown: -------------------------------------------------------------------------------- 1 | ## Release Notes 2 | 3 | #### 0.8 2017/9/16 4 | 5 | * Added "Show main window at activation" preference (thanks melomac) 6 | * Fix memory leaks 7 | 8 | #### 0.7 2016/8/3 9 | 10 | * Fix order of disks 11 | * Fix crash during eject 12 | 13 | #### 0.6 2016/4/21 14 | 15 | * Fix read-only automatic mounts not working on OS X 10.11+ 16 | * Disable the "ignore journal" mount option for non-HFS disks 17 | 18 | #### 0.5 2015/9/9 19 | 20 | * Added lock icon to disks that are read-only 21 | * Fixed launchd plist containing hard coded app path 22 | * Improved compatibility with 10.10 23 | * Improved code stability 24 | 25 | #### 0.4.2 2012/11/23 26 | 27 | * Added Developer ID signature for Gatekeeper. 28 | * Updated for Xcode 4.5 (should have no affect on behavior). 29 | * Removed PPC architecture. 30 | 31 | #### 0.4.1 2012/4/8 32 | 33 | * Added feature to install/uninstall a launchd agent plist. 34 | * Added labels and tool tips to make mount options easier to understand. 35 | 36 | #### 0.4.0 2011/4/20 37 | 38 | * Fixed overwriting AppLogLevel preference at startup. 39 | * Added log message for mount approval callback. 40 | * Activate application when showing the main window, Preferences, or About panel. 41 | * Changed Attach Disk Image icon. 42 | * Changed mode and activation state to persist across application restart. 43 | * Enabled Sudden Termination (new in Snow Leopard for faster shutdowns). 44 | 45 | #### 0.3.2 2010/8/23 46 | 47 | * Fixed use of Snow Leopard API to retrieve disk icons. 48 | * Minor code cleanup. 49 | 50 | #### 0.3.1 2010/8/9 51 | 52 | * Added Preferences window and an option to show the main window at launch. 53 | 54 | #### 0.3.0 2010/3/29 55 | 56 | * Added Attach Disk Image feature. 57 | * Added check for encrypted disk image. 58 | * Added support for attaching a disk image by dragging the file into the disks window. 59 | * Added a progress window while a disk image is verified. 60 | * Added disk menu to the status item menu. 61 | * Improved validation of toolbar items. 62 | 63 | #### 0.2.0 2010/2/14 64 | 65 | * Added Disk Info window. 66 | * Added mounting feature with dialog for options and mountpoint path. 67 | * Added unmounting and ejecting. 68 | * Added toolbar and custom icons. 69 | * Improved logging. 70 | 71 | #### 0.1.1 2010/2/7 72 | 73 | * Added target to build the distribution DMG. 74 | * Fixed refresh bug when a disk is mounted or unmounted. 75 | * Added controls to the main window to activate and change mode. 76 | 77 | #### 0.1.0 2010/1/31 78 | 79 | * Initial release 80 | -------------------------------------------------------------------------------- /Resources/DiskArbitrator-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | App Icon.icns 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 0.8 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | LSMinimumSystemVersionByArchitecture 28 | 29 | x86_64 30 | 10.6 31 | 32 | LSUIElement 33 | 34 | NSMainNibFile 35 | MainMenu 36 | NSPrincipalClass 37 | NSApplication 38 | NSSupportsSuddenTermination 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Resources/StatusItem Disabled 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/StatusItem Disabled 1.png -------------------------------------------------------------------------------- /Resources/StatusItem Disabled 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/StatusItem Disabled 2.png -------------------------------------------------------------------------------- /Resources/StatusItem Green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/StatusItem Green.png -------------------------------------------------------------------------------- /Resources/StatusItem Orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/StatusItem Orange.png -------------------------------------------------------------------------------- /Resources/ToolbarItem Attach Disk Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/ToolbarItem Attach Disk Image.png -------------------------------------------------------------------------------- /Resources/ToolbarItem Attach Disk Plug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/ToolbarItem Attach Disk Plug.png -------------------------------------------------------------------------------- /Resources/ToolbarItem Eject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/ToolbarItem Eject.png -------------------------------------------------------------------------------- /Resources/ToolbarItem Info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/ToolbarItem Info.png -------------------------------------------------------------------------------- /Resources/ToolbarItem Mount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aburgh/Disk-Arbitrator/23e687f868ed9e084660b8620801413297546fc5/Resources/ToolbarItem Mount.png -------------------------------------------------------------------------------- /Source/AppError.h: -------------------------------------------------------------------------------- 1 | /* 2 | * AppError.h 3 | * DiskArbitrator 4 | * 5 | * Created by Aaron Burghardt on 1/24/10. 6 | * Copyright 2010 Aaron Burghardt. All rights reserved. 7 | * 8 | */ 9 | 10 | /* 11 | Log levels: 12 | 13 | LOG_EMERG A panic condition. This is normally broadcast to all users. 14 | 15 | LOG_ALERT A condition that should be corrected immediately, such as a corrupted system database. 16 | 17 | LOG_CRIT Critical conditions, e.g., hard device errors. 18 | 19 | LOG_ERR Errors. 20 | 21 | LOG_WARNING Warning messages. 22 | 23 | LOG_NOTICE Conditions that are not error conditions, but should possibly be handled specially. 24 | 25 | LOG_INFO Informational messages. 26 | 27 | LOG_DEBUG Messages that contain information normally of use only when debugging a program. 28 | 29 | */ 30 | 31 | #include 32 | 33 | void Log(NSInteger level, NSString *format, ...); 34 | 35 | void SetAppLogLevel(NSInteger level); 36 | void SetShouldLogToSyslog(BOOL flag); 37 | 38 | extern NSString * const AppErrorDomain; 39 | extern NSString * const AppLogLevelDefaultsKey; 40 | extern NSString * const AppShouldEnableSyslogDefaultsKey; -------------------------------------------------------------------------------- /Source/AppError.m: -------------------------------------------------------------------------------- 1 | /* 2 | * AppError.m 3 | * DiskArbitrator 4 | * 5 | * Created by Aaron Burghardt on 1/24/10. 6 | * Copyright 2010 Aaron Burghardt. All rights reserved. 7 | * 8 | */ 9 | 10 | #import "AppError.h" 11 | #include 12 | #include 13 | 14 | void Log(NSInteger level, NSString *format, ...) 15 | { 16 | va_list args; 17 | NSString *formattedError; 18 | 19 | if (level > [[NSUserDefaults standardUserDefaults] integerForKey:AppLogLevelDefaultsKey]) 20 | return; 21 | 22 | va_start(args, format); 23 | 24 | formattedError = [[NSString alloc] initWithFormat:format arguments:args]; 25 | 26 | va_end(args); 27 | 28 | const char *utfFormattedError = [formattedError UTF8String]; 29 | 30 | BOOL shouldUseSyslog = [[NSUserDefaults standardUserDefaults] boolForKey:AppShouldEnableSyslogDefaultsKey]; 31 | 32 | if (shouldUseSyslog) 33 | syslog((int)level, "%s\n", utfFormattedError); 34 | 35 | os_log(OS_LOG_DEFAULT, "%s\n", utfFormattedError); 36 | } 37 | 38 | void SetAppLogLevel(NSInteger level) 39 | { 40 | [[NSUserDefaults standardUserDefaults] setInteger:level forKey:AppLogLevelDefaultsKey]; 41 | } 42 | 43 | void SetShouldLogToSyslog(BOOL flag) 44 | { 45 | [[NSUserDefaults standardUserDefaults] setBool:flag forKey:AppShouldEnableSyslogDefaultsKey]; 46 | } 47 | 48 | NSString * const AppErrorDomain = @"AppErrorDomain"; 49 | NSString * const AppLogLevelDefaultsKey = @"AppLogLevel"; 50 | NSString * const AppShouldEnableSyslogDefaultsKey = @"AppShouldEnableSyslog"; 51 | -------------------------------------------------------------------------------- /Source/Arbitrator.h: -------------------------------------------------------------------------------- 1 | // 2 | // Arbitrator.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/10/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | // Mount Modes 13 | #define MM_BLOCK 0 14 | #define MM_READONLY 1 15 | 16 | 17 | @interface Arbitrator : NSObject 18 | 19 | @property (retain) NSMutableSet *disks; 20 | @property (readonly) NSSet *wholeDisks; 21 | @property BOOL isActivated; 22 | @property NSInteger mountMode; 23 | @property (readonly) NSString *dissenterMessage; 24 | 25 | - (BOOL)registerSession; 26 | - (void)unregisterSession; 27 | - (BOOL)registerApprovalSession; 28 | - (void)unregisterApprovalSession; 29 | 30 | - (BOOL)activate; 31 | - (void)deactivate; 32 | 33 | @end 34 | 35 | DADissenterRef DiskMountApprovalCallback(DADiskRef disk, void *arbitrator); 36 | 37 | extern NSString * const ArbitratorIsEnabled; 38 | extern NSString * const ArbitratorMountMode; 39 | -------------------------------------------------------------------------------- /Source/Arbitrator.m: -------------------------------------------------------------------------------- 1 | // 2 | // Arbitrator.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/10/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import "Arbitrator.h" 10 | #import "AppError.h" 11 | #import "Disk.h" 12 | #import "DiskArbitrationPrivateFunctions.h" 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | @interface Arbitrator () 17 | { 18 | DAApprovalSessionRef approvalSession; 19 | } 20 | 21 | @end 22 | 23 | //////////////////////////////////////////////////////////////////////////////// 24 | 25 | @implementation Arbitrator 26 | 27 | @synthesize disks; 28 | 29 | + (void)initialize 30 | { 31 | InitializeDiskArbitration(); 32 | 33 | NSMutableDictionary *defaults = [NSMutableDictionary dictionary]; 34 | [defaults setObject:[NSNumber numberWithBool:YES] forKey:ArbitratorIsEnabled]; 35 | [defaults setObject:[NSNumber numberWithInteger:0] forKey:ArbitratorMountMode]; 36 | [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; 37 | } 38 | 39 | + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key 40 | { 41 | if ([key isEqual:@"wholeDisks"]) 42 | return [NSSet setWithObject:@"disks"]; 43 | 44 | return [super keyPathsForValuesAffectingValueForKey:key]; 45 | } 46 | 47 | - (id)init 48 | { 49 | self = [super init]; 50 | if (self) 51 | { 52 | disks = [NSMutableSet new]; 53 | [self registerSession]; 54 | 55 | if ([[NSUserDefaults standardUserDefaults] boolForKey:ArbitratorIsEnabled]) { 56 | if ([self activate] == NO) { 57 | [self release]; 58 | return nil; 59 | } 60 | } 61 | } 62 | return self; 63 | } 64 | 65 | - (void)dealloc 66 | { 67 | if (approvalSession) 68 | [self deactivate]; 69 | 70 | [self unregisterSession]; 71 | 72 | [disks release]; 73 | [super dealloc]; 74 | } 75 | 76 | - (BOOL)registerSession 77 | { 78 | NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 79 | 80 | [nc addObserver:self selector:@selector(diskDidAppear:) name:DADiskDidAppearNotification object:nil]; 81 | [nc addObserver:self selector:@selector(diskDidDisappear:) name:DADiskDidDisappearNotification object:nil]; 82 | [nc addObserver:self selector:@selector(diskDidChange:) name:DADiskDidChangeNotification object:nil]; 83 | 84 | return YES; 85 | } 86 | 87 | - (void)unregisterSession 88 | { 89 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 90 | } 91 | 92 | - (void)diskDidAppear:(NSNotification *)notif 93 | { 94 | Disk *disk = notif.object; 95 | 96 | Log(LOG_DEBUG, @"%s disk: %@", __func__, disk.BSDName); 97 | 98 | [self addDisksObject:disk]; 99 | } 100 | 101 | - (void)diskDidDisappear:(NSNotification *)notif 102 | { 103 | [self removeDisksObject:notif.object]; 104 | } 105 | 106 | - (void)diskDidChange:(NSNotification *)notif 107 | { 108 | Log(LOG_DEBUG, @"Changed disk notification: %@", notif.description); 109 | } 110 | 111 | - (BOOL)registerApprovalSession 112 | { 113 | approvalSession = DAApprovalSessionCreate(kCFAllocatorDefault); 114 | if (!approvalSession) { 115 | Log(LOG_CRIT, @"Failed to create Disk Arbitration approval session."); 116 | return NO; 117 | } 118 | 119 | DAApprovalSessionScheduleWithRunLoop(approvalSession, CFRunLoopGetMain(), kCFRunLoopCommonModes); 120 | 121 | DARegisterDiskMountApprovalCallback(approvalSession, NULL, DiskMountApprovalCallback, self); 122 | 123 | return YES; 124 | } 125 | 126 | - (void)unregisterApprovalSession 127 | { 128 | if (approvalSession) { 129 | DAUnregisterApprovalCallback(approvalSession, DiskMountApprovalCallback, self); 130 | 131 | DAApprovalSessionUnscheduleFromRunLoop(approvalSession, CFRunLoopGetMain(), kCFRunLoopCommonModes); 132 | SafeCFRelease(approvalSession); 133 | approvalSession = NULL; 134 | } 135 | } 136 | 137 | - (void)mountApprovedDisk:(Disk *)disk 138 | { 139 | NSAssert(self.isActivated, @"bug"); 140 | 141 | NSArray *args = disk.mountArgs; 142 | NSString *path = disk.mountPath; 143 | if (!args || !args.count) { 144 | NSAssert(self.mountMode == MM_READONLY, @"Unknown mount mode"); 145 | 146 | // Arguments will be passed via the -o flag of mount. If the file system specific mount, e.g. mount_hfs, 147 | // supports additional flags that mount(8) doesn't, they can be passed to -o. That feature is used to 148 | // pass -j to mount_hfs, which instructs HFS to ignore journal. Normally, an HFS volume that 149 | // has a dirty journal will fail to mount read-only because the file system is inconsistent. "-j" is 150 | // a work-around. 151 | 152 | if (disk.isHFS) 153 | args = [NSArray arrayWithObjects:@"-j", @"rdonly", nil]; 154 | else 155 | args = [NSArray arrayWithObjects:@"rdonly", nil]; 156 | path = nil; 157 | } 158 | [disk mountAtPath:path withArguments:args]; 159 | } 160 | 161 | - (NSString *)dissenterMessage 162 | { 163 | return @"Disk Arbitrator is in charge"; 164 | } 165 | 166 | - (DADissenterRef)defaultDissenter __attribute__((cf_returns_retained)) 167 | { 168 | return DADissenterCreate(kCFAllocatorDefault, kDAReturnNotPermitted, (CFStringRef)self.dissenterMessage); 169 | } 170 | 171 | - (DADissenterRef)approveMount:(Disk *)disk __attribute__((cf_returns_retained)) 172 | { 173 | if (self.isActivated) { 174 | // Block mode prevents everything from mounting, unless this disk is being mounted from our GUI 175 | if (self.mountMode == MM_BLOCK && !disk.isMounting) { 176 | return [self defaultDissenter]; 177 | } 178 | 179 | // When an approve mount callback is received, we have no idea if this approval was from 180 | // a mount that belongs to us, or someone else. So we track whether we have rejected a 181 | // mount request, and only allow mounts after we have rejected the initial request. 182 | if (!disk.rejectedMount) { 183 | disk.rejectedMount = YES; 184 | // Do the mount after a slight delay to allow time for this approval to finish 185 | [self performSelector:@selector(mountApprovedDisk:) withObject:disk afterDelay:0.1]; 186 | return [self defaultDissenter]; 187 | } else { 188 | // Allow the mount since we previously rejected it 189 | NSAssert(disk.isMounting == YES, @"invalid state"); 190 | disk.isMounting = NO; 191 | disk.rejectedMount = NO; 192 | return NULL; 193 | } 194 | } 195 | 196 | // Not activated, all mounting of everything 197 | return NULL; 198 | } 199 | 200 | - (BOOL)activate 201 | { 202 | BOOL success; 203 | 204 | [self willChangeValueForKey:@"isActivated"]; 205 | success = [self registerApprovalSession]; 206 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:ArbitratorIsEnabled]; 207 | [self didChangeValueForKey:@"isActivated"]; 208 | 209 | return success; 210 | } 211 | 212 | - (void)deactivate 213 | { 214 | [self willChangeValueForKey:@"isActivated"]; 215 | [self unregisterApprovalSession]; 216 | [[NSUserDefaults standardUserDefaults] setBool:NO forKey:ArbitratorIsEnabled]; 217 | [self didChangeValueForKey:@"isActivated"]; 218 | } 219 | 220 | - (BOOL)isActivated 221 | { 222 | return approvalSession ? YES : NO; 223 | } 224 | 225 | - (void)setIsActivated:(BOOL)shouldActivate 226 | { 227 | if (shouldActivate && !self.isActivated) 228 | [self activate]; 229 | 230 | else if (!shouldActivate && self.isActivated) 231 | [self deactivate]; 232 | } 233 | 234 | - (NSInteger)mountMode { 235 | return [[NSUserDefaults standardUserDefaults] integerForKey:ArbitratorMountMode]; 236 | } 237 | 238 | - (void)setMountMode:(NSInteger)mountMode { 239 | NSInteger currentMode = [[NSUserDefaults standardUserDefaults] integerForKey:ArbitratorMountMode]; 240 | if (currentMode != mountMode) { 241 | [self willChangeValueForKey:@"mountMode"]; 242 | [[NSUserDefaults standardUserDefaults] setInteger:mountMode forKey:ArbitratorMountMode]; 243 | [self didChangeValueForKey:@"mountMode"]; 244 | } 245 | } 246 | 247 | - (NSSet *)wholeDisks 248 | { 249 | NSMutableSet *wholeDisks = [NSMutableSet new]; 250 | 251 | for (Disk *disk in disks) 252 | if (disk.isWholeDisk) 253 | [wholeDisks addObject:disk]; 254 | 255 | return [wholeDisks autorelease]; 256 | } 257 | 258 | #pragma mark Disks KVC Methods 259 | 260 | - (NSUInteger)countOfDisks 261 | { 262 | return disks.count; 263 | } 264 | 265 | - (NSEnumerator *)enumeratorOfDisks 266 | { 267 | return [disks objectEnumerator]; 268 | } 269 | 270 | - (Disk *)memberOfDisks:(Disk *)anObject 271 | { 272 | return [disks member:anObject]; 273 | } 274 | 275 | - (void)addDisksObject:(Disk *)object 276 | { 277 | [disks addObject:object]; 278 | } 279 | 280 | - (void)addDisks:(NSSet *)objects 281 | { 282 | [disks unionSet:objects]; 283 | } 284 | 285 | - (void)removeDisksObject:(Disk *)anObject 286 | { 287 | if (anObject) { 288 | [disks removeObject:anObject]; 289 | } 290 | } 291 | 292 | - (void)removeDisks:(NSSet *)objects 293 | { 294 | [disks minusSet:objects]; 295 | } 296 | 297 | @end 298 | 299 | #pragma mark Callbacks 300 | 301 | DADissenterRef __attribute__((cf_returns_retained)) DiskMountApprovalCallback(DADiskRef diskRef, void *arbitrator) 302 | { 303 | Log(LOG_DEBUG, @"%s called: %p %s", __func__, diskRef, DADiskGetBSDName(diskRef)); 304 | Log(LOG_DEBUG, @"\t claimed: %s", DADiskIsClaimed(diskRef) ? "Yes" : "No"); 305 | 306 | Disk *disk = [Disk uniqueDiskForDADisk:diskRef create:YES]; 307 | 308 | Log(LOG_DEBUG, @"%@", disk.diskDescription); 309 | 310 | DADissenterRef dissenter = [(Arbitrator*)arbitrator approveMount:disk]; 311 | 312 | Log(LOG_DEBUG, @"Mount allowed: %s", dissenter ? "No" : "Yes"); 313 | return dissenter; 314 | } 315 | 316 | NSString * const ArbitratorIsEnabled = @"ArbitratorIsEnabled"; 317 | NSString * const ArbitratorMountMode = @"ArbitratorMountMode"; 318 | 319 | -------------------------------------------------------------------------------- /Source/AttachDiskImageController.h: -------------------------------------------------------------------------------- 1 | // 2 | // AttachDiskImageController.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/21/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import 10 | #import "SheetController.h" 11 | 12 | @interface AttachDiskImageController : SheetController 13 | 14 | @property (retain) IBOutlet NSView *view; 15 | @property (retain) NSTask *task; 16 | @property (copy) NSString *title; 17 | @property (copy) NSString *message; 18 | @property (copy) NSString *errorMessage; 19 | @property double progress; 20 | @property BOOL isVerifying; 21 | @property BOOL canceled; 22 | 23 | + (NSArray *)diskImageFileExtensions; 24 | 25 | - (NSTask *)hdiutilTaskWithCommand:(NSString *)command path:(NSString *)path options:(NSArray *)options password:(NSString *)password; 26 | 27 | - (BOOL)getDiskImagePropertyList:(id *)outPlist atPath:(NSString *)path command:(NSString *)command password:(NSString *)password error:(NSError **)outError; 28 | 29 | - (BOOL)getDiskImageEncryptionStatus:(BOOL *)outFlag atPath:(NSString *)path error:(NSError **)outError; 30 | 31 | - (BOOL)getDiskImageSLAStatus:(BOOL *)outFlag atPath:(NSString *)path password:(NSString *)password error:(NSError **)outError; 32 | 33 | - (BOOL)attachDiskImageAtPath:(NSString *)path options:(NSArray *)options password:(NSString *)password error:(NSError **)error; 34 | 35 | - (void)attachDiskImageOptionsSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; 36 | 37 | - (void)performAttachDiskImage; 38 | 39 | - (IBAction)performAttachDiskImage:(id)sender; 40 | 41 | - (IBAction)cancel:(id)sender; 42 | 43 | - (IBAction)skip:(id)sender; 44 | 45 | #pragma mark Delegates 46 | 47 | - (void)panelSelectionDidChange:(id)sender; 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /Source/AttachDiskImageController.m: -------------------------------------------------------------------------------- 1 | // 2 | // AttachDiskImageController.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/21/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import "AttachDiskImageController.h" 10 | #import 11 | #import 12 | #import "AppError.h" 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | @interface AttachDiskImageController () 17 | { 18 | NSMutableString *stdoutBuffer; 19 | NSMutableString *stderrBuffer; 20 | } 21 | 22 | @end 23 | 24 | //////////////////////////////////////////////////////////////////////////////// 25 | 26 | @implementation AttachDiskImageController 27 | 28 | @synthesize view; 29 | @synthesize task; 30 | @synthesize title; 31 | @synthesize message; 32 | @synthesize errorMessage; 33 | @synthesize progress; 34 | @synthesize isVerifying; 35 | @synthesize canceled; 36 | 37 | + (NSArray *)diskImageFileExtensions; 38 | { 39 | static NSArray *diskImageFileExtensions = nil; 40 | 41 | if (!diskImageFileExtensions) 42 | diskImageFileExtensions = [[NSArray alloc] initWithObjects:@"img", @"dmg", @"sparseimage", @"sparsebundle", @"iso", @"cdr", nil]; 43 | 44 | return diskImageFileExtensions; 45 | } 46 | 47 | - (void)dealloc 48 | { 49 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 50 | [stdoutBuffer release]; 51 | [stderrBuffer release]; 52 | [errorMessage release]; 53 | [message release]; 54 | [title release]; 55 | [task release]; 56 | [view release]; 57 | [super dealloc]; 58 | } 59 | 60 | - (NSTask *)hdiutilTaskWithCommand:(NSString *)command path:(NSString *)path options:(NSArray *)options password:(NSString *)password 61 | { 62 | Log(LOG_DEBUG, @"%s command: %@ path: %@ options: %@", __func__, command, path, options); 63 | 64 | NSTask *newTask; 65 | NSFileHandle *stdinHandle; 66 | 67 | newTask = [[NSTask new] autorelease]; 68 | newTask.launchPath = @"/usr/bin/hdiutil"; 69 | 70 | NSMutableArray *arguments = [NSMutableArray arrayWithObject:command]; 71 | [arguments addObject:path]; 72 | [arguments addObjectsFromArray:options]; 73 | 74 | newTask.standardOutput = NSPipe.pipe; 75 | newTask.standardError = NSPipe.pipe; 76 | newTask.standardInput = NSPipe.pipe; 77 | 78 | if (password) { 79 | stdinHandle = [newTask.standardInput fileHandleForWriting]; 80 | [stdinHandle writeData:[password dataUsingEncoding:NSUTF8StringEncoding]]; 81 | [stdinHandle writeData:[NSData dataWithBytes:"" length:1]]; 82 | 83 | [arguments addObject:@"-stdinpass"]; 84 | } 85 | 86 | newTask.arguments = arguments; 87 | 88 | return newTask; 89 | } 90 | 91 | - (BOOL)getDiskImagePropertyList:(id *)outPlist atPath:(NSString *)path command:(NSString *)command password:(NSString *)password error:(NSError **)outError 92 | { 93 | BOOL retval = YES; 94 | NSMutableDictionary *info; 95 | NSString *failureReason = nil; 96 | NSData *outputData; 97 | NSTask *newTask; 98 | 99 | NSArray *options = [NSArray arrayWithObjects:@"-plist", nil]; 100 | newTask = [self hdiutilTaskWithCommand:command path:path options:options password:password]; 101 | [newTask launch]; 102 | [newTask waitUntilExit]; 103 | 104 | outputData = [[newTask.standardOutput fileHandleForReading] readDataToEndOfFile]; 105 | 106 | if (newTask.terminationStatus == 0) { 107 | NSError *plistErr = nil; 108 | *outPlist = [NSPropertyListSerialization propertyListWithData:outputData options:NSPropertyListImmutable format:nil error:&plistErr]; 109 | if (plistErr) { 110 | failureReason = plistErr.localizedDescription; 111 | } 112 | if (!*outPlist) { 113 | Log(LOG_ERR, @"Plist deserialization error: %@", failureReason); 114 | failureReason = NSLocalizedString(@"hdiutil output is not a property list.", nil); 115 | retval = NO; 116 | } 117 | } 118 | else { 119 | Log(LOG_ERR, @"hdiutil termination status: %d", newTask.terminationStatus); 120 | failureReason = NSLocalizedString(@"hdiutil ended abnormally.", nil); 121 | retval = NO; 122 | } 123 | 124 | if (retval == NO && outError) { 125 | info = [NSMutableDictionary dictionaryWithObjectsAndKeys: 126 | NSLocalizedString(@"Error executing hdiutil command", nil), NSLocalizedDescriptionKey, 127 | failureReason, NSLocalizedFailureReasonErrorKey, 128 | failureReason, NSLocalizedRecoverySuggestionErrorKey, 129 | nil]; 130 | *outError = [NSError errorWithDomain:AppErrorDomain code:-1 userInfo:info]; 131 | } 132 | 133 | return retval; 134 | } 135 | 136 | - (BOOL)getDiskImageEncryptionStatus:(BOOL *)outFlag atPath:(NSString *)path error:(NSError **)outError 137 | { 138 | BOOL isOK = YES; 139 | NSMutableDictionary *plist; 140 | id value; 141 | 142 | isOK = [self getDiskImagePropertyList:&plist atPath:path command:@"isencrypted" password:nil error:outError]; 143 | if (isOK) { 144 | value = [plist objectForKey:@"encrypted"]; 145 | if (value) { 146 | *outFlag = [value boolValue]; 147 | } 148 | else { 149 | NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys: 150 | NSLocalizedString(@"Failed to get encryption property", nil), NSLocalizedDescriptionKey, 151 | NSLocalizedString(@"Check that \"/usr/bin/hdiutil isencrypted\" is functioning correctly.", nil), 152 | NSLocalizedRecoverySuggestionErrorKey, 153 | nil]; 154 | if (outError != nil) { 155 | *outError = [NSError errorWithDomain:AppErrorDomain code:-1 userInfo:info]; 156 | } 157 | isOK = NO; 158 | } 159 | } 160 | 161 | return isOK; 162 | } 163 | 164 | - (BOOL)getDiskImageSLAStatus:(BOOL *)outFlag atPath:(NSString *)path password:(NSString *)password error:(NSError **)outError 165 | { 166 | BOOL isOK = YES; 167 | NSMutableDictionary *plist; 168 | 169 | isOK = [self getDiskImagePropertyList:&plist atPath:path command:@"imageinfo" password:password error:outError]; 170 | if (isOK) { 171 | id value = [plist valueForKeyPath:@"Properties.Software License Agreement"]; 172 | if (value) { 173 | *outFlag = [value boolValue]; 174 | } 175 | else { 176 | NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys: 177 | NSLocalizedString(@"Failed to get SLA property", nil), NSLocalizedDescriptionKey, 178 | NSLocalizedString(@"Check that \"/usr/bin/hdiutil imageinfo\" is functioning correctly.", nil), 179 | NSLocalizedRecoverySuggestionErrorKey, 180 | nil]; 181 | if (outError != nil) { 182 | *outError = [NSError errorWithDomain:AppErrorDomain code:-1 userInfo:info]; 183 | } 184 | isOK = NO; 185 | } 186 | } 187 | return isOK; 188 | } 189 | 190 | #pragma mark Attaching 191 | 192 | - (void)hdiutilAttachDidTerminate:(NSNotification *)notif 193 | { 194 | [self.window orderOut:self]; 195 | 196 | NSTask *theTask = notif.object; 197 | 198 | if (!canceled && theTask.terminationStatus != 0) { 199 | 200 | NSMutableDictionary *info = [NSMutableDictionary dictionary]; 201 | 202 | [info setObject:[NSString stringWithFormat:@"%@: %@.", NSLocalizedString(@"Error running hdiutil", nil), self.errorMessage] 203 | forKey:NSLocalizedDescriptionKey]; 204 | 205 | [info setObject:self.errorMessage 206 | forKey:NSLocalizedFailureReasonErrorKey]; 207 | 208 | [info setObject:NSLocalizedString(@"Check the system log for details.", nil) 209 | forKey:NSLocalizedRecoverySuggestionErrorKey]; 210 | 211 | Log(LOG_ERR, @"%s termination status: (%d) %@", __func__, theTask.terminationStatus, self.errorMessage); 212 | 213 | NSError *error = [NSError errorWithDomain:AppErrorDomain code:theTask.terminationStatus userInfo:info]; 214 | [NSApp presentError:error]; 215 | } 216 | self.task = nil; 217 | [self autorelease]; 218 | } 219 | 220 | - (NSString *)promptUserForPasswordAtPath:(NSString *)path error:(NSError **)outError 221 | { 222 | SFAuthorization *authorization; 223 | AuthorizationRights rights; 224 | AuthorizationEnvironment env; 225 | AuthorizationFlags flags; 226 | AuthorizationItemSet *info; 227 | OSStatus status; 228 | NSString *password; 229 | 230 | NSString *fileName = path.lastPathComponent; 231 | NSString *prompt = [NSString stringWithFormat:NSLocalizedString(@"Enter password to access %@", nil), fileName]; 232 | 233 | AuthorizationItem rightsItems[1] = { { "com.apple.builtin.generic-unlock", 0, NULL, 0 } }; 234 | rights.count = sizeof(rightsItems) / sizeof(AuthorizationItem);; 235 | rights.items = rightsItems; 236 | 237 | AuthorizationItem envItems[1] = { 238 | { kAuthorizationEnvironmentPrompt, strlen([prompt UTF8String]), (void *)[prompt UTF8String], 0 } 239 | // { kAuthorizationEnvironmentPassword, 0, NULL, 0 } 240 | }; 241 | env.count = sizeof(envItems) / sizeof(AuthorizationItem); 242 | env.items = envItems; 243 | 244 | flags = kAuthorizationFlagDefaults| kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize; 245 | 246 | authorization = [SFAuthorization authorization]; 247 | 248 | if (![authorization obtainWithRights:&rights flags:flags environment:&env authorizedRights:NULL error:outError]) 249 | { 250 | if (outError && [*outError code] != errAuthorizationCanceled) 251 | Log(LOG_ERR, @"Authorization error: %@", *outError); 252 | return nil; 253 | } 254 | 255 | password = nil; 256 | 257 | status = AuthorizationCopyInfo([authorization authorizationRef], kAuthorizationEnvironmentPassword, &info); 258 | 259 | if (status == noErr) { 260 | if (info->count > 0 && info->items[0].valueLength > 0) 261 | password = [NSString stringWithUTF8String:info->items[0].value]; 262 | } 263 | else { 264 | if (outError) { 265 | NSDictionary *info; 266 | info = [NSDictionary dictionaryWithObject:NSLocalizedString(@"Authorization did not return a password.", nil) 267 | forKey:NSLocalizedDescriptionKey]; 268 | *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:info]; 269 | } 270 | } 271 | 272 | AuthorizationFreeItemSet(info); 273 | 274 | return password; 275 | } 276 | 277 | - (BOOL)attachDiskImageAtPath:(NSString *)path options:(NSArray *)options password:(NSString *)password error:(NSError **)outError 278 | { 279 | Log(LOG_DEBUG, @"%s path: %@ options: %@", __func__, path, options); 280 | 281 | BOOL isEncrypted = NO, hasSLA = NO; 282 | NSTask *newTask; 283 | 284 | if (!password) { 285 | if ([self getDiskImageEncryptionStatus:&isEncrypted atPath:path error:outError]) { 286 | if (isEncrypted) { 287 | password = [self promptUserForPasswordAtPath:path error:outError]; 288 | if (!password) 289 | return [*outError code] == errAuthorizationCanceled ? YES : NO; 290 | } 291 | } 292 | else { 293 | return NO; // get encryption status failed 294 | } 295 | } 296 | 297 | if ([self getDiskImageSLAStatus:&hasSLA atPath:path password:password error:outError] == NO) 298 | return NO; 299 | 300 | self.title = [NSString stringWithFormat:@"Attaching \"%@\" ...", path.lastPathComponent]; 301 | 302 | NSMutableArray *arguments = [NSMutableArray array]; 303 | [arguments addObject:@"-plist"]; 304 | [arguments addObject:@"-puppetstrings"]; 305 | [arguments addObjectsFromArray:options]; 306 | 307 | newTask = [self hdiutilTaskWithCommand:@"attach" path:path options:arguments password:password]; 308 | 309 | if (hasSLA) { 310 | [[newTask.standardInput fileHandleForWriting] writeData:[NSData dataWithBytes:"Y\n" length:3]]; 311 | } 312 | 313 | [[NSNotificationCenter defaultCenter] addObserver:self 314 | selector:@selector(processStandardOutput:) 315 | name:NSFileHandleReadCompletionNotification 316 | object:[newTask.standardOutput fileHandleForReading]]; 317 | [[newTask.standardOutput fileHandleForReading] readInBackgroundAndNotify]; 318 | 319 | [[NSNotificationCenter defaultCenter] addObserver:self 320 | selector:@selector(processStandardError:) 321 | name:NSFileHandleReadCompletionNotification 322 | object:[newTask.standardError fileHandleForReading]]; 323 | [[newTask.standardError fileHandleForReading] readInBackgroundAndNotify]; 324 | 325 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hdiutilAttachDidTerminate:) name:NSTaskDidTerminateNotification object:newTask]; 326 | [newTask launch]; 327 | self.task = newTask; 328 | [self retain]; 329 | return YES; 330 | } 331 | 332 | - (void)attachDiskImageOptionsSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; 333 | { 334 | if (sheet.isSheet) 335 | [sheet orderOut:self]; 336 | 337 | NSString *password = nil; 338 | BOOL isOK; 339 | NSError *error; 340 | NSMutableArray *attachOptions = [NSMutableArray array]; 341 | 342 | if (returnCode == NSModalResponseOK) { 343 | NSDictionary *options = self.userInfo; 344 | 345 | if ([[options objectForKey:@"readOnly"] boolValue] == YES) 346 | [attachOptions addObject:@"-readonly"]; 347 | 348 | if ([[options objectForKey:@"noVerify"] boolValue] == YES) 349 | [attachOptions addObject:@"-noverify"]; 350 | else 351 | [attachOptions addObject:@"-verify"]; 352 | 353 | if ([[options objectForKey:@"attemptMount"] boolValue] == YES) 354 | { 355 | [attachOptions addObject:@"-mount"]; 356 | [attachOptions addObject:@"optional"]; 357 | 358 | if ([[options objectForKey:@"noOwners"] boolValue] == YES) { 359 | [attachOptions addObject:@"-owners"]; 360 | [attachOptions addObject:@"off"]; 361 | } 362 | 363 | if ([[options objectForKey:@"noBrowse"] boolValue] == YES) 364 | [attachOptions addObject:@"-nobrowse"]; 365 | 366 | NSString *rootPath = [options objectForKey:@"rootPath"]; 367 | if (rootPath && [rootPath length]) { 368 | [attachOptions addObject:@"-mountroot"]; 369 | [attachOptions addObject:rootPath]; 370 | } 371 | } 372 | else { 373 | [attachOptions addObject:@"-nomount"]; 374 | } 375 | 376 | password = [options objectForKey:@"password"]; 377 | 378 | isOK = [self attachDiskImageAtPath:[options objectForKey:@"filePath"] options:attachOptions password:password error:&error]; 379 | if (!isOK) [NSApp presentError:error]; 380 | } 381 | } 382 | 383 | #pragma mark Actions 384 | 385 | - (void)performAttachDiskImage 386 | { 387 | NSOpenPanel *panel = [NSOpenPanel openPanel]; 388 | panel.canChooseFiles = YES; 389 | panel.canChooseDirectories = NO; 390 | panel.allowsMultipleSelection = NO; 391 | panel.message = NSLocalizedString(@"Select a disk image to attach:", nil); 392 | 393 | panel.allowedFileTypes = [self.class diskImageFileExtensions]; 394 | 395 | [self.userInfo setObject:[NSNumber numberWithBool:YES] forKey:@"readOnly"]; 396 | 397 | panel.accessoryView = self.view; 398 | panel.delegate = self; 399 | 400 | // This is a little strange, but left over from an initial implementation which used cascading sheets on 401 | // the main window. The code sheetDidEnd code is usable for this variation, though 402 | 403 | if ([panel runModal] == NSModalResponseOK) { 404 | [self.userInfo setObject:panel.URL.path forKey:@"filePath"]; 405 | [self attachDiskImageOptionsSheetDidEnd:panel returnCode:NSModalResponseOK contextInfo:self]; 406 | } 407 | } 408 | 409 | - (IBAction)performAttachDiskImage:(id)sender 410 | { 411 | [self performAttachDiskImage]; 412 | } 413 | 414 | - (IBAction)cancel:(id)sender 415 | { 416 | self.canceled = YES; 417 | [task terminate]; 418 | } 419 | 420 | - (IBAction)skip:(id)sender 421 | { 422 | } 423 | 424 | #pragma mark Delegates 425 | 426 | - (void)panelSelectionDidChange:(id)sender 427 | { 428 | NSString *filename; 429 | NSFileHandle *handle; 430 | NSData *header; 431 | 432 | Log(LOG_DEBUG, @"%s ", __func__); 433 | 434 | filename = [sender filename]; 435 | 436 | if (!filename) { 437 | filename = [[[sender directoryURL] path] stringByAppendingPathComponent:@"token"]; // SparseBundle 438 | } 439 | 440 | Log(LOG_DEBUG, @"filename: %@\n", filename); 441 | 442 | handle = [NSFileHandle fileHandleForReadingAtPath:filename]; 443 | 444 | if (handle) { 445 | 446 | header = [handle readDataOfLength:8]; 447 | [handle closeFile]; 448 | 449 | // This check only works for Version 2 encrypted disk images, the default in Tiger and beyond 450 | // We check only to remind the user if a password is needed, so do not need to catch every case. 451 | // 452 | // http://lorenzo.yellowspace.net/corrupt-sparseimage.html 453 | // 454 | 455 | if (header && [header length] == 8) { 456 | 457 | if (memcmp("encrcdsa", [header bytes], 8) == 0) 458 | [self.userInfo setObject:[NSNumber numberWithBool:YES] forKey:@"needPassword"]; 459 | else 460 | [self.userInfo setObject:[NSNumber numberWithBool:NO] forKey:@"needPassword"]; 461 | } 462 | } 463 | else { 464 | [self.userInfo setObject:[NSNumber numberWithBool:NO] forKey:@"needPassword"]; 465 | } 466 | } 467 | 468 | #pragma mark Notification Callbacks 469 | 470 | - (NSString *)_parseNextMessage:(NSMutableString **)bufferRef newData:(NSData *)data 471 | { 472 | NSMutableString **buffer = bufferRef; 473 | NSString *returnString = nil; 474 | NSString *newString; 475 | 476 | // If data, append to buffer 477 | 478 | if (data && [data length] > 0) { 479 | newString = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding]; 480 | 481 | if (newString) { 482 | if (*buffer) 483 | [*buffer appendString:newString]; 484 | else 485 | *buffer = [newString mutableCopy]; 486 | [newString release]; 487 | } 488 | } 489 | 490 | // Parse either a plist or a single-line message 491 | 492 | NSString *endOfMessage = [*buffer hasPrefix:@"\n" : @"\n"; 493 | 494 | NSRange range = [*buffer rangeOfString:endOfMessage]; 495 | if (range.location != NSNotFound) 496 | { 497 | returnString = [*buffer substringToIndex:(range.location + range.length - 1)]; 498 | [*buffer deleteCharactersInRange:NSMakeRange(0, [returnString length] + 1)]; 499 | } 500 | 501 | return returnString; 502 | } 503 | 504 | - (void)processStandardOutput:(NSNotification *)notif 505 | { 506 | Log(LOG_DEBUG, @"%s", __func__); 507 | 508 | NSString *mymessage; 509 | NSFileHandle *stdoutHandle = notif.object; 510 | double percentage; 511 | 512 | // NSData *data = [stdoutHandle availableData]; 513 | NSData *data = [[notif userInfo] objectForKey:NSFileHandleNotificationDataItem]; 514 | 515 | while ((mymessage = [self _parseNextMessage:&stdoutBuffer newData:data])) 516 | { 517 | data = nil; 518 | 519 | if ([mymessage hasPrefix:@"PERCENT:"]) { 520 | percentage = [[mymessage substringFromIndex:[@"PERCENT:" length]] doubleValue]; 521 | Log(LOG_DEBUG, @"Percent: %f", percentage); 522 | 523 | if (percentage > 0.0) { 524 | if (!self.isVerifying) { 525 | self.isVerifying = YES; 526 | [self showWindow:self]; 527 | [NSApp unhide:self]; 528 | // [self.window makeKeyAndOrderFront:self]; 529 | } 530 | self.progress = percentage; 531 | } 532 | } 533 | 534 | else if ([mymessage hasPrefix:@"MESSAGE:"]) { 535 | mymessage = [mymessage substringFromIndex:[@"MESSAGE:" length]]; 536 | Log(LOG_DEBUG, @"Message: %@", mymessage); 537 | 538 | self.message = mymessage; 539 | } 540 | 541 | else if ([mymessage hasPrefix:@"hdiutil:"]) { 542 | mymessage = [mymessage substringFromIndex:[@"hdiutil:" length]]; 543 | // error? 544 | Log(LOG_ERR, @"Error: %@", mymessage); 545 | } 546 | 547 | else if ([mymessage hasPrefix:@" 10 | 11 | extern NSString * const DADiskDidAppearNotification; 12 | extern NSString * const DADiskDidDisappearNotification; 13 | extern NSString * const DADiskDidChangeNotification; 14 | extern NSString * const DADiskDidAttemptMountNotification; 15 | extern NSString * const DADiskDidAttemptUnmountNotification; 16 | extern NSString * const DADiskDidAttemptEjectNotification; 17 | 18 | extern NSString * const DAStatusErrorKey; 19 | 20 | enum { 21 | kDiskUnmountOptionDefault = 0x00000000, 22 | kDiskUnmountOptionForce = 0x00080000, 23 | kDiskUnmountOptionWhole = 0x00000001 24 | }; 25 | 26 | /* The mounting attribute exists because of a quirk when attaching a volume using "hdiutil attach". Even if "-nomount" 27 | * is specified, the mount approval callback is called. This confuses the situation when attempting to initiate 28 | * a mount when Disk Arbitrator is in read-only mode. "mounting" distinguishes between a callback that is triggered by hdiutil 29 | * and the one that results from calling DADiskMount(). 30 | * 31 | * Even if the extra callback issue didn't exist, "mounting" (or some other means) is needed to know when to allow a mount 32 | * in the mount approval callback. 33 | */ 34 | 35 | @interface Disk : NSObject 36 | 37 | @property (copy) NSString *BSDName; 38 | @property (readonly) int BSDNameNumber; 39 | @property (nonatomic, retain) NSDictionary *diskDescription; 40 | @property (readonly) BOOL isMountable; 41 | @property (readonly) BOOL isMounted; 42 | @property (readwrite) BOOL isMounting; 43 | @property (readwrite) BOOL rejectedMount; 44 | @property (readonly) BOOL isWritable; 45 | @property (readonly) BOOL isFileSystemWritable; 46 | @property (readonly) BOOL isEjectable; 47 | @property (readonly) BOOL isRemovable; 48 | @property (readonly) BOOL isWholeDisk; 49 | @property (readonly) BOOL isLeaf; 50 | @property (readonly) BOOL isNetworkVolume; 51 | @property (readonly, retain) NSImage *icon; 52 | @property (assign) Disk *parent; 53 | @property (retain) NSMutableSet *children; 54 | @property (readwrite, retain) NSArray *mountArgs; 55 | @property (readwrite, retain) NSString *mountPath; 56 | @property (readonly) BOOL isHFS; 57 | 58 | - (void)mount; 59 | - (void)mountAtPath:(NSString *)path withArguments:(NSArray *)args; 60 | - (void)unmountWithOptions:(NSUInteger)options; 61 | - (void)eject; 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /Source/Disk.m: -------------------------------------------------------------------------------- 1 | // 2 | // Disk.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/10/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import "Disk.h" 10 | #import "AppError.h" 11 | #import 12 | #import "DiskArbitrationPrivateFunctions.h" 13 | #import 14 | #include 15 | #include 16 | 17 | //////////////////////////////////////////////////////////////////////////////// 18 | 19 | @interface Disk () 20 | { 21 | CFTypeRef disk; 22 | } 23 | 24 | @end 25 | 26 | //////////////////////////////////////////////////////////////////////////////// 27 | 28 | @implementation Disk 29 | 30 | @synthesize diskDescription = _diskDescription; 31 | @synthesize BSDName; 32 | @synthesize isMounting; 33 | @synthesize rejectedMount; 34 | @synthesize icon; 35 | @synthesize parent; 36 | @synthesize children; 37 | @synthesize mountArgs; 38 | @synthesize mountPath; 39 | 40 | + (void)initialize 41 | { 42 | InitializeDiskArbitration(); 43 | } 44 | 45 | + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key 46 | { 47 | if ([key isEqual:@"isMountable"]) 48 | return [NSSet setWithObject:@"diskDescription"]; 49 | 50 | if ([key isEqual:@"isMounted"]) 51 | return [NSSet setWithObject:@"diskDescription"]; 52 | 53 | if ([key isEqual:@"isEjectable"]) 54 | return [NSSet setWithObject:@"diskDescription"]; 55 | 56 | if ([key isEqual:@"isWritable"]) 57 | return [NSSet setWithObject:@"diskDescription"]; 58 | 59 | if ([key isEqual:@"isRemovable"]) 60 | return [NSSet setWithObject:@"diskDescription"]; 61 | 62 | if ([key isEqual:@"isFileSystemWritable"]) 63 | return [NSSet setWithObject:@"diskDescription"]; 64 | 65 | if ([key isEqual:@"icon"]) 66 | return [NSSet setWithObject:@"diskDescription"]; 67 | 68 | return [super keyPathsForValuesAffectingValueForKey:key]; 69 | } 70 | 71 | + (id)uniqueDiskForDADisk:(DADiskRef)diskRef create:(BOOL)create 72 | { 73 | for (Disk *disk in uniqueDisks) { 74 | if (disk.hash == CFHash(diskRef)) 75 | return disk; 76 | } 77 | 78 | return create ? [[[self.class alloc] initWithDADisk:diskRef shouldCreateParent:YES] autorelease] : nil; 79 | } 80 | 81 | - (id)initWithDADisk:(DADiskRef)diskRef shouldCreateParent:(BOOL)shouldCreateParent 82 | { 83 | NSAssert(diskRef, @"No Disk Arbitration disk provided to initializer."); 84 | 85 | self = [super init]; 86 | 87 | // Return unique instance 88 | Disk *uniqueDisk = [Disk uniqueDiskForDADisk:diskRef create:NO]; 89 | if (uniqueDisk) 90 | { 91 | [self release]; 92 | return [uniqueDisk retain]; 93 | } 94 | 95 | if (self) 96 | { 97 | disk = CFRetain(diskRef); 98 | const char *bsdName = DADiskGetBSDName(diskRef); 99 | BSDName = [[NSString alloc] initWithUTF8String:bsdName ? bsdName : ""]; 100 | children = [NSMutableSet new]; 101 | _diskDescription = (NSDictionary *)DADiskCopyDescription(diskRef); 102 | 103 | // CFShow(description); 104 | 105 | if (self.isWholeDisk == NO) 106 | { 107 | DADiskRef parentRef = DADiskCopyWholeDisk(diskRef); 108 | if (parentRef) 109 | { 110 | Disk *parentDisk = [Disk uniqueDiskForDADisk:parentRef create:shouldCreateParent]; 111 | if (parentDisk) 112 | { 113 | parent = parentDisk; // weak reference 114 | [[parent mutableSetValueForKey:@"children"] addObject:self]; 115 | } 116 | SafeCFRelease(parentRef); 117 | } 118 | } 119 | [uniqueDisks addObject:self]; 120 | } 121 | return self; 122 | } 123 | 124 | - (void)dealloc 125 | { 126 | SafeCFRelease(disk); 127 | [_diskDescription release]; 128 | [BSDName release]; 129 | [icon release]; 130 | parent = nil; 131 | [children release]; 132 | [mountArgs release]; 133 | [mountPath release]; 134 | [super dealloc]; 135 | } 136 | 137 | - (NSUInteger)hash 138 | { 139 | return CFHash(disk); 140 | } 141 | 142 | - (BOOL)isEqual:(id)object 143 | { 144 | return (CFHash(disk) == [object hash]); 145 | } 146 | 147 | - (NSString *)description 148 | { 149 | return [NSString stringWithFormat:@"<%@ 0x%p %@>", self.class, self, BSDName]; 150 | } 151 | 152 | - (void)mount 153 | { 154 | [self mountAtPath:nil withArguments:[NSArray array]]; 155 | } 156 | 157 | - (void)mountAtPath:(NSString *)path withArguments:(NSArray *)args 158 | { 159 | NSAssert(self.isMountable, @"Disk isn't mountable."); 160 | NSAssert(self.isMounted == NO, @"Disk is already mounted."); 161 | 162 | self.isMounting = YES; 163 | self.mountArgs = args; 164 | self.mountPath = path; 165 | 166 | Log(LOG_DEBUG, @"%s mount %@ at mountpoint: %@ arguments: %@", __func__, BSDName, path, args.description); 167 | 168 | // ensure arg list is NULL terminated 169 | id *argv = calloc(args.count + 1, sizeof(id)); 170 | 171 | [args getObjects:argv range:NSMakeRange(0, args.count)]; 172 | 173 | NSURL *url = path ? [NSURL fileURLWithPath:path.stringByExpandingTildeInPath] : NULL; 174 | 175 | DADiskMountWithArguments((DADiskRef) disk, (CFURLRef) url, kDADiskMountOptionDefault, 176 | DiskMountCallback, self, (CFStringRef *)argv); 177 | 178 | free(argv); 179 | } 180 | 181 | - (void)unmountWithOptions:(NSUInteger)options 182 | { 183 | NSAssert(self.isMountable, @"Disk isn't mountable."); 184 | NSAssert(self.isMounted, @"Disk isn't mounted."); 185 | 186 | DADiskUnmount((DADiskRef) disk, (DADiskUnmountOptions)options, DiskUnmountCallback, self); 187 | } 188 | 189 | - (void)eject 190 | { 191 | NSAssert1(self.isEjectable, @"Disk is not ejectable: %@", self); 192 | 193 | DADiskEject((DADiskRef) disk, kDADiskEjectOptionDefault, DiskEjectCallback, [self retain]); // self is released in DiskEjectCallback 194 | } 195 | 196 | - (void)diskDidDisappear 197 | { 198 | [uniqueDisks removeObject:self]; 199 | [[parent mutableSetValueForKey:@"children"] removeObject:self]; 200 | 201 | SafeCFRelease(disk); 202 | disk = NULL; 203 | 204 | self.parent = nil; 205 | [children removeAllObjects]; 206 | 207 | self.rejectedMount = NO; 208 | self.mountArgs = [NSArray array]; 209 | self.mountPath = nil; 210 | } 211 | 212 | - (BOOL)isMountable 213 | { 214 | CFBooleanRef value = self.diskDescription ? 215 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionVolumeMountableKey] : NULL; 216 | return value ? CFBooleanGetValue(value) : NO; 217 | } 218 | 219 | - (BOOL)isMounted 220 | { 221 | CFStringRef value = self.diskDescription ? 222 | (CFStringRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionVolumePathKey] : NULL; 223 | 224 | return value ? YES : NO; 225 | } 226 | 227 | - (BOOL)isWholeDisk 228 | { 229 | CFBooleanRef value = self.diskDescription ? 230 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionMediaWholeKey] : NULL; 231 | return value ? CFBooleanGetValue(value) : NO; 232 | } 233 | 234 | - (BOOL)isLeaf 235 | { 236 | CFBooleanRef value = self.diskDescription ? 237 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionMediaLeafKey] : NULL; 238 | return value ? CFBooleanGetValue(value) : NO; 239 | } 240 | 241 | - (BOOL)isNetworkVolume 242 | { 243 | CFBooleanRef value = self.diskDescription ? 244 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionVolumeNetworkKey] : NULL; 245 | return value ? CFBooleanGetValue(value) : NO; 246 | } 247 | 248 | - (BOOL)isWritable 249 | { 250 | CFBooleanRef value = self.diskDescription ? 251 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionMediaWritableKey] : NULL; 252 | return value ? CFBooleanGetValue(value) : NO; 253 | } 254 | 255 | - (BOOL)isEjectable 256 | { 257 | CFBooleanRef value = self.diskDescription ? 258 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionMediaEjectableKey] : NULL; 259 | return value ? CFBooleanGetValue(value) : NO; 260 | } 261 | 262 | - (BOOL)isRemovable 263 | { 264 | CFBooleanRef value = self.diskDescription ? 265 | (CFBooleanRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionMediaRemovableKey] : NULL; 266 | return value ? CFBooleanGetValue(value) : NO; 267 | } 268 | 269 | - (BOOL)isHFS 270 | { 271 | CFStringRef volumeKind = self.diskDescription ? 272 | (CFStringRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionVolumeKindKey] : NULL; 273 | return volumeKind ? CFEqual(CFSTR("hfs"), volumeKind) : NO; 274 | } 275 | 276 | - (BOOL)isFileSystemWritable 277 | { 278 | BOOL retval = NO; 279 | struct statfs fsstat; 280 | CFURLRef volumePath; 281 | UInt8 fsrep[MAXPATHLEN]; 282 | 283 | // if the media is not writable, the file system cannot be either 284 | if (self.isWritable == NO) 285 | return NO; 286 | 287 | volumePath = (CFURLRef)[self.diskDescription objectForKey:(NSString *)kDADiskDescriptionVolumePathKey]; 288 | if (volumePath) { 289 | 290 | if (CFURLGetFileSystemRepresentation(volumePath, true, fsrep, sizeof(fsrep))) { 291 | 292 | if (statfs((char *)fsrep, &fsstat) == 0) 293 | retval = (fsstat.f_flags & MNT_RDONLY) ? NO : YES; 294 | } 295 | } 296 | 297 | return retval; 298 | } 299 | 300 | - (void)setDiskDescription:(NSDictionary *)desc 301 | { 302 | NSAssert(desc, @"A NULL disk description is not allowed."); 303 | 304 | if (desc != _diskDescription) 305 | { 306 | [self willChangeValueForKey:@"diskDescription"]; 307 | 308 | SafeCFRelease(_diskDescription); 309 | _diskDescription = desc ? [desc retain] : nil; 310 | 311 | [self didChangeValueForKey:@"diskDescription"]; 312 | } 313 | } 314 | 315 | - (NSImage *)icon 316 | { 317 | if (!icon) 318 | { 319 | if (self.diskDescription) 320 | { 321 | CFDictionaryRef iconRef = (CFDictionaryRef)[self.diskDescription 322 | objectForKey:(NSString *)kDADiskDescriptionMediaIconKey]; 323 | if (iconRef) 324 | { 325 | 326 | CFStringRef identifier = CFDictionaryGetValue(iconRef, CFSTR("CFBundleIdentifier")); 327 | NSURL *url = [(NSURL *)KextManagerCreateURLForBundleIdentifier(kCFAllocatorDefault, identifier) autorelease]; 328 | if (url) { 329 | NSString *bundlePath = [url path]; 330 | 331 | NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; 332 | if (bundle) { 333 | NSString *filename = (NSString *) CFDictionaryGetValue(iconRef, CFSTR("IOBundleResourceFile")); 334 | NSString *basename = [filename stringByDeletingPathExtension]; 335 | NSString *fileext = [filename pathExtension]; 336 | 337 | NSString *path = [bundle pathForResource:basename ofType:fileext]; 338 | if (path) { 339 | icon = [[NSImage alloc] initWithContentsOfFile:path]; 340 | } 341 | } 342 | else { 343 | Log(LOG_WARNING, @"Failed to load bundle with URL: %@", [url absoluteString]); 344 | CFShow(self.diskDescription); 345 | } 346 | } 347 | else { 348 | Log(LOG_WARNING, @"Failed to create URL for bundle identifier: %@", (NSString *)identifier); 349 | CFShow(self.diskDescription); 350 | } 351 | } 352 | } 353 | } 354 | 355 | return icon; 356 | } 357 | 358 | - (int)BSDNameNumber 359 | { 360 | // Take the BSDName and convert it into a number that can be compared with other disks for sorting. 361 | // For example, "disk2s1" would become 2 * 1000 + 1 = 2001. 362 | // If we just compare by the string value itself, then disk10 would come after disk1, instead of disk9. 363 | NSString *s = self.BSDName; 364 | int device = 0; 365 | int slice = 0; 366 | const int found = sscanf(s.UTF8String, "disk%ds%d", &device, &slice); 367 | if (found == 0 || device < 0 || slice < 0) { 368 | NSLog(@"Invalid BSD Name %@", s); 369 | } 370 | return (device * 1000) + slice; 371 | } 372 | 373 | @end 374 | -------------------------------------------------------------------------------- /Source/DiskArbitrationPrivateFunctions.h: -------------------------------------------------------------------------------- 1 | /* 2 | * DiskArbitrationPrivateFunctions.h 3 | * DiskArbitrator 4 | * 5 | * Created by Aaron Burghardt on 1/28/10. 6 | * Copyright 2010 Aaron Burghardt. All rights reserved. 7 | * 8 | */ 9 | 10 | #import 11 | #import "Disk.h" 12 | 13 | void InitializeDiskArbitration(void); 14 | BOOL DADiskValidate(DADiskRef diskRef); 15 | void DiskAppearedCallback(DADiskRef diskRef, void *context); 16 | void DiskDisappearedCallback(DADiskRef diskRef, void *context); 17 | void DiskDescriptionChangedCallback(DADiskRef diskRef, CFArrayRef keys, void *context); 18 | void DiskMountCallback(DADiskRef diskRef, DADissenterRef dissenter, void *context); 19 | void DiskUnmountCallback(DADiskRef diskRef, DADissenterRef dissenter, void *context); 20 | void DiskEjectCallback(DADiskRef diskRef, DADissenterRef dissenter, void *context); 21 | 22 | 23 | @interface Disk (DiskPrivate) 24 | 25 | + (id)uniqueDiskForDADisk:(DADiskRef)diskRef create:(BOOL)create; 26 | 27 | - (id)initWithDADisk:(DADiskRef)diskRef shouldCreateParent:(BOOL)shouldCreateParent; 28 | - (void)refreshFromDescription; 29 | - (void)diskDidDisappear; 30 | @end 31 | 32 | extern NSMutableSet *uniqueDisks; 33 | -------------------------------------------------------------------------------- /Source/DiskArbitrationPrivateFunctions.m: -------------------------------------------------------------------------------- 1 | /* 2 | * DiskArbitrationPrivateFunctions.m 3 | * DiskArbitrator 4 | * 5 | * Created by Aaron Burghardt on 1/28/10. 6 | * Copyright 2010 Aaron Burghardt. All rights reserved. 7 | * 8 | */ 9 | 10 | #import "DiskArbitrationPrivateFunctions.h" 11 | #import "AppError.h" 12 | 13 | // Globals 14 | NSMutableSet *uniqueDisks; 15 | DASessionRef session; 16 | 17 | 18 | void InitializeDiskArbitration(void) 19 | { 20 | static BOOL isInitialized = NO; 21 | 22 | if (isInitialized) return; 23 | 24 | isInitialized = YES; 25 | 26 | uniqueDisks = [NSMutableSet new]; 27 | 28 | session = DASessionCreate(kCFAllocatorDefault); 29 | if (!session) { 30 | [NSException raise:NSInternalInconsistencyException format:@"Failed to create Disk Arbitration session."]; 31 | return; 32 | } 33 | 34 | DASessionScheduleWithRunLoop(session, CFRunLoopGetMain(), kCFRunLoopCommonModes); 35 | 36 | CFMutableDictionaryRef matching = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 37 | CFDictionaryAddValue(matching, kDADiskDescriptionVolumeNetworkKey, kCFBooleanFalse); 38 | 39 | DARegisterDiskAppearedCallback(session, matching, DiskAppearedCallback, [Disk class]); 40 | DARegisterDiskDisappearedCallback(session, matching, DiskDisappearedCallback, [Disk class]); 41 | DARegisterDiskDescriptionChangedCallback(session, matching, NULL, DiskDescriptionChangedCallback, [Disk class]); 42 | 43 | SafeCFRelease(matching); 44 | } 45 | 46 | BOOL DADiskValidate(DADiskRef diskRef) 47 | { 48 | // 49 | // Reject certain disk media 50 | // 51 | 52 | BOOL isOK = YES; 53 | 54 | // Reject if no BSDName 55 | if (DADiskGetBSDName(diskRef) == NULL) 56 | [NSException raise:NSInternalInconsistencyException format:@"Disk without BSDName"]; 57 | // return NO; 58 | 59 | CFDictionaryRef desc = DADiskCopyDescription(diskRef); 60 | // CFShow(desc); 61 | 62 | // Reject if no key-value for Whole Media 63 | CFBooleanRef wholeMediaValue = CFDictionaryGetValue(desc, kDADiskDescriptionMediaWholeKey); 64 | if (isOK && !wholeMediaValue) isOK = NO; 65 | 66 | // If not a whole disk, then must be a media leaf 67 | if (isOK && CFBooleanGetValue(wholeMediaValue) == false) 68 | { 69 | CFBooleanRef mediaLeafValue = CFDictionaryGetValue(desc, kDADiskDescriptionMediaLeafKey); 70 | if (!mediaLeafValue || CFBooleanGetValue(mediaLeafValue) == false) isOK = NO; 71 | } 72 | SafeCFRelease(desc); 73 | 74 | return isOK; 75 | } 76 | 77 | void DiskAppearedCallback(DADiskRef diskRef, void *context) 78 | { 79 | if (context != [Disk class]) return; 80 | 81 | Log(LOG_DEBUG, @"%s <%p> %s", __func__, diskRef, DADiskGetBSDName(diskRef)); 82 | 83 | if (DADiskValidate(diskRef)) 84 | { 85 | Disk *disk = [Disk uniqueDiskForDADisk:diskRef create:YES]; 86 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidAppearNotification object:disk]; 87 | } 88 | } 89 | 90 | void DiskDisappearedCallback(DADiskRef diskRef, void *context) 91 | { 92 | if (context != [Disk class]) return; 93 | 94 | Log(LOG_DEBUG, @"%s <%p> %s", __func__, diskRef, DADiskGetBSDName(diskRef)); 95 | 96 | Disk *tmpDisk = [Disk uniqueDiskForDADisk:diskRef create:NO]; 97 | if (!tmpDisk) { 98 | return; 99 | } 100 | 101 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidDisappearNotification object:tmpDisk]; 102 | 103 | [tmpDisk diskDidDisappear]; 104 | } 105 | 106 | void DiskDescriptionChangedCallback(DADiskRef diskRef, CFArrayRef keys, void *context) 107 | { 108 | if (context != [Disk class]) return; 109 | 110 | Log(LOG_DEBUG, @"%s <%p> %s, keys changed:", __func__, diskRef, DADiskGetBSDName(diskRef)); 111 | Log(LOG_DEBUG, @"%@", keys); 112 | 113 | for (Disk *disk in uniqueDisks) { 114 | if (CFHash(diskRef) == disk.hash) { 115 | CFDictionaryRef desc = DADiskCopyDescription(diskRef); 116 | disk.diskDescription = (NSDictionary *)desc; 117 | SafeCFRelease(desc); 118 | 119 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidChangeNotification object:disk]; 120 | } 121 | } 122 | } 123 | 124 | void DiskMountCallback(DADiskRef diskRef, DADissenterRef dissenter, void *context) 125 | { 126 | // Disk *disk = (Disk *)context; 127 | NSMutableDictionary *info = nil; 128 | 129 | Log(LOG_DEBUG, @"%s %@ dissenter: %p", __func__, context, dissenter); 130 | 131 | if (dissenter) 132 | { 133 | DAReturn status = DADissenterGetStatus(dissenter); 134 | 135 | NSString *statusString = (NSString *) DADissenterGetStatusString(dissenter); 136 | if (!statusString) 137 | { 138 | IOReturn systemCode = err_get_system(status); 139 | IOReturn subSystemCode = err_get_sub(status); 140 | IOReturn errorCode = err_get_code(status); 141 | 142 | statusString = [NSString stringWithFormat:@"%@: system: %d; subsystem: %d; error: %d", 143 | NSLocalizedString(@"Dissenter status code", nil), systemCode, subSystemCode, errorCode]; 144 | } 145 | 146 | Log(LOG_INFO, @"%s %@ dissenter: (%#x) %@", __func__, context, status, statusString); 147 | 148 | info = [NSMutableDictionary dictionary]; 149 | [info setObject:statusString forKey:NSLocalizedFailureReasonErrorKey]; 150 | [info setObject:[NSNumber numberWithInt:status] forKey:DAStatusErrorKey]; 151 | } 152 | else 153 | { 154 | Log(LOG_DEBUG, @"%s disk %@ mounted", __func__, context); 155 | } 156 | 157 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidAttemptMountNotification object:context userInfo:info]; 158 | } 159 | 160 | void DiskUnmountCallback(DADiskRef diskRef, DADissenterRef dissenter, void *context) 161 | { 162 | NSDictionary *info = nil; 163 | 164 | if (dissenter) { 165 | DAReturn status = DADissenterGetStatus(dissenter); 166 | 167 | NSString *statusString = (NSString *) DADissenterGetStatusString(dissenter); 168 | if (!statusString) 169 | statusString = [NSString stringWithFormat:@"Error code: %d", status]; 170 | 171 | Log(LOG_DEBUG, @"%s disk %@ dissenter: (%d) %@", __func__, context, status, statusString); 172 | 173 | info = [NSDictionary dictionaryWithObjectsAndKeys: 174 | [NSNumber numberWithInt:status], DAStatusErrorKey, 175 | statusString, NSLocalizedFailureReasonErrorKey, 176 | statusString, NSLocalizedRecoverySuggestionErrorKey, 177 | nil]; 178 | } 179 | else { 180 | Log(LOG_DEBUG, @"%s disk %@ unmounted", __func__, context); 181 | } 182 | 183 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidAttemptUnmountNotification object:context userInfo:info]; 184 | } 185 | 186 | void DiskEjectCallback(DADiskRef diskRef, DADissenterRef dissenter, void *context) 187 | { 188 | [(id)context autorelease]; // got retained when passed in 189 | NSDictionary *info = nil; 190 | 191 | if (dissenter) { 192 | DAReturn status = DADissenterGetStatus(dissenter); 193 | 194 | NSString *statusString = (NSString *) DADissenterGetStatusString(dissenter); 195 | if (!statusString) 196 | statusString = [NSString stringWithFormat:@"Error code: %d", status]; 197 | 198 | Log(LOG_INFO, @"%s disk: %@ dissenter: (%d) %@", __func__, context, status, statusString); 199 | 200 | info = [NSDictionary dictionaryWithObjectsAndKeys: 201 | [NSNumber numberWithInt:status], DAStatusErrorKey, 202 | statusString, NSLocalizedFailureReasonErrorKey, 203 | statusString, NSLocalizedRecoverySuggestionErrorKey, 204 | nil]; 205 | } 206 | else { 207 | Log(LOG_DEBUG, @"%s disk ejected: %@ ", __func__, context); 208 | } 209 | 210 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidAttemptEjectNotification object:context userInfo:info]; 211 | } 212 | 213 | 214 | NSString * const DADiskDidAppearNotification = @"DADiskDidAppearNotification"; 215 | NSString * const DADiskDidDisappearNotification = @"DADiskDidDisppearNotification"; 216 | NSString * const DADiskDidChangeNotification = @"DADiskDidChangeNotification"; 217 | NSString * const DADiskDidAttemptMountNotification = @"DADiskDidAttemptMountNotification"; 218 | NSString * const DADiskDidAttemptUnmountNotification = @"DADiskDidAttemptUnmountNotification"; 219 | NSString * const DADiskDidAttemptEjectNotification = @"DADiskDidAttemptEjectNotification"; 220 | 221 | NSString * const DAStatusErrorKey = @"DAStatusErrorKey"; 222 | -------------------------------------------------------------------------------- /Source/DiskArbitratorAppController+Toolbar.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskArbitratorAppController+Toolbar.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/7/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import 10 | #import "DiskArbitratorAppController.h" 11 | 12 | @interface AppController (AppControllerToolbar) 13 | 14 | 15 | @end 16 | 17 | void SetupToolbar(NSWindow *window, id delegate); 18 | 19 | // Toolbar Item Identifier constants 20 | extern NSString * const ToolbarItemMainIdentifier; 21 | extern NSString * const ToolbarItemInfoIdentifier; 22 | extern NSString * const ToolbarItemMountIdentifier; 23 | extern NSString * const ToolbarItemEjectIdentifier; 24 | extern NSString * const ToolbarItemAttachDiskImageIdentifier; 25 | -------------------------------------------------------------------------------- /Source/DiskArbitratorAppController+Toolbar.m: -------------------------------------------------------------------------------- 1 | // 2 | // DiskArbitratorAppController+Toolbar.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/7/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import "DiskArbitratorAppController+Toolbar.h" 10 | #import "Disk.h" 11 | 12 | 13 | @implementation AppController (AppControllerToolbar) 14 | 15 | - (NSImage *)attachDiskImageIcon 16 | { 17 | NSImage *dmgIcon = [[NSWorkspace sharedWorkspace] iconForFileType:@"dmg"]; 18 | NSImage *plugImage = [NSImage imageNamed:@"ToolbarItem Attach Disk Plug"]; 19 | 20 | NSBitmapImageRep *compositedImage = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:32 pixelsHigh:32 bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bytesPerRow:0 bitsPerPixel:0]; 21 | 22 | [NSGraphicsContext saveGraphicsState]; 23 | [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:compositedImage]]; 24 | [[NSGraphicsContext currentContext] setShouldAntialias:NO]; 25 | 26 | [dmgIcon drawInRect:NSMakeRect(0, 0, 32, 32) fromRect:NSZeroRect operation:NSCompositingOperationCopy fraction:1.0]; 27 | [plugImage drawInRect:NSMakeRect(2, 16, 16, 16) fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; 28 | 29 | [NSGraphicsContext restoreGraphicsState]; 30 | 31 | return [[NSImage alloc] initWithData:[compositedImage TIFFRepresentation]]; 32 | } 33 | 34 | - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag 35 | { 36 | NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier]; 37 | 38 | if ([itemIdentifier isEqual:ToolbarItemInfoIdentifier]) { // Info/Inspect 39 | item.label = NSLocalizedString(@"Info", nil); 40 | item.paletteLabel = NSLocalizedString(@"Info", nil); 41 | item.image = [NSImage imageNamed:@"ToolbarItem Info"]; // NSImageNameInfo]]; 42 | item.target = self; 43 | item.action = @selector(performGetInfo:); 44 | item.toolTip = NSLocalizedString(@"Show detailed disk info", nil); 45 | } 46 | else if ([itemIdentifier isEqual:ToolbarItemEjectIdentifier]) { // Eject 47 | item.label = NSLocalizedString(@"Eject", nil); 48 | item.paletteLabel = NSLocalizedString(@"Eject", nil); 49 | item.image = [NSImage imageNamed:@"ToolbarItem Eject"]; 50 | item.target = self; 51 | item.action = @selector(performEject:); 52 | item.toolTip = NSLocalizedString(@"Eject removable media.", nil); 53 | } 54 | else if ([itemIdentifier isEqual:ToolbarItemMountIdentifier]) { // Mount 55 | item.label = NSLocalizedString(@"Mount", nil); 56 | item.paletteLabel = NSLocalizedString(@"Mount/Unmount", nil); 57 | item.image = [NSImage imageNamed:@"ToolbarItem Mount"]; 58 | item.target = self; 59 | item.action = @selector(performMountOrUnmount:); 60 | item.toolTip = NSLocalizedString(@"Select a volume, then click to mount or unmount.", nil); 61 | } 62 | else if ([itemIdentifier isEqual:ToolbarItemAttachDiskImageIdentifier]) { // Attach Disk Image 63 | item.label = NSLocalizedString(@"Attach", nil); 64 | item.paletteLabel = NSLocalizedString(@"Attach Disk Image", nil); 65 | // item.image = [NSImage imageNamed:@"ToolbarItem Attach Disk Image"]; 66 | item.image = self.attachDiskImageIcon; 67 | item.target = self; 68 | item.action = @selector(performAttachDiskImage:); 69 | item.toolTip = NSLocalizedString(@"Attach Disk Image", nil); 70 | } 71 | return item; 72 | } 73 | 74 | 75 | //---------------------------------------------------------- 76 | - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar { 77 | return [NSArray arrayWithObjects: 78 | NSToolbarSeparatorItemIdentifier, 79 | NSToolbarSpaceItemIdentifier, 80 | NSToolbarFlexibleSpaceItemIdentifier, 81 | NSToolbarCustomizeToolbarItemIdentifier, 82 | ToolbarItemInfoIdentifier, 83 | ToolbarItemMountIdentifier, 84 | ToolbarItemEjectIdentifier, 85 | ToolbarItemAttachDiskImageIdentifier, 86 | nil]; 87 | } 88 | 89 | 90 | //---------------------------------------------------------- 91 | - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar { 92 | return [NSArray arrayWithObjects: 93 | ToolbarItemInfoIdentifier, 94 | ToolbarItemEjectIdentifier, 95 | ToolbarItemMountIdentifier, 96 | NSToolbarSpaceItemIdentifier, 97 | ToolbarItemAttachDiskImageIdentifier, 98 | NSToolbarFlexibleSpaceItemIdentifier, 99 | NSToolbarCustomizeToolbarItemIdentifier, 100 | nil]; 101 | } 102 | 103 | 104 | //---------------------------------------------------------- 105 | -(BOOL)validateToolbarItem:(NSToolbarItem*)toolbarItem { 106 | BOOL enabled = YES; 107 | 108 | if ([[toolbarItem itemIdentifier] isEqual:ToolbarItemInfoIdentifier]) 109 | enabled = YES; 110 | 111 | else if ([[toolbarItem itemIdentifier] isEqual:ToolbarItemMountIdentifier]) { 112 | 113 | // Enable the item if the disk is mountable 114 | // Set the label "Unmount" if mounted, otherwise to "Mount" 115 | 116 | if (self.canUnmountSelectedDisk) { 117 | [toolbarItem setLabel:NSLocalizedString(@"Unmount", nil)]; 118 | enabled = YES; 119 | } 120 | else { 121 | [toolbarItem setLabel:NSLocalizedString(@"Mount", nil)]; 122 | 123 | if (!self.canMountSelectedDisk) 124 | enabled = NO; 125 | } 126 | } 127 | 128 | else if ([[toolbarItem itemIdentifier] isEqual:ToolbarItemEjectIdentifier]) { 129 | 130 | enabled = self.canEjectSelectedDisk; 131 | } 132 | 133 | return enabled; 134 | } 135 | 136 | @end 137 | 138 | 139 | void SetupToolbar(NSWindow *window, id delegate) 140 | { 141 | NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:ToolbarItemMainIdentifier]; 142 | [toolbar setDelegate:delegate]; 143 | [toolbar setAllowsUserCustomization:YES]; 144 | [toolbar setAutosavesConfiguration:YES]; 145 | [window setToolbar:toolbar]; 146 | } 147 | 148 | 149 | 150 | // Toolbar Item Identifier constants 151 | NSString * const ToolbarItemMainIdentifier = @"ToolbarItemMainIdentifier"; 152 | NSString * const ToolbarItemInfoIdentifier = @"ToolbarItemInfoIdentifier"; 153 | NSString * const ToolbarItemMountIdentifier = @"ToolbarItemMountIdentifier"; 154 | NSString * const ToolbarItemEjectIdentifier = @"ToolbarItemEjectIdentifier"; 155 | NSString * const ToolbarItemAttachDiskImageIdentifier = @"ToolbarItemAttachDiskImageIdentifier"; 156 | 157 | -------------------------------------------------------------------------------- /Source/DiskArbitratorAppController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskArbitratorAppController.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/10/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class Arbitrator; 12 | @class Disk; 13 | 14 | @interface AppController : NSObject // 15 | 16 | @property (assign) IBOutlet NSPanel *window; 17 | @property (assign) IBOutlet NSMenu *statusMenu; 18 | @property (assign) IBOutlet NSTableView *tableView; 19 | @property (assign) IBOutlet NSArrayController *disksArrayController; 20 | @property (copy) NSArray *sortDescriptors; 21 | @property (strong) NSStatusItem *statusItem; 22 | @property (strong) Arbitrator *arbitrator; 23 | @property (assign) BOOL hasUserLaunchAgent; 24 | @property (readonly) BOOL canInstallLaunchAgent; 25 | @property (strong) NSString *installUserLaunchAgentMenuTitle; 26 | 27 | - (IBAction)showAboutPanel:(id)sender; 28 | - (IBAction)showMainWindow:(id)sender; 29 | - (IBAction)showPreferences:(id)sender; 30 | - (IBAction)performActivation:(id)sender; 31 | - (IBAction)performDeactivation:(id)sender; 32 | - (IBAction)toggleActivation:(id)sender; 33 | 34 | - (IBAction)performSetMountBlockMode:(id)sender; 35 | - (IBAction)performSetMountReadOnlyMode:(id)sender; 36 | 37 | - (IBAction)performMount:(id)sender; 38 | - (IBAction)performUnmount:(id)sender; 39 | - (IBAction)performMountOrUnmount:(id)sender; 40 | - (IBAction)performEject:(id)sender; 41 | - (IBAction)performGetInfo:(id)sender; 42 | - (IBAction)performAttachDiskImage:(id)sender; 43 | 44 | - (void)refreshLaunchAgentStatus; 45 | - (IBAction)installUserLaunchAgent:(id)sender; 46 | 47 | - (Disk *)selectedDisk; 48 | - (BOOL)canEjectSelectedDisk; 49 | - (BOOL)canMountSelectedDisk; 50 | - (BOOL)canUnmountSelectedDisk; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Source/DiskArbitratorAppController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DiskArbitratorAppController.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/10/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import "DiskArbitratorAppController.h" 10 | #import "DiskArbitratorAppController+Toolbar.h" 11 | #import "AppError.h" 12 | #import "Arbitrator.h" 13 | #import "Disk.h" 14 | #import "SheetController.h" 15 | #import "DiskInfoController.h" 16 | #import "AttachDiskImageController.h" 17 | 18 | //////////////////////////////////////////////////////////////////////////////// 19 | 20 | @interface AppController () 21 | { 22 | NSMutableArray *displayErrorQueue; // 23 | NSMutableArray *diskInfoControllers; 24 | } 25 | 26 | @end 27 | 28 | //////////////////////////////////////////////////////////////////////////////// 29 | 30 | @implementation AppController 31 | 32 | @synthesize window; 33 | @synthesize statusMenu; 34 | @synthesize tableView; 35 | @synthesize disksArrayController; 36 | @synthesize sortDescriptors; 37 | @synthesize statusItem; 38 | @synthesize arbitrator; 39 | @synthesize hasUserLaunchAgent; 40 | @synthesize installUserLaunchAgentMenuTitle; 41 | 42 | - (void)dealloc 43 | { 44 | if (arbitrator.isActivated) 45 | [arbitrator deactivate]; 46 | } 47 | 48 | - (void)setStatusItemIconWithName:(NSString *)name 49 | { 50 | NSString *iconPath = [[NSBundle mainBundle] pathForResource:name ofType:@"png"]; 51 | NSImage *statusIcon = [[NSImage alloc] initWithContentsOfFile:iconPath]; 52 | statusItem.image = statusIcon; 53 | } 54 | 55 | - (void)refreshStatusItemIcon 56 | { 57 | if (arbitrator.isActivated == NO) 58 | [self setStatusItemIconWithName:@"StatusItem Disabled 1"]; 59 | 60 | else if (arbitrator.mountMode == MM_BLOCK) 61 | [self setStatusItemIconWithName:@"StatusItem Green"]; 62 | 63 | else if (arbitrator.mountMode == MM_READONLY) 64 | [self setStatusItemIconWithName:@"StatusItem Orange"]; 65 | 66 | else 67 | NSAssert1(NO, @"Invalid mount mode: %ld\n", (long)arbitrator.mountMode); 68 | } 69 | 70 | - (void)applicationDidFinishLaunching:(NSNotification *)notification 71 | { 72 | [self refreshLaunchAgentStatus]; 73 | 74 | NSMutableDictionary *defaults = [NSMutableDictionary dictionary]; 75 | [defaults setObject:[NSNumber numberWithInt:LOG_INFO] forKey:AppLogLevelDefaultsKey]; 76 | [defaults setObject:[NSNumber numberWithBool:YES] forKey:@"ShowMainWindowAtStartup"]; 77 | [defaults setObject:[NSNumber numberWithBool:NO] forKey:@"ShowMainWindowAtActivate"]; 78 | [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; 79 | 80 | displayErrorQueue = [NSMutableArray new]; 81 | 82 | NSStatusBar *bar = [NSStatusBar systemStatusBar]; 83 | self.statusItem = [bar statusItemWithLength:NSSquareStatusItemLength]; 84 | NSImage *altImage = [[NSImage imageNamed:@"StatusItem Disabled 1.png"] copy]; 85 | [altImage setTemplate:YES]; 86 | self.statusItem.alternateImage = altImage; 87 | [self.statusItem setHighlightMode:YES]; 88 | [self setStatusItemIconWithName:@"StatusItem Disabled 1"]; 89 | statusItem.menu = statusMenu; 90 | 91 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 92 | [center addObserver:self selector:@selector(diskDidChange:) name:DADiskDidChangeNotification object:nil]; 93 | [center addObserver:self selector:@selector(didAttemptEject:) name:DADiskDidAttemptEjectNotification object:nil]; 94 | [center addObserver:self selector:@selector(didAttemptMount:) name:DADiskDidAttemptMountNotification object:nil]; 95 | [center addObserver:self selector:@selector(didAttemptUnmount:) name:DADiskDidAttemptUnmountNotification object:nil]; 96 | 97 | self.arbitrator = [Arbitrator new]; 98 | [arbitrator addObserver:self forKeyPath:@"isActivated" options:0 context:NULL]; 99 | [arbitrator addObserver:self forKeyPath:@"mountMode" options:0 context:NULL]; 100 | [self refreshStatusItemIcon]; // arbitrator status initial state is taken from user defaults, which was initialized before KVO initialized 101 | 102 | self.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"BSDNameNumber" ascending:YES]]; 103 | 104 | SetupToolbar(window, self); 105 | window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces; 106 | window.worksWhenModal = YES; 107 | 108 | [tableView registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; 109 | 110 | if ([[NSUserDefaults standardUserDefaults] boolForKey:@"ShowMainWindowAtStartup"]) 111 | [window makeKeyAndOrderFront:self]; 112 | } 113 | 114 | - (void)applicationDidBecomeActive:(NSNotification *)notification 115 | { 116 | if ([[NSUserDefaults standardUserDefaults] boolForKey:@"ShowMainWindowAtActivate"]) 117 | [window makeKeyAndOrderFront:self]; 118 | } 119 | 120 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 121 | { 122 | if (object == arbitrator) 123 | if ([keyPath isEqual:@"isActivated"] || [keyPath isEqual:@"mountMode"]) 124 | [self refreshStatusItemIcon]; 125 | } 126 | 127 | - (IBAction)showAboutPanel:(id)sender 128 | { 129 | [NSApp orderFrontStandardAboutPanel:sender]; 130 | [NSApp activateIgnoringOtherApps:YES]; 131 | } 132 | 133 | - (IBAction)showMainWindow:(id)sender 134 | { 135 | [window makeKeyAndOrderFront:sender]; 136 | [NSApp activateIgnoringOtherApps:YES]; 137 | } 138 | 139 | - (IBAction)showPreferences:(id)sender 140 | { 141 | static NSWindowController *controller = nil; 142 | 143 | if (!controller) 144 | controller = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"]; 145 | 146 | [controller.window makeKeyAndOrderFront:self]; 147 | [NSApp activateIgnoringOtherApps:YES]; 148 | } 149 | 150 | - (IBAction)performActivation:(id)sender 151 | { 152 | [arbitrator activate]; 153 | } 154 | 155 | - (IBAction)performDeactivation:(id)sender 156 | { 157 | [arbitrator deactivate]; 158 | } 159 | 160 | - (IBAction)toggleActivation:(id)sender 161 | { 162 | if (arbitrator.isActivated) 163 | [self performDeactivation:sender]; 164 | else 165 | [self performActivation:sender]; 166 | } 167 | 168 | - (IBAction)performSetMountBlockMode:(id)sender 169 | { 170 | arbitrator.mountMode = MM_BLOCK; 171 | } 172 | 173 | - (IBAction)performSetMountReadOnlyMode:(id)sender 174 | { 175 | arbitrator.mountMode = MM_READONLY; 176 | } 177 | 178 | - (void)performMountSheetDidEnd:(NSWindow *)sheet 179 | returnCode:(NSInteger)returnCode sheetController:(SheetController *)controller; 180 | { 181 | [sheet orderOut:self]; 182 | 183 | Disk *selectedDisk = self.selectedDisk; 184 | NSMutableArray *arguments = [NSMutableArray array]; 185 | 186 | if (returnCode == NSModalResponseOK) { 187 | NSDictionary *options = controller.userInfo; 188 | 189 | if ([[options objectForKey:@"readOnly"] boolValue] == YES) 190 | [arguments addObject:@"rdonly"]; 191 | 192 | if ([[options objectForKey:@"noOwners"] boolValue] == YES) 193 | [arguments addObject:@"noowners"]; 194 | 195 | if ([[options objectForKey:@"noBrowse"] boolValue] == YES) 196 | [arguments addObject:@"nobrowse"]; 197 | 198 | if ([[options objectForKey:@"ignoreJournal"] boolValue] == YES) 199 | [arguments addObject:@"-j"]; 200 | 201 | NSString *path = [options objectForKey:@"path"]; 202 | 203 | [selectedDisk mountAtPath:path withArguments:arguments]; 204 | } 205 | } 206 | 207 | - (IBAction)performMount:(id)sender 208 | { 209 | Disk *selectedDisk = self.selectedDisk; 210 | 211 | NSAssert(selectedDisk, @"No disk selected."); 212 | NSAssert(selectedDisk.isMounted == NO, @"Disk is already mounted."); 213 | 214 | SheetController *controller = [[SheetController alloc] initWithWindowNibName:@"MountOptions"]; 215 | [controller window]; // triggers controller to load the NIB 216 | 217 | [[controller userInfo] setObject:[NSNumber numberWithBool:YES] forKey:@"readOnly"]; 218 | [[controller userInfo] setObject:[NSNumber numberWithBool:selectedDisk.isHFS] forKey:@"ignoreJournal"]; 219 | [[controller userInfo] setObject:[NSNumber numberWithBool:selectedDisk.isHFS] forKey:@"isHFS"]; 220 | 221 | [window makeKeyAndOrderFront:self]; 222 | 223 | [window beginSheet:controller.window completionHandler:^(NSModalResponse returnCode) 224 | { 225 | [self performMountSheetDidEnd:controller.window 226 | returnCode:returnCode sheetController:controller]; 227 | }]; 228 | } 229 | 230 | - (IBAction)performUnmount:(id)sender 231 | { 232 | Disk *theDisk = self.selectedDisk; 233 | 234 | if (!theDisk) return; 235 | 236 | [theDisk unmountWithOptions: theDisk.isWholeDisk ? kDiskUnmountOptionWhole : kDiskUnmountOptionDefault]; 237 | } 238 | 239 | - (IBAction)performMountOrUnmount:(id)sender 240 | { 241 | Disk *theDisk = self.selectedDisk; 242 | 243 | if (theDisk.isMounted) 244 | [self performUnmount:sender]; 245 | else 246 | [self performMount:sender]; 247 | } 248 | 249 | - (void)_childDidAttemptUnmountBeforeEject:(NSNotification *)notif 250 | { 251 | Disk *disk = notif.object; 252 | 253 | // Disk may be a mountable whole disk that we were waiting on, so the parent may be the disk itself 254 | 255 | Disk *parent = disk.isWholeDisk ? disk : disk.parent; 256 | 257 | Log(LOG_DEBUG, @"%s disk: %@ child: %@", __func__, parent, disk); 258 | 259 | [[NSNotificationCenter defaultCenter] removeObserver:self name:DADiskDidAttemptUnmountNotification object:disk]; 260 | 261 | // Confirm the child unmounted 262 | 263 | if (disk.isMounted) { 264 | // Unmount of child failed 265 | 266 | NSMutableDictionary *info = [[notif userInfo] mutableCopy]; 267 | 268 | Log(LOG_INFO, @"%s eject disk: %@ canceled due to mounted child: %@", __func__, disk, info); 269 | 270 | NSString *statusString = [NSString stringWithFormat:@"%@:\n\n%@", 271 | NSLocalizedString(@"Failed to unmount child", nil), 272 | [info objectForKey:NSLocalizedFailureReasonErrorKey]]; 273 | 274 | [info setObject:statusString forKey:NSLocalizedFailureReasonErrorKey]; 275 | [info setObject:statusString forKey:NSLocalizedRecoverySuggestionErrorKey]; 276 | 277 | [[NSNotificationCenter defaultCenter] postNotificationName:DADiskDidAttemptEjectNotification object:disk userInfo:info]; 278 | } 279 | 280 | // Child from notification is unmounted, check for remaining children to unmount 281 | 282 | for (Disk *child in parent.children) { 283 | if (child.isMounted) 284 | return; // Still waiting for child 285 | } 286 | 287 | // Need to test if parent is ejectable because we enable "Eject" for a disk 288 | // that has children that can be unmounted (ala Disk Utility) 289 | 290 | if (parent.isEjectable) 291 | [parent eject]; 292 | } 293 | 294 | - (IBAction)performEject:(id)sender 295 | { 296 | Disk *selectedDisk = self.selectedDisk; 297 | BOOL waitForChildren = NO; 298 | 299 | NSSet *disks; 300 | if (selectedDisk.isWholeDisk && selectedDisk.isLeaf) 301 | disks = [NSSet setWithObject:selectedDisk]; 302 | else 303 | disks = selectedDisk.children; 304 | 305 | for (Disk *disk in disks) { 306 | if (disk.isMountable && disk.isMounted) { 307 | [[NSNotificationCenter defaultCenter] addObserver:self 308 | selector:@selector(_childDidAttemptUnmountBeforeEject:) 309 | name:DADiskDidAttemptUnmountNotification 310 | object:disk]; 311 | [disk unmountWithOptions:0]; 312 | waitForChildren = YES; 313 | } 314 | } 315 | 316 | if (!waitForChildren) { 317 | if (selectedDisk.isEjectable) 318 | [selectedDisk eject]; 319 | } 320 | } 321 | 322 | - (IBAction)performGetInfo:(id)sender 323 | { 324 | DiskInfoController *controller = [[DiskInfoController alloc] initWithWindowNibName:@"DiskInfo"]; 325 | controller.disk = self.selectedDisk; 326 | [controller showWindow:self]; 327 | [controller refreshDiskInfo]; 328 | if (!diskInfoControllers) { 329 | diskInfoControllers = [[NSMutableArray alloc] init]; 330 | } 331 | [diskInfoControllers addObject:controller]; 332 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(diskInfoWindowDidClose:) name:NSWindowWillCloseNotification object:[controller window]]; 333 | } 334 | 335 | - (void)diskInfoWindowDidClose:(NSNotification *)notif 336 | { 337 | DiskInfoController *controllerToRemove = nil; 338 | for (DiskInfoController *controller in diskInfoControllers) { 339 | if ([controller window] == [notif object]) { 340 | controllerToRemove = controller; 341 | break; 342 | } 343 | } 344 | if (controllerToRemove) { 345 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:[notif object]]; 346 | [diskInfoControllers removeObject:controllerToRemove]; 347 | } 348 | } 349 | 350 | - (IBAction)performAttachDiskImage:(id)sender 351 | { 352 | AttachDiskImageController *controller = [[AttachDiskImageController alloc] initWithWindowNibName:@"AttachDiskImageAccessory"]; 353 | [controller window]; 354 | [controller performAttachDiskImage:sender]; 355 | } 356 | 357 | #pragma mark Launch Agent 358 | 359 | - (NSString *)userLaunchAgentPath { 360 | 361 | NSString *path = nil; 362 | 363 | CFStringRef identifier = CFBundleGetIdentifier(CFBundleGetMainBundle()); 364 | if (identifier) { 365 | path = [NSString stringWithFormat:@"%@/%@/%@.plist", NSHomeDirectory(), @"Library/LaunchAgents", identifier]; 366 | } 367 | 368 | return path; 369 | } 370 | 371 | - (void)refreshLaunchAgentStatus { 372 | NSFileManager *fm = [NSFileManager defaultManager]; 373 | 374 | self.hasUserLaunchAgent = [fm fileExistsAtPath:self.userLaunchAgentPath]; 375 | if (self.hasUserLaunchAgent) 376 | self.installUserLaunchAgentMenuTitle = @"Uninstall User Launch Agent..."; 377 | else 378 | self.installUserLaunchAgentMenuTitle = @"Install User Launch Agent..."; 379 | } 380 | 381 | - (BOOL)canInstallLaunchAgent { 382 | return (self.hasUserLaunchAgent == NO); 383 | } 384 | 385 | - (IBAction)installUserLaunchAgent:(id)sender { 386 | NSError *error; 387 | NSAlert *alert = nil; 388 | NSString *dstPath; 389 | NSFileManager *fm = [NSFileManager defaultManager]; 390 | 391 | [NSApp activateIgnoringOtherApps:YES]; 392 | 393 | if (self.hasUserLaunchAgent) { 394 | 395 | dstPath = self.userLaunchAgentPath; 396 | 397 | if ([fm removeItemAtPath:dstPath error:&error] == YES) { 398 | alert = [[NSAlert alloc] init]; 399 | alert.messageText = @"Launch Agent removed. Changes will take effect on next login."; 400 | } 401 | else { 402 | alert = [NSAlert alertWithError:error]; 403 | } 404 | } 405 | else { 406 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 407 | [dict setObject:@"us.burghardt.Disk-Arbitrator" forKey:@"Label"]; 408 | [dict setObject:[NSArray arrayWithObject:[[NSBundle mainBundle] executablePath]] forKey:@"ProgramArguments"]; 409 | [dict setObject:[NSNumber numberWithBool:NO] forKey:@"Disabled"]; 410 | [dict setObject:[NSNumber numberWithBool:YES] forKey:@"EnableTransactions"]; 411 | [dict setObject:[NSNumber numberWithBool:YES] forKey:@"KeepAlive"]; 412 | 413 | dstPath = [self userLaunchAgentPath]; 414 | 415 | NSData *plistData; 416 | plistData = [NSPropertyListSerialization dataWithPropertyList:dict format:NSPropertyListXMLFormat_v1_0 options:0 error:&error]; 417 | if (!plistData) { 418 | alert = [NSAlert alertWithError:error]; 419 | } else { 420 | if (![plistData writeToFile:dstPath options:NSDataWritingAtomic error:&error]) { 421 | alert = [NSAlert alertWithError:error]; 422 | } else { 423 | alert = [[NSAlert alloc] init]; 424 | alert.messageText = @"Launch Agent installed. Changes will take effect on next login."; 425 | } 426 | } 427 | } 428 | if (alert) 429 | [alert runModal]; 430 | 431 | [self refreshLaunchAgentStatus]; 432 | } 433 | 434 | #pragma mark Selected Disk 435 | 436 | - (Disk *)selectedDisk 437 | { 438 | NSIndexSet *indexes = [disksArrayController selectionIndexes]; 439 | 440 | if (indexes.count == 1) 441 | return [disksArrayController.arrangedObjects objectAtIndex:indexes.lastIndex]; 442 | else 443 | return nil; 444 | } 445 | 446 | - (BOOL)canEjectSelectedDisk 447 | { 448 | /* "Eject" in the GUI means eject or unmount (like Disk Utility) 449 | * To the Disk class, "ejectable" means the media object is ejectable. 450 | */ 451 | 452 | Disk *selectedDisk = self.selectedDisk; 453 | BOOL canEject = [selectedDisk isEjectable]; 454 | 455 | if (!canEject) { 456 | for (Disk *child in selectedDisk.children) { 457 | if (child.isMountable && child.isMounted) 458 | canEject = YES; 459 | } 460 | } 461 | return canEject; 462 | } 463 | 464 | - (BOOL)canMountSelectedDisk 465 | { 466 | Disk *disk = self.selectedDisk; 467 | 468 | if (disk.isMountable && !disk.isMounted) 469 | return YES; 470 | else 471 | return NO; 472 | } 473 | 474 | - (BOOL)canUnmountSelectedDisk 475 | { 476 | Disk *disk = self.selectedDisk; 477 | 478 | return (disk.isMountable && disk.isMounted); 479 | 480 | // // Yes if the disk or any children are mounted 481 | // 482 | // if (disk.mountable && disk.mounted) return YES; 483 | // 484 | // for (Disk *child in disk.children) 485 | // if (child.mountable && child.mounted) 486 | // return YES; 487 | } 488 | 489 | #pragma mark TableView Delegates 490 | 491 | // A custom cell is used for the media description column. Couldn't find a way to bind it to the disk 492 | // object, so implemented the dataSource delegate. 493 | 494 | - (id)tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)column row:(int)rowIndex 495 | { 496 | Disk *disk; 497 | 498 | NSParameterAssert(rowIndex >= 0 && rowIndex < arbitrator.disks.count); 499 | disk = [disksArrayController.arrangedObjects objectAtIndex:rowIndex]; 500 | 501 | if ([column.identifier isEqual:@"BSDName"]) 502 | return disk.BSDName; 503 | 504 | // fprintf(stdout, "getting value: %s\n", disk.BSDName.UTF8String); 505 | return disk; 506 | } 507 | 508 | - (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id )info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)op 509 | { 510 | Log(LOG_DEBUG, @"%s op: %ld info: %@", __func__, op, info); 511 | 512 | NSPasteboard* pboard = [info draggingPasteboard]; 513 | 514 | if (op == NSDragOperationCopy && [pboard.types containsObject:NSFilenamesPboardType]) { 515 | 516 | NSArray *files = [pboard propertyListForType:NSFilenamesPboardType]; 517 | NSArray *extensions = [AttachDiskImageController diskImageFileExtensions]; 518 | 519 | for (NSString *file in files) { 520 | if ([extensions containsObject:[file pathExtension]] == NO) 521 | return NSDragOperationNone; 522 | } 523 | return NSDragOperationCopy; 524 | } 525 | return NSDragOperationNone; 526 | } 527 | 528 | - (void)doAttachDiskImageAtPath:(NSString *)path 529 | { 530 | NSError *error; 531 | 532 | AttachDiskImageController *controller = [[AttachDiskImageController alloc] initWithWindowNibName:@"AttachDiskImageAccessory"]; 533 | [controller window]; 534 | 535 | NSArray *options; 536 | if (arbitrator.isActivated) 537 | options = [NSArray arrayWithObjects:@"-readonly", @"-nomount", nil]; 538 | else 539 | options = [NSArray arrayWithObjects:@"-readonly", @"-mount", @"optional", nil]; 540 | 541 | if (![controller attachDiskImageAtPath:path options:options password:nil error:&error]) 542 | { 543 | [NSApp presentError:error]; 544 | } 545 | } 546 | 547 | - (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id )info 548 | row:(int)row dropOperation:(NSTableViewDropOperation)operation 549 | { 550 | NSPasteboard* pboard = [info draggingPasteboard]; 551 | 552 | Log(LOG_DEBUG, @"%s", __func__); 553 | 554 | if (operation == NSDragOperationCopy && [pboard.types containsObject:NSFilenamesPboardType] ) { 555 | NSArray *files = [pboard propertyListForType:NSFilenamesPboardType]; 556 | 557 | Log(LOG_DEBUG, @"files: %@", files); 558 | 559 | for (NSString *file in files) 560 | [self performSelector:@selector(doAttachDiskImageAtPath:) withObject:file afterDelay:0.01]; 561 | } 562 | return YES; 563 | } 564 | 565 | #pragma mark Window Delegates 566 | 567 | - (void)windowWillClose:(NSNotification *)notification 568 | { 569 | [NSApp deactivate]; 570 | } 571 | 572 | #pragma mark Disk Notifications 573 | 574 | - (void)didPresentErrorWithRecovery:(BOOL)didRecover contextInfo:(void *)contextInfo 575 | { 576 | // If another sheet has unexpected been displayed, recover gracefully 577 | 578 | if (window.attachedSheet) { 579 | Log(LOG_INFO, @"Discarding pending errors: %@", displayErrorQueue); 580 | [displayErrorQueue removeAllObjects]; 581 | return; 582 | } 583 | 584 | if (displayErrorQueue.count > 0) 585 | { 586 | NSError *nextError = [displayErrorQueue objectAtIndex:0]; 587 | 588 | [window makeKeyAndOrderFront:self]; 589 | [NSApp presentError:nextError 590 | modalForWindow:window 591 | delegate:self 592 | didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) 593 | contextInfo:NULL]; 594 | 595 | [displayErrorQueue removeObjectAtIndex:0]; 596 | } 597 | } 598 | 599 | - (void)diskDidChange:(NSNotification *)notif 600 | { 601 | NSUInteger row = [disksArrayController.arrangedObjects indexOfObject:notif.object]; 602 | 603 | [tableView setNeedsDisplayInRect:[tableView rectOfRow:row]]; 604 | [window.toolbar validateVisibleItems]; 605 | } 606 | 607 | - (void)didAttemptMount:(NSNotification *)notif 608 | { 609 | Disk *disk = notif.object; 610 | NSMutableDictionary *info; 611 | 612 | if (disk.isMounted) { 613 | Log(LOG_DEBUG, @"%s: Mounted: %@", __func__, disk.BSDName); 614 | } 615 | else { 616 | // If the mount failed, the notification userInfo will have keys/values that correspond to an NSError 617 | 618 | info = [[notif userInfo] mutableCopy]; 619 | 620 | NSString *reason = [info objectForKey:NSLocalizedFailureReasonErrorKey]; 621 | Log(LOG_ERR, @"Mount failed: %@ (%@) %@", disk.BSDName, [info objectForKey:DAStatusErrorKey], reason); 622 | 623 | [info setObject:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Mount rejected", nil), disk.BSDName] 624 | forKey:NSLocalizedDescriptionKey]; 625 | 626 | NSError *error = [NSError errorWithDomain:AppErrorDomain 627 | code:[[info objectForKey:DAStatusErrorKey] intValue] 628 | userInfo:info]; 629 | 630 | // Don't show our internal dissenter error 631 | if (![reason isEqualToString:arbitrator.dissenterMessage]) { 632 | if (window.attachedSheet) { 633 | [displayErrorQueue addObject:error]; 634 | } 635 | else { 636 | [window makeKeyAndOrderFront:self]; 637 | [NSApp presentError:error 638 | modalForWindow:window 639 | delegate:self 640 | didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) 641 | contextInfo:NULL]; 642 | } 643 | } 644 | } 645 | [window.toolbar validateVisibleItems]; 646 | } 647 | 648 | - (void)didAttemptUnmount:(NSNotification *)notif 649 | { 650 | Disk *disk = notif.object; 651 | NSMutableDictionary *info; 652 | 653 | Log(LOG_DEBUG, @"%s: Unmount %@: %@", __func__, (disk.isMounted ? @"failed" : @"succeeded"), disk.BSDName); 654 | 655 | if (disk.isMounted) { 656 | // If the unmount failed, the notification userInfo will have keys/values that correspond to an NSError 657 | 658 | info = [[notif userInfo] mutableCopy]; 659 | 660 | Log(LOG_INFO, @"Unmount %@ failed: (%@) %@", disk.BSDName, [info objectForKey:DAStatusErrorKey], [info objectForKey:NSLocalizedFailureReasonErrorKey]); 661 | 662 | [info setObject:NSLocalizedString(@"Unmount failed", nil) forKey:NSLocalizedDescriptionKey]; 663 | 664 | NSError *error = [NSError errorWithDomain:AppErrorDomain 665 | code:[[info objectForKey:DAStatusErrorKey] intValue] 666 | userInfo:info]; 667 | 668 | if (window.attachedSheet) { 669 | [displayErrorQueue addObject:error]; 670 | } 671 | else { 672 | 673 | [window makeKeyAndOrderFront:self]; 674 | [NSApp presentError:error 675 | modalForWindow:window 676 | delegate:self 677 | didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) 678 | contextInfo:NULL]; 679 | } 680 | } 681 | [window.toolbar validateVisibleItems]; 682 | } 683 | 684 | - (void)didAttemptEject:(NSNotification *)notif 685 | { 686 | Disk *disk = notif.object; 687 | 688 | if ([notif userInfo]) { 689 | 690 | NSMutableDictionary *info = [[notif userInfo] mutableCopy]; 691 | 692 | // If the eject failed, the notification userInfo will have keys/values that correspond to an NSError 693 | 694 | Log(LOG_INFO, @"Ejecting %@ failed: (%@) %@", disk.BSDName, [info objectForKey:DAStatusErrorKey], [info objectForKey:NSLocalizedFailureReasonErrorKey]); 695 | 696 | [info setObject:NSLocalizedString(@"Eject failed", nil) forKey:NSLocalizedDescriptionKey]; 697 | 698 | NSError *error = [NSError errorWithDomain:AppErrorDomain 699 | code:[[info objectForKey:DAStatusErrorKey] intValue] 700 | userInfo:info]; 701 | 702 | if (window.attachedSheet) { 703 | [displayErrorQueue addObject:error]; 704 | } 705 | else { 706 | 707 | [window makeKeyAndOrderFront:self]; 708 | [NSApp presentError:error 709 | modalForWindow:window 710 | delegate:self 711 | didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:) 712 | contextInfo:NULL]; 713 | } 714 | } 715 | else { 716 | Log(LOG_DEBUG, @"%s: Ejected: %@", __func__, disk); 717 | } 718 | [window.toolbar validateVisibleItems]; 719 | } 720 | 721 | @end 722 | -------------------------------------------------------------------------------- /Source/DiskArbitrator_Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'DiskArbitrator' target in the 'DiskArbitrator' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | 9 | #define SafeCFRelease(v) if (v) { CFRelease(v); } 10 | -------------------------------------------------------------------------------- /Source/DiskCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskCell.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/23/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DiskCell : NSTextFieldCell 12 | 13 | @property (retain) NSImageCell *iconCell; 14 | @property (retain) NSCell *textCell; 15 | @property CGFloat indentation; 16 | @property BOOL mountable; 17 | @property BOOL mounted; 18 | @property BOOL isDiskWritable; 19 | @property BOOL isFileSystemWritable; 20 | @property (copy) NSString *BSDName; 21 | @property (copy) NSString *mediaName; 22 | @property (copy) NSNumber *mediaSize; 23 | @property (copy) NSString *volumeName; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Source/DiskCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // DiskCell.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/23/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import "DiskCell.h" 10 | #import "AppError.h" 11 | #import "Disk.h" 12 | 13 | 14 | #define ICONPADDING 3.0 15 | 16 | @implementation DiskCell 17 | 18 | @synthesize iconCell; 19 | @synthesize textCell; 20 | @synthesize indentation; 21 | @synthesize mountable; 22 | @synthesize mounted; 23 | @synthesize isDiskWritable; 24 | @synthesize isFileSystemWritable; 25 | @synthesize mediaName; 26 | @synthesize mediaSize; 27 | @synthesize BSDName; 28 | @synthesize volumeName; 29 | 30 | 31 | - (id)copyWithZone:(NSZone *)zone 32 | { 33 | id obj = [super copyWithZone:zone]; 34 | if (iconCell) iconCell = [iconCell copyWithZone:zone]; 35 | if (textCell) textCell = [textCell copyWithZone:zone]; 36 | [BSDName retain]; 37 | [mediaName retain]; 38 | [mediaSize retain]; 39 | [volumeName retain]; 40 | 41 | return obj; 42 | } 43 | 44 | - (void)dealloc 45 | { 46 | [iconCell release]; 47 | [textCell release]; 48 | [BSDName release]; 49 | [mediaName release]; 50 | [mediaSize release]; 51 | [volumeName release]; 52 | 53 | [super dealloc]; 54 | } 55 | 56 | - (void)drawInteriorWithFrame:(NSRect)frame inView:(NSView *)controlView 57 | { 58 | // Log(LOG_DEBUG, @"Frame: %s\t\tView Frame: %@", NSStringFromRect(frame), NSStringFromRect([view frame])); 59 | 60 | NSRect iconFrame, textFrame; 61 | 62 | CGFloat minwh = MIN(frame.size.width - indentation, frame.size.height); 63 | iconFrame = NSMakeRect(frame.origin.x + indentation + ICONPADDING, frame.origin.y, minwh, minwh); 64 | 65 | iconCell.enabled = (mountable && !mounted) ? NO : YES; // dimmed if a volume but not mounted 66 | iconCell.highlighted = self.isHighlighted; 67 | [iconCell drawWithFrame:iconFrame inView:controlView]; 68 | 69 | // A disk may be mounted R/O even though the underlying disk is R/W. To avoid giving the false 70 | // impression that a disk is completely protected, display a transparent, faint lock when the 71 | // disk is R/W, and display a solid black lock when the disk and the FS are both R/O. 72 | 73 | if (mountable && mounted && !isFileSystemWritable) { 74 | 75 | NSImage *lockImage = [NSImage imageNamed:NSImageNameLockLockedTemplate]; 76 | 77 | CGFloat scale; 78 | if (frame.size.height <= 16.0) 79 | scale = 0.5; 80 | else if (frame.size.height <= 32.0) 81 | scale = 0.75; 82 | else if (frame.size.height <= 64.0) 83 | scale = 2.0; 84 | else if (frame.size.height <= 128.0) 85 | scale = 4.0; 86 | else if (frame.size.height <= 256.0) 87 | scale = 8.0; 88 | else if (frame.size.height <= 512.0) 89 | scale = 16.0; 90 | else if (frame.size.height <= 1024.0) 91 | scale = 32.0; 92 | else 93 | scale = 2.0; 94 | 95 | CGFloat opacity = isDiskWritable ? 0.40 : 1.0; 96 | 97 | CGFloat scaledWidth = lockImage.size.width * scale; 98 | CGFloat scaledHeight = lockImage.size.height * scale; 99 | NSRect rect = NSMakeRect(NSMaxX(iconFrame) - scaledWidth, NSMaxY(iconFrame) - scaledHeight, scaledWidth, scaledHeight); 100 | 101 | [NSGraphicsContext saveGraphicsState]; 102 | if ([controlView isFlipped]) { 103 | NSAffineTransform *transform = [NSAffineTransform transform]; 104 | [transform translateXBy:NSMidX(rect) yBy:NSMidY(rect)]; 105 | [transform rotateByDegrees:180]; 106 | [transform translateXBy:-NSMidX(rect) yBy:-NSMidY(rect)]; 107 | [transform concat]; 108 | } 109 | 110 | [lockImage drawInRect:rect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:opacity]; 111 | 112 | [NSGraphicsContext restoreGraphicsState]; 113 | } 114 | 115 | textFrame = NSMakeRect(NSMaxX(iconFrame) + ICONPADDING, frame.origin.y, 116 | MAX(NSWidth(frame) - (NSMaxX(iconFrame) + ICONPADDING), 0.0), NSHeight(frame)); 117 | NSSize textCellSize = textCell.cellSize; 118 | textFrame.origin.y = frame.origin.y + floor((frame.size.height - textCellSize.height) / 2); 119 | 120 | textCell.enabled = (mountable && !mounted) ? NO : YES; // dimmed if a volume but not mounted 121 | textCell.highlighted = self.isHighlighted; 122 | [textCell drawWithFrame:textFrame inView:controlView]; 123 | } 124 | 125 | 126 | - (void)setObjectValue:(id)value 127 | { 128 | Disk *disk = (Disk *)value; 129 | 130 | if (disk) { 131 | // Log(LOG_DEBUG, @"%s self: %p disk: %p", __func__, self, disk); 132 | 133 | self.BSDName = disk.BSDName; 134 | self.indentation = disk.isWholeDisk ? 0.0 : 17.0; 135 | self.mounted = disk.isMounted; 136 | self.mountable = disk.isMountable; 137 | self.isDiskWritable = disk.isWritable; 138 | self.isFileSystemWritable = disk.isFileSystemWritable; 139 | 140 | self.mediaName = (NSString *)[disk.diskDescription objectForKey: (NSString *)kDADiskDescriptionMediaNameKey]; 141 | self.mediaSize = (NSNumber *)[disk.diskDescription objectForKey: (NSString *)kDADiskDescriptionMediaSizeKey]; 142 | self.volumeName = (NSString *)[disk.diskDescription objectForKey: (NSString *)kDADiskDescriptionVolumeNameKey]; 143 | 144 | // Create Text description cell 145 | 146 | NSString *sizeDisplayValue = nil; 147 | if (mediaSize) { 148 | double size = [mediaSize doubleValue]; 149 | if (size > 999 && size < 1000000) 150 | sizeDisplayValue = [NSString stringWithFormat:@"%03.02f KB ", (size / 1000.0)]; 151 | else if (size > 999999 && size < 1000000000) 152 | sizeDisplayValue = [NSString stringWithFormat:@"%03.02f MB ", (size / 1000000.0)]; 153 | else if (size > 999999999 && size < 1000000000000) 154 | sizeDisplayValue = [NSString stringWithFormat:@"%03.02f GB ", (size / 1000000000.0)]; 155 | else if (size > 999999999999) 156 | sizeDisplayValue = [NSString stringWithFormat:@"%03.02f TB ", (size / 1000000000000.0)]; 157 | } 158 | NSMutableString *desc = sizeDisplayValue ? [sizeDisplayValue mutableCopy] : [NSMutableString new]; 159 | 160 | if (volumeName) 161 | [desc appendString:volumeName]; 162 | else if (mediaName) 163 | [desc appendString:mediaName]; 164 | 165 | self.textCell = [[[NSCell alloc] initTextCell:desc] autorelease]; 166 | self.textCell.lineBreakMode = NSLineBreakByTruncatingTail; 167 | [desc release]; 168 | 169 | // Create Icon cell 170 | 171 | self.iconCell = [[[NSImageCell alloc] initImageCell:disk.icon] autorelease]; 172 | iconCell.imageScaling = NSImageScaleProportionallyDown; 173 | iconCell.alignment = NSTextAlignmentLeft; 174 | } 175 | else { 176 | self.textCell = nil; 177 | self.iconCell = nil; 178 | } 179 | } 180 | 181 | @end 182 | -------------------------------------------------------------------------------- /Source/DiskInfoController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DiskInfoController.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/11/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class Disk; 12 | 13 | @interface DiskInfoController : NSWindowController 14 | 15 | @property (strong) IBOutlet NSTextView *textView; 16 | @property (strong, nonatomic) Disk *disk; 17 | @property (copy) NSDictionary *diskDescription; 18 | @property (copy) NSAttributedString *diskInfo; 19 | 20 | - (void)refreshDiskInfo; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Source/DiskInfoController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DiskInfoController.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/11/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import "DiskInfoController.h" 10 | #import "AppError.h" 11 | #import 12 | #import "Disk.h" 13 | 14 | 15 | @implementation DiskInfoController 16 | 17 | @synthesize textView; 18 | @synthesize diskDescription; 19 | @synthesize diskInfo; 20 | 21 | + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key 22 | { 23 | if ([key isEqual:@"diskDescription"]) 24 | return [NSSet setWithObject:@"disk.diskDescription"]; 25 | 26 | return [super keyPathsForValuesAffectingValueForKey:key]; 27 | } 28 | 29 | - (void)dealloc 30 | { 31 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 32 | } 33 | 34 | - (NSString *)localizedStringForDADiskKey:(NSString *)key 35 | { 36 | static dispatch_once_t sOnceToken; 37 | static NSDictionary *localizedStrings = nil; 38 | dispatch_once(&sOnceToken, ^{ 39 | localizedStrings = 40 | @{ 41 | (NSString *)kDADiskDescriptionVolumeKindKey: NSLocalizedString(@"Volume Kind", nil), 42 | (NSString *)kDADiskDescriptionVolumeMountableKey: NSLocalizedString(@"Volume Mountable", nil), 43 | (NSString *)kDADiskDescriptionVolumeNameKey: NSLocalizedString(@"Volume Name", nil), 44 | (NSString *)kDADiskDescriptionVolumeNetworkKey: NSLocalizedString(@"Volume Network", nil), 45 | (NSString *)kDADiskDescriptionVolumePathKey: NSLocalizedString(@"Mount Path", nil), 46 | (NSString *)kDADiskDescriptionVolumeTypeKey: NSLocalizedString(@"Volume Type", nil), 47 | (NSString *)kDADiskDescriptionVolumeUUIDKey: NSLocalizedString(@"Volume UUID", nil), 48 | (NSString *)kDADiskDescriptionMediaBlockSizeKey: NSLocalizedString(@"Media Block Size", nil), 49 | (NSString *)kDADiskDescriptionMediaBSDMajorKey: NSLocalizedString(@"Media BSD Major", nil), 50 | (NSString *)kDADiskDescriptionMediaBSDMinorKey: NSLocalizedString(@"Media BSD Minor", nil), 51 | (NSString *)kDADiskDescriptionMediaBSDNameKey: NSLocalizedString(@"Media BSD Name", nil), 52 | (NSString *)kDADiskDescriptionMediaBSDUnitKey: NSLocalizedString(@"Media BSD Unit", nil), 53 | (NSString *)kDADiskDescriptionMediaContentKey: NSLocalizedString(@"Media Content", nil), 54 | (NSString *)kDADiskDescriptionMediaEjectableKey: NSLocalizedString(@"Media Ejectable", nil), 55 | (NSString *)kDADiskDescriptionMediaIconKey: NSLocalizedString(@"Media Icon", nil), 56 | (NSString *)kDADiskDescriptionMediaKindKey: NSLocalizedString(@"Media Kind", nil), 57 | (NSString *)kDADiskDescriptionMediaLeafKey: NSLocalizedString(@"Media Is Leaf", nil), 58 | (NSString *)kDADiskDescriptionMediaNameKey: NSLocalizedString(@"Media Name", nil), 59 | (NSString *)kDADiskDescriptionMediaPathKey: NSLocalizedString(@"Media Path", nil), 60 | (NSString *)kDADiskDescriptionMediaRemovableKey: NSLocalizedString(@"Media Is Removable", nil), 61 | (NSString *)kDADiskDescriptionMediaSizeKey: NSLocalizedString(@"Media Size", nil), 62 | (NSString *)kDADiskDescriptionMediaTypeKey: NSLocalizedString(@"Media Type", nil), 63 | (NSString *)kDADiskDescriptionMediaUUIDKey: NSLocalizedString(@"Media UUID", nil), 64 | (NSString *)kDADiskDescriptionMediaWholeKey: NSLocalizedString(@"Media Is Whole", nil), 65 | (NSString *)kDADiskDescriptionMediaWritableKey: NSLocalizedString(@"Media Is Writable", nil), 66 | (NSString *)kDADiskDescriptionMediaEncryptedKey: NSLocalizedString(@"Encrypted", nil), 67 | (NSString *)kDADiskDescriptionMediaEncryptionDetailKey: NSLocalizedString(@"Encryption Detail", nil), 68 | (NSString *)kDADiskDescriptionDeviceGUIDKey: NSLocalizedString(@"Device GUID", nil), 69 | (NSString *)kDADiskDescriptionDeviceInternalKey: NSLocalizedString(@"Device Is Internal", nil), 70 | (NSString *)kDADiskDescriptionDeviceModelKey: NSLocalizedString(@"Device Model", nil), 71 | (NSString *)kDADiskDescriptionDevicePathKey: NSLocalizedString(@"Device Path", nil), 72 | (NSString *)kDADiskDescriptionDeviceProtocolKey: NSLocalizedString(@"Device Protocol", nil), 73 | (NSString *)kDADiskDescriptionDeviceRevisionKey: NSLocalizedString(@"Device Revision", nil), 74 | (NSString *)kDADiskDescriptionDeviceUnitKey: NSLocalizedString(@"Device Unit", nil), 75 | (NSString *)kDADiskDescriptionDeviceVendorKey: NSLocalizedString(@"Device Vendor", nil), 76 | (NSString *)kDADiskDescriptionDeviceTDMLockedKey: NSLocalizedString(@"TDM Locked", nil), 77 | (NSString *)kDADiskDescriptionBusNameKey: NSLocalizedString(@"Bus", nil), 78 | (NSString *)kDADiskDescriptionBusPathKey: NSLocalizedString(@"Bus Path", nil), 79 | @"DAAppearanceTime": NSLocalizedString(@"Appearance Time", nil) 80 | }; 81 | }); 82 | 83 | NSString *description = localizedStrings[key]; 84 | 85 | if (nil == description) 86 | { 87 | Log(LOG_INFO, @"Unknown disk description key: %@", key); 88 | description = @"N/A"; 89 | } 90 | 91 | return description; 92 | } 93 | 94 | - (NSString *)formattedSizeDescriptionFromNumber:(NSNumber *)sizeValue 95 | { 96 | NSString *formattedValue; 97 | 98 | double size = [sizeValue doubleValue]; 99 | 100 | if (size > 999.0 && size < 1000000.0) 101 | formattedValue = [NSString stringWithFormat:@"%03.02f KB (%@ bytes)", (size / 1000.0), sizeValue]; 102 | else if (size > 999999.0 && size < 1000000000.0) 103 | formattedValue = [NSString stringWithFormat:@"%03.02f MB (%@ bytes)", (size / 1000000.0), sizeValue]; 104 | else if (size > 999999999.0 && size < 1000000000000.0) 105 | formattedValue = [NSString stringWithFormat:@"%03.02f GB (%@ bytes)", (size / 1000000000.0), sizeValue]; 106 | else if (size > 999999999999.0) 107 | formattedValue = [NSString stringWithFormat:@"%03.02f TB (%@ bytes)", (size / 1000000000000.0), sizeValue]; 108 | else 109 | formattedValue = sizeValue.stringValue; 110 | 111 | return formattedValue; 112 | } 113 | 114 | - (NSString *)localizedValueStringForDADiskKey:(NSString *)key value:(id)value 115 | { 116 | CFStringRef keyRef = (__bridge CFStringRef) key; 117 | 118 | if (CFEqual(keyRef, kDADiskDescriptionVolumeKindKey)) /* ( CFString ) */ 119 | return value; 120 | 121 | if (CFEqual(keyRef, kDADiskDescriptionVolumeMountableKey) || /* ( CFBoolean ) */ 122 | CFEqual(keyRef, kDADiskDescriptionVolumeNetworkKey) || 123 | CFEqual(keyRef, kDADiskDescriptionMediaLeafKey) || 124 | CFEqual(keyRef, kDADiskDescriptionMediaEjectableKey) || 125 | CFEqual(keyRef, kDADiskDescriptionMediaRemovableKey) || 126 | CFEqual(keyRef, kDADiskDescriptionMediaWholeKey) || 127 | CFEqual(keyRef, kDADiskDescriptionMediaWritableKey) || 128 | CFEqual(keyRef, kDADiskDescriptionDeviceInternalKey) 129 | ) 130 | return [value boolValue] ? @"Yes" : @"No"; 131 | 132 | 133 | if (CFEqual(keyRef, kDADiskDescriptionVolumeNameKey)) /* ( CFString ) */ 134 | return value; 135 | 136 | if (CFEqual(keyRef, kDADiskDescriptionVolumePathKey)) /* ( CFURL ) */ 137 | return [(NSURL *)value path]; 138 | 139 | if (CFEqual(keyRef, kDADiskDescriptionVolumeUUIDKey) || /* ( CFUUID ) */ 140 | CFEqual(keyRef, kDADiskDescriptionMediaUUIDKey)) 141 | { 142 | NSString *uuidString = (NSString *) CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, (CFUUIDRef)value)); 143 | 144 | return uuidString; 145 | } 146 | 147 | if (CFEqual(keyRef, kDADiskDescriptionMediaBlockSizeKey)) /* ( CFNumber ) */ 148 | return [value stringValue]; 149 | 150 | if (CFEqual(keyRef, kDADiskDescriptionMediaBSDMajorKey)) /* ( CFNumber ) */ 151 | return [value stringValue]; 152 | 153 | if (CFEqual(keyRef, kDADiskDescriptionMediaBSDMinorKey)) /* ( CFNumber ) */ 154 | return [value stringValue]; 155 | 156 | if (CFEqual(keyRef, kDADiskDescriptionMediaBSDNameKey)) /* ( CFString ) */ 157 | return value; 158 | 159 | if (CFEqual(keyRef, kDADiskDescriptionMediaBSDUnitKey)) /* ( CFNumber ) */ 160 | return [value stringValue]; 161 | 162 | if (CFEqual(keyRef, kDADiskDescriptionMediaContentKey)) /* ( CFString ) */ 163 | return value; 164 | 165 | if (CFEqual(keyRef, kDADiskDescriptionMediaIconKey)) /* ( CFDictionary ) */ 166 | return [value description]; 167 | 168 | if (CFEqual(keyRef, kDADiskDescriptionMediaKindKey)) /* ( CFString ) */ 169 | return value; 170 | 171 | if (CFEqual(keyRef, kDADiskDescriptionMediaNameKey)) /* ( CFString ) */ 172 | return value; 173 | 174 | if (CFEqual(keyRef, kDADiskDescriptionMediaPathKey)) /* ( CFString ) */ 175 | return value; 176 | 177 | if (CFEqual(keyRef, kDADiskDescriptionMediaSizeKey)) /* ( CFNumber ) */ 178 | return [self formattedSizeDescriptionFromNumber:(NSNumber *)value]; 179 | 180 | if (CFEqual(keyRef, kDADiskDescriptionMediaTypeKey)) /* ( CFString ) */ 181 | return value; 182 | 183 | if (CFEqual(keyRef, kDADiskDescriptionDeviceGUIDKey)) /* ( CFData ) */ 184 | return [value description]; 185 | 186 | if (CFEqual(keyRef, kDADiskDescriptionDeviceModelKey)) /* ( CFString ) */ 187 | return value; 188 | 189 | if (CFEqual(keyRef, kDADiskDescriptionDevicePathKey)) /* ( CFString ) */ 190 | return value; 191 | 192 | if (CFEqual(keyRef, kDADiskDescriptionDeviceProtocolKey)) /* ( CFString ) */ 193 | return value; 194 | 195 | if (CFEqual(keyRef, kDADiskDescriptionDeviceRevisionKey)) /* ( CFString ) */ 196 | return value; 197 | 198 | if (CFEqual(keyRef, kDADiskDescriptionDeviceUnitKey)) /* ( CFNumber ) */ 199 | return [value stringValue]; 200 | 201 | if (CFEqual(keyRef, kDADiskDescriptionDeviceVendorKey)) /* ( CFString ) */ 202 | return value; 203 | 204 | if (CFEqual(keyRef, kDADiskDescriptionBusNameKey)) /* ( CFString ) */ 205 | return value; 206 | 207 | if (CFEqual(keyRef, kDADiskDescriptionBusPathKey)) /* ( CFString ) */ 208 | return value; 209 | 210 | if (CFEqual(keyRef, CFSTR("DAAppearanceTime"))) 211 | return [[NSDate dateWithTimeIntervalSinceReferenceDate:[value doubleValue]] description]; 212 | 213 | Log(LOG_INFO, @"Unknown disk description key: %@", keyRef); 214 | 215 | return @"N/A"; 216 | } 217 | 218 | - (NSString *)stringForDADiskKey:(NSString *)key value:(id)value 219 | { 220 | return [NSString stringWithFormat:@"\t%@\t%@\n", 221 | [self localizedStringForDADiskKey:key], 222 | [self localizedValueStringForDADiskKey:key value:value]]; 223 | } 224 | 225 | - (void)refreshDiskInfo 226 | { 227 | self.diskDescription = self.disk.diskDescription; 228 | 229 | NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@""]; 230 | 231 | NSFont *font = [NSFont fontWithName:@"Helvetica Bold" size:12.0]; 232 | NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, nil]; 233 | 234 | NSArray *keys = [self.disk.diskDescription allKeys]; 235 | keys = [keys sortedArrayUsingSelector:@selector(compare:)]; 236 | 237 | for (NSString *key in keys) 238 | { 239 | // Ignore certain keys 240 | if ([key isEqual: (NSString *)kDADiskDescriptionMediaIconKey]) 241 | continue; 242 | 243 | id value = [self.disk.diskDescription objectForKey:key]; 244 | 245 | NSString *string; 246 | NSAttributedString *attrString; 247 | 248 | string = [NSString stringWithFormat:@"\t%@\t", [self localizedStringForDADiskKey:key]]; 249 | attrString = [[NSAttributedString alloc] initWithString:string attributes:attrs]; 250 | [text appendAttributedString:attrString]; 251 | 252 | string = [NSString stringWithFormat:@"%@\n", [self localizedValueStringForDADiskKey:key value:value]]; 253 | attrString = [[NSAttributedString alloc] initWithString:string]; 254 | [text appendAttributedString:attrString]; 255 | } 256 | 257 | 258 | NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; 259 | NSMutableArray *tabStops = [NSMutableArray array]; 260 | [tabStops addObject:[[NSTextTab alloc] initWithType:NSRightTabStopType location:2.0 * 72.0]]; 261 | [tabStops addObject:[[NSTextTab alloc] initWithType:NSLeftTabStopType location:2.125 * 72.0]]; 262 | style.tabStops = tabStops; 263 | style.headIndent = (2.125 * 72.0); 264 | 265 | attrs = [NSDictionary dictionaryWithObjectsAndKeys:style, NSParagraphStyleAttributeName, nil]; 266 | [text addAttributes:attrs range:NSMakeRange(0, [text length])]; 267 | 268 | self.diskInfo = text; 269 | } 270 | 271 | - (void)diskDidChange:(NSNotification *)notif 272 | { 273 | [self refreshDiskInfo]; 274 | } 275 | 276 | - (void)setDisk:(Disk *)newDisk 277 | { 278 | if (newDisk != _disk) { 279 | [[NSNotificationCenter defaultCenter] removeObserver:self 280 | name:DADiskDidChangeNotification object:_disk]; 281 | _disk = newDisk; 282 | [[NSNotificationCenter defaultCenter] addObserver:self 283 | selector:@selector(diskDidChange:) name:DADiskDidChangeNotification 284 | object:_disk]; 285 | } 286 | } 287 | 288 | @end 289 | -------------------------------------------------------------------------------- /Source/SheetController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SheetController.h 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/10/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface SheetController : NSWindowController 13 | 14 | @property (strong) NSMutableDictionary *userInfo; 15 | 16 | - (IBAction)alternate:(id)sender; 17 | - (IBAction)cancel:(id)sender; 18 | - (IBAction)ok:(id)sender; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Source/SheetController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SheetController.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 2/10/10. 6 | // Copyright 2010 . All rights reserved. 7 | // 8 | 9 | #import "SheetController.h" 10 | 11 | 12 | @implementation SheetController 13 | 14 | @synthesize userInfo; 15 | 16 | - (void)windowWillLoad 17 | { 18 | self.userInfo = [NSMutableDictionary dictionary]; 19 | } 20 | 21 | - (IBAction)alternate:(id)sender 22 | { 23 | [self.window endEditingFor:nil]; 24 | [NSApp endSheet:self.window returnCode:-1]; 25 | } 26 | 27 | - (IBAction)cancel:(id)sender 28 | { 29 | [NSApp endSheet:self.window returnCode:NSModalResponseCancel]; 30 | } 31 | 32 | - (IBAction)ok:(id)sender 33 | { 34 | [self.window endEditingFor:nil]; 35 | [NSApp endSheet:self.window returnCode:NSModalResponseOK]; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Source/build_dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # build_dmg.sh 4 | # DiskArbitrator 5 | # 6 | # Created by Aaron Burghardt on 2/6/10. 7 | # Copyright 2010 . All rights reserved. 8 | 9 | set -e # fail on any error 10 | 11 | SRC_PRODUCT="$1" 12 | SRC_PRODUCT_PATH="${BUILT_PRODUCTS_DIR}/${SRC_PRODUCT}.app" 13 | SRC_PRODUCT_CONTENTS="${SRC_PRODUCT_PATH}/Contents" 14 | SRC_PRODUCT_RESOURCES="${SRC_PRODUCT_CONTENTS}/Resources" 15 | SRC_PRODUCT_VERSION=`/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "${SRC_PRODUCT_CONTENTS}/Info.plist"` 16 | 17 | echo "Building DMG for ${SRC_PRODUCT} ${SRC_PRODUCT_VERSION}" 18 | 19 | DMG_DST_PATH="${BUILT_PRODUCTS_DIR}/${SRC_PRODUCT}-${SRC_PRODUCT_VERSION}.dmg" 20 | 21 | DMG_SRC_DIR="${CONFIGURATION_TEMP_DIR}/${SRC_PRODUCT}-${SRC_PRODUCT_VERSION}" 22 | 23 | if [ -e "$DMG_DST_PATH" ] ; then 24 | rm -rf "$DMG_DST_PATH" 25 | fi 26 | 27 | mkdir -p "${DMG_SRC_DIR}" 28 | cp -LR "${SRC_PRODUCT_PATH}" "${DMG_SRC_DIR}" 29 | cp "${SRC_PRODUCT_PATH}/Contents/Resources/README.html" "${DMG_SRC_DIR}" 30 | cp "${SRC_PRODUCT_PATH}/Contents/Resources/ReleaseNotes.html" "${DMG_SRC_DIR}" 31 | 32 | hdiutil create -layout NONE -srcfolder "${DMG_SRC_DIR}" "$DMG_DST_PATH" 33 | hdiutil verify "$DMG_DST_PATH" 34 | 35 | rm -r "${DMG_SRC_DIR}" 36 | 37 | # Reveal the file in Finder 38 | open -R "$DMG_DST_PATH" 39 | -------------------------------------------------------------------------------- /Source/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // DiskArbitrator 4 | // 5 | // Created by Aaron Burghardt on 1/10/10. 6 | // Copyright 2010 Aaron Burghardt. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **) argv); 14 | } 15 | --------------------------------------------------------------------------------