├── .gitignore ├── examples ├── crankd │ ├── sample-of-events │ │ ├── .gitignore │ │ ├── README.markdown │ │ ├── tunnel.sh │ │ └── generate-event-plist.py │ ├── folder-watcher │ │ ├── FolderWatcher.py │ │ └── folder-watcher.plist │ ├── socks-proxy │ │ ├── README │ │ ├── com.googlecode.pymacadmin.crankd.LaunchDaemon.plist │ │ ├── com.googlecode.pymacadmin.crankd.plist │ │ ├── org.openssh.dynamic-proxy.plist │ │ └── ProxyManager.py │ ├── NetworkConfig.py │ └── MountManager.py └── ctypes │ ├── keychain-delete.py │ ├── airport-update.py │ └── keychain-update.py ├── .hgignore ├── pymacds-dist ├── setup.py └── pymacds │ └── __init__.py ├── Refactor Thoughts.rst ├── install-crankd.sh ├── lib └── PyMacAdmin │ ├── SCUtilities │ ├── __init__.py │ └── SCPreferences.py │ ├── crankd │ ├── __init__.py │ └── handlers │ │ └── __init__.py │ ├── __init__.py │ └── Security │ ├── __init__.py │ ├── tests │ └── test_Keychain.py │ └── Keychain.py ├── LICENSE ├── bin ├── proxy-setenv.py ├── airport-update.py ├── delete-certificate.py ├── keychain-delete.py ├── set-proxy.py ├── create-location.py ├── crankd.py └── usb-notifier.c ├── ReadMe.rst ├── utilities ├── diskimage_unittesting │ ├── file_and_directory.blacklist │ ├── writables.whitelist │ ├── tests │ │ ├── skipsetup_test.py │ │ ├── empty_directory_test.py │ │ ├── symlink_checker_test.py │ │ ├── size_test.py │ │ ├── software_update_test.py │ │ ├── network_plist_test.py │ │ ├── blacklist_test.py │ │ ├── zz_plint_test.py │ │ ├── zz_suidguid_test.py │ │ ├── zz_world_writable_test.py │ │ ├── ouradmin_test.py │ │ ├── example_plist_test.py │ │ └── applications_dir_test.py │ ├── macdmgtest.py │ ├── dmgtestutilities.py │ ├── suidguid.whitelist │ └── run_image_tests.py ├── wtfUpdate.html └── wtfUpdate.py ├── ReadMe.html └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.py[co] 4 | .noseids 5 | -------------------------------------------------------------------------------- /examples/crankd/sample-of-events/.gitignore: -------------------------------------------------------------------------------- 1 | crankd-config.plist 2 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.pyc 4 | build 5 | dist 6 | *.py[co] 7 | .noseids 8 | -------------------------------------------------------------------------------- /examples/crankd/folder-watcher/FolderWatcher.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | class FolderWatcher(object): 4 | def folder_changed(self, *args, **kwargs): 5 | print "Folder %(path)s changed" % kwargs 6 | -------------------------------------------------------------------------------- /examples/crankd/socks-proxy/README: -------------------------------------------------------------------------------- 1 | This example uses crankd to dynamically enable or disable the SOCKS proxy when the network changes: 2 | 3 | http://code.google.com/p/pymacadmin/wiki/SOCKSProxyExample 4 | -------------------------------------------------------------------------------- /examples/crankd/NetworkConfig.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class NetworkConfig(object): 4 | """Handles network related changes for crankd""" 5 | def atalk_change(self, context=None, **kwargs): 6 | logger.info("Atalk? You poor person, you…") 7 | -------------------------------------------------------------------------------- /pymacds-dist/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup(name='pymacds', 6 | version='0.2', 7 | description='Python wrappers for Mac DirectoryService functions', 8 | author='Nigel Kersten', 9 | author_email='nigelk@google.com', 10 | packages=['pymacds'], 11 | ) 12 | -------------------------------------------------------------------------------- /Refactor Thoughts.rst: -------------------------------------------------------------------------------- 1 | Refactor Goals 2 | ============== 3 | 4 | * Cleaner core code 5 | * Easier start for new users 6 | * Easier testing with mock events 7 | 8 | Global variable cleanup 9 | ----------------------- 10 | 11 | * Finish cleaning up "magic" variables: e.g. handlers should do something like:: 12 | 13 | from PyMacAdmin import crankd 14 | if crankd.debug: 15 | … 16 | 17 | * Remove all specific event handling code to separate classes which inherit from `EventSource` 18 | 19 | -------------------------------------------------------------------------------- /examples/crankd/MountManager.py: -------------------------------------------------------------------------------- 1 | from PyMacAdmin.crankd.handlers import BaseHandler 2 | 3 | class MountManager(BaseHandler): 4 | def onNSWorkspaceDidMountNotification_(self, aNotification): 5 | path = aNotification.userInfo()['NSDevicePath'] 6 | self.logger.info("Mount: %s" % path) 7 | 8 | def onNSWorkspaceDidUnmountNotification_(self, aNotification): 9 | path = aNotification.userInfo()['NSDevicePath'] 10 | self.logger.info("Unmount: %s" % path) 11 | 12 | -------------------------------------------------------------------------------- /install-crankd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script to install crankd 4 | # It is very rudimentary, and doesn't install any plist files 5 | 6 | if [[ $UID -ne 0 ]]; then 7 | echo "$0 must be run as root" 8 | exit 1 9 | fi 10 | 11 | 12 | INITIAL_DIR=`pwd` 13 | BASE_DIR=`dirname "$0"` 14 | cd "$BASE_DIR" 15 | 16 | mkdir -p /usr/local/sbin 17 | cp bin/crankd.py /usr/local/sbin/ 18 | mkdir -p /Library/Application\ Support/crankd 19 | cp -R lib/PyMacAdmin /Library/Application\ Support/crankd/ 20 | 21 | -------------------------------------------------------------------------------- /examples/crankd/socks-proxy/com.googlecode.pymacadmin.crankd.LaunchDaemon.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | KeepAlive 6 | 7 | Label 8 | com.googlecode.pymacadmin.crankd 9 | ProgramArguments 10 | 11 | /usr/local/sbin/crankd.py 12 | 13 | RunAtLoad 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/PyMacAdmin/SCUtilities/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | SCUtilities: tools for dealing with Apple's SystemConfiguration framework 5 | 6 | Created by Chris Adams on 2008-05-28. 7 | """ 8 | 9 | # TODO: Import code related to SystemConfiguration events 10 | 11 | import sys 12 | import os 13 | import unittest 14 | 15 | class SCUtilitiesTests(unittest.TestCase): 16 | def setUp(self): 17 | raise RuntimeError("Thwack Chris about not writing these yet") 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. -------------------------------------------------------------------------------- /examples/crankd/folder-watcher/folder-watcher.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FSEvents 6 | 7 | /tmp 8 | 9 | method 10 | 11 | FolderWatcher 12 | folder_changed 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/crankd/socks-proxy/com.googlecode.pymacadmin.crankd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SystemConfiguration 6 | 7 | State:/Network/Global/IPv4 8 | 9 | method 10 | 11 | ProxyManager 12 | update_proxy_settings 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /bin/proxy-setenv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Usage: eval `proxy-setenv.py` 4 | 5 | Generates Bourne-shell environmental variable declarations based on the 6 | current system proxy settings 7 | """ 8 | 9 | from SystemConfiguration import SCDynamicStoreCopyProxies 10 | 11 | proxies = SCDynamicStoreCopyProxies(None) 12 | 13 | if 'HTTPEnable' in proxies and proxies['HTTPEnable']: 14 | print "export http_proxy=http://%s:%s/" % (proxies['HTTPProxy'], proxies['HTTPPort']) 15 | else: 16 | print "unset http_proxy" 17 | 18 | if 'FTPEnable' in proxies and proxies['FTPEnable']: 19 | print "export ftp_proxy=http://%s:%s/" % (proxies['FTPProxy'], proxies['FTPPort']) 20 | else: 21 | print "unset ftp_proxy" 22 | -------------------------------------------------------------------------------- /lib/PyMacAdmin/crankd/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import sys 5 | 6 | def not_implemented(*args, **kwargs): 7 | """A dummy function which exists only to catch configuration errors""" 8 | # TODO: Is there a better way to report the caller's location? 9 | import inspect 10 | stack = inspect.stack() 11 | my_name = stack[0][3] 12 | caller = stack[1][3] 13 | raise NotImplementedError( 14 | "%s should have been overridden. Called by %s as: %s(%s)" % ( 15 | my_name, 16 | caller, 17 | my_name, 18 | ", ".join(map(repr, args) + [ "%s=%s" % (k, repr(v)) for k,v in kwargs.items() ]) 19 | ) 20 | ) 21 | 22 | from . import handlers 23 | sys.modules[handlers.__name__] = handlers -------------------------------------------------------------------------------- /examples/crankd/socks-proxy/org.openssh.dynamic-proxy.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disabled 6 | 7 | KeepAlive 8 | 9 | Label 10 | org.openssh.dynamic-proxy 11 | LimitLoadToSessionType 12 | Aqua 13 | OnDemand 14 | 15 | ProgramArguments 16 | 17 | /usr/bin/ssh 18 | -D1080 19 | -Nn 20 | -n 21 | -C 22 | server.example.org 23 | 24 | RunAtLoad 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ReadMe.rst: -------------------------------------------------------------------------------- 1 | A collection of Python utilities for Mac OS X system administration. 2 | 3 | The PyMacAdmin project started as a collaboration between Chris Adams and 4 | Nigel Kersten to develop a replacement for the unsupported 'kicker' feature 5 | included in OS X prior to 10.5. That replacement eventually became crankd, 6 | which provides a way to execute Python code or a shell script in response to 7 | many system events: network changes, filesystem activity, application 8 | launching, etc. 9 | 10 | A second major features is the disk image unit testing framework designed to 11 | test OS X installation images before deployment. 12 | 13 | We've also added utilities to create network locations, manage proxy settings 14 | or update AirPort passwords using our Keychain wrapper. 15 | 16 | You're invited join the mailing list: 17 | 18 | http://groups.google.com/group/pymacadmin 19 | 20 | Please see http://pymacadmin.googlecode.com/ for more information. 21 | -------------------------------------------------------------------------------- /bin/airport-update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | """ 4 | Usage: airport-update.py SSID NEW_PASSWORD 5 | 6 | Updates the System keychain to replace the existing password for the specified 7 | SSID with NEW_PASSWORD 8 | 9 | BUG: Currently provides no way to set a password for a previously-unseen SSID 10 | """ 11 | 12 | import sys 13 | import os 14 | from PyMacAdmin.Security.Keychain import Keychain 15 | 16 | 17 | def main(): 18 | if len(sys.argv) < 3: 19 | print >> sys.stderr, __doc__.strip() 20 | sys.exit(1) 21 | 22 | ssid, new_password = sys.argv[1:3] 23 | 24 | if os.getuid() == 0: 25 | keychain = Keychain("/Library/Keychains/System.keychain") 26 | else: 27 | keychain = Keychain() 28 | 29 | try: 30 | item = keychain.find_generic_password(account_name=ssid) 31 | if item.password != new_password: 32 | item.update_password(new_password) 33 | 34 | except RuntimeError, exc: 35 | print >> sys.stderr, "Unable to change password for Airport network %s: %s" % (ssid, exc) 36 | sys.exit(1) 37 | 38 | print "Changed password for AirPort network %s to %s" % (ssid, new_password) 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /examples/ctypes/keychain-delete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Demonstrates how to delete a Keychain item using Python's ctypes library 5 | """ 6 | 7 | import ctypes 8 | 9 | service_name = 'Service Name' 10 | account_name = 'Account Name' 11 | password_length = ctypes.c_uint32(256) 12 | password_pointer = ctypes.c_char_p() 13 | item = ctypes.c_char_p() 14 | 15 | print "Loading Security.framework" 16 | Security = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Security.framework/Versions/Current/Security') 17 | 18 | print "Searching for the password" 19 | 20 | rc = Security.SecKeychainFindGenericPassword( 21 | None, 22 | len(service_name), 23 | service_name, 24 | len(account_name), 25 | account_name, 26 | # Used if you want to retrieve the password: 27 | None, # ctypes.byref(password_length), 28 | None, # ctypes.pointer(password_pointer), 29 | ctypes.pointer(item) 30 | ) 31 | 32 | if rc != 0: 33 | raise RuntimeError('SecKeychainFindGenericPassword failed: rc=%d' % rc) 34 | 35 | print "Deleting Keychain item" 36 | 37 | rc = Security.SecKeychainItemDelete( item ) 38 | 39 | if rc != 0: 40 | raise RuntimeError('SecKeychainItemDelete failed: rc=%d' % rc) 41 | 42 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/file_and_directory.blacklist: -------------------------------------------------------------------------------- 1 | # these directories must not be on the image. 2 | /etc/puppet 3 | /var/puppet 4 | /.Spotlight-V100 5 | 6 | # these files must not be on the image. 7 | /var/vm/sleepimage 8 | 9 | # must not be on the image. These are typically caused when someone boots 10 | # a machine and pulls a dmg from it after touching System Preferences 11 | /etc/cups/printers.conf 12 | /Library/Preferences/SystemConfiguration/NetworkInterfaces.plist 13 | /Library/Preferences/SystemConfiguration/com.apple.PowerManagement.plist 14 | /Library/Preferences/SystemConfiguration/com.apple.network.identification.plist 15 | /Library/Preferences/SystemConfiguration/com.apple.smb.server.plist 16 | /Library/Preferences/com.apple.AppleFileServer.plist 17 | /Library/Preferences/com.apple.AppleShareClient.plist 18 | /Library/Preferences/com.apple.audio.DeviceSettings.plist 19 | /Library/Preferences/com.apple.audio.SystemSettings.plist 20 | /Library/Preferences/com.apple.driver.AppleUSBDisplays.plist 21 | /Library/Preferences/com.apple.keyboardtype.plist 22 | /Library/Preferences/com.apple.mediaio.DeviceSettings.plist 23 | /Library/Preferences/com.apple.security.systemidentities.plist 24 | /Library/Preferences/com.apple.virtualMemory.plist 25 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/writables.whitelist: -------------------------------------------------------------------------------- 1 | /Library/Caches 2 | /System/Library/User Template/Dutch.lproj/Public/Drop Box 3 | /System/Library/User Template/English.lproj/Public/Drop Box 4 | /System/Library/User Template/French.lproj/Public/Drop Box 5 | /System/Library/User Template/German.lproj/Public/Drop Box 6 | /System/Library/User Template/Italian.lproj/Public/Drop Box 7 | /System/Library/User Template/Japanese.lproj/Public/Drop Box 8 | /System/Library/User Template/Spanish.lproj/Public/Drop Box 9 | /System/Library/User Template/da.lproj/Public/Drop Box 10 | /System/Library/User Template/fi.lproj/Public/Drop Box 11 | /System/Library/User Template/ko.lproj/Public/Drop Box 12 | /System/Library/User Template/no.lproj/Public/Drop Box 13 | /System/Library/User Template/pl.lproj/Public/Drop Box 14 | /System/Library/User Template/pt.lproj/Public/Drop Box 15 | /System/Library/User Template/pt_PT.lproj/Public/Drop Box 16 | /System/Library/User Template/ru.lproj/Public/Drop Box 17 | /System/Library/User Template/sv.lproj/Public/Drop Box 18 | /System/Library/User Template/zh_CN.lproj/Public/Drop Box 19 | /System/Library/User Template/zh_TW.lproj/Public/Drop Box 20 | /Users/Shared 21 | /Volumes 22 | /private/tmp 23 | /private/var/tmp 24 | /private/var/tmp/mds 25 | /.Trashes 26 | -------------------------------------------------------------------------------- /examples/crankd/sample-of-events/README.markdown: -------------------------------------------------------------------------------- 1 | sample-of-events 2 | ================ 3 | 4 | This sample is designed to allow you to easily get a feel for what sort of 5 | events you can tap into with `crankd`. `generate-event-plist.py` will create a 6 | file called `crankd-config.plist`, which configures crankd to call our wrapper 7 | script, `tunnel.sh` when any of a large sampling of system events (such as 8 | joining networks or mounting volumes) occur. 9 | 10 | To use it, open up a `Terminal` window to the directory containing the files. 11 | 12 | 1. Generate the plist: 13 | 14 | > `python generate-event-plist.py` 15 | 16 | 2. If desired, edit `tunnel.sh` until you are satisfied with what commands it 17 | will trigger. It is intially set up to log the event (so you can see it in 18 | `Console`) and to [Growl](http://growl.info/) the event (but you need to have 19 | `growlnotify` installed), to `say` that an event occured, and to `echo` the 20 | event. 21 | 22 | 3. Run crankd: 23 | 24 | > `/path/to/bin/crankd.py --config=crankd-config.plist` 25 | 26 | 4. Generate some events -- (dis)connect to/from a network, (un)mount a volume, 27 | (un)plug the power adapter, and so on. Watch as the events are triggered. 28 | 29 | 5. When you are done, press `Ctrl-C` to kill `crankd`. 30 | -------------------------------------------------------------------------------- /lib/PyMacAdmin/crankd/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Handlers for different types of events 5 | """ 6 | 7 | import sys 8 | import os 9 | import unittest 10 | import logging 11 | from Cocoa import NSObject 12 | from .. import not_implemented 13 | 14 | __all__ = [ 'BaseHandler', 'NSNotificationHandler' ] 15 | 16 | class BaseHandler(object): 17 | # pylint: disable-msg=C0111,R0903 18 | pass 19 | 20 | class NSNotificationHandler(NSObject): 21 | """Simple base class for handling NSNotification events""" 22 | # Method names and class structure are dictated by Cocoa & PyObjC, which 23 | # is substantially different from PEP-8: 24 | # pylint: disable-msg=C0103,W0232,R0903 25 | 26 | def init(self): 27 | """NSObject-compatible initializer""" 28 | self = super(NSNotificationHandler, self).init() 29 | if self is None: return None 30 | self.callable = not_implemented 31 | return self # NOTE: Unlike Python, NSObject's init() must return self! 32 | 33 | def onNotification_(self, the_notification): 34 | """Pass an NSNotifications to our handler""" 35 | if the_notification.userInfo: 36 | user_info = the_notification.userInfo() 37 | else: 38 | user_info = None 39 | self.callable(user_info=user_info) # pylint: disable-msg=E1101 -------------------------------------------------------------------------------- /bin/delete-certificate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.5 2 | 3 | from PyMacAdmin import carbon_call, load_carbon_framework 4 | from PyMacAdmin.Security import kSecCertificateItemClass 5 | from PyMacAdmin.Security.Keychain import SecKeychainAttribute, SecKeychainAttributeList 6 | 7 | import sys 8 | import ctypes 9 | from CoreFoundation import CFRelease 10 | 11 | Security = load_carbon_framework('/System/Library/Frameworks/Security.framework/Versions/Current/Security') 12 | 13 | label = "" 14 | plabel = ctypes.c_char_p(label) 15 | tag = 'labl' 16 | 17 | attr = SecKeychainAttribute(tag, 1, plabel) 18 | attrList = SecKeychainAttributeList(1, attr) 19 | 20 | # http://developer.apple.com/DOCUMENTATION/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/c/tdef/SecItemClass 21 | 22 | searchRef = ctypes.c_void_p() 23 | itemRef = ctypes.c_void_p() 24 | 25 | try: 26 | Security.SecKeychainSearchCreateFromAttributes( 27 | None, 28 | kSecCertificateItemClass, 29 | ctypes.byref(attrList), 30 | ctypes.pointer(searchRef) 31 | ) 32 | 33 | Security.SecKeychainSearchCopyNext( 34 | searchRef, 35 | ctypes.byref(itemRef) 36 | ) 37 | 38 | if searchRef: 39 | CFRelease(searchRef) 40 | 41 | Security.SecKeychainItemDelete(itemRef) 42 | 43 | if itemRef: 44 | CFRelease(itemRef) 45 | except RuntimeError, e: 46 | print >>sys.stderr, "ERROR: %s" % e 47 | sys.exit(1) -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/skipsetup_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Make sure dmg is set to not run Apple setup application on first boot.""" 18 | __author__ = 'jpb@google.com (Joe Block)' 19 | 20 | import macdmgtest 21 | 22 | 23 | class TestEnsureAppleSetupWillNotRun(macdmgtest.DMGUnitTest): 24 | 25 | def testAppleSetupDone(self): 26 | """Ensure first boot setup already done: .AppleSetupDone is on the dmg.""" 27 | apple_setup_done = 'var/db/.AppleSetupDone' 28 | self.assertEqual(self.CheckForExistence(apple_setup_done), True) 29 | 30 | def testSetupRegCompletePresent(self): 31 | """Ensure first boot setup already done: .SetupRegComplete is on the dmg.""" 32 | setup_reg_complete = 'Library/Receipts/.SetupRegComplete' 33 | self.assertEqual(self.CheckForExistence(setup_reg_complete), True) 34 | 35 | if __name__ == '__main__': 36 | macdmgtest.main() 37 | -------------------------------------------------------------------------------- /bin/keychain-delete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Usage: %prog [--service=SERVICE_NAME] [--account=ACCOUNT_NAME] [--keychain=/path/to/keychain] 5 | 6 | Remove the specified password from the keychain 7 | """ 8 | 9 | from PyMacAdmin.Security.Keychain import Keychain 10 | import os 11 | import sys 12 | from optparse import OptionParser 13 | 14 | 15 | def main(): 16 | parser = OptionParser(__doc__.strip()) 17 | 18 | parser.add_option('-a', '--account', '--account-name', 19 | help="Set the account name" 20 | ) 21 | 22 | parser.add_option('-s', '--service', '--service-name', 23 | help="Set the service name" 24 | ) 25 | 26 | parser.add_option('-k', '--keychain', 27 | help="Path to the keychain file" 28 | ) 29 | 30 | (options, args) = parser.parse_args() 31 | 32 | if not options.keychain and os.getuid() == 0: 33 | options.keychain = "/Library/Keychains/System.keychain" 34 | 35 | if not (options.account or options.service): 36 | parser.error("You must specify either an account or service name") 37 | 38 | try: 39 | keychain = Keychain(options.keychain) 40 | item = keychain.find_generic_password( 41 | service_name=options.service, 42 | account_name=options.account 43 | ) 44 | 45 | print "Removing %s" % item 46 | keychain.remove(item) 47 | except KeyError, exc: 48 | print >>sys.stderr, exc.message 49 | sys.exit(0) 50 | except RuntimeError, exc: 51 | print >>sys.stderr, "Unable to delete keychain item: %s" % exc 52 | sys.exit(1) 53 | 54 | if __name__ == "__main__": 55 | main() -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/empty_directory_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Ensure directories that are supposed to be empty actually are.""" 18 | 19 | __author__ = 'jpb@google.com (Joe Block)' 20 | 21 | 22 | import os 23 | import macdmgtest 24 | 25 | 26 | class TestEmptyDirectories(macdmgtest.DMGUnitTest): 27 | 28 | def setUp(self): 29 | self.empty_directories = ['var/vm', 30 | '/private/tmp', 31 | 'Volumes', 32 | 'Library/Logs'] 33 | 34 | def DirectoryEmpty(self, dirname): 35 | """Make sure dirname is empty.""" 36 | path = self.PathOnDMG(dirname) 37 | if os.listdir(path): 38 | return False 39 | else: 40 | return True 41 | 42 | def testEmptyDirectories(self): 43 | """Ensure every directory that is supposed to be empty on the image, is.""" 44 | full_dirs = [] 45 | for d in self.empty_directories: 46 | if not self.DirectoryEmpty(d): 47 | full_dirs.append(d) 48 | self.assertEqual(len(full_dirs), 0) 49 | 50 | if __name__ == '__main__': 51 | macdmgtest.main() 52 | -------------------------------------------------------------------------------- /examples/crankd/sample-of-events/tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "$#" -lt 2 ]] ; then 4 | cat < 2 | 4 | 5 | 6 | 7 | 8 | PyMacAdmin README 9 | 10 | 17 | 18 | 19 |

20 | PyMacAdmin 21 |

22 |

23 | A collection of Python utilities for Mac OS X system administration 24 |

25 |

26 | The PyMacAdmin project started as a collaboration between Chris Adams 27 | and Nigel Kersten to develop a replacement for the unsupported 'kicker' 28 | feature included in OS X prior to 10.5. That replacement eventually 29 | became crankd, which provides a way to execute Python code or a shell 30 | script in response to many system events: network changes, filesystem 31 | activity, application launching, etc. 32 |

33 |

34 | A second major features was the disk image unit testing framework 35 | designed to test OS X installation images before deployment and we've 36 | since gained or had contributed utilities to create network locations, 37 | manage proxy settings or update AirPort passwords using our Keychain 38 | wrapper. 39 |

40 |

41 | You're invited join the mailing list: http://groups.google.com/group/pymacadmin 42 |

43 |

44 | Please see http://pymacadmin.googlecode.com/ for more information. 45 |

46 | 47 | 48 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/size_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Check size of dmg for sanity. 18 | 19 | When I was starting to use InstaDMG/InstaUp2Date, bad configurations tended 20 | to generate ridiculously sized dmg files, so confirm the dmg is in the window 21 | we expected. 22 | 23 | Yes, this will need updating if we add a significant amount of new items 24 | to the dmg, but it catches the cases when the Instadmg run failed 25 | spectacularly and creates an absurd output dmg. 26 | """ 27 | __author__ = "jpb@google.com (Joe Block)" 28 | 29 | import os 30 | import macdmgtest 31 | 32 | 33 | TOO_BIG = 6000000000 34 | TOO_SMALL = 5000000000 35 | 36 | 37 | class TestDMGSize(macdmgtest.DMGUnitTest): 38 | 39 | def setUp(self): 40 | self.dmgpath = self.options.dmg 41 | 42 | def testDMGTooSmall(self): 43 | """Sanity check on dmg size: the dmg should be at least 5G.""" 44 | if not self.dmgpath: 45 | print "..skipping DMGTooSmall check - not testing a dmg" 46 | else: 47 | dmg_size = os.path.getsize(self.dmgpath) 48 | self.failUnless(dmg_size > TOO_SMALL) 49 | 50 | def testDMGTooBig(self): 51 | """Sanity check on dmg size: the dmg should be no more than 6G.""" 52 | if not self.dmgpath: 53 | print "..skipping DMGTooBig check - not testing a dmg" 54 | else: 55 | dmg_size = os.path.getsize(self.dmgpath) 56 | self.failUnless(dmg_size < TOO_BIG) 57 | 58 | 59 | if __name__ == "__main__": 60 | macdmgtest.main() 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import glob 5 | import os 6 | from distutils.command.install import INSTALL_SCHEMES 7 | 8 | from bdist_mpkg.cmd_bdist_mpkg import bdist_mpkg as bdist_mpkg 9 | 10 | class pma_bdist_mpkg(bdist_mpkg): 11 | def initialize_options(self): 12 | bdist_mpkg.initialize_options(self) 13 | self.readme = "ReadMe.html" 14 | 15 | def finalize_options(self): 16 | bdist_mpkg.finalize_options(self) 17 | self.scheme_map['scripts'] = '/usr/local/sbin' 18 | 19 | # For non-pkg installs, tarballs, etc. 20 | INSTALL_SCHEMES['unix_prefix']['scripts'] = '$base/sbin' 21 | 22 | setup( 23 | version = '1.0', 24 | name = 'PyMacAdmin', 25 | description = "Python tools for Mac administration", 26 | author = "Chris Adams", 27 | author_email = "chris@improbable.org", 28 | url = "http://pymacadmin.googlecode.com/", 29 | platforms = [ 'macosx-10.5' ], 30 | license = 'Apache Software License', 31 | classifiers = [ 32 | 'Development Status :: 4 - Beta', 33 | 'Environment :: Console', 34 | 'Environment :: MacOS X', 35 | 'Intended Audience :: Developers', 36 | 'Intended Audience :: System Administrators', 37 | 'License :: OSI Approved :: Apache Software License', 38 | 'License :: OSI Approved :: Python Software Foundation License', 39 | 'Operating System :: MacOS :: MacOS X', 40 | 'Programming Language :: Python', 41 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 42 | 'Topic :: System :: Systems Administration', 43 | 'Topic :: Utilities', 44 | ], 45 | package_dir = { '' : 'lib' }, 46 | packages = [ 47 | ".".join(dirpath.split("/")[1:]) for dirpath, dirnames, filenames in os.walk('lib') if "__init__.py" in filenames 48 | ], 49 | scripts = glob.glob(os.path.join(os.path.dirname(__file__), 'bin', '*.py')), 50 | cmdclass = { 51 | 'bdist_mpkg': pma_bdist_mpkg 52 | } 53 | ) 54 | -------------------------------------------------------------------------------- /lib/PyMacAdmin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from functools import wraps 4 | import ctypes 5 | 6 | def mac_strerror(errno): 7 | """Returns an error string for a classic MacOS error return code""" 8 | # TODO: Find a replacement which isn't deprecated in Python 3000: 9 | try: 10 | import MacOS 11 | return MacOS.GetErrorString(errno) 12 | except ImportError: 13 | return "Unknown error %d: MacOS.GetErrorString is not available by this Python" 14 | 15 | def checked_carbon_call(rc, func, args): 16 | """ 17 | Call a carbon function and raise an exception when the return code is 18 | less than 0. This is intended for use with load_carbon_framework but 19 | can also be used as a standalone function similar to 20 | subprocess.check_call. 21 | 22 | Most errors will raise RuntimeError but the intent is to raise a more 23 | precise exception where applicable - e.g KeyError for 24 | errKCItemNotFound (returned when a Keychain query matches no items) 25 | """ 26 | 27 | if rc < 0: 28 | if rc == -25300: #errKCItemNotFound 29 | exc_class = KeyError 30 | else: 31 | exc_class = RuntimeError 32 | 33 | raise exc_class("%s(%s) returned %d: %s" % (func.__name__, map(repr, args), rc, mac_strerror(rc))) 34 | return rc 35 | 36 | def load_carbon_framework(f_path): 37 | """ 38 | Load a Carbon framework using ctypes.CDLL and add an errcheck wrapper to 39 | replace traditional errno-style error checks with exception handling. 40 | 41 | Example: 42 | >>> load_carbon_framework('/System/Library/Frameworks/Security.framework/Versions/Current/Security') # doctest: +ELLIPSIS 43 | 44 | """ 45 | framework = ctypes.cdll.LoadLibrary(f_path) 46 | 47 | # TODO: Do we ever need to wrap framework.__getattr__ too? 48 | old_getitem = framework.__getitem__ 49 | @wraps(old_getitem) 50 | def new_getitem(k): 51 | v = old_getitem(k) 52 | if hasattr(v, "errcheck") and not v.errcheck: 53 | v.errcheck = checked_carbon_call 54 | return v 55 | framework.__getitem__ = new_getitem 56 | 57 | return framework 58 | 59 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/software_update_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Ensure correct software update catalog url is set on image. 18 | 19 | Author: Joe Block (jpb@google.com) 20 | """ 21 | 22 | import os 23 | import stat 24 | import dmgtestutilities 25 | import macdmgtest 26 | 27 | 28 | class TestSoftwareUpdateURL(macdmgtest.DMGUnitTest): 29 | """Check validity of Software Update preferences on image.""" 30 | 31 | def setUp(self): 32 | """Setup paths and load plist data.""" 33 | self.su_pref_path = self.PathOnDMG( 34 | "/Library/Preferences/com.apple.SoftwareUpdate.plist") 35 | self.su_prefs = dmgtestutilities.ReadPlist(self.su_pref_path) 36 | if not self.su_prefs: 37 | self.su_prefs = {} 38 | 39 | def testSoftwareUpdatePlist(self): 40 | """Ensure com.apple.SoftwareUpdate.plist is installed on the image.""" 41 | self.assertEqual(self.CheckForExistence( 42 | "/Library/Preferences/com.apple.SoftwareUpdate.plist"), True) 43 | 44 | def testOwnerGroupMode(self): 45 | """test owner, group and mode of com.apple.SoftwareUpdate.plist.""" 46 | software_update_stat = os.stat(self.su_pref_path) 47 | owner = software_update_stat[stat.ST_UID] 48 | group = software_update_stat[stat.ST_GID] 49 | mode = software_update_stat[stat.ST_MODE] 50 | num_mode = oct(mode & 0777) 51 | self.assertEqual(0, owner) 52 | self.assertEqual(80, group) 53 | self.assertEqual("0644", num_mode) 54 | 55 | def testSoftwareUpdateCatalogURL(self): 56 | """test that Software Update is set to use internal CatalogURL.""" 57 | self.assertEqual("http://path/to/your/internal/swupd/", 58 | self.su_prefs["CatalogURL"]) 59 | 60 | if __name__ == "__main__": 61 | macdmgtest.main() 62 | 63 | -------------------------------------------------------------------------------- /examples/crankd/socks-proxy/ProxyManager.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import os 3 | import logging 4 | 5 | from PyMacAdmin import crankd 6 | from PyMacAdmin.SCUtilities.SCPreferences import SCPreferences 7 | 8 | class ProxyManager(crankd.handlers.BaseHandler): 9 | """ 10 | crankd event handler which selectively enables a SOCKS process based 11 | on the current network address 12 | """ 13 | 14 | def __init__(self): 15 | super(crankd.handlers.BaseHandler, self).__init__() 16 | self.socks_server = 'localhost' 17 | self.socks_port = '1080' 18 | 19 | # Fire once at startup to handle situations like system bootup or a 20 | # crankd restart: 21 | self.update_proxy_settings() 22 | 23 | def onNSWorkspaceDidMountNotification_(self, aNotification): 24 | """ 25 | Dummy handler for testing purposes which calls the update code when a 26 | volume is mounted - this simplifies testing or demos using a DMG. 27 | 28 | BUG: Although harmless, this should be removed in production 29 | """ 30 | self.update_proxy_settings() 31 | 32 | def update_proxy_settings(self, *args, **kwargs): 33 | """ 34 | When the network configuration changes, this updates the SOCKS proxy 35 | settings based the current IP address(es) 36 | """ 37 | # Open a SystemConfiguration preferences session: 38 | sc_prefs = SCPreferences() 39 | 40 | # We want to enable the server when our hostname is not on the corporate network: 41 | # BUG: This does not handle multi-homed systems well: 42 | current_address = socket.gethostbyname(socket.getfqdn()) 43 | new_state = not current_address.startswith('10.0.1.') 44 | 45 | logging.info( 46 | "Current address is now %s: SOCKS proxy will be %s" % ( 47 | current_address, 48 | "Enabled" if new_state else "Disabled" 49 | ) 50 | ) 51 | 52 | try: 53 | sc_prefs.set_proxy( 54 | enable=new_state, 55 | protocol='SOCKS', 56 | server=self.socks_server, 57 | port=self.socks_port 58 | ) 59 | sc_prefs.save() 60 | 61 | logging.info("Successfully updated SOCKS proxy setting") 62 | except RuntimeError, e: 63 | logging.error("Unable to set SOCKS proxy setting: %s" % e.message) 64 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/network_plist_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Network settings tests to cope with Airbook brain damage.""" 18 | __author__ = 'jpb@google.com (Joe Block)' 19 | 20 | import dmgtestutilities 21 | import macdmgtest 22 | 23 | 24 | class TestNetworkAirbookCompliant(macdmgtest.DMGUnitTest): 25 | """Check that network settings suitably munged for Airbooks.""" 26 | 27 | def setUp(self): 28 | """Setup paths.""" 29 | self.system_pref_path = self.PathOnDMG( 30 | '/Library/Preferences/SystemConfiguration/preferences.plist') 31 | self.sys_prefs = dmgtestutilities.ReadPlist(self.system_pref_path) 32 | if not self.sys_prefs: 33 | self.sys_prefs = {} 34 | 35 | def testNetworkPlistIsAbsent(self): 36 | """Ensure NetworkInterfaces.plist absent, it will be rebuilt for Airbook.""" 37 | nw = '/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist' 38 | self.assertEqual(self.CheckForExistence(nw), False) 39 | 40 | def testSystemPreferencesPlistIsAbsent(self): 41 | """SystemConfiguration/preferences absent? will be rebuilt for Airbook.""" 42 | self.assertEqual(self.CheckForExistence(self.system_pref_path), False) 43 | 44 | def testEnsureNoCurrentSet(self): 45 | """SystemConfiguration/preferences.plist must not have CurrentSet key.""" 46 | self.assertEqual('CurrentSet' in self.sys_prefs, False) 47 | 48 | def testEnsureNoNetworkServices(self): 49 | """SystemConfiguration/preferences.plist can't have NetworkServices key.""" 50 | self.assertEqual('NetworkServices' in self.sys_prefs, False) 51 | 52 | def testEnsureNoSets(self): 53 | """SystemConfiguration/preferences.plist must not have Sets key.""" 54 | self.assertEqual('Sets' in self.sys_prefs, False) 55 | 56 | 57 | if __name__ == '__main__': 58 | macdmgtest.main() 59 | -------------------------------------------------------------------------------- /bin/set-proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | """ 4 | %prog --enable --protocol=HTTP --server=proxy.example.edu --port=3128 5 | 6 | Configures the system proxy settings from the command-line using the 7 | PyMacAdmin SystemConfiguration module 8 | """ 9 | 10 | from PyMacAdmin.SCUtilities.SCPreferences import SCPreferences 11 | from socket import gethostbyname, gaierror 12 | import sys 13 | 14 | 15 | def main(): 16 | sc_prefs = SCPreferences() 17 | 18 | from optparse import OptionParser 19 | parser = OptionParser(__doc__.strip()) 20 | 21 | parser.add_option('--enable', dest='enable', action="store_true", default=True, 22 | help='Enable proxy for the specified protocol' 23 | ) 24 | parser.add_option('--disable', dest='enable', action='store_false', 25 | help='Disable proxy for the specified protocol' 26 | ) 27 | parser.add_option('--protocol', choices=sc_prefs.proxy_protocols, metavar='PROTOCOL', 28 | help='Specify the protocol (%s)' % ", ".join(sc_prefs.proxy_protocols) 29 | ) 30 | parser.add_option('--server', metavar='SERVER', 31 | help="Specify the proxy server's hostname" 32 | ) 33 | parser.add_option('--port', type='int', metavar='PORT', 34 | help="Specify the proxy server's port" 35 | ) 36 | 37 | (options, args) = parser.parse_args() 38 | 39 | # optparser inexplicably lacks a require option due to extreme 40 | # pedanticism but it's not worth switching to argparse: 41 | if not options.protocol: 42 | print >> sys.stderr, "ERROR: You must specify a protocol to %s" % ("enable" if options.enable else "disable") 43 | sys.exit(1) 44 | 45 | if options.enable and not ( options.server and options.port ): 46 | print >> sys.stderr, "ERROR: You must specify a %s proxy server and port" % options.protocol 47 | sys.exit(1) 48 | 49 | if options.server: 50 | try: 51 | gethostbyname(options.server) 52 | except gaierror, exc: 53 | print >> sys.stderr, "ERROR: couldn't resolve server hostname %s: %s" % (options.server, exc.args[1]) # e.message is broken in the standard socket.gaierror! 54 | sys.exit(1) 55 | 56 | try: 57 | sc_prefs.set_proxy(enable=options.enable, protocol=options.protocol, server=options.server, port=options.port) 58 | sc_prefs.save() 59 | except RuntimeError, exc: 60 | print >> sys.stderr, exc.message 61 | 62 | if __name__ == '__main__': 63 | main() 64 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/blacklist_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Make sure blacklisted files or directories are not present on the image.""" 18 | 19 | __author__ = 'jpb@google.com (Joe Block)' 20 | 21 | import macdmgtest 22 | import dmgtestutilities 23 | 24 | 25 | def ReadBlackList(path): 26 | """Read a blacklist of forbidden directories and files. 27 | 28 | Ignore lines starting with a # so we can comment the datafile. 29 | 30 | Args: 31 | path: file to load the blacklist from. 32 | Returns: 33 | dictionary of path:True mappings 34 | """ 35 | blacklist_file = open(path, 'r') 36 | catalog = [] 37 | for entry in blacklist_file: 38 | if not entry or entry[:1] == '#': 39 | pass # ignore comment and empty lines in blacklist file 40 | else: 41 | catalog.append(entry.strip()) 42 | return catalog 43 | 44 | 45 | class TestBlacklists(macdmgtest.DMGUnitTest): 46 | 47 | def setUp(self): 48 | blacklist_path = self.ConfigPath('file_and_directory.blacklist') 49 | self.blacklist = ReadBlackList(blacklist_path) 50 | 51 | def ProcessList(self, the_list): 52 | """files/directories from the_list should be absent from the image. 53 | 54 | Args: 55 | the_list: A list of paths to file or directories that should be absent 56 | from the image. 57 | Returns: 58 | list of directories/files that are present that shouldn't be. 59 | """ 60 | bad = [] 61 | for d in the_list: 62 | if self.CheckForExistence(d) == True: 63 | bad.append(d) 64 | return bad 65 | 66 | def testBlacklistedDirectories(self): 67 | """Ensure directories from blacklist are absent from the image.""" 68 | badfound = self.ProcessList(self.blacklist) 69 | if badfound: 70 | print 'These files and directories should not exist:' 71 | print '%s' % badfound 72 | self.assertEqual(len(badfound), 0) 73 | 74 | 75 | if __name__ == '__main__': 76 | macdmgtest.main() 77 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/macdmgtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Use 2.5 so we can import objc & Foundation in tests 4 | # 5 | # Copyright 2008 Google Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """Base class for dmg unit tests.""" 20 | __author__ = 'jpb@google.com (Joe Block)' 21 | 22 | import os 23 | import unittest 24 | 25 | 26 | def main(): 27 | """Print usage warning.""" 28 | print 'This is not a standalone test suite. Run with run_image_tests.py.' 29 | 30 | 31 | class DMGUnitTest(unittest.TestCase): 32 | """Helper functions for DMG unit tests.""" 33 | 34 | def SetMountpoint(self, mountpoint): 35 | """Set mountpoint.""" 36 | self.mountpoint = mountpoint 37 | 38 | def SetOptions(self, options): 39 | """Set options parsed from command line. 40 | 41 | Args: 42 | options: command line options passed in by image testing driver script. 43 | """ 44 | self.options = options 45 | 46 | def Mountpoint(self): 47 | """Return mountpoint of dmg being tested.""" 48 | return self.mountpoint 49 | 50 | def ConfigPath(self, configfile): 51 | """Returns path to a config file with configdir prepended. 52 | Args: 53 | path: relative path of config file 54 | Returns: 55 | Actual path to that file, based on configdir""" 56 | return os.path.join(self.options.configdir, configfile) 57 | 58 | def PathOnDMG(self, path): 59 | """Returns path with dmg mount path prepended. 60 | 61 | Args: 62 | path: path to a file on the dmg 63 | """ 64 | # deal with leading /es in path var. 65 | while path[:1] == '/': 66 | path = path[1:] 67 | return os.path.join(self.mountpoint, path) 68 | 69 | def CheckForExistence(self, filename): 70 | """Make sure filename doesn't exist on the tested image. 71 | 72 | Args: 73 | filename: file to look for on the dmg 74 | """ 75 | if filename: 76 | path = self.PathOnDMG(filename) 77 | return os.path.exists(path) 78 | else: 79 | return False 80 | 81 | 82 | if __name__ == '__main__': 83 | Main() 84 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/dmgtestutilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Use 2.5 so we can import objc & Foundation in tests 4 | # 5 | # Copyright 2008 Google Inc. All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # This has to run under Apple's python2.5 so it can import Cocoa classes. 20 | 21 | """Helper functions for DMG unit tests.""" 22 | __author__ = 'jpb@google.com (Joe Block)' 23 | 24 | import subprocess 25 | import Foundation 26 | 27 | 28 | def RemoveEmpties(a_list): 29 | """Returns a list with no empty lines.""" 30 | cleaned = [] 31 | for a in a_list: 32 | if a: 33 | cleaned.append(a) 34 | return cleaned 35 | 36 | 37 | def ProcessCommand(command, strip_empty_lines=True): 38 | """Return a dict containing command's stdout, stderr & error code. 39 | 40 | Args: 41 | command: list containing the command line we want run 42 | strip_empty_lines: Boolean to tell us to strip empty lines or not. 43 | 44 | Returns: 45 | dict with stdout, stderr and error code from the command run. 46 | """ 47 | cmd = subprocess.Popen(command, stdout=subprocess.PIPE, 48 | stderr=subprocess.PIPE) 49 | (stdout, stderr) = cmd.communicate() 50 | info = {} 51 | info['errorcode'] = cmd.returncode 52 | if not strip_empty_lines: 53 | info['stdout'] = stdout.split('\n') 54 | info['stderr'] = stderr.split('\n') 55 | else: 56 | info['stdout'] = RemoveEmpties(stdout.split('\n')) 57 | info['stderr'] = RemoveEmpties(stderr.split('\n')) 58 | return info 59 | 60 | 61 | def LintPlist(path): 62 | """plutil -lint path. 63 | 64 | Args: 65 | path: file to lint 66 | 67 | Returns: 68 | errorcode of plutil -lint 69 | """ 70 | cmd = ProcessCommand(['/usr/bin/plutil', '-lint', path]) 71 | return cmd['errorcode'] 72 | 73 | 74 | def ReadPlist(plistfile): 75 | """Read a plist, return a dict. 76 | 77 | Args: 78 | plistfile: Path to plist file to read 79 | 80 | Returns: 81 | dict of plist contents. 82 | """ 83 | return Foundation.NSDictionary.dictionaryWithContentsOfFile_(plistfile) 84 | 85 | 86 | if __name__ == '__main__': 87 | print 'This is not a standalone script. It contains only helper functions.' 88 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/zz_plint_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008-2009 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Check that all .plist files on the dmg pass plutil lint.""" 18 | __author__ = 'jpb@google.com (Joe Block)' 19 | 20 | 21 | import os 22 | import dmgtestutilities 23 | import macdmgtest 24 | 25 | 26 | class TestLintPlistsOnDMG(macdmgtest.DMGUnitTest): 27 | """Checks all plist files on the dmg with plutil -lint. 28 | 29 | This is by far the slowest test we run, so force it to run last by starting 30 | the filename with zz. This way we don't have to wait for it to finish when 31 | we're testing other unit tests. 32 | """ 33 | 34 | def setUp(self): 35 | """Setup Error statistics.""" 36 | self.lint_output = [] 37 | self.bad_plists = [] 38 | 39 | def _CheckPlistFiles(self, unused_a, path, namelist): 40 | """Run plutil -lint on all the plist files in namelist.""" 41 | for name in namelist: 42 | if os.path.splitext(name)[1] == '.plist': 43 | plistfile = os.path.join(path, name) 44 | cmd = dmgtestutilities.ProcessCommand(['/usr/bin/plutil', '-lint', 45 | plistfile]) 46 | if cmd['errorcode']: 47 | self.bad_plists.append(plistfile) 48 | self.lint_output.append('Error found in %s' % plistfile) 49 | for x in cmd['stdout']: 50 | self.lint_output.append(x) 51 | 52 | def testPlistsOnDMG(self): 53 | """SLOW: Check all plists on dmg with plutil -lint. Can take 5 minutes.""" 54 | dirname = self.PathOnDMG('') 55 | os.path.walk(dirname, self._CheckPlistFiles, None) 56 | # Print out the bad list. Normally it would be better practice to just 57 | # let the assert fail, but we want to know exactly what plists are bad on 58 | # the image so we can fix them. 59 | if self.bad_plists: 60 | print 61 | print 'Found %s bad plist files.' % len(self.bad_plists) 62 | print '\n\t'.join(self.bad_plists) 63 | print '\nErrors detected:' 64 | print '\n'.join(self.lint_output) 65 | self.assertEqual(len(self.lint_output), 0) 66 | self.assertEqual(len(self.bad_plists), 0) 67 | 68 | if __name__ == '__main__': 69 | macdmgtest.main() 70 | -------------------------------------------------------------------------------- /lib/PyMacAdmin/Security/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import PyMacAdmin 3 | import ctypes 4 | import struct 5 | import sys 6 | 7 | # This is not particularly elegant but to avoid everything having to load the 8 | # Security framework we use a single copy hanging of this module so everything 9 | # else can simply use Security.lib.SecKeychainFoo(…) 10 | lib = PyMacAdmin.load_carbon_framework('/System/Library/Frameworks/Security.framework/Versions/Current/Security') 11 | 12 | CSSM_DB_RECORDTYPE_APP_DEFINED_START = 0x80000000 13 | CSSM_DL_DB_RECORD_X509_CERTIFICATE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x1000 14 | 15 | # This is somewhat gross: we define a bunch of module-level constants based on 16 | # the SecKeychainItem.h defines (FourCharCodes) by passing them through 17 | # struct.unpack and converting them to ctypes.c_long() since we'll never use 18 | # them for non-native APIs 19 | 20 | CARBON_DEFINES = { 21 | 'kSecCreationDateItemAttr': 'cdat', 22 | 'kSecModDateItemAttr': 'mdat', 23 | 'kSecDescriptionItemAttr': 'desc', 24 | 'kSecCommentItemAttr': 'icmt', 25 | 'kSecCreatorItemAttr': 'crtr', 26 | 'kSecTypeItemAttr': 'type', 27 | 'kSecScriptCodeItemAttr': 'scrp', 28 | 'kSecLabelItemAttr': 'labl', 29 | 'kSecInvisibleItemAttr': 'invi', 30 | 'kSecNegativeItemAttr': 'nega', 31 | 'kSecCustomIconItemAttr': 'cusi', 32 | 'kSecAccountItemAttr': 'acct', 33 | 'kSecServiceItemAttr': 'svce', 34 | 'kSecGenericItemAttr': 'gena', 35 | 'kSecSecurityDomainItemAttr': 'sdmn', 36 | 'kSecServerItemAttr': 'srvr', 37 | 'kSecAuthenticationTypeItemAttr': 'atyp', 38 | 'kSecPortItemAttr': 'port', 39 | 'kSecPathItemAttr': 'path', 40 | 'kSecVolumeItemAttr': 'vlme', 41 | 'kSecAddressItemAttr': 'addr', 42 | 'kSecSignatureItemAttr': 'ssig', 43 | 'kSecProtocolItemAttr': 'ptcl', 44 | 'kSecCertificateType': 'ctyp', 45 | 'kSecCertificateEncoding': 'cenc', 46 | 'kSecCrlType': 'crtp', 47 | 'kSecCrlEncoding': 'crnc', 48 | 'kSecAlias': 'alis', 49 | 'kSecInternetPasswordItemClass': 'inet', 50 | 'kSecGenericPasswordItemClass': 'genp', 51 | 'kSecAppleSharePasswordItemClass': 'ashp', 52 | 'kSecCertificateItemClass': CSSM_DL_DB_RECORD_X509_CERTIFICATE 53 | } 54 | 55 | for k in CARBON_DEFINES: 56 | v = CARBON_DEFINES[k] 57 | if isinstance(v, str): 58 | assert(len(v) == 4) 59 | v = ctypes.c_ulong(struct.unpack(">L", v)[0]) 60 | setattr(sys.modules[__name__], k, v) -------------------------------------------------------------------------------- /lib/PyMacAdmin/Security/tests/test_Keychain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import sys 5 | import unittest 6 | from PyMacAdmin.Security.Keychain import Keychain, GenericPassword, InternetPassword 7 | 8 | class KeychainTests(unittest.TestCase): 9 | """Unit test for the Keychain module""" 10 | 11 | def setUp(self): 12 | pass 13 | 14 | def test_load_default_keychain(self): 15 | k = Keychain() 16 | self.failIfEqual(k, None) 17 | 18 | def test_load_system_keychain(self): 19 | k = Keychain('/Library/Keychains/System.keychain') 20 | self.failIfEqual(k, None) 21 | 22 | def test_find_airport_password(self): 23 | system_keychain = Keychain("/Library/Keychains/System.keychain") 24 | try: 25 | system_keychain.find_generic_password(account_name="linksys") 26 | except KeyError: 27 | print >> sys.stderr, "test_find_airport_password: assuming the non-existence of linksys SSID is correct" 28 | pass 29 | 30 | def test_find_nonexistent_generic_password(self): 31 | import uuid 32 | system_keychain = Keychain("/Library/Keychains/System.keychain") 33 | self.assertRaises(KeyError, system_keychain.find_generic_password, **{ 'account_name': "NonExistantGenericPassword-%s" % uuid.uuid4() }) 34 | 35 | def test_add_and_remove_generic_password(self): 36 | import uuid 37 | k = Keychain() 38 | service_name = "PyMacAdmin Keychain Unit Test" 39 | account_name = str(uuid.uuid4()) 40 | password = str(uuid.uuid4()) 41 | 42 | i = GenericPassword(service_name=service_name, account_name=account_name, password=password) 43 | 44 | k.add(i) 45 | 46 | self.assertEquals(i.password, k.find_generic_password(service_name, account_name).password) 47 | 48 | k.remove(i) 49 | self.assertRaises(KeyError, k.find_generic_password, **{"service_name": service_name, "account_name": account_name}) 50 | 51 | def test_find_internet_password(self): 52 | keychain = Keychain() 53 | i = keychain.find_internet_password(server_name="connect.apple.com") 54 | self.failIfEqual(i, None) 55 | 56 | def test_add_and_remove_internet_password(self): 57 | import uuid 58 | k = Keychain() 59 | kwargs = { 60 | 'server_name': "pymacadmin.googlecode.com", 61 | 'account_name': "unittest", 62 | 'protocol_type': 'http', 63 | 'authentication_type': 'http', 64 | 'password': str(uuid.uuid4()) 65 | } 66 | 67 | i = InternetPassword(**kwargs) 68 | k.add(i) 69 | 70 | self.assertEquals(i.password, k.find_internet_password(server_name=kwargs['server_name'], account_name=kwargs['account_name']).password) 71 | 72 | k.remove(i) 73 | self.assertRaises(KeyError, k.find_internet_password, **{"server_name": kwargs['server_name'], "account_name": kwargs['account_name']}) 74 | 75 | 76 | if __name__ == '__main__': 77 | unittest.main() 78 | 79 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/suidguid.whitelist: -------------------------------------------------------------------------------- 1 | # Blank lines and lines starting with a # are ignored. 2 | /Applications/System Preferences.app/Contents/Resources/installAssistant 3 | /Applications/Utilities/Activity Monitor.app/Contents/Resources/pmTool 4 | /Applications/Utilities/Keychain Access.app/Contents/Resources/kcproxy 5 | /Applications/Utilities/ODBC Administrator.app/Contents/Resources/iodbcadmintool 6 | /System/Library/CoreServices/Expansion Slot Utility.app/Contents/Resources/PCIELaneConfigTool 7 | /System/Library/CoreServices/Finder.app/Contents/Resources/OwnerGroupTool 8 | /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/MacOS/ARDAgent 9 | /System/Library/CoreServices/SecurityFixer.app/Contents/Resources/securityFixerTool 10 | /System/Library/Extensions/webdav_fs.kext/Contents/Resources/load_webdav 11 | /System/Library/Filesystems/AppleShare/afpLoad 12 | /System/Library/Filesystems/AppleShare/check_afp.app/Contents/MacOS/check_afp 13 | /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/Resources/PrinterSharingTool 14 | /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/Resources/SCHelper 15 | /System/Library/PreferencePanes/DateAndTime.prefPane/Contents/Resources/TimeZone.prefPane/Contents/Resources/TimeZoneSettingTool 16 | /System/Library/Printers/IOMs/LPRIOM.plugin/Contents/MacOS/LPRIOMHelper 17 | /System/Library/Printers/Libraries/aehelper 18 | /System/Library/Printers/Libraries/csregprinter 19 | /System/Library/PrivateFrameworks/Admin.framework/Versions/A/Resources/readconfig 20 | /System/Library/PrivateFrameworks/Admin.framework/Versions/A/Resources/writeconfig 21 | /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/Locum 22 | /System/Library/PrivateFrameworks/DiskManagement.framework/Versions/A/Resources/DiskManagementTool 23 | /System/Library/PrivateFrameworks/Install.framework/Versions/A/Resources/runner 24 | /System/Library/PrivateFrameworks/NetworkConfig.framework/Versions/A/Resources/NetCfgTool 25 | /bin/ps 26 | /bin/rcp 27 | /sbin/mount_nfs 28 | /sbin/ping 29 | /sbin/ping6 30 | /sbin/route 31 | /sbin/umount 32 | /usr/bin/at 33 | /usr/bin/atq 34 | /usr/bin/atrm 35 | /usr/bin/batch 36 | /usr/bin/chfn 37 | /usr/bin/chpass 38 | /usr/bin/chsh 39 | /usr/bin/crontab 40 | /usr/bin/cu 41 | /usr/bin/ipcs 42 | /usr/bin/lockfile 43 | /usr/bin/login 44 | /usr/bin/lppasswd 45 | /usr/bin/newgrp 46 | /usr/bin/passwd 47 | /usr/bin/procmail 48 | /usr/bin/quota 49 | /usr/bin/rlogin 50 | /usr/bin/rsh 51 | /usr/bin/sample 52 | /usr/bin/setregion 53 | /usr/bin/su 54 | /usr/bin/sudo 55 | /usr/bin/top 56 | /usr/bin/uucp 57 | /usr/bin/uuname 58 | /usr/bin/uustat 59 | /usr/bin/uux 60 | /usr/bin/vmmap 61 | /usr/bin/wall 62 | /usr/bin/write 63 | /usr/lib/sa/sadc 64 | /usr/libexec/authopen 65 | /usr/libexec/chkpasswd 66 | /usr/libexec/dumpemacs 67 | /usr/libexec/load_hdi 68 | /usr/libexec/security_authtrampoline 69 | /usr/libexec/security_privportserver 70 | /usr/libexec/ssh-keysign 71 | /usr/libexec/xgrid/IdleTool 72 | /usr/sbin/netstat 73 | /usr/sbin/postdrop 74 | /usr/sbin/postqueue 75 | /usr/sbin/pppd 76 | /usr/sbin/screenreaderd 77 | /usr/sbin/scselect 78 | /usr/sbin/traceroute 79 | /usr/sbin/traceroute6 80 | /usr/sbin/trpt 81 | /usr/sbin/uucico 82 | /usr/sbin/uuxqt 83 | /usr/sbin/vpnd 84 | -------------------------------------------------------------------------------- /examples/ctypes/airport-update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Updates the password for an Airport network""" 3 | 4 | import ctypes 5 | import sys 6 | 7 | SECURITY = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Security.framework/Versions/Current/Security') 8 | 9 | def find_airport_password(ssid): 10 | """Returns the password and a Keychain item reference for the requested Airport network""" 11 | item = ctypes.c_void_p() 12 | password_length = ctypes.c_uint32(0) 13 | password_data = ctypes.c_char_p(256) 14 | 15 | system_keychain = ctypes.c_void_p() 16 | keychain_ptr = ctypes.pointer(system_keychain) 17 | 18 | rc = SECURITY.SecKeychainOpen("/Library/Keychains/System.keychain", keychain_ptr) 19 | if rc != 0: 20 | raise RuntimeError("Couldn't open system keychain: rc=%d" % rc) 21 | 22 | # n.b. The service name is often "AirPort Network" in the user's keychain 23 | # but in the system keychain it appears to be a UUID. It might be 24 | # necessary to check the description for network names which are not 25 | # unique in the keychain. 26 | rc = SECURITY.SecKeychainFindGenericPassword ( 27 | system_keychain, 28 | None, # Length of service name 29 | None, # Service name 30 | len(ssid), # Account name length 31 | ssid, # Account name 32 | ctypes.byref(password_length), # Will be filled with pw length 33 | ctypes.pointer(password_data), # Will be filled with pw data 34 | ctypes.pointer(item) 35 | ) 36 | 37 | if rc == -25300: 38 | raise RuntimeError('No existing password for Airport network %s: rc=%d' % (ssid, rc)) 39 | elif rc != 0: 40 | raise RuntimeError('Failed to find password for Airport network %s: rc=%d' % (ssid, rc)) 41 | 42 | password = password_data.value[0:password_length.value] 43 | 44 | SECURITY.SecKeychainItemFreeContent(None, password_data) 45 | 46 | return (password, item) 47 | 48 | def change_airport_password(ssid, new_password): 49 | """Sets the password for the specified Airport network to the provided value""" 50 | password, item = find_airport_password(ssid) 51 | 52 | if password != new_password: 53 | rc = SECURITY.SecKeychainItemModifyAttributesAndData( 54 | item, 55 | None, 56 | len(new_password), 57 | new_password 58 | ) 59 | 60 | if rc == -61: 61 | raise RuntimeError("Unable to update password for Airport network %s: permission denied (rc = %d)" % (ssid, rc)) 62 | elif rc != 0: 63 | raise RuntimeError("Unable to update password for Airport network %s: rc = %d" % (ssid, rc)) 64 | 65 | def main(): 66 | if len(sys.argv) < 3: 67 | print >> sys.stderr, "Usage: %s SSID NEW_PASSWORD" % (sys.argv[0]) 68 | sys.exit(1) 69 | 70 | ssid, new_password = sys.argv[1:3] 71 | 72 | try: 73 | change_airport_password(ssid, new_password) 74 | except RuntimeError, exc: 75 | print >> sys.stderr, "Unable to change password for Airport network %s: %s" % ( ssid, exc) 76 | sys.exit(1) 77 | 78 | print "Changed password for Airport network %s to %s" % (ssid, new_password) 79 | 80 | if __name__ == "__main__": 81 | main() -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/zz_suidguid_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # tests are done in alphabetical order by file name, so since this 18 | # traverses the entire dmg, using zz to push it to the end with the 19 | # other slow unit tests. 20 | 21 | """Check that all setuid & setgid files on the dmg are in our whitelist.""" 22 | __author__ = 'jpb@google.com (Joe Block)' 23 | 24 | 25 | import logging 26 | import dmgtestutilities 27 | import macdmgtest 28 | 29 | 30 | def ListSuidSgid(path): 31 | """Finds all the suid/sgid files under path. 32 | 33 | Args: 34 | path: root of directory tree to examine. 35 | Returns: 36 | dictionary of Suid/Sgid files. 37 | """ 38 | 39 | cmd = ['/usr/bin/find', path, '-type', 'f', '(', '-perm', '-004000', '-o', 40 | '-perm', '-002000', ')', '-exec', 'ls', '-l', '{}', ';'] 41 | res = dmgtestutilities.ProcessCommand(cmd) 42 | catalog = [] 43 | prefix_length = len(path) 44 | for f in res['stdout']: 45 | if f: 46 | snip = f.find('/') 47 | if snip: 48 | snip_index = snip + prefix_length 49 | rawpath = f[snip_index:] 50 | catalog.append(rawpath) 51 | else: 52 | logging.warn('snip: %s' % snip) 53 | logging.warn(f) 54 | return catalog 55 | 56 | 57 | def ReadWhiteList(path): 58 | """Read a whitelist of setuid/setgid files. 59 | 60 | Ignore lines starting with a # so we can comment the whitelist. 61 | 62 | Args: 63 | path: file to load the whitelist from. 64 | Returns: 65 | dictionary of path:True mappings 66 | """ 67 | white_file = open(path, 'r') 68 | catalog = {} 69 | for entry in white_file: 70 | if not entry or entry[:1] == '#': 71 | pass # ignore comment and empty lines in whitelist file 72 | else: 73 | catalog[entry.strip()] = True 74 | return catalog 75 | 76 | 77 | class TestSUIDGUIDFiles(macdmgtest.DMGUnitTest): 78 | 79 | def setUp(self): 80 | whitelist_path = self.ConfigPath('suidguid.whitelist') 81 | self.whitelisted_suids = ReadWhiteList(whitelist_path) 82 | 83 | def testForUnknownSUIDsAndGUIDs(self): 84 | """SLOW: Search for non-whitelisted suid/guid files on dmg.""" 85 | scrutinize = ListSuidSgid(self.Mountpoint()) 86 | illegal_suids = [] 87 | for s in scrutinize: 88 | if s not in self.whitelisted_suids: 89 | illegal_suids.append(s) 90 | if illegal_suids: 91 | # make it easier to update the whitelist when a new Apple update adds 92 | # a suid/sgid file 93 | print '\n\n# suid/sgid files suitable for pasting into whitelist.' 94 | '\n'.join(illegal_suids) 95 | print '# end paste\n\n' 96 | self.assertEqual(0, len(illegal_suids)) 97 | 98 | 99 | if __name__ == '__main__': 100 | macdmgtest.main() 101 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/zz_world_writable_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Tests are done in alphabetical order by file name, so since this 18 | # traverses the entire dmg, using zz to push it to the end with the 19 | # other slow unit tests. 20 | 21 | """Confirm all world-writable files and dirs on the dmg are in our whitelist.""" 22 | __author__ = 'jpb@google.com (Joe Block)' 23 | 24 | 25 | import logging 26 | import dmgtestutilities 27 | import macdmgtest 28 | 29 | 30 | def CatalogWritables(path): 31 | """Finds all the files and directories that are world-writeable. 32 | 33 | Args: 34 | path: root of directory tree to examine. 35 | Returns: 36 | dictionary of paths to world-writeable files & directories under root, 37 | with the base path to the root peeled off. 38 | """ 39 | 40 | dir_cmd = ['/usr/bin/find', path, '-type', 'd', '-perm', '+o=w', '-exec', 41 | 'ls', '-ld', '{}', ';'] 42 | file_cmd = ['/usr/bin/find', path, '-type', 'f', '-perm', '+o=w', '-exec', 43 | 'ls', '-l', '{}', ';'] 44 | logging.debug('Searching dmg for world writable files') 45 | files = dmgtestutilities.ProcessCommand(file_cmd) 46 | logging.debug('Searching dmg for world writable directories') 47 | dirs = dmgtestutilities.ProcessCommand(dir_cmd) 48 | state_of_sin = dirs['stdout'] + files['stdout'] 49 | 50 | writeables = [] 51 | prefix_length = len(path) 52 | for s in state_of_sin: 53 | if s: 54 | snip = s.find('/') 55 | if snip: 56 | snip_index = snip + prefix_length 57 | rawpath = s[snip_index:] 58 | writeables.append(rawpath) 59 | else: 60 | logging.warn('snip: %s' % snip) 61 | logging.warn(s) 62 | return writeables 63 | 64 | 65 | def ReadWhiteList(path): 66 | """Read a whitelist of world writable files and directories into a dict. 67 | 68 | Ignore lines starting with a # so we can comment the whitelist. 69 | 70 | Args: 71 | path: file to load the whitelist from. 72 | Returns: 73 | dictionary of path:True mappings 74 | """ 75 | white_file = open(path, 'r') 76 | catalog = {} 77 | for entry in white_file: 78 | if not entry or entry[:1] == '#': 79 | pass # ignore comment and empty lines 80 | else: 81 | catalog[entry.strip()] = True 82 | return catalog 83 | 84 | 85 | class TestWritableDirectoriesAndFiles(macdmgtest.DMGUnitTest): 86 | 87 | def setUp(self): 88 | whitelist_path = self.ConfigPath('writables.whitelist') 89 | self.whitelisted_writables = ReadWhiteList(whitelist_path) 90 | 91 | def testForWorldWritableFilesOrDirectories(self): 92 | """SLOW: Search for non-whitelisted world-writable files and directories.""" 93 | scrutinize = CatalogWritables(self.Mountpoint()) 94 | sinners = [] 95 | for s in scrutinize: 96 | if s not in self.whitelisted_writables: 97 | sinners.append(s) 98 | if sinners: 99 | print '\n\n# world-writable files & dirs for pasting into whitelist.' 100 | print '\n'.join(sinners) 101 | print '# end paste\n\n' 102 | self.assertEqual(0, len(sinners)) 103 | 104 | 105 | if __name__ == '__main__': 106 | macdmgtest.main() 107 | -------------------------------------------------------------------------------- /examples/crankd/sample-of-events/generate-event-plist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Generates a list of OS X system events into a plist for crankd. 5 | 6 | This is designed to create a large (but probably not comprehensive) sample 7 | of the events generated by Mac OS X that crankd can tap into. The generated 8 | file will call the 'tunnel.sh' as the command for each event; said fail can 9 | be easily edited to redirect the output to wherever you would like it to go. 10 | 11 | """ 12 | 13 | OUTPUT_FILE = "crankd-config.plist" 14 | 15 | from SystemConfiguration import SCDynamicStoreCopyKeyList, SCDynamicStoreCreate 16 | 17 | # Each event has a general event type, and a specific event 18 | # The category is the key, and the value is a list of specific events 19 | event_dict = {} 20 | 21 | def AddEvent(event_category, specific_event): 22 | """Adds an event to the event dictionary""" 23 | if event_category not in event_dict: 24 | event_dict[event_category] = [] 25 | event_dict[event_category].append(specific_event) 26 | 27 | def AddCategoryOfEvents(event_category, events): 28 | """Adds a list of events that all belong to the same category""" 29 | for specific_event in events: 30 | AddEvent(event_category, specific_event) 31 | 32 | def AddKnownEvents(): 33 | """Here we add all the events that we know of to the dictionary""" 34 | 35 | # Add a bunch of dynamic events 36 | store = SCDynamicStoreCreate(None, "generate_event_plist", None, None) 37 | AddCategoryOfEvents(u"SystemConfiguration", 38 | SCDynamicStoreCopyKeyList(store, ".*")) 39 | 40 | # Add some standard NSWorkspace events 41 | AddCategoryOfEvents(u"NSWorkspace", 42 | u''' 43 | NSWorkspaceDidLaunchApplicationNotification 44 | NSWorkspaceDidMountNotification 45 | NSWorkspaceDidPerformFileOperationNotification 46 | NSWorkspaceDidTerminateApplicationNotification 47 | NSWorkspaceDidUnmountNotification 48 | NSWorkspaceDidWakeNotification 49 | NSWorkspaceSessionDidBecomeActiveNotification 50 | NSWorkspaceSessionDidResignActiveNotification 51 | NSWorkspaceWillLaunchApplicationNotification 52 | NSWorkspaceWillPowerOffNotification 53 | NSWorkspaceWillSleepNotification 54 | NSWorkspaceWillUnmountNotification 55 | '''.split()) 56 | 57 | def PrintEvents(): 58 | """Prints all the events, for debugging purposes""" 59 | for category in sorted(event_dict): 60 | 61 | print category 62 | 63 | for event in sorted(event_dict[category]): 64 | print "\t" + event 65 | 66 | def OutputEvents(): 67 | """Outputs all the events to a file""" 68 | 69 | # print the header for the file 70 | plist = open(OUTPUT_FILE, 'w') 71 | 72 | print >>plist, ''' 73 | 74 | 75 | ''' 76 | 77 | for category in sorted(event_dict): 78 | 79 | # print out the category 80 | print >>plist, " %s\n " % category 81 | 82 | for event in sorted(event_dict[category]): 83 | print >>plist, """ 84 | %s 85 | 86 | command 87 | %s '%s' '%s' 88 | """ % ( event, 'tunnel.sh', category, event ) 89 | 90 | # end the category 91 | print >>plist, " " 92 | 93 | # end the plist file 94 | print >>plist, '' 95 | print >>plist, '' 96 | 97 | plist.close() 98 | 99 | def main(): 100 | """Runs the program""" 101 | AddKnownEvents() 102 | #PrintEvents() 103 | OutputEvents() 104 | 105 | main() 106 | 107 | 108 | -------------------------------------------------------------------------------- /examples/ctypes/keychain-update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Updates existing keychain internet password items with a new password. 5 | Usage: keychain-internet-password-update.py account_name new_password 6 | 7 | Contributed by Matt Rosenberg 8 | """ 9 | 10 | import ctypes 11 | import sys 12 | 13 | # Load Security.framework 14 | Security = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Security.framework/Versions/Current/Security') 15 | 16 | def FourCharCode(fcc): 17 | """Create an integer from the provided 4-byte string, required for finding keychain items based on protocol type""" 18 | return ord(fcc[0]) << 24 | ord(fcc[1]) << 16 | ord(fcc[2]) << 8 | ord(fcc[3]) 19 | 20 | def UpdatePassword(account_name, new_password, server_name, protocol_type_string=''): 21 | """ 22 | Function to update an existing internet password keychain item 23 | 24 | Search for the existing item is based on account name, password, server, and 25 | protocol (optional). Additional search parameters are available, but are 26 | hard-coded to null here. 27 | 28 | The list of protocol type codes is in 29 | /System/Library/Frameworks/Security.framework/Versions/Current/Headers/SecKeychain.h 30 | """ 31 | 32 | item = ctypes.c_char_p() 33 | port_number = ctypes.c_uint16(0) # Set port number to 0, works like setting null for most other search parameters 34 | password_length = ctypes.c_uint32(256) 35 | password_pointer = ctypes.c_char_p() 36 | 37 | if protocol_type_string: 38 | protocol_type_code = FourCharCode(protocol_type_string) 39 | else: 40 | protocol_type_code = 0 41 | 42 | # Call function to locate existing keychain item 43 | rc = Security.SecKeychainFindInternetPassword( 44 | None, 45 | len(server_name), 46 | server_name, 47 | None, 48 | None, 49 | len(account_name), 50 | account_name, 51 | None, 52 | None, 53 | port_number, 54 | protocol_type_code, 55 | None, 56 | None, # To retrieve the current password, change this argument to: ctypes.byref(password_length) 57 | None, # To retrieve the current password, change this argument to: ctypes.pointer(password_pointer) 58 | ctypes.pointer(item) 59 | ) 60 | 61 | if rc != 0: 62 | raise RuntimeError('Did not find existing password for server %s, protocol type %s, account name %s: rc=%d' % (server_name, protocol_type_code, account_name, rc)) 63 | 64 | # Call function to update password 65 | rc = Security.SecKeychainItemModifyAttributesAndData( 66 | item, 67 | None, 68 | len(new_password), 69 | new_password 70 | ) 71 | 72 | if rc != 0: 73 | raise RuntimeError('Failed to record new password for server %s, protocol type %s, account name %s: rc=%d' % (server_name, protocol_type_code, account_name, rc)) 74 | 75 | return 0 76 | 77 | # Start execution 78 | 79 | # Check to make sure needed arguments were passed 80 | if len(sys.argv) != 3: 81 | raise RuntimeError('ERROR: Incorrect number of arguments. Required usage: keychain-internet-password-update.py account_name new_password') 82 | 83 | # Set variables from the argument list 84 | account_name = sys.argv[1] 85 | new_password = sys.argv[2] 86 | 87 | # Call UpdatePassword for each password to update. 88 | # 89 | # If more than one keychain item will match a server and account name, you must 90 | # specify a protocol type. Otherwise, only the first matching item will be 91 | # updated. 92 | # 93 | # The list of protocol type codes is in 94 | # /System/Library/Frameworks/Security.framework/Versions/Current/Headers/SecKeychain.h 95 | 96 | # Update a password without specifying a protocol type 97 | print "Updating password for site.domain.com" 98 | UpdatePassword(account_name, new_password, 'site.domain.com') 99 | 100 | # Update the password for an HTTP proxy 101 | print "Updating HTTP Proxy password" 102 | UpdatePassword(account_name, new_password, 'webproxy.domain.com', 'htpx') 103 | 104 | # Update the password for an HTTPS proxy 105 | print "Updating HTTPS Proxy password" 106 | UpdatePassword(account_name, new_password, 'webproxy.domain.com', 'htsx') 107 | 108 | print "Done!" -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/ouradmin_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Copyright 2008-2009 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Change ouradmin to whatever local admin account you're adding to your image. 18 | # 19 | # We also sanity check that the group plists pass lint to ensure our localadmin 20 | # creation package didn't somehow break them. 21 | 22 | """Make sure there's an ouradmin local account on the machine.""" 23 | __author__ = 'jpb@google.com (Joe Block)' 24 | 25 | import os 26 | import stat 27 | import dmgtestutilities 28 | import macdmgtest 29 | 30 | 31 | class TestCheckForouradmin(macdmgtest.DMGUnitTest): 32 | """Make sure there's an ouradmin local account on the machine.""" 33 | 34 | def setUp(self): 35 | """Setup paths.""" 36 | self.localnode = 'var/db/dslocal/nodes/Default' 37 | self.admin_plist = self.PathOnDMG('%s/groups/admin.plist' % self.localnode) 38 | self.ouradmin_plist = self.PathOnDMG('%s/users/ouradmin.plist' % 39 | self.localnode) 40 | self.ouradmin_stat = os.stat(self.ouradmin_plist) 41 | self.lpadmin_plist = self.PathOnDMG('%s/groups/_lpadmin.plist' % 42 | self.localnode) 43 | self.appserveradm_plist = self.PathOnDMG('%s/groups/_appserveradm.plist' % 44 | self.localnode) 45 | 46 | def testOuradminIsMemberOfLPadminGroup(self): 47 | """Check that ouradmin user is in _lpadmin group.""" 48 | pf = dmgtestutilities.ReadPlist(self.lpadmin_plist) 49 | self.assertEqual('ouradmin' in pf['users'], True) 50 | 51 | def testOuradminIsMemberOfAppserverAdminGroup(self): 52 | """Check that ouradmin user is in _appserveradm group.""" 53 | pf = dmgtestutilities.ReadPlist(self.appserveradm_plist) 54 | self.assertEqual('ouradmin' in pf['users'], True) 55 | 56 | def testOuradminIsMemberOfAdminGroup(self): 57 | """Check that ouradmin user is in admin group.""" 58 | pf = dmgtestutilities.ReadPlist(self.admin_plist) 59 | self.assertEqual('ouradmin' in pf['users'], True) 60 | 61 | def testOuradminIsInDSLocal(self): 62 | """Check for ouradmin user in local ds node.""" 63 | plistpath = self.PathOnDMG('%s/users/ouradmin.plist' % self.localnode) 64 | self.assertEqual(os.path.exists(plistpath), True) 65 | 66 | def testOuradminPlistMode(self): 67 | """ouradmin.plist is supposed to be mode 600.""" 68 | mode = self.ouradmin_stat[stat.ST_MODE] 69 | num_mode = oct(mode & 0777) 70 | self.assertEqual('0600', num_mode) 71 | 72 | def testOuradminPlistCheckGroup(self): 73 | """ouradmin.plist should be group wheel.""" 74 | group = self.ouradmin_stat[stat.ST_GID] 75 | self.assertEqual(0, group) 76 | 77 | def testOuradminPlistCheckOwnership(self): 78 | """ouradmin.plist should be owned by root.""" 79 | owner = self.ouradmin_stat[stat.ST_UID] 80 | self.assertEqual(0, owner) 81 | 82 | # lint every plist the localadmin creation package had to touch. 83 | 84 | def testPlistLintAdminGroup(self): 85 | """Make sure admin.plist passes lint.""" 86 | cmd = dmgtestutilities.LintPlist(self.admin_plist) 87 | self.assertEqual(cmd, 0) 88 | 89 | def testPlistLintAppserverAdminGroup(self): 90 | """Make sure _appserveradm.plist passes lint.""" 91 | cmd = dmgtestutilities.LintPlist(self.appserveradm_plist) 92 | self.assertEqual(cmd, 0) 93 | 94 | def testPlistLintLPAdminGroup(self): 95 | """Make sure _lpadmin.plist passes lint.""" 96 | cmd = dmgtestutilities.LintPlist(self.lpadmin_plist) 97 | self.assertEqual(cmd, 0) 98 | 99 | def testOuradminPlistLint(self): 100 | """Make sure ouradmin.plist passes lint.""" 101 | cmd = dmgtestutilities.LintPlist(self.ouradmin_plist) 102 | self.assertEqual(cmd, 0) 103 | 104 | 105 | if __name__ == '__main__': 106 | macdmgtest.main() 107 | -------------------------------------------------------------------------------- /bin/create-location.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Usage: %prog USER_VISIBLE_NAME 4 | 5 | Creates a new SystemConfiguration location for use in Network Preferences by 6 | copying the Automatic location 7 | """ 8 | 9 | from SystemConfiguration import * 10 | from CoreFoundation import * 11 | import sys 12 | import re 13 | import logging 14 | from optparse import OptionParser 15 | 16 | 17 | def copy_set(path, old_id, old_set): 18 | new_set = CFPropertyListCreateDeepCopy(None, old_set, kCFPropertyListMutableContainersAndLeaves) 19 | new_set_path = SCPreferencesPathCreateUniqueChild(sc_prefs, path) 20 | 21 | if not new_set_path \ 22 | or not re.match(r"^%s/[^/]+$" % path, new_set_path): 23 | raise RuntimeError("SCPreferencesPathCreateUniqueChild() returned an invalid path for the new location: %s" % new_set_path) 24 | 25 | return new_set_path, new_set 26 | 27 | 28 | def main(): 29 | # Ugly but this is easiest until we refactor this into an SCPrefs class: 30 | global sc_prefs 31 | 32 | logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') 33 | 34 | parser = OptionParser(__doc__.strip()) 35 | 36 | parser.add_option('-v', '--verbose', action="store_true", 37 | help="Print more information" 38 | ) 39 | 40 | (options, args) = parser.parse_args() 41 | 42 | if not args: 43 | parser.error("You must specify the user-visible name for the new location") 44 | 45 | if options.verbose: 46 | logging.getLogger().setLevel(logging.DEBUG) 47 | 48 | 49 | new_name = " ".join(args) 50 | sc_prefs = SCPreferencesCreate(None, "create_location", None) 51 | sets = SCPreferencesGetValue(sc_prefs, kSCPrefSets) 52 | old_set_id = None 53 | old_set = None 54 | 55 | for k in sets: 56 | if sets[k][kSCPropUserDefinedName] == new_name: 57 | raise RuntimeError("A set named %s already exists" % new_name) 58 | elif sets[k][kSCPropUserDefinedName] == "Automatic": 59 | old_set_id = k 60 | 61 | if not old_set_id: 62 | raise RuntimeError("Couldn't find Automatic set") 63 | 64 | old_set = sets[old_set_id] 65 | logging.debug("Old set %s:\n%s" % (old_set_id, old_set)) 66 | 67 | logging.info('Creating "%s" using a copy of "%s"' % (new_name, old_set[kSCPropUserDefinedName])) 68 | new_set_path, new_set = copy_set("/%s" % kSCPrefSets, old_set_id, old_set) 69 | new_set_id = new_set_path.split('/')[-1] 70 | new_set[kSCPropUserDefinedName] = new_name 71 | 72 | service_map = dict() 73 | 74 | for old_service_id in old_set[kSCCompNetwork][kSCCompService]: 75 | assert( 76 | old_set[kSCCompNetwork][kSCCompService][old_service_id][kSCResvLink].startswith("/%s" % kSCPrefNetworkServices) 77 | ) 78 | 79 | new_service_path = SCPreferencesPathCreateUniqueChild(sc_prefs, "/%s" % kSCPrefNetworkServices) 80 | new_service_id = new_service_path.split("/")[2] 81 | new_service_cf = CFPropertyListCreateDeepCopy( 82 | None, 83 | SCPreferencesGetValue(sc_prefs, kSCPrefNetworkServices)[old_service_id], 84 | kCFPropertyListMutableContainersAndLeaves 85 | ) 86 | SCPreferencesPathSetValue(sc_prefs, new_service_path, new_service_cf) 87 | 88 | new_set[kSCCompNetwork][kSCCompService][new_service_id] = { 89 | kSCResvLink: new_service_path 90 | } 91 | del new_set[kSCCompNetwork][kSCCompService][old_service_id] 92 | 93 | service_map[old_service_id] = new_service_id 94 | 95 | for proto in new_set[kSCCompNetwork][kSCCompGlobal]: 96 | new_set[kSCCompNetwork][kSCCompGlobal][proto][kSCPropNetServiceOrder] = map( 97 | lambda k: service_map[k], 98 | old_set[kSCCompNetwork][kSCCompGlobal][proto][kSCPropNetServiceOrder] 99 | ) 100 | 101 | SCPreferencesPathSetValue(sc_prefs, new_set_path, new_set) 102 | 103 | logging.debug("New Set %s:\n%s\n" % (new_set_id, new_set)) 104 | 105 | if not SCPreferencesCommitChanges(sc_prefs): 106 | raise RuntimeError("Unable to save SystemConfiguration changes") 107 | 108 | if not SCPreferencesApplyChanges(sc_prefs): 109 | raise RuntimeError("Unable to apply SystemConfiguration changes") 110 | 111 | 112 | if __name__ == '__main__': 113 | try: 114 | main() 115 | except RuntimeError, e: 116 | logging.critical(str(e)) 117 | sys.exit(1) 118 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/example_plist_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2008 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Example test showing how to check values inside a plist. 17 | 18 | This example tests the existence of certain fields within: 19 | /Library/Preferences/com.foo.corp.imageinfo.plist 20 | 21 | This plist identifies the date at which an image was created, so this series 22 | of tests simply checks whether the plist exists, whether it is a proper file 23 | as opposed to a symlink, whether the imageVersion field exists, whether it can 24 | be made into a valid date, and whether the date is a sane value. 25 | 26 | As with this whole framework, the attribute self.mountpoint refers to the 27 | location at which the image to be tested is mounted. 28 | 29 | Note that we're copying the plist to a temporary location and converting it 30 | to xml1 format rather than binary. We do this so that plistlib works 31 | (it doesn't work on binary plists) and so that we're not actually trying to 32 | modify the image, which is mounted in read-only mode. 33 | 34 | Original Author: Nigel Kersten (nigelk@google.com) 35 | """ 36 | 37 | import datetime 38 | import os 39 | import re 40 | import shutil 41 | import stat 42 | import subprocess 43 | import tempfile 44 | import unittest 45 | import plistlib 46 | 47 | 48 | # don"t use absolute paths with os.path.join 49 | imageinfo_plist = "Library/Preferences/com.foo.corp.imageinfo.plist" 50 | 51 | 52 | class TestMachineInfo(unittest.TestCase): 53 | 54 | def setUp(self): 55 | """copy the original file to a temp plist and convert it to xml1.""" 56 | self.tempdir = tempfile.mkdtemp() 57 | self.orig_imageinfo_file = os.path.join(self.mountpoint, imageinfo_plist) 58 | imageinfo_file = os.path.join(self.tempdir, "imageinfo.plist") 59 | shutil.copyfile(self.orig_imageinfo_file, imageinfo_file) 60 | command = ["plutil", "-convert", "xml1", imageinfo_file] 61 | returncode = subprocess.call(command) 62 | if returncode: 63 | raise StandardError("unable to convert plist to xml1") 64 | self.imageinfo = plistlib.readPlist(imageinfo_file) 65 | 66 | def tearDown(self): 67 | """clean up the temporary location.""" 68 | if self.tempdir: 69 | if os.path.isdir(self.tempdir): 70 | shutil.rmtree(self.tempdir) 71 | 72 | def testFile(self): 73 | """test the original file is a proper file.""" 74 | self.assert_(os.path.isfile(self.orig_imageinfo_file)) 75 | 76 | def testOwnerGroupMode(self): 77 | """test owner, group and mode of original file.""" 78 | orig_imageinfo_stat = os.stat(self.orig_imageinfo_file) 79 | owner = orig_imageinfo_stat[stat.ST_UID] 80 | group = orig_imageinfo_stat[stat.ST_GID] 81 | mode = orig_imageinfo_stat[stat.ST_MODE] 82 | num_mode = oct(mode & 0777) 83 | self.assertEqual(0, owner) 84 | self.assertEqual(80, group) 85 | self.assertEqual('0644', num_mode) 86 | 87 | def testImageVersionPresent(self): 88 | """test that the ImageVersion field is present.""" 89 | self.failUnless("ImageVersion" in self.imageinfo) 90 | 91 | def testImageVersionFormat(self): 92 | """test that the ImageVersion field is well formed.""" 93 | pattern = re.compile("^\d{8}$") 94 | self.failUnless(pattern.match(self.imageinfo["ImageVersion"])) 95 | 96 | def testImageVersionValueIsDate(self): 97 | """test that the ImageVersion value is actually a date""" 98 | image_version = self.imageinfo["ImageVersion"] 99 | year = int(image_version[:4]) 100 | month = int(image_version[4:-2]) 101 | day = int(image_version[6:]) 102 | now = datetime.datetime.now() 103 | self.failUnless(now.replace(year=year,month=month,day=day)) 104 | 105 | def testImageVersionValueIsCurrentDate(self): 106 | """test that the ImageVersion value is a current date.""" 107 | image_version = self.imageinfo["ImageVersion"] 108 | year = int(image_version[:4]) 109 | year_range = range(2006, 2100) 110 | self.failUnless(year in year_range) 111 | 112 | 113 | 114 | if __name__ == "__main__": 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /utilities/wtfUpdate.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | wtfupdate 8 | 9 | 10 | 36 | 134 | 135 | 136 |
137 |

138 |
139 |

140 | Files 141 |

142 |
    143 |
  • Files will be listed here 144 |
  • 145 |
  • or possibly here 146 |
      147 |
    • Or maybe even here? 148 |
    • 149 |
    150 |
  • 151 |
152 |

153 | Scripts 154 |

155 |
    156 |
  • Script output will go here 157 |
  • 158 |
  • or possibly here 159 |
  • 160 |
161 |
162 | 163 | 164 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/tests/applications_dir_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2008 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | """Check user/group/permissions in /Applications & /Applications/Utilities. 19 | 20 | Author: Joe Block (jpb@google.com) 21 | """ 22 | 23 | import logging 24 | import os 25 | import pprint 26 | import stat 27 | import macdmgtest 28 | 29 | 30 | class TestAppDirectories(macdmgtest.DMGUnitTest): 31 | 32 | def setUp(self): 33 | """Set up exceptions to standard permissions requirements.""" 34 | self.errors_found = [] 35 | self.standard_stat = {'uid': 0, 'gid': 80, 'mode': '0775'} 36 | self.application_exceptions = {} 37 | self.application_exceptions['System Preferences'] = {} 38 | self.application_exceptions['System Preferences']['gid'] = 0 39 | self.application_exceptions['System Preferences']['mode'] = '0775' 40 | self.application_exceptions['System Preferences']['uid'] = 0 41 | self.utilities_exceptions = {} 42 | # Here are a couple of examples of making exceptions for stuff we 43 | # symlink into Applications or Applications/Utilities 44 | self.utilities_exceptions['Kerberos'] = {} 45 | self.utilities_exceptions['Kerberos']['gid'] = 0 46 | self.utilities_exceptions['Kerberos']['mode'] = '0755' 47 | self.utilities_exceptions['Kerberos']['symlink_ok'] = True 48 | self.utilities_exceptions['Kerberos']['uid'] = 0 49 | self.utilities_exceptions['Screen Sharing'] = {} 50 | self.utilities_exceptions['Screen Sharing']['gid'] = 0 51 | self.utilities_exceptions['Screen Sharing']['mode'] = '0755' 52 | self.utilities_exceptions['Screen Sharing']['symlink_ok'] = True 53 | self.utilities_exceptions['Screen Sharing']['uid'] = 0 54 | 55 | def _SanityCheckApp(self, statmatrix, overrides, thedir, name): 56 | """Check a .app directory and ensure it has sane perms and ownership.""" 57 | o = os.path.splitext(name)[0] 58 | if o in overrides: 59 | g_uid = overrides[o]['uid'] 60 | g_gid = overrides[o]['gid'] 61 | g_mode = overrides[o]['mode'] 62 | else: 63 | g_uid = statmatrix['uid'] 64 | g_gid = statmatrix['gid'] 65 | g_mode = statmatrix['mode'] 66 | path = os.path.join(self.mountpoint, thedir, name) 67 | check_stats = os.stat(path) 68 | a_mode = oct(check_stats[stat.ST_MODE] & 0777) 69 | a_gid = check_stats[stat.ST_GID] 70 | a_uid = check_stats[stat.ST_UID] 71 | if os.path.islink(path): 72 | if o in overrides: 73 | if 'symlink_ok' in overrides[o]: 74 | if not overrides[o]['symlink_ok']: 75 | msg = '%s/%s is a symlink and should not be.' % (thedir, name) 76 | self.errors_found.append(msg) 77 | logging.debug(msg) 78 | else: 79 | msg = '%s/%s is a symlink, not an application.' % (thedir, name) 80 | self.errors_found.append(msg) 81 | logging.debug(msg) 82 | if a_uid != g_uid: 83 | msg = '%s/%s is owned by %s, should be owned by %s' % (thedir, name, 84 | a_uid, g_uid) 85 | self.errors_found.append(msg) 86 | logging.debug(msg) 87 | if a_gid != g_gid: 88 | msg = '%s/%s is group %s, should be group %s' % (thedir, name, a_gid, 89 | g_gid) 90 | self.errors_found.append(msg) 91 | logging.debug(msg) 92 | if a_mode != g_mode: 93 | msg = '%s/%s was mode %s, should be %s' % (thedir, name, a_mode, g_mode) 94 | self.errors_found.append(msg) 95 | logging.debug(msg) 96 | 97 | def testApplicationDirectory(self): 98 | """Sanity check all applications in /Applications.""" 99 | self.errors_found = [] 100 | appdir = 'Applications' 101 | for application in os.listdir(os.path.join(self.mountpoint, appdir)): 102 | if os.path.splitext(application)[1] == '.app': 103 | self._SanityCheckApp(self.standard_stat, self.application_exceptions, 104 | appdir, application) 105 | if self.errors_found: 106 | print 107 | pprint.pprint(self.errors_found) 108 | self.assertEqual(len(self.errors_found), 0) 109 | 110 | def testUtilitiesDirectory(self): 111 | """Sanity check applications in /Applications/Utilities.""" 112 | self.errors_found = [] 113 | appdir = 'Applications/Utilities' 114 | for application in os.listdir(os.path.join(self.mountpoint, appdir)): 115 | if application[-3:] == 'app': 116 | self._SanityCheckApp(self.standard_stat, self.utilities_exceptions, 117 | appdir, application) 118 | if self.errors_found: 119 | print 120 | pprint.pprint(self.errors_found) 121 | self.assertEqual(len(self.errors_found), 0) 122 | 123 | 124 | if __name__ == '__main__': 125 | macdmgtest.main() 126 | -------------------------------------------------------------------------------- /utilities/diskimage_unittesting/run_image_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Use 2.5 so we can import objc & Foundation in tests 4 | # 5 | # Copyright 2008 Google Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """Parent script for unit testing a Mac OS X image candidate. 20 | 21 | This script expects to be passed the path to a dmg that is a Mac OS X 22 | image candidate. It mounts the image, imports the modules in the tests dir, 23 | builds up a test suite of their test functions and runs them on the image. 24 | 25 | Each test has "self.mountpoint" set that is the mountpoint of the dmg 26 | 27 | Unit tests in the test directory must be subclasses of macdmgtest.DMGUnitTest. 28 | 29 | Naming formats must be as follows: 30 | Files: *_test.py 31 | Classes: Test* 32 | Tests: test* 33 | 34 | Author: Nigel Kersten (nigelk@google.com) 35 | Modified by: Joe Block (jpb@google.com) 36 | """ 37 | 38 | import optparse 39 | import os 40 | import re 41 | import subprocess 42 | import sys 43 | import types 44 | import unittest 45 | import plistlib 46 | 47 | 48 | def AttachDiskImage(path): 49 | """attaches a dmg, returns mountpoint, assuming only one filesystem.""" 50 | 51 | command = ["/usr/bin/hdiutil", "attach", path, "-mountrandom", "/tmp", 52 | "-readonly", "-nobrowse", "-noautoopen", "-plist", 53 | "-owners", "on"] 54 | task = subprocess.Popen(command, 55 | stdout=subprocess.PIPE, 56 | stderr=subprocess.PIPE) 57 | (stdout, stderr) = task.communicate() 58 | if task.returncode: 59 | print "There was an error attaching dmg: %s" % path 60 | print stderr 61 | return False 62 | else: 63 | mountpoint = False 64 | dmg_plist = plistlib.readPlistFromString(stdout) 65 | entities = dmg_plist["system-entities"] 66 | for entity in entities: 67 | if "mount-point" in entity: 68 | mountpoint = entity["mount-point"] 69 | return mountpoint 70 | 71 | 72 | def DetachDiskImage(path): 73 | """forcibly unmounts a given dmg from the mountpoint path.""" 74 | 75 | command = ["/usr/bin/hdiutil", "detach", path] 76 | returncode = subprocess.call(command) 77 | if returncode: 78 | command = ["/usr/bin/hdiutil", "detach", "-force", path] 79 | returncode = subprocess.call(command) 80 | if returncode: 81 | raise StandardError("Unable to unmount dmg mounted at: %s" % path) 82 | return True 83 | 84 | 85 | def TestClasses(module): 86 | """returns test classes in a module.""" 87 | classes = [] 88 | pattern = re.compile("^Test\w+$") # only classes starting with "Test" 89 | for name in dir(module): 90 | obj = getattr(module, name) 91 | if type(obj) in (types.ClassType, types.TypeType): 92 | if pattern.match(name): 93 | classes.append(name) 94 | return classes 95 | 96 | 97 | def GetTestSuite(path, mountpoint, options): 98 | """Given path to module, returns suite of test methods.""" 99 | dirname = os.path.dirname(path) 100 | filename = os.path.basename(path) 101 | 102 | if not dirname in sys.path: 103 | sys.path.append(dirname) 104 | 105 | modulename = re.sub("\.py$", "", filename) 106 | module = __import__(modulename) 107 | for classname in TestClasses(module): 108 | test_loader = unittest.TestLoader() 109 | test_suite = test_loader.loadTestsFromName(classname, module) 110 | # there must be a better way than this protected member access... 111 | for test in test_suite._tests: 112 | test.SetMountpoint(mountpoint) 113 | test.SetOptions(options) 114 | return test_suite 115 | 116 | 117 | def ListTests(path): 118 | """lists tests in directory "path" ending in _test.py.""" 119 | 120 | pattern = re.compile("^\w*_test.py$", re.IGNORECASE) 121 | tests = [] 122 | for test in os.listdir(path): 123 | if pattern.match(test): 124 | tests.append(test) 125 | tests.sort() 126 | return tests 127 | 128 | 129 | def SummarizeResults(result): 130 | """Print a summary of a test result.""" 131 | print 132 | print "Results" 133 | print "===============" 134 | 135 | print "total tests run: %s" % result.testsRun 136 | print " errors found: %s" % len(result.errors) 137 | print " failures found: %s" % len(result.failures) 138 | print 139 | 140 | 141 | def ParseCLIArgs(): 142 | """Parse command line arguments and set options accordingly.""" 143 | cli = optparse.OptionParser() 144 | cli.add_option("-c", "--configdir", dest="configdir", default=".", 145 | type="string", help="specify directory for test config files") 146 | cli.add_option("-d", "--dmg", dest="dmg", type="string", 147 | help="specify path to dmg to test.") 148 | cli.add_option("-p", "--pkgdir", dest="pkgdir", type="string", 149 | help="specify directory to look for packages in.") 150 | cli.add_option("-r", "--root", dest="root", type="string", 151 | help="specify path to root of a directory tree to test.") 152 | cli.add_option("-t", "--testdir", dest="testdir", default="tests", 153 | type="string", help="specify directory with tests") 154 | cli.add_option("-v", "--verbosity", type="int", dest="verbosity", 155 | help="specify verbosity level", default=0) 156 | (options, args) = cli.parse_args() 157 | return (options, args) 158 | 159 | 160 | def main(): 161 | """entry point.""" 162 | (options, unused_args) = ParseCLIArgs() 163 | dmg = options.dmg 164 | verbosity = options.verbosity 165 | tests_dir = options.testdir 166 | config_dir = options.configdir 167 | root = options.root 168 | if not dmg and not root: 169 | print "Use --dmg to specify a dmg file or --root to specify a directory." 170 | sys.exit(1) 171 | if dmg: 172 | print "Mounting disk image... (this may take some time)" 173 | mountpoint = AttachDiskImage(dmg) 174 | if not mountpoint: 175 | print "Unable to mount %s" % dmg 176 | sys.exit(2) 177 | elif root: 178 | if not os.path.isdir(root): 179 | print "%s not a directory" % root 180 | sys.exit(2) 181 | mountpoint = root 182 | print "Checking %s" % mountpoint 183 | print 184 | 185 | dirname = os.path.dirname(sys.argv[0]) 186 | os.chdir(dirname) 187 | tests = ListTests(tests_dir) 188 | test_results = {} 189 | combo_suite = unittest.TestSuite() 190 | for test in tests: 191 | test_path = os.path.join(tests_dir, test) 192 | combo_suite.addTests(GetTestSuite(test_path, mountpoint, options)) 193 | 194 | test_results = unittest.TextTestRunner(verbosity=verbosity).run(combo_suite) 195 | 196 | if dmg: 197 | DetachDiskImage(mountpoint) 198 | 199 | if test_results.wasSuccessful(): 200 | sys.exit(0) 201 | else: 202 | SummarizeResults(test_results) 203 | bad = len(test_results.errors) + len(test_results.failures) 204 | sys.exit(bad) 205 | 206 | 207 | if __name__ == "__main__": 208 | main() 209 | -------------------------------------------------------------------------------- /utilities/wtfUpdate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Generate HTML documentation from an Apple .pkg update 5 | 6 | Given a .pkg file this program will generate a list of the installed files, 7 | installer scripts. 8 | 9 | Contributed by Chris Barker (chrisb@sneezingdog.com): 10 | 11 | 12 | 13 | """ 14 | 15 | # Licensed under the Apache License, Version 2.0 (the "License"); 16 | # you may not use this file except in compliance with the License. 17 | # You may obtain a copy of the License at 18 | # 19 | # http://www.apache.org/licenses/LICENSE-2.0 20 | # 21 | # Unless required by applicable law or agreed to in writing, software 22 | # distributed under the License is distributed on an "AS IS" BASIS, 23 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | # See the License for the specific language governing permissions and 25 | # limitations under the License. 26 | 27 | import subprocess 28 | import os 29 | import sys 30 | import shutil 31 | import tempfile 32 | import hashlib 33 | from BeautifulSoup import BeautifulSoup, Tag, NavigableString 34 | from os.path import basename, splitext, exists, join, isfile, isdir 35 | 36 | # This will contain a BeautifulSoup object so we can avoid passing it around 37 | # to every function: 38 | SOUP = None 39 | 40 | def expand_pkg(pkg_file): 41 | """ Expand the provided .pkg file and return the temp directory path """ 42 | 43 | # n.b. This is a potential security issue but there's not really a good 44 | # way to avoid it because pkgutil can't handle an existing directory: 45 | dest = tempfile.mktemp() 46 | subprocess.check_call(["/usr/sbin/pkgutil", "--expand", pkg_file, dest]) 47 | return dest 48 | 49 | 50 | def get_description(pkg): 51 | """Return the HTML description """ 52 | 53 | su_desc = pkg + '/Resources/English.lproj/SUDescription.html' 54 | 55 | if not exists(su_desc): 56 | return "no description" 57 | 58 | soup = BeautifulSoup(open(su_desc).read()) 59 | return soup.body.contents 60 | 61 | 62 | def load_scripts(pkg): 63 | """ 64 | Given a package expand ul#scripts to include the contents of any scripts 65 | """ 66 | 67 | script_ul = SOUP.find("ul", {"id": "scripts"}) 68 | script_ul.contents = [] 69 | 70 | for f in os.listdir(pkg): 71 | if splitext(f)[1] != '.pkg': 72 | continue 73 | 74 | script_dir = join(pkg, f, 'Scripts') 75 | script_list = Tag(SOUP, 'ul') 76 | 77 | for script in os.listdir(script_dir): 78 | if script == "Tools": 79 | continue 80 | 81 | script_li = Tag(SOUP, 'li') 82 | script_li['class'] = 'code' 83 | script_path = join(script_dir, script) 84 | 85 | if isfile(script_path): 86 | script_li.append(join(f, 'Scripts', script)) 87 | script_li.append(anchor_for_name(script_path)) 88 | script_pre = Tag(SOUP, 'pre') 89 | script_pre.append(NavigableString(open(script_path).read())) 90 | script_li.append(script_pre) 91 | elif isdir(script_path): 92 | subscript_files = os.listdir(script_path) 93 | if not subscript_files: 94 | continue 95 | 96 | script_li.append("%s Scripts" % join(f, 'Scripts', script)) 97 | subscripts = Tag(SOUP, 'ul') 98 | 99 | for subscript in subscript_files: 100 | subscript_path = join(script_path, subscript) 101 | subscript_li = Tag(SOUP, 'li') 102 | subscript_li.append(subscript) 103 | subscript_li.append(anchor_for_name(subscript_path)) 104 | 105 | subscript_pre = Tag(SOUP, 'pre') 106 | subscript_pre.append(NavigableString(open(subscript_path).read())) 107 | subscript_li.append(subscript_pre) 108 | 109 | subscripts.append(subscript_li) 110 | 111 | script_li.append(subscripts) 112 | 113 | script_list.append(script_li) 114 | 115 | if script_list.contents: 116 | new_scripts = Tag(SOUP, 'li') 117 | new_scripts.append(NavigableString("%s Scripts" % f)) 118 | new_scripts.append(script_list) 119 | script_ul.append(new_scripts) 120 | 121 | def get_file_list(pkg, sub_package): 122 | """ 123 | Expand the ul#files list in the template with a listing of the files 124 | contained in the sub package's BOM 125 | """ 126 | 127 | file_ul = SOUP.find("ul", {'id': 'files'}) 128 | if not file_ul: 129 | raise RuntimeError("""Template doesn't appear to have a
    !""") 130 | 131 | if not "cleaned" in file_ul.get("class", ""): 132 | file_ul.contents = [] # Remove any template content 133 | 134 | for k, v in get_bom_contents(pkg + '/' + sub_package + '/Bom').items(): 135 | file_ul.append(get_list_for_key(k, v)) 136 | 137 | def get_list_for_key(name, children): 138 | """ 139 | Takes a key and a dictionary containing its children and recursively 140 | generates HTML lists items. Each item will contain the name and, if it has 141 | children, an unordered list containing those child items. 142 | """ 143 | 144 | li = Tag(SOUP, "li") 145 | li.append(NavigableString(name)) 146 | 147 | if children: 148 | ul = Tag(SOUP, "ul") 149 | for k, v in children.items(): 150 | ul.append(get_list_for_key(k, v)) 151 | li.append(ul) 152 | 153 | return li 154 | 155 | 156 | def get_bom_contents(bom_file): 157 | """ 158 | Run lsbom on the provided file and return a nested dict representing 159 | the file structure 160 | """ 161 | 162 | lsbom = subprocess.Popen( 163 | ["/usr/bin/lsbom", bom_file], stdout=subprocess.PIPE 164 | ).communicate()[0] 165 | 166 | file_list = filter(None, 167 | [ l.split("\t")[0].lstrip("./") for l in lsbom.split("\n") ] 168 | ) 169 | file_list.sort(key=str.lower) 170 | 171 | contents = dict() 172 | 173 | for f in file_list: 174 | contents = merge_list(contents, f.split('/')) 175 | 176 | return contents 177 | 178 | 179 | def merge_list(master_dict, parts): 180 | """Given a dict and a list of elements, recursively create sub-dicts to represent each "row" """ 181 | if parts: 182 | head = parts.pop(0) 183 | master_dict[head] = merge_list(master_dict.setdefault(head, dict()), parts) 184 | 185 | return master_dict 186 | 187 | def anchor_for_name(*args): 188 | file_name = join(*args) 189 | digest = hashlib.md5(file_name).hexdigest() 190 | return Tag(SOUP, "a", [("name", digest)]) 191 | 192 | def generate_package_report(pkg): 193 | """Given an expanded package, create an HTML listing of the contents""" 194 | 195 | SOUP.find('div', {'id': 'description'}).contents = get_description(pkg) 196 | 197 | load_scripts(pkg) 198 | 199 | if exists(pkg + "/Bom"): 200 | get_file_list(pkg, "") 201 | 202 | for f in os.listdir(pkg): 203 | if splitext(f)[1] == '.pkg': 204 | get_file_list(pkg, f) 205 | 206 | 207 | def main(pkg_file_name, html_file_name): 208 | global SOUP 209 | 210 | print "Generating %s from %s" % (html_file_name, pkg_file_name) 211 | 212 | pkg = expand_pkg(pkg_file_name) 213 | SOUP = BeautifulSoup(open("wtfUpdate.html").read()) 214 | 215 | SOUP.find('title').contents = [ 216 | NavigableString("wtfUpdate: %s" % basename(pkg_file_name)) 217 | ] 218 | 219 | try: 220 | generate_package_report(pkg) 221 | html_file = open(html_file_name, 'w') 222 | html_file.write(str(SOUP)) 223 | html_file.close() 224 | except RuntimeError, exc: 225 | print >> sys.stderr, "ERROR: %s" % exc 226 | sys.exit(1) 227 | finally: 228 | shutil.rmtree(pkg) 229 | 230 | if __name__ == "__main__": 231 | if len(sys.argv) < 2: 232 | print >> sys.stderr, 'Usage: %s file.pkg [output_file.html]' % sys.argv[0] 233 | sys.exit(1) 234 | 235 | if len(sys.argv) < 3: 236 | sys.argv.append("%s.html" % splitext(basename(sys.argv[1]))[0]) 237 | 238 | main(*sys.argv[1:3]) 239 | -------------------------------------------------------------------------------- /pymacds-dist/pymacds/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """pymacds - various directoryservice related functions.""" 19 | 20 | __author__ = 'Nigel Kersten (nigelk@google.com)' 21 | __version__ = '0.2' 22 | 23 | 24 | import filecmp 25 | import os 26 | import shutil 27 | import subprocess 28 | import syslog 29 | from Foundation import NSString 30 | import plistlib 31 | 32 | 33 | _DSCL = '/usr/bin/dscl' 34 | _DSCACHEUTIL = '/usr/bin/dscacheutil' 35 | _DSEDITGROUP = '/usr/sbin/dseditgroup' 36 | 37 | 38 | class DSException(Exception): 39 | """Module specific error class.""" 40 | pass 41 | 42 | 43 | def RunProcess(cmd, stdinput=None, env=None, cwd=None, sudo=False, 44 | sudo_password=None): 45 | """Executes cmd using suprocess. 46 | 47 | Args: 48 | cmd: An array of strings as the command to run 49 | stdinput: An optional sting as stdin 50 | env: An optional dictionary as the environment 51 | cwd: An optional string as the current working directory 52 | sudo: An optional boolean on whether to do the command via sudo 53 | sudo_password: An optional string of the password to use for sudo 54 | Returns: 55 | A tuple of two strings and an integer: (stdout, stderr, returncode). 56 | Raises: 57 | DSException: if both stdinput and sudo_password are specified 58 | """ 59 | if sudo: 60 | sudo_cmd = ['sudo'] 61 | if sudo_password and not stdinput: 62 | # Set sudo to get password from stdin 63 | sudo_cmd = sudo_cmd + ['-S'] 64 | stdinput = sudo_password + '\n' 65 | elif sudo_password and stdinput: 66 | raise DSException('stdinput and sudo_password ' 67 | 'are mutually exclusive') 68 | else: 69 | sudo_cmd = sudo_cmd + ['-p', 70 | "%u's password is required for admin access: "] 71 | cmd = sudo_cmd + cmd 72 | environment = os.environ 73 | environment.update(env) 74 | task = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 75 | stdin=subprocess.PIPE, env=environment, cwd=cwd) 76 | (stdout, stderr) = task.communicate(input=stdinput) 77 | return (stdout, stderr, task.returncode) 78 | 79 | 80 | def FlushCache(): 81 | """Flushes the DirectoryService cache.""" 82 | command = [_DSCACHEUTIL, '-flushcache'] 83 | RunProcess(command) 84 | 85 | 86 | def _GetCSPSearchPathForPath(path): 87 | """Returns list of search nodes for a given path. 88 | 89 | Args: 90 | path: One of '/Search' or '/Search/Contacts' only. 91 | Returns: 92 | nodes: list of search nodes for given path. 93 | Raises: 94 | DSException: Unable to retrieve search nodes in path. 95 | """ 96 | 97 | command = [_DSCL, '-plist', path, '-read', '/', 'CSPSearchPath'] 98 | (stdout, stderr, unused_returncode) = RunProcess(command) 99 | result = plistlib.readPlistFromString(stdout) 100 | if 'dsAttrTypeStandard:CSPSearchPath' in result: 101 | search_nodes = result['dsAttrTypeStandard:CSPSearchPath'] 102 | return search_nodes 103 | else: 104 | raise DSException('Unable to retrieve search nodes: %s' % stderr) 105 | 106 | 107 | def _ModifyCSPSearchPathForPath(action, node, path): 108 | """Modifies the search nodes for a given path. 109 | 110 | Args: 111 | action: one of (["append", "delete"]) only. 112 | node: the node to append or delete. 113 | path: the DS path to modify. 114 | Returns: 115 | True on success 116 | Raises: 117 | DSException: Could not modify nodes for path. 118 | """ 119 | 120 | command = [_DSCL, path, '-%s' % action, '/', 'CSPSearchPath', node] 121 | (unused_stdout, stderr, returncode) = RunProcess(command) 122 | if returncode: 123 | raise DSException('Unable to perform %s on CSPSearchPath ' 124 | 'for node: %s on path: %s ' 125 | 'Error: %s '% (action, node, path, stderr)) 126 | FlushCache() 127 | return True 128 | 129 | 130 | def GetSearchNodes(): 131 | """Returns search nodes for DS /Search path.""" 132 | return _GetCSPSearchPathForPath('/Search') 133 | 134 | 135 | def GetContactsNodes(): 136 | """Returns search nodes for DS /Search/Contacts path.""" 137 | return _GetCSPSearchPathForPath('/Search/Contacts') 138 | 139 | 140 | def AddNodeToSearchPath(node): 141 | """Adds a given DS node to the /Search path.""" 142 | _ModifyCSPSearchPathForPath('append', node, '/Search') 143 | 144 | 145 | def AddNodeToContactsPath(node): 146 | """Adds a given DS node to the /Search/Contacts path.""" 147 | _ModifyCSPSearchPathForPath('append', node, '/Search/Contacts') 148 | 149 | 150 | def DeleteNodeFromSearchPath(node): 151 | """Deletes a given DS node from the /Search path.""" 152 | _ModifyCSPSearchPathForPath('delete', node, '/Search') 153 | 154 | 155 | def DeleteNodeFromContactsPath(node): 156 | """Deletes a given DS node from the /Search/Contacts path.""" 157 | _ModifyCSPSearchPathForPath('delete', node, '/Search/Contacts') 158 | 159 | 160 | def EnsureSearchNodePresent(node): 161 | """Ensures a given DS node is present in the /Search path.""" 162 | if node not in GetSearchNodes(): 163 | AddNodeToSearchPath(node) 164 | 165 | 166 | def EnsureSearchNodeAbsent(node): 167 | """Ensures a given DS node is absent from the /Search path.""" 168 | if node in GetSearchNodes(): 169 | DeleteNodeFromSearchPath(node) 170 | 171 | 172 | def EnsureContactsNodePresent(node): 173 | """Ensures a given DS node is present in the /Search/Contacts path.""" 174 | if node not in GetContactsNodes(): 175 | AddNodeToContactsPath(node) 176 | 177 | 178 | def EnsureContactsNodeAbsent(node): 179 | """Ensures a given DS node is absent from the /Search path.""" 180 | if node in GetContactsNodes(): 181 | DeleteNodeFromContactsPath(node) 182 | 183 | 184 | def DSQuery(dstype, objectname, attribute=None): 185 | """DirectoryServices query. 186 | 187 | Args: 188 | dstype: The type of objects to query. user, group. 189 | objectname: the object to query. 190 | attribute: the optional attribute to query. 191 | Returns: 192 | If an attribute is specified, the value of the attribute. Otherwise, the 193 | entire plist. 194 | Raises: 195 | DSException: Cannot query DirectoryServices. 196 | """ 197 | ds_path = '/%ss/%s' % (dstype.capitalize(), objectname) 198 | cmd = [_DSCL, '-plist', '.', '-read', ds_path] 199 | if attribute: 200 | cmd.append(attribute) 201 | (stdout, stderr, returncode) = RunProcess(cmd) 202 | if returncode: 203 | raise DSException('Cannot query %s for %s: %s' % (ds_path, 204 | attribute, 205 | stderr)) 206 | plist = NSString.stringWithString_(stdout).propertyList() 207 | if attribute: 208 | value = None 209 | if 'dsAttrTypeStandard:%s' % attribute in plist: 210 | value = plist['dsAttrTypeStandard:%s' % attribute] 211 | elif attribute in plist: 212 | value = plist[attribute] 213 | try: 214 | # We're copying to a new list to convert from NSCFArray 215 | return value[:] 216 | except TypeError: 217 | # ... unless we can't 218 | return value 219 | else: 220 | return plist 221 | 222 | 223 | def DSSet(dstype, objectname, attribute=None, value=None): 224 | """DirectoryServices attribute set. 225 | 226 | This uses dscl create, wmich overwrites any existing objects or attributes. 227 | 228 | Args: 229 | dstype: The type of objects to query. user, group. 230 | objectname: the object to set. 231 | attribute: the optional attribute to set. 232 | value: the optional value to set, only handles strings and simple lists 233 | Raises: 234 | DSException: Cannot modify DirectoryServices. 235 | """ 236 | ds_path = '/%ss/%s' % (dstype.capitalize(), objectname) 237 | cmd = [_DSCL, '.', '-create', ds_path] 238 | if attribute: 239 | cmd.append(attribute) 240 | if value: 241 | if type(value) == type(list()): 242 | cmd.extend(value) 243 | else: 244 | cmd.append(value) 245 | (unused_stdout, stderr, returncode) = RunProcess(cmd) 246 | if returncode: 247 | raise DSException('Cannot set %s for %s: %s' % (attribute, 248 | ds_path, 249 | stderr)) 250 | 251 | 252 | def DSDelete(dstype, objectname, attribute=None, value=None): 253 | """DirectoryServices attribute delete. 254 | 255 | Args: 256 | dstype: The type of objects to delete. user, group. 257 | objectname: the object to delete. 258 | attribute: the attribute to delete. 259 | value: the value to delete 260 | Raises: 261 | DSException: Cannot modify DirectoryServices. 262 | """ 263 | ds_path = '/%ss/%s' % (dstype.capitalize(), objectname) 264 | cmd = [_DSCL, '.', '-delete', ds_path] 265 | if attribute: 266 | cmd.append(attribute) 267 | if value: 268 | cmd.extend([value]) 269 | (unused_stdout, stderr, returncode) = RunProcess(cmd) 270 | if returncode: 271 | raise DSException('Cannot delete %s for %s: %s' % (attribute, 272 | ds_path, 273 | stderr)) 274 | 275 | 276 | def UserAttribute(username, attribute): 277 | """Returns the requested DirectoryService attribute for this user. 278 | 279 | Args: 280 | username: the user to retrieve a value for. 281 | attribute: the attribute to retrieve. 282 | Returns: 283 | the value of the attribute. 284 | """ 285 | return DSQuery('user', username, attribute) 286 | 287 | 288 | def GroupAttribute(groupname, attribute): 289 | """Returns the requested DirectoryService attribute for this group. 290 | 291 | Args: 292 | groupname: the group to retrieve a value for. 293 | attribute: the attribute to retrieve. 294 | Returns: 295 | the value of the attribute. 296 | """ 297 | return DSQuery('group', groupname, attribute) 298 | 299 | 300 | def AddUserToLocalGroup(username, group): 301 | """Adds user to a local group, uses dseditgroup to deal with GUIDs. 302 | 303 | Args: 304 | username: user to add 305 | group: local group to add user to 306 | Returns: 307 | Nothing 308 | Raises: 309 | DSException: Can't add user to group 310 | """ 311 | cmd = [_DSEDITGROUP, '-o', 'edit', '-n', '.', 312 | '-a', username, '-t', 'user', group] 313 | (stdout, stderr, rc) = RunProcess(cmd) 314 | if rc is not 0: 315 | raise DSException('Error adding %s to group %s, returned %s\n%s' % 316 | (username, group, stdout, stderr)) 317 | 318 | 319 | def RemoveUserFromLocalGroup(username, group): 320 | """Removes user from a local group, uses dseditgroup to deal with GUIDs. 321 | 322 | Args: 323 | username: user to remove 324 | group: local group to remove user from 325 | Returns: 326 | Nothing 327 | Raises: 328 | DSException: Can't remove user from 329 | """ 330 | cmd = [_DSEDITGROUP, '-o', 'edit', '-n', '.', 331 | '-d', username, '-t', 'user', group] 332 | (unused_stdout, stderr, rc) = RunProcess(cmd) 333 | if rc is not 0: 334 | raise DSException('Error removing %s from group %s, returned %s' % 335 | (username, group, stderr)) 336 | -------------------------------------------------------------------------------- /lib/PyMacAdmin/Security/Keychain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | Wrapper for the core Keychain API 5 | 6 | Most of the internals are directly based on the native Keychain API. Apple's developer documentation is highly relevant: 7 | 8 | http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/uid/TP30000898-CH1-SW1 9 | """ 10 | 11 | import os 12 | import ctypes 13 | from PyMacAdmin import Security 14 | 15 | class Keychain(object): 16 | """A friendlier wrapper for the Keychain API""" 17 | # TODO: Add support for SecKeychainSetUserInteractionAllowed 18 | 19 | def __init__(self, keychain_name=None): 20 | self.keychain_handle = self.open_keychain(keychain_name) 21 | 22 | def open_keychain(self, path=None): 23 | """Open a keychain file - if no path is provided, the user's default keychain will be used""" 24 | if not path: 25 | return None 26 | 27 | if path and not os.path.exists(path): 28 | raise IOError("Keychain %s does not exist" % path) 29 | 30 | keychain = ctypes.c_void_p() 31 | keychain_ptr = ctypes.pointer(keychain) 32 | 33 | rc = Security.lib.SecKeychainOpen(path, keychain_ptr) 34 | if rc != 0: 35 | raise RuntimeError("Couldn't open system keychain: rc=%d" % rc) 36 | 37 | return keychain 38 | 39 | def find_generic_password(self, service_name="", account_name=""): 40 | """Pythonic wrapper for SecKeychainFindGenericPassword""" 41 | item_p = ctypes.c_uint32() 42 | password_length = ctypes.c_uint32(0) 43 | password_data = ctypes.c_char_p(256) 44 | 45 | # For our purposes None and "" should be equivalent but we need a real 46 | # string for len() below: 47 | if not service_name: 48 | service_name = "" 49 | if not account_name: 50 | account_name = "" 51 | 52 | rc = Security.lib.SecKeychainFindGenericPassword ( 53 | self.keychain_handle, 54 | len(service_name), # Length of service name 55 | service_name, # Service name 56 | len(account_name), # Account name length 57 | account_name, # Account name 58 | ctypes.byref(password_length), # Will be filled with pw length 59 | ctypes.pointer(password_data), # Will be filled with pw data 60 | ctypes.byref(item_p) 61 | ) 62 | 63 | if rc == -25300: 64 | raise KeyError('No keychain entry for generic password: service=%s, account=%s' % (service_name, account_name)) 65 | elif rc != 0: 66 | raise RuntimeError('Unable to retrieve generic password (service=%s, account=%s): rc=%d' % (service_name, account_name, rc)) 67 | 68 | password = password_data.value[0:password_length.value] 69 | 70 | Security.lib.SecKeychainItemFreeContent(None, password_data) 71 | 72 | # itemRef: A reference to the keychain item from which you wish to 73 | # retrieve data or attributes. 74 | # 75 | # info: A pointer to a list of tags of attributes to retrieve. 76 | # 77 | # itemClass: A pointer to the item’s class. You should pass NULL if not 78 | # required. See “Keychain Item Class Constants” for valid constants. 79 | # 80 | # attrList: On input, the list of attributes in this item to get; on 81 | # output the attributes are filled in. You should call the function 82 | # SecKeychainItemFreeAttributesAndData when you no longer need the 83 | # attributes and data. 84 | # 85 | # length: On return, a pointer to the actual length of the data. 86 | # 87 | # outData: A pointer to a buffer containing the data in this item. Pass 88 | # NULL if not required. You should call the function 89 | # SecKeychainItemFreeAttributesAndData when you no longer need the 90 | # attributes and data. 91 | 92 | info = SecKeychainAttributeInfo() 93 | attrs_p = SecKeychainAttributeList_p() 94 | 95 | # Thank you Wil Shipley: 96 | # http://www.wilshipley.com/blog/2006/10/pimp-my-code-part-12-frozen-in.html 97 | info.count = 1 98 | info.tag.contents = Security.kSecLabelItemAttr 99 | 100 | Security.lib.SecKeychainItemCopyAttributesAndData(item_p, ctypes.pointer(info), None, ctypes.byref(attrs_p), None, None) 101 | attrs = attrs_p.contents 102 | assert(attrs.count >= 1) 103 | 104 | label = attrs.attr[0].data[:attrs.attr[0].length] 105 | 106 | Security.lib.SecKeychainItemFreeAttributesAndData(attrs_p) 107 | 108 | return GenericPassword(service_name=service_name, account_name=account_name, password=password, keychain_item=item_p, label=label) 109 | 110 | def find_internet_password(self, account_name="", password="", server_name="", security_domain="", path="", port=0, protocol_type=None, authentication_type=None): 111 | """Pythonic wrapper for SecKeychainFindInternetPassword""" 112 | item = ctypes.c_void_p() 113 | password_length = ctypes.c_uint32(0) 114 | password_data = ctypes.c_char_p(256) 115 | 116 | if protocol_type and len(protocol_type) != 4: 117 | raise TypeError("protocol_type must be a valid FourCharCode - see http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/c_ref/SecProtocolType") 118 | 119 | if authentication_type and len(authentication_type) != 4: 120 | raise TypeError("authentication_type must be a valid FourCharCode - see http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/c_ref/SecAuthenticationType") 121 | 122 | if not isinstance(port, int): 123 | port = int(port) 124 | 125 | rc = Security.lib.SecKeychainFindInternetPassword( 126 | self.keychain_handle, 127 | len(server_name), 128 | server_name, 129 | len(security_domain) if security_domain else 0, 130 | security_domain, 131 | len(account_name), 132 | account_name, 133 | len(path), 134 | path, 135 | port, 136 | protocol_type, 137 | authentication_type, 138 | ctypes.byref(password_length), # Will be filled with pw length 139 | ctypes.pointer(password_data), # Will be filled with pw data 140 | ctypes.pointer(item) 141 | ) 142 | 143 | if rc == -25300: 144 | raise KeyError('No keychain entry for internet password: server=%s, account=%s' % (server_name, account_name)) 145 | elif rc != 0: 146 | raise RuntimeError('Unable to retrieve internet password (server=%s, account=%s): rc=%d' % (server_name, account_name, rc)) 147 | 148 | password = password_data.value[0:password_length.value] 149 | 150 | Security.lib.SecKeychainItemFreeContent(None, password_data) 151 | 152 | return InternetPassword(server_name=server_name, account_name=account_name, password=password, keychain_item=item, security_domain=security_domain, path=path, port=port, protocol_type=protocol_type, authentication_type=authentication_type) 153 | 154 | def add(self, item): 155 | """Add the provided GenericPassword or InternetPassword object to this Keychain""" 156 | assert(isinstance(item, GenericPassword)) 157 | 158 | item_ref = ctypes.c_void_p() 159 | 160 | if isinstance(item, InternetPassword): 161 | rc = Security.lib.SecKeychainAddInternetPassword( 162 | self.keychain_handle, 163 | len(item.server_name), 164 | item.server_name, 165 | len(item.security_domain), 166 | item.security_domain, 167 | len(item.account_name), 168 | item.account_name, 169 | len(item.path), 170 | item.path, 171 | item.port, 172 | item.protocol_type, 173 | item.authentication_type, 174 | len(item.password), 175 | item.password, 176 | ctypes.pointer(item_ref) 177 | ) 178 | else: 179 | rc = Security.lib.SecKeychainAddGenericPassword( 180 | self.keychain_handle, 181 | len(item.service_name), 182 | item.service_name, 183 | len(item.account_name), 184 | item.account_name, 185 | len(item.password), 186 | item.password, 187 | ctypes.pointer(item_ref) 188 | ) 189 | 190 | if rc != 0: 191 | raise RuntimeError("Error adding %s: rc=%d" % (item, rc)) 192 | 193 | item.keychain_item = item_ref 194 | 195 | def remove(self, item): 196 | """Remove the provided keychain item as the reverse of Keychain.add()""" 197 | assert(isinstance(item, GenericPassword)) 198 | item.delete() 199 | 200 | 201 | class GenericPassword(object): 202 | """Generic keychain password used with SecKeychainAddGenericPassword and SecKeychainFindGenericPassword""" 203 | # TODO: Add support for access control and attributes 204 | 205 | account_name = None 206 | service_name = None 207 | label = None 208 | password = None 209 | keychain_item = None # An SecKeychainItemRef treated as an opaque object 210 | 211 | def __init__(self, **kwargs): 212 | super(GenericPassword, self).__init__() 213 | for k, v in kwargs.items(): 214 | if not hasattr(self, k): 215 | raise AttributeError("Unknown property %s" % k) 216 | setattr(self, k, v) 217 | 218 | def update_password(self, new_password): 219 | """Change the stored password""" 220 | 221 | rc = Security.lib.SecKeychainItemModifyAttributesAndData( 222 | self.keychain_item, 223 | None, 224 | len(new_password), 225 | new_password 226 | ) 227 | 228 | if rc == -61: 229 | raise RuntimeError("Permission denied updating %s" % self) 230 | elif rc != 0: 231 | raise RuntimeError("Unable to update password for %s: rc = %d" % rc) 232 | 233 | def delete(self): 234 | """Removes this item from the keychain""" 235 | rc = Security.lib.SecKeychainItemDelete(self.keychain_item) 236 | if rc != 0: 237 | raise RuntimeError("Unable to delete %s: rc=%d" % (self, rc)) 238 | 239 | from CoreFoundation import CFRelease 240 | CFRelease(self.keychain_item) 241 | 242 | self.keychain_item = None 243 | self.service_name = None 244 | self.account_name = None 245 | self.password = None 246 | 247 | def __str__(self): 248 | return repr(self) 249 | 250 | def __repr__(self): 251 | props = [] 252 | for k in ['service_name', 'account_name', 'label']: 253 | props.append("%s=%s" % (k, repr(getattr(self, k)))) 254 | 255 | return "%s(%s)" % (self.__class__.__name__, ", ".join(props)) 256 | 257 | 258 | class InternetPassword(GenericPassword): 259 | """Specialized keychain item for internet passwords used with SecKeychainAddInternetPassword and SecKeychainFindInternetPassword""" 260 | account_name = "" 261 | password = None 262 | keychain_item = None 263 | server_name = "" 264 | security_domain = "" 265 | path = "" 266 | port = 0 267 | protocol_type = None 268 | authentication_type = None 269 | 270 | def __init__(self, **kwargs): 271 | super(InternetPassword, self).__init__(**kwargs) 272 | 273 | def __repr__(self): 274 | props = [] 275 | for k in ['account_name', 'server_name', 'security_domain', 'path', 'port', 'protocol_type', 'authentication_type']: 276 | if getattr(self, k): 277 | props.append("%s=%s" % (k, repr(getattr(self, k)))) 278 | 279 | return "%s(%s)" % (self.__class__.__name__, ", ".join(props)) 280 | 281 | class SecKeychainAttribute(ctypes.Structure): 282 | """Contains keychain attributes 283 | 284 | tag: A 4-byte attribute tag. 285 | length: The length of the buffer pointed to by data. 286 | data: A pointer to the attribute data. 287 | """ 288 | _fields_ = [ 289 | ('tag', ctypes.c_uint32), 290 | ('length', ctypes.c_uint32), 291 | ('data', ctypes.c_char_p) 292 | ] 293 | 294 | class SecKeychainAttributeList(ctypes.Structure): 295 | """Represents a list of keychain attributes 296 | 297 | count: An unsigned 32-bit integer that represents the number of keychain attributes in the array. 298 | attr: A pointer to the first keychain attribute in the array. 299 | """ 300 | 301 | # TODO: Standard iterator support for SecKeychainAttributeList: 302 | # 303 | # for offset in range(0, attrs.count): 304 | # print "[%d]: %s: %s" % (offset, attrs.attr[offset].tag, attrs.attr[offset].data[:attrs.attr[offset].length]) 305 | # 306 | # becomes: 307 | # 308 | # for tag, data in attrs: 309 | # … 310 | # 311 | # attrs[tag] should also work 312 | # 313 | 314 | _fields_ = [ 315 | ('count', ctypes.c_uint), 316 | ('attr', ctypes.POINTER(SecKeychainAttribute)) 317 | ] 318 | 319 | class SecKeychainAttributeInfo(ctypes.Structure): 320 | """Represents a keychain attribute as a pair of tag and format values. 321 | 322 | count: The number of tag-format pairs in the respective arrays 323 | tag: A pointer to the first attribute tag in the array 324 | format: A pointer to the first CSSM_DB_ATTRIBUTE_FORMAT in the array 325 | """ 326 | # TODO: SecKeychainAttributeInfo should allow .append(tag, [data]) 327 | _fields_ = [ 328 | ('count', ctypes.c_uint), 329 | ('tag', ctypes.POINTER(ctypes.c_uint)), 330 | ('format', ctypes.POINTER(ctypes.c_uint)) 331 | ] 332 | 333 | # The APIs expect pointers to SecKeychainAttributeInfo objects: 334 | SecKeychainAttributeInfo_p = ctypes.POINTER(SecKeychainAttributeInfo) 335 | SecKeychainAttributeList_p = ctypes.POINTER(SecKeychainAttributeList) 336 | -------------------------------------------------------------------------------- /bin/crankd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | # encoding: utf-8 3 | 4 | """ 5 | Usage: %prog 6 | 7 | Monitor system event notifications 8 | 9 | Configuration: 10 | 11 | The configuration file is divided into sections for each class of 12 | events. Each section is a dictionary using the event condition as the 13 | key ("NSWorkspaceDidWakeNotification", "State:/Network/Global/IPv4", 14 | etc). Each event must have one of the following properties: 15 | 16 | command: a shell command 17 | function: the name of a python function 18 | class: the name of a python class which will be instantiated once 19 | and have methods called as events occur. 20 | method: (class, method) tuple 21 | """ 22 | 23 | from Cocoa import \ 24 | CFAbsoluteTimeGetCurrent, \ 25 | CFRunLoopAddSource, \ 26 | CFRunLoopAddTimer, \ 27 | CFRunLoopTimerCreate, \ 28 | NSObject, \ 29 | NSRunLoop, \ 30 | NSWorkspace, \ 31 | kCFRunLoopCommonModes 32 | 33 | from SystemConfiguration import \ 34 | SCDynamicStoreCopyKeyList, \ 35 | SCDynamicStoreCreate, \ 36 | SCDynamicStoreCreateRunLoopSource, \ 37 | SCDynamicStoreSetNotificationKeys 38 | 39 | from FSEvents import \ 40 | FSEventStreamCreate, \ 41 | FSEventStreamStart, \ 42 | FSEventStreamScheduleWithRunLoop, \ 43 | kFSEventStreamEventIdSinceNow, \ 44 | kCFRunLoopDefaultMode, \ 45 | kFSEventStreamEventFlagMustScanSubDirs, \ 46 | kFSEventStreamEventFlagUserDropped, \ 47 | kFSEventStreamEventFlagKernelDropped 48 | 49 | import os 50 | import os.path 51 | import logging 52 | import logging.handlers 53 | import sys 54 | import re 55 | from subprocess import call 56 | from optparse import OptionParser 57 | from plistlib import readPlist, writePlist 58 | from PyObjCTools import AppHelper 59 | from functools import partial 60 | import signal 61 | from datetime import datetime 62 | 63 | 64 | VERSION = '$Revision: #4 $' 65 | 66 | HANDLER_OBJECTS = dict() # Events which have a "class" handler use an instantiated object; we want to load only one copy 67 | SC_HANDLERS = dict() # Callbacks indexed by SystemConfiguration keys 68 | FS_WATCHED_FILES = dict() # Callbacks indexed by filesystem path 69 | WORKSPACE_HANDLERS = dict() # handlers for workspace events 70 | 71 | 72 | class BaseHandler(object): 73 | # pylint: disable-msg=C0111,R0903 74 | pass 75 | 76 | class NotificationHandler(NSObject): 77 | """Simple base class for handling NSNotification events""" 78 | # Method names and class structure are dictated by Cocoa & PyObjC, which 79 | # is substantially different from PEP-8: 80 | # pylint: disable-msg=C0103,W0232,R0903 81 | 82 | def init(self): 83 | """NSObject-compatible initializer""" 84 | self = super(NotificationHandler, self).init() 85 | if self is None: return None 86 | self.callable = self.not_implemented 87 | return self # NOTE: Unlike Python, NSObject's init() must return self! 88 | 89 | def not_implemented(self, *args, **kwargs): 90 | """A dummy function which exists only to catch configuration errors""" 91 | # TODO: Is there a better way to report the caller's location? 92 | import inspect 93 | stack = inspect.stack() 94 | my_name = stack[0][3] 95 | caller = stack[1][3] 96 | raise NotImplementedError( 97 | "%s should have been overridden. Called by %s as: %s(%s)" % ( 98 | my_name, 99 | caller, 100 | my_name, 101 | ", ".join(map(repr, args) + [ "%s=%s" % (k, repr(v)) for k,v in kwargs.items() ]) 102 | ) 103 | ) 104 | 105 | def onNotification_(self, the_notification): 106 | """Pass an NSNotifications to our handler""" 107 | if the_notification.userInfo: 108 | user_info = the_notification.userInfo() 109 | else: 110 | user_info = None 111 | self.callable(user_info=user_info) # pylint: disable-msg=E1101 112 | 113 | 114 | def log_list(msg, items, level=logging.INFO): 115 | """ 116 | Record a a list of values with a message 117 | 118 | This would ordinarily be a simple logging call but we want to keep the 119 | length below the 1024-byte syslog() limitation and we'll format things 120 | nicely by repeating our message with as many of the values as will fit. 121 | 122 | Individual items longer than the maximum length will be truncated. 123 | """ 124 | 125 | max_len = 1024 - len(msg % "") 126 | cur_len = 0 127 | cur_items = list() 128 | 129 | while [ i[:max_len] for i in items]: 130 | i = items.pop() 131 | if cur_len + len(i) + 2 > max_len: 132 | logging.info(msg % ", ".join(cur_items)) 133 | cur_len = 0 134 | cur_items = list() 135 | 136 | cur_items.append(i) 137 | cur_len += len(i) + 2 138 | 139 | logging.log(level, msg % ", ".join(cur_items)) 140 | 141 | def get_callable_for_event(name, event_config, context=None): 142 | """ 143 | Returns a callable object which can be used as a callback for any 144 | event. The returned function has context information, logging, etc. 145 | included so they do not need to be passed when the actual event 146 | occurs. 147 | 148 | NOTE: This function does not process "class" handlers - by design they 149 | are passed to the system libraries which expect a delegate object with 150 | various event handling methods 151 | """ 152 | 153 | kwargs = { 154 | 'context': context, 155 | 'key': name, 156 | 'config': event_config, 157 | } 158 | 159 | if "command" in event_config: 160 | f = partial(do_shell, event_config["command"], **kwargs) 161 | elif "function" in event_config: 162 | f = partial(get_callable_from_string(event_config["function"]), **kwargs) 163 | elif "method" in event_config: 164 | f = partial(getattr(get_handler_object(event_config['method'][0]), event_config['method'][1]), **kwargs) 165 | else: 166 | raise AttributeError("%s have a class, method, function or command" % name) 167 | 168 | return f 169 | 170 | 171 | def get_mod_func(callback): 172 | """Convert a fully-qualified module.function name to (module, function) - stolen from Django""" 173 | try: 174 | dot = callback.rindex('.') 175 | except ValueError: 176 | return (callback, '') 177 | return (callback[:dot], callback[dot+1:]) 178 | 179 | 180 | def get_callable_from_string(f_name): 181 | """Takes a string containing a function name (optionally module qualified) and returns a callable object""" 182 | try: 183 | mod_name, func_name = get_mod_func(f_name) 184 | if mod_name == "" and func_name == "": 185 | raise AttributeError("%s couldn't be converted to a module or function name" % f_name) 186 | 187 | module = __import__(mod_name) 188 | 189 | if func_name == "": 190 | func_name = mod_name # The common case is an eponymous class 191 | 192 | return getattr(module, func_name) 193 | 194 | except (ImportError, AttributeError), exc: 195 | raise RuntimeError("Unable to create a callable object for '%s': %s" % (f_name, exc)) 196 | 197 | 198 | def get_handler_object(class_name): 199 | """Return a single instance of the given class name, instantiating it if necessary""" 200 | 201 | if class_name not in HANDLER_OBJECTS: 202 | h_obj = get_callable_from_string(class_name)() 203 | if isinstance(h_obj, BaseHandler): 204 | pass # TODO: Do we even need BaseHandler any more? 205 | HANDLER_OBJECTS[class_name] = h_obj 206 | 207 | return HANDLER_OBJECTS[class_name] 208 | 209 | 210 | def handle_sc_event(store, changed_keys, info): 211 | """Fire every event handler for one or more events""" 212 | 213 | for key in changed_keys: 214 | SC_HANDLERS[key](key=key, info=info) 215 | 216 | 217 | def list_events(option, opt_str, value, parser): 218 | """Displays the list of events which can be monitored on the current system""" 219 | 220 | print 'On this system SystemConfiguration supports these events:' 221 | for event in sorted(SCDynamicStoreCopyKeyList(get_sc_store(), '.*')): 222 | print "\t", event 223 | 224 | print 225 | print "Standard NSWorkspace Notification messages:\n\t", 226 | print "\n\t".join(''' 227 | NSWorkspaceDidLaunchApplicationNotification 228 | NSWorkspaceDidMountNotification 229 | NSWorkspaceDidPerformFileOperationNotification 230 | NSWorkspaceDidTerminateApplicationNotification 231 | NSWorkspaceDidUnmountNotification 232 | NSWorkspaceDidWakeNotification 233 | NSWorkspaceSessionDidBecomeActiveNotification 234 | NSWorkspaceSessionDidResignActiveNotification 235 | NSWorkspaceWillLaunchApplicationNotification 236 | NSWorkspaceWillPowerOffNotification 237 | NSWorkspaceWillSleepNotification 238 | NSWorkspaceWillUnmountNotification 239 | '''.split()) 240 | 241 | sys.exit(0) 242 | 243 | 244 | def process_commandline(): 245 | """ 246 | Process command-line options 247 | Load our preference file 248 | Configure the module path to add Application Support directories 249 | """ 250 | parser = OptionParser(__doc__.strip()) 251 | support_path = '/Library/' if os.getuid() == 0 else os.path.expanduser('~/Library/') 252 | preference_file = os.path.join(support_path, 'Preferences', 'com.googlecode.pymacadmin.crankd.plist') 253 | module_path = os.path.join(support_path, 'Application Support/crankd') 254 | 255 | if os.path.exists(module_path): 256 | sys.path.append(module_path) 257 | else: 258 | print >> sys.stderr, "Module directory %s does not exist: Python handlers will need to use absolute pathnames" % module_path 259 | 260 | parser.add_option("-f", "--config", dest="config_file", help='Use an alternate config file instead of %default', default=preference_file) 261 | parser.add_option("-l", "--list-events", action="callback", callback=list_events, help="List the events which can be monitored") 262 | parser.add_option("-d", "--debug", action="count", default=False, help="Log detailed progress information") 263 | (options, args) = parser.parse_args() 264 | 265 | if len(args): 266 | parser.error("Unknown command-line arguments: %s" % args) 267 | 268 | options.support_path = support_path 269 | options.config_file = os.path.realpath(options.config_file) 270 | 271 | # This is somewhat messy but we want to alter the command-line to use full 272 | # file paths in case someone's code changes the current directory or the 273 | sys.argv = [ os.path.realpath(sys.argv[0]), ] 274 | 275 | if options.debug: 276 | logging.getLogger().setLevel(logging.DEBUG) 277 | sys.argv.append("--debug") 278 | 279 | if options.config_file: 280 | sys.argv.append("--config") 281 | sys.argv.append(options.config_file) 282 | 283 | return options 284 | 285 | 286 | def load_config(options): 287 | """Load our configuration from plist or create a default file if none exists""" 288 | if not os.path.exists(options.config_file): 289 | logging.info("%s does not exist - initializing with an example configuration" % CRANKD_OPTIONS.config_file) 290 | print >>sys.stderr, 'Creating %s with default options for you to customize' % options.config_file 291 | print >>sys.stderr, '%s --list-events will list the events you can monitor on this system' % sys.argv[0] 292 | example_config = { 293 | 'SystemConfiguration': { 294 | 'State:/Network/Global/IPv4': { 295 | 'command': '/bin/echo "Global IPv4 config changed"' 296 | } 297 | }, 298 | 'NSWorkspace': { 299 | 'NSWorkspaceDidMountNotification': { 300 | 'command': '/bin/echo "A new volume was mounted!"' 301 | }, 302 | 'NSWorkspaceDidWakeNotification': { 303 | 'command': '/bin/echo "The system woke from sleep!"' 304 | }, 305 | 'NSWorkspaceWillSleepNotification': { 306 | 'command': '/bin/echo "The system is about to go to sleep!"' 307 | } 308 | } 309 | } 310 | writePlist(example_config, options.config_file) 311 | sys.exit(1) 312 | 313 | logging.info("Loading configuration from %s" % CRANKD_OPTIONS.config_file) 314 | 315 | plist = readPlist(options.config_file) 316 | 317 | if "imports" in plist: 318 | for module in plist['imports']: 319 | try: 320 | __import__(module) 321 | except ImportError, exc: 322 | print >> sys.stderr, "Unable to import %s: %s" % (module, exc) 323 | sys.exit(1) 324 | return plist 325 | 326 | 327 | def configure_logging(): 328 | """Configures the logging module""" 329 | logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') 330 | 331 | # Enable logging to syslog as well: 332 | # Normally this would not be necessary but logging assumes syslog listens on 333 | # localhost syslog/udp, which is disabled on 10.5 (rdar://5871746) 334 | syslog = logging.handlers.SysLogHandler('/var/run/syslog') 335 | syslog.setFormatter(logging.Formatter('%(name)s: %(message)s')) 336 | syslog.setLevel(logging.INFO) 337 | logging.getLogger().addHandler(syslog) 338 | 339 | 340 | def get_sc_store(): 341 | """Returns an SCDynamicStore instance""" 342 | return SCDynamicStoreCreate(None, "crankd", handle_sc_event, None) 343 | 344 | 345 | def add_workspace_notifications(nsw_config): 346 | # See http://developer.apple.com/documentation/Cocoa/Conceptual/Workspace/Workspace.html 347 | notification_center = NSWorkspace.sharedWorkspace().notificationCenter() 348 | 349 | for event in nsw_config: 350 | event_config = nsw_config[event] 351 | 352 | if "class" in event_config: 353 | obj = get_handler_object(event_config['class']) 354 | objc_method = "on%s:" % event 355 | py_method = objc_method.replace(":", "_") 356 | if not hasattr(obj, py_method) or not callable(getattr(obj, py_method)): 357 | print >> sys.stderr, \ 358 | "NSWorkspace Notification %s: handler class %s must define a %s method" % (event, event_config['class'], py_method) 359 | sys.exit(1) 360 | 361 | notification_center.addObserver_selector_name_object_(obj, objc_method, event, None) 362 | else: 363 | handler = NotificationHandler.new() 364 | handler.name = "NSWorkspace Notification %s" % event 365 | handler.callable = get_callable_for_event(event, event_config, context=handler.name) 366 | 367 | assert(callable(handler.onNotification_)) 368 | 369 | notification_center.addObserver_selector_name_object_(handler, "onNotification:", event, None) 370 | WORKSPACE_HANDLERS[event] = handler 371 | 372 | log_list("Listening for these NSWorkspace notifications: %s", nsw_config.keys()) 373 | 374 | 375 | def add_sc_notifications(sc_config): 376 | """ 377 | This uses the SystemConfiguration framework to get a SCDynamicStore session 378 | and register for certain events. See the Apple SystemConfiguration 379 | documentation for details: 380 | 381 | 382 | 383 | TN1145 may also be of interest: 384 | 385 | 386 | Inspired by the PyObjC SystemConfiguration callback demos: 387 | 388 | """ 389 | 390 | keys = sc_config.keys() 391 | 392 | try: 393 | for key in keys: 394 | SC_HANDLERS[key] = get_callable_for_event(key, sc_config[key], context="SystemConfiguration: %s" % key) 395 | except AttributeError, exc: 396 | print >> sys.stderr, "Error configuring SystemConfiguration events: %s" % exc 397 | sys.exit(1) 398 | 399 | store = get_sc_store() 400 | 401 | SCDynamicStoreSetNotificationKeys(store, None, keys) 402 | 403 | # Get a CFRunLoopSource for our store session and add it to the application's runloop: 404 | CFRunLoopAddSource( 405 | NSRunLoop.currentRunLoop().getCFRunLoop(), 406 | SCDynamicStoreCreateRunLoopSource(None, store, 0), 407 | kCFRunLoopCommonModes 408 | ) 409 | 410 | log_list("Listening for these SystemConfiguration events: %s", keys) 411 | 412 | 413 | def add_fs_notifications(fs_config): 414 | for path in fs_config: 415 | add_fs_notification(path, get_callable_for_event(path, fs_config[path], context="FSEvent: %s" % path)) 416 | 417 | 418 | def add_fs_notification(f_path, callback): 419 | """Adds an FSEvent notification for the specified path""" 420 | path = os.path.realpath(os.path.expanduser(f_path)) 421 | if not os.path.exists(path): 422 | raise AttributeError("Cannot add an FSEvent notification: %s does not exist!" % path) 423 | 424 | if not os.path.isdir(path): 425 | path = os.path.dirname(path) 426 | 427 | try: 428 | FS_WATCHED_FILES[path].append(callback) 429 | except KeyError: 430 | FS_WATCHED_FILES[path] = [callback] 431 | 432 | 433 | def start_fs_events(): 434 | stream_ref = FSEventStreamCreate( 435 | None, # Use the default CFAllocator 436 | fsevent_callback, 437 | None, # We don't need a FSEventStreamContext 438 | FS_WATCHED_FILES.keys(), 439 | kFSEventStreamEventIdSinceNow, # We only want events which happen in the future 440 | 1.0, # Process events within 1 second 441 | 0 # We don't need any special flags for our stream 442 | ) 443 | 444 | if not stream_ref: 445 | raise RuntimeError("FSEventStreamCreate() failed!") 446 | 447 | FSEventStreamScheduleWithRunLoop(stream_ref, NSRunLoop.currentRunLoop().getCFRunLoop(), kCFRunLoopDefaultMode) 448 | 449 | if not FSEventStreamStart(stream_ref): 450 | raise RuntimeError("Unable to start FSEvent stream!") 451 | 452 | logging.debug("FSEventStream started for %d paths: %s" % (len(FS_WATCHED_FILES), ", ".join(FS_WATCHED_FILES))) 453 | 454 | 455 | def fsevent_callback(stream_ref, full_path, event_count, paths, masks, ids): 456 | """Process an FSEvent (consult the Cocoa docs) and call each of our handlers which monitors that path or a parent""" 457 | for i in range(event_count): 458 | path = os.path.dirname(paths[i]) 459 | 460 | if masks[i] & kFSEventStreamEventFlagMustScanSubDirs: 461 | recursive = True 462 | 463 | if masks[i] & kFSEventStreamEventFlagUserDropped: 464 | logging.error("We were too slow processing FSEvents and some events were dropped") 465 | recursive = True 466 | 467 | if masks[i] & kFSEventStreamEventFlagKernelDropped: 468 | logging.error("The kernel was too slow processing FSEvents and some events were dropped!") 469 | recursive = True 470 | else: 471 | recursive = False 472 | 473 | for i in [k for k in FS_WATCHED_FILES if path.startswith(k)]: 474 | logging.debug("FSEvent: %s: processing %d callback(s) for path %s" % (i, len(FS_WATCHED_FILES[i]), path)) 475 | for j in FS_WATCHED_FILES[i]: 476 | j(i, path=path, recursive=recursive) 477 | 478 | 479 | def timer_callback(*args): 480 | """Handles the timer events which we use simply to have the runloop run regularly. Currently this logs a timestamp for debugging purposes""" 481 | logging.debug("timer callback at %s" % datetime.now()) 482 | 483 | 484 | def main(): 485 | configure_logging() 486 | 487 | global CRANKD_OPTIONS, CRANKD_CONFIG 488 | CRANKD_OPTIONS = process_commandline() 489 | CRANKD_CONFIG = load_config(CRANKD_OPTIONS) 490 | 491 | if "NSWorkspace" in CRANKD_CONFIG: 492 | add_workspace_notifications(CRANKD_CONFIG['NSWorkspace']) 493 | 494 | if "SystemConfiguration" in CRANKD_CONFIG: 495 | add_sc_notifications(CRANKD_CONFIG['SystemConfiguration']) 496 | 497 | if "FSEvents" in CRANKD_CONFIG: 498 | add_fs_notifications(CRANKD_CONFIG['FSEvents']) 499 | 500 | # We reuse our FSEvents code to watch for changes to our files and 501 | # restart if any of our libraries have been updated: 502 | add_conditional_restart(CRANKD_OPTIONS.config_file, "Configuration file %s changed" % CRANKD_OPTIONS.config_file) 503 | for m in filter(lambda i: i and hasattr(i, '__file__'), sys.modules.values()): 504 | if m.__name__ == "__main__": 505 | msg = "%s was updated" % m.__file__ 506 | else: 507 | msg = "Module %s was updated" % m.__name__ 508 | 509 | add_conditional_restart(m.__file__, msg) 510 | 511 | signal.signal(signal.SIGHUP, partial(restart, "SIGHUP received")) 512 | 513 | start_fs_events() 514 | 515 | # NOTE: This timer is basically a kludge around the fact that we can't reliably get 516 | # signals or Control-C inside a runloop. This wakes us up often enough to 517 | # appear tolerably responsive: 518 | CFRunLoopAddTimer( 519 | NSRunLoop.currentRunLoop().getCFRunLoop(), 520 | CFRunLoopTimerCreate(None, CFAbsoluteTimeGetCurrent(), 2.0, 0, 0, timer_callback, None), 521 | kCFRunLoopCommonModes 522 | ) 523 | 524 | try: 525 | AppHelper.runConsoleEventLoop(installInterrupt=True) 526 | except KeyboardInterrupt: 527 | logging.info("KeyboardInterrupt received, exiting") 528 | 529 | sys.exit(0) 530 | 531 | def create_env_name(name): 532 | """ 533 | Converts input names into more traditional shell environment name style 534 | 535 | >>> create_env_name("NSApplicationBundleIdentifier") 536 | 'NSAPPLICATION_BUNDLE_IDENTIFIER' 537 | >>> create_env_name("NSApplicationBundleIdentifier-1234$foobar!") 538 | 'NSAPPLICATION_BUNDLE_IDENTIFIER_1234_FOOBAR' 539 | """ 540 | new_name = re.sub(r'''(?<=[a-z])([A-Z])''', '_\\1', name) 541 | new_name = re.sub(r'\W+', '_', new_name) 542 | new_name = re.sub(r'_{2,}', '_', new_name) 543 | return new_name.upper().strip("_") 544 | 545 | def do_shell(command, context=None, **kwargs): 546 | """Executes a shell command with logging""" 547 | logging.info("%s: executing %s" % (context, command)) 548 | 549 | child_env = {'CRANKD_CONTEXT': context} 550 | 551 | # We'll pull a subset of the available information in for shell scripts. 552 | # Anyone who needs more will probably want to write a Python handler 553 | # instead so they can reuse things like our logger & config info and avoid 554 | # ordeals like associative arrays in Bash 555 | for k in [ 'info', 'key' ]: 556 | if k in kwargs and kwargs[k]: 557 | child_env['CRANKD_%s' % k.upper()] = str(kwargs[k]) 558 | 559 | user_info = kwargs.get("user_info") 560 | if user_info: 561 | for k, v in user_info.items(): 562 | child_env[create_env_name(k)] = str(v) 563 | 564 | try: 565 | rc = call(command, shell=True, env=child_env) 566 | if rc == 0: 567 | logging.debug("`%s` returned %d" % (command, rc)) 568 | elif rc < 0: 569 | logging.error("`%s` was terminated by signal %d" % (command, -rc)) 570 | else: 571 | logging.error("`%s` returned %d" % (command, rc)) 572 | except OSError, exc: 573 | logging.error("Got an exception when executing %s:" % (command, exc)) 574 | 575 | 576 | def add_conditional_restart(file_name, reason): 577 | """FSEvents monitors directories, not files. This function uses stat to 578 | restart only if the file's mtime has changed""" 579 | file_name = os.path.realpath(file_name) 580 | while not os.path.exists(file_name): 581 | file_name = os.path.dirname(file_name) 582 | orig_stat = os.stat(file_name).st_mtime 583 | 584 | def cond_restart(*args, **kwargs): 585 | try: 586 | if os.stat(file_name).st_mtime != orig_stat: 587 | restart(reason) 588 | except (OSError, IOError, RuntimeError), exc: 589 | restart("Exception while checking %s: %s" % (file_name, exc)) 590 | 591 | add_fs_notification(file_name, cond_restart) 592 | 593 | 594 | def restart(reason, *args, **kwargs): 595 | """Perform a complete restart of the current process using exec()""" 596 | logging.info("Restarting: %s" % reason) 597 | os.execv(sys.argv[0], sys.argv) 598 | 599 | if __name__ == '__main__': 600 | main() 601 | -------------------------------------------------------------------------------- /bin/usb-notifier.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2003 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 | * © Copyright 2001-2002 Apple Computer, Inc. All rights reserved. 25 | * 26 | * IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. (ÒAppleÓ) in 27 | * consideration of your agreement to the following terms, and your use, installation, 28 | * modification or redistribution of this Apple software constitutes acceptance of these 29 | * terms. If you do not agree with these terms, please do not use, install, modify or 30 | * redistribute this Apple software. 31 | * 32 | * In consideration of your agreement to abide by the following terms, and subject to these 33 | * terms, Apple grants you a personal, non exclusive license, under AppleÕs copyrights in this 34 | * original Apple software (the ÒApple SoftwareÓ), to use, reproduce, modify and redistribute 35 | * the Apple Software, with or without modifications, in source and/or binary forms; provided 36 | * that if you redistribute the Apple Software in its entirety and without modifications, you 37 | * must retain this notice and the following text and disclaimers in all such redistributions 38 | * of the Apple Software. Neither the name, trademarks, service marks or logos of Apple 39 | * Computer, Inc. may be used to endorse or promote products derived from the Apple Software 40 | * without specific prior written permission from Apple. Except as expressly stated in this 41 | * notice, no other rights or licenses, express or implied, are granted by Apple herein, 42 | * including but not limited to any patent rights that may be infringed by your derivative 43 | * works or by other works in which the Apple Software may be incorporated. 44 | * 45 | * The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, 46 | * EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON- 47 | * INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE 48 | * SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 49 | * 50 | * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL 51 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 52 | * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, 53 | * REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND 54 | * WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR 55 | * OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 56 | */ 57 | #include 58 | 59 | #include 60 | 61 | #include 62 | #include 63 | #include 64 | 65 | #include 66 | 67 | #include "hex2c.h" 68 | 69 | extern INTEL_HEX_RECORD bulktest[]; 70 | 71 | //#define USE_ASYNC_IO // Comment this line out if you want to use sync calls for read/write 72 | 73 | #define k8051_USBCS 0x7f92 74 | #define kOurVendorID 1351 75 | #define kOurProductID 8193 76 | #define kOurProductIDBulkTest 4098 77 | #define kTestMessage "Bulk I/O Test" 78 | 79 | // globals 80 | static IONotificationPortRef gNotifyPort; 81 | static io_iterator_t gRawAddedIter; 82 | static io_iterator_t gRawRemovedIter; 83 | static io_iterator_t gBulkTestAddedIter; 84 | static io_iterator_t gBulkTestRemovedIter; 85 | static char gBuffer[64]; 86 | 87 | IOReturn ConfigureAnchorDevice(IOUSBDeviceInterface245 **dev) 88 | { 89 | UInt8 numConf; 90 | IOReturn kr; 91 | IOUSBConfigurationDescriptorPtr confDesc; 92 | 93 | kr = (*dev)->GetNumberOfConfigurations(dev, &numConf); 94 | if (!numConf) 95 | return -1; 96 | 97 | // get the configuration descriptor for index 0 98 | kr = (*dev)->GetConfigurationDescriptorPtr(dev, 0, &confDesc); 99 | if (kr) 100 | { 101 | printf("\tunable to get config descriptor for index %d (err = %08x)\n", 0, kr); 102 | return -1; 103 | } 104 | kr = (*dev)->SetConfiguration(dev, confDesc->bConfigurationValue); 105 | if (kr) 106 | { 107 | printf("\tunable to set configuration to value %d (err=%08x)\n", 0, kr); 108 | return -1; 109 | } 110 | 111 | return kIOReturnSuccess; 112 | } 113 | 114 | IOReturn AnchorWrite(IOUSBDeviceInterface245 **dev, UInt16 anchorAddress, UInt16 count, UInt8 writeBuffer[]) 115 | { 116 | IOUSBDevRequest request; 117 | 118 | request.bmRequestType = USBmakebmRequestType(kUSBOut, kUSBVendor, kUSBDevice); 119 | request.bRequest = 0xa0; 120 | request.wValue = anchorAddress; 121 | request.wIndex = 0; 122 | request.wLength = count; 123 | request.pData = writeBuffer; 124 | 125 | return (*dev)->DeviceRequest(dev, &request); 126 | } 127 | 128 | IOReturn DownloadToAnchorDevice(IOUSBDeviceInterface245 **dev) 129 | { 130 | int i; 131 | UInt8 writeVal; 132 | IOReturn kr; 133 | 134 | // Assert reset 135 | writeVal = 1; 136 | kr = AnchorWrite(dev, k8051_USBCS, 1, &writeVal); 137 | if (kIOReturnSuccess != kr) 138 | { 139 | printf("AnchorWrite reset returned err 0x%x!\n", kr); 140 | (*dev)->USBDeviceClose(dev); 141 | (*dev)->Release(dev); 142 | return kr; 143 | } 144 | 145 | i = 0; 146 | // Download code 147 | while (bulktest[i].Type == 0) 148 | { 149 | kr = AnchorWrite(dev, bulktest[i].Address, bulktest[i].Length, bulktest[i].Data); 150 | if (kIOReturnSuccess != kr) 151 | { 152 | printf("AnchorWrite download %i returned err 0x%x!\n", i, kr); 153 | (*dev)->USBDeviceClose(dev); 154 | (*dev)->Release(dev); 155 | return kr; 156 | } 157 | i++; 158 | } 159 | 160 | // De-assert reset 161 | writeVal = 0; 162 | kr = AnchorWrite(dev, k8051_USBCS, 1, &writeVal); 163 | if (kIOReturnSuccess != kr) 164 | { 165 | printf("AnchorWrite run returned err 0x%x!\n", kr); 166 | } 167 | 168 | return kr; 169 | } 170 | 171 | void ReadCompletion(void *refCon, IOReturn result, void *arg0) 172 | { 173 | IOUSBInterfaceInterface245 **intf = (IOUSBInterfaceInterface245 **) refCon; 174 | UInt32 numBytesRead = (UInt32) arg0; 175 | UInt32 i; 176 | 177 | printf("Async bulk read complete.\n"); 178 | if (kIOReturnSuccess != result) 179 | { 180 | printf("error from async bulk read (%08x)\n", result); 181 | (void) (*intf)->USBInterfaceClose(intf); 182 | (void) (*intf)->Release(intf); 183 | return; 184 | } 185 | 186 | // The firmware we downloaded echoes the 1's complement of what we wrote, so 187 | // complement the buffer contents to see if we get the original data 188 | for (i = 0; i < numBytesRead; i++) 189 | gBuffer[i] = ~gBuffer[i]; 190 | 191 | printf("Read \"%s\" (%ld bytes) from bulk endpoint\n", gBuffer, numBytesRead); 192 | } 193 | 194 | void WriteCompletion(void *refCon, IOReturn result, void *arg0) 195 | { 196 | IOUSBInterfaceInterface245 **intf = (IOUSBInterfaceInterface245 **) refCon; 197 | UInt32 numBytesWritten = (UInt32) arg0; 198 | UInt32 numBytesRead; 199 | 200 | printf("Async write complete.\n"); 201 | if (kIOReturnSuccess != result) 202 | { 203 | printf("error from async bulk write (%08x)\n", result); 204 | (void) (*intf)->USBInterfaceClose(intf); 205 | (void) (*intf)->Release(intf); 206 | return; 207 | } 208 | printf("Wrote \"%s\" (%ld bytes) to bulk endpoint\n", kTestMessage, numBytesWritten); 209 | 210 | numBytesRead = sizeof(gBuffer) - 1; // leave one byte at the end for NUL termination 211 | result = (*intf)->ReadPipeAsync(intf, 9, gBuffer, numBytesRead, ReadCompletion, refCon); 212 | if (kIOReturnSuccess != result) 213 | { 214 | printf("unable to do async bulk read (%08x)\n", result); 215 | (void) (*intf)->USBInterfaceClose(intf); 216 | (void) (*intf)->Release(intf); 217 | return; 218 | } 219 | 220 | } 221 | 222 | IOReturn FindInterfaces(IOUSBDeviceInterface245 **dev) 223 | { 224 | IOReturn kr; 225 | IOUSBFindInterfaceRequest request; 226 | io_iterator_t iterator; 227 | io_service_t usbInterface; 228 | IOCFPlugInInterface **plugInInterface = NULL; 229 | IOUSBInterfaceInterface245 **intf = NULL; 230 | HRESULT res; 231 | SInt32 score; 232 | UInt8 intfClass; 233 | UInt8 intfSubClass; 234 | UInt8 intfNumEndpoints; 235 | int pipeRef; 236 | #ifndef USE_ASYNC_IO 237 | UInt32 numBytesRead; 238 | UInt32 i; 239 | #else 240 | CFRunLoopSourceRef runLoopSource; 241 | #endif 242 | 243 | request.bInterfaceClass = kIOUSBFindInterfaceDontCare; 244 | request.bInterfaceSubClass = kIOUSBFindInterfaceDontCare; 245 | request.bInterfaceProtocol = kIOUSBFindInterfaceDontCare; 246 | request.bAlternateSetting = kIOUSBFindInterfaceDontCare; 247 | 248 | kr = (*dev)->CreateInterfaceIterator(dev, &request, &iterator); 249 | 250 | while ( (usbInterface = IOIteratorNext(iterator)) ) 251 | { 252 | printf("Interface found.\n"); 253 | 254 | kr = IOCreatePlugInInterfaceForService(usbInterface, kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, &plugInInterface, &score); 255 | kr = IOObjectRelease(usbInterface); // done with the usbInterface object now that I have the plugin 256 | if ((kIOReturnSuccess != kr) || !plugInInterface) 257 | { 258 | printf("unable to create a plugin (%08x)\n", kr); 259 | break; 260 | } 261 | 262 | // I have the interface plugin. I need the interface interface 263 | res = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID245), (LPVOID) &intf); 264 | IODestroyPlugInInterface(plugInInterface); // done with this 265 | 266 | if (res || !intf) 267 | { 268 | printf("couldn't create an IOUSBInterfaceInterface245 (%08x)\n", (int) res); 269 | break; 270 | } 271 | 272 | kr = (*intf)->GetInterfaceClass(intf, &intfClass); 273 | kr = (*intf)->GetInterfaceSubClass(intf, &intfSubClass); 274 | 275 | printf("Interface class %d, subclass %d\n", intfClass, intfSubClass); 276 | 277 | // Now open the interface. This will cause the pipes to be instantiated that are 278 | // associated with the endpoints defined in the interface descriptor. 279 | kr = (*intf)->USBInterfaceOpen(intf); 280 | if (kIOReturnSuccess != kr) 281 | { 282 | printf("unable to open interface (%08x)\n", kr); 283 | (void) (*intf)->Release(intf); 284 | break; 285 | } 286 | 287 | kr = (*intf)->GetNumEndpoints(intf, &intfNumEndpoints); 288 | if (kIOReturnSuccess != kr) 289 | { 290 | printf("unable to get number of endpoints (%08x)\n", kr); 291 | (void) (*intf)->USBInterfaceClose(intf); 292 | (void) (*intf)->Release(intf); 293 | break; 294 | } 295 | 296 | printf("Interface has %d endpoints.\n", intfNumEndpoints); 297 | 298 | for (pipeRef = 1; pipeRef < intfNumEndpoints; pipeRef++) 299 | { 300 | IOReturn kr2; 301 | UInt8 direction; 302 | UInt8 number; 303 | UInt8 transferType; 304 | UInt16 maxPacketSize; 305 | UInt8 interval; 306 | char *message; 307 | 308 | kr2 = (*intf)->GetPipeProperties(intf, pipeRef, &direction, &number, &transferType, &maxPacketSize, &interval); 309 | if (kIOReturnSuccess != kr) 310 | printf("unable to get properties of pipe %d (%08x)\n", pipeRef, kr2); 311 | else { 312 | printf("pipeRef %d: ", pipeRef); 313 | 314 | switch (direction) { 315 | case kUSBOut: 316 | message = "out"; 317 | break; 318 | case kUSBIn: 319 | message = "in"; 320 | break; 321 | case kUSBNone: 322 | message = "none"; 323 | break; 324 | case kUSBAnyDirn: 325 | message = "any"; 326 | break; 327 | default: 328 | message = "???"; 329 | } 330 | printf("direction %s, ", message); 331 | 332 | switch (transferType) { 333 | case kUSBControl: 334 | message = "control"; 335 | break; 336 | case kUSBIsoc: 337 | message = "isoc"; 338 | break; 339 | case kUSBBulk: 340 | message = "bulk"; 341 | break; 342 | case kUSBInterrupt: 343 | message = "interrupt"; 344 | break; 345 | case kUSBAnyType: 346 | message = "any"; 347 | break; 348 | default: 349 | message = "???"; 350 | } 351 | printf("transfer type %s, maxPacketSize %d\n", message, maxPacketSize); 352 | } 353 | } 354 | 355 | // We can now address endpoints 1 through intfNumEndpoints. Or, we can also address endpoint 0, 356 | // the default control endpoint. But it's usually better to use (*usbDevice)->DeviceRequest() instead. 357 | #ifndef USE_ASYNC_IO 358 | kr = (*intf)->WritePipe(intf, 2, kTestMessage, strlen(kTestMessage)); 359 | if (kIOReturnSuccess != kr) 360 | { 361 | printf("unable to do bulk write (%08x)\n", kr); 362 | (void) (*intf)->USBInterfaceClose(intf); 363 | (void) (*intf)->Release(intf); 364 | break; 365 | } 366 | 367 | printf("Wrote \"%s\" (%ld bytes) to bulk endpoint\n", kTestMessage, (UInt32) strlen(kTestMessage)); 368 | 369 | numBytesRead = sizeof(gBuffer) - 1; // leave one byte at the end for NUL termination 370 | kr = (*intf)->ReadPipe(intf, 9, gBuffer, &numBytesRead); 371 | if (kIOReturnSuccess != kr) 372 | { 373 | printf("unable to do bulk read (%08x)\n", kr); 374 | (void) (*intf)->USBInterfaceClose(intf); 375 | (void) (*intf)->Release(intf); 376 | break; 377 | } 378 | // The firmware we downloaded echoes the 1's complement of what we wrote, so 379 | // complement the buffer contents to see if we get the original data 380 | for (i = 0; i < numBytesRead; i++) 381 | gBuffer[i] = ~gBuffer[i]; 382 | 383 | printf("Read \"%s\" (%ld bytes) from bulk endpoint\n", gBuffer, numBytesRead); 384 | #else 385 | // Just like with service matching notifications, we need to create an event source and add it 386 | // to our run loop in order to receive async completion notifications. 387 | kr = (*intf)->CreateInterfaceAsyncEventSource(intf, &runLoopSource); 388 | if (kIOReturnSuccess != kr) 389 | { 390 | printf("unable to create async event source (%08x)\n", kr); 391 | (void) (*intf)->USBInterfaceClose(intf); 392 | (void) (*intf)->Release(intf); 393 | break; 394 | } 395 | CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode); 396 | 397 | printf("Async event source added to run loop.\n"); 398 | 399 | bzero(gBuffer, sizeof(gBuffer)); 400 | strcpy(gBuffer, kTestMessage); 401 | kr = (*intf)->WritePipeAsync(intf, 2, gBuffer, strlen(gBuffer), WriteCompletion, (void *) intf); 402 | if (kIOReturnSuccess != kr) 403 | { 404 | printf("unable to do async bulk write (%08x)\n", kr); 405 | (void) (*intf)->USBInterfaceClose(intf); 406 | (void) (*intf)->Release(intf); 407 | break; 408 | } 409 | #endif 410 | 411 | // For this test we just want to use the first interface, so exit the loop. 412 | break; 413 | } 414 | 415 | return kr; 416 | } 417 | 418 | void RawDeviceAdded(void *refCon, io_iterator_t iterator) 419 | { 420 | kern_return_t kr; 421 | io_service_t usbDevice; 422 | IOCFPlugInInterface **plugInInterface=NULL; 423 | IOUSBDeviceInterface245 **dev=NULL; 424 | HRESULT res; 425 | SInt32 score; 426 | UInt16 vendor; 427 | UInt16 product; 428 | UInt16 release; 429 | 430 | while ( (usbDevice = IOIteratorNext(iterator)) ) 431 | { 432 | printf("Raw device added.\n"); 433 | 434 | kr = IOCreatePlugInInterfaceForService(usbDevice, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugInInterface, &score); 435 | kr = IOObjectRelease(usbDevice); // done with the device object now that I have the plugin 436 | if ((kIOReturnSuccess != kr) || !plugInInterface) 437 | { 438 | printf("unable to create a plugin (%08x)\n", kr); 439 | continue; 440 | } 441 | 442 | // I have the device plugin, I need the device interface 443 | res = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID245), (LPVOID)&dev); 444 | IODestroyPlugInInterface(plugInInterface); // done with this 445 | 446 | if (res || !dev) 447 | { 448 | printf("couldn't create a device interface (%08x)\n", (int) res); 449 | continue; 450 | } 451 | // technically should check these kr values 452 | kr = (*dev)->GetDeviceVendor(dev, &vendor); 453 | kr = (*dev)->GetDeviceProduct(dev, &product); 454 | kr = (*dev)->GetDeviceReleaseNumber(dev, &release); 455 | if ((vendor != kOurVendorID) || (product != kOurProductID) || (release != 1)) 456 | { 457 | // We should never get here because the matching criteria we specified above 458 | // will return just those devices with our vendor and product IDs 459 | printf("found device i didn't want (vendor = %d, product = %d)\n", vendor, product); 460 | (void) (*dev)->Release(dev); 461 | continue; 462 | } 463 | 464 | // need to open the device in order to change its state 465 | kr = (*dev)->USBDeviceOpen(dev); 466 | if (kIOReturnSuccess != kr) 467 | { 468 | printf("unable to open device: %08x\n", kr); 469 | (void) (*dev)->Release(dev); 470 | continue; 471 | } 472 | kr = ConfigureAnchorDevice(dev); 473 | if (kIOReturnSuccess != kr) 474 | { 475 | printf("unable to configure device: %08x\n", kr); 476 | (void) (*dev)->USBDeviceClose(dev); 477 | (void) (*dev)->Release(dev); 478 | continue; 479 | } 480 | 481 | kr = DownloadToAnchorDevice(dev); 482 | if (kIOReturnSuccess != kr) 483 | { 484 | printf("unable to download to device: %08x\n", kr); 485 | (void) (*dev)->USBDeviceClose(dev); 486 | (void) (*dev)->Release(dev); 487 | continue; 488 | } 489 | 490 | kr = (*dev)->USBDeviceClose(dev); 491 | kr = (*dev)->Release(dev); 492 | } 493 | } 494 | 495 | void SignalHandler(int sigraised) 496 | { 497 | IONotificationPortDestroy(gNotifyPort); 498 | _exit(0); 499 | } 500 | 501 | int main (int argc, const char *argv[]) 502 | { 503 | mach_port_t masterPort; 504 | CFMutableDictionaryRef matchingDict; 505 | CFRunLoopSourceRef runLoopSource; 506 | kern_return_t kr; 507 | SInt32 usbVendor = kOurVendorID; 508 | SInt32 usbProduct = kOurProductID; 509 | sig_t oldHandler; 510 | 511 | // pick up command line arguments 512 | if (argc > 1) 513 | usbVendor = atoi(argv[1]); 514 | if (argc > 2) 515 | usbProduct = atoi(argv[2]); 516 | 517 | // Set up a signal handler so we can clean up when we're interrupted from the command line 518 | // Otherwise we stay in our run loop forever. 519 | oldHandler = signal(SIGINT, SignalHandler); 520 | if (oldHandler == SIG_ERR) 521 | printf("Could not establish new signal handler"); 522 | 523 | // first create a master_port for my task 524 | kr = IOMasterPort(MACH_PORT_NULL, &masterPort); 525 | if (kr || !masterPort) 526 | { 527 | printf("ERR: Couldn't create a master IOKit Port(%08x)\n", kr); 528 | return -1; 529 | } 530 | 531 | printf("Looking for devices matching vendor ID=%ld and product ID=%ld\n", usbVendor, usbProduct); 532 | 533 | // Set up the matching criteria for the devices we're interested in 534 | matchingDict = IOServiceMatching(kIOUSBDeviceClassName); // Interested in instances of class IOUSBDevice and its subclasses 535 | if (!matchingDict) 536 | { 537 | printf("Can't create a USB matching dictionary\n"); 538 | mach_port_deallocate(mach_task_self(), masterPort); 539 | return -1; 540 | } 541 | 542 | // Add our vendor and product IDs to the matching criteria 543 | CFDictionarySetValue( 544 | matchingDict, 545 | CFSTR(kUSBVendorID), 546 | CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &usbVendor)); 547 | CFDictionarySetValue( 548 | matchingDict, 549 | CFSTR(kUSBProductID), 550 | CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &usbProduct)); 551 | 552 | // Create a notification port and add its run loop event source to our run loop 553 | // This is how async notifications get set up. 554 | gNotifyPort = IONotificationPortCreate(masterPort); 555 | runLoopSource = IONotificationPortGetRunLoopSource(gNotifyPort); 556 | 557 | CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode); 558 | 559 | // Retain additional references because we use this same dictionary with four calls to 560 | // IOServiceAddMatchingNotification, each of which consumes one reference. 561 | matchingDict = (CFMutableDictionaryRef) CFRetain( matchingDict ); 562 | matchingDict = (CFMutableDictionaryRef) CFRetain( matchingDict ); 563 | matchingDict = (CFMutableDictionaryRef) CFRetain( matchingDict ); 564 | 565 | // Now set up two notifications, one to be called when a raw device is first matched by I/O Kit, and the other to be 566 | // called when the device is terminated. 567 | kr = IOServiceAddMatchingNotification( gNotifyPort, 568 | kIOFirstMatchNotification, 569 | matchingDict, 570 | RawDeviceAdded, 571 | NULL, 572 | &gRawAddedIter ); 573 | 574 | RawDeviceAdded(NULL, gRawAddedIter); // Iterate once to get already-present devices and 575 | // arm the notification 576 | 577 | kr = IOServiceAddMatchingNotification( gNotifyPort, 578 | kIOTerminatedNotification, 579 | matchingDict, 580 | RawDeviceRemoved, 581 | NULL, 582 | &gRawRemovedIter ); 583 | 584 | RawDeviceRemoved(NULL, gRawRemovedIter); // Iterate once to arm the notification 585 | 586 | // Change the USB product ID in our matching dictionary to the one the device will have once the 587 | // bulktest firmware has been downloaded. 588 | usbProduct = kOurProductIDBulkTest; 589 | 590 | CFDictionarySetValue( 591 | matchingDict, 592 | CFSTR(kUSBProductID), 593 | CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &usbProduct)); 594 | 595 | // Now set up two more notifications, one to be called when a bulk test device is first matched by I/O Kit, and the other to be 596 | // called when the device is terminated. 597 | kr = IOServiceAddMatchingNotification( gNotifyPort, 598 | kIOFirstMatchNotification, 599 | matchingDict, 600 | BulkTestDeviceAdded, 601 | NULL, 602 | &gBulkTestAddedIter ); 603 | 604 | BulkTestDeviceAdded(NULL, gBulkTestAddedIter); // Iterate once to get already-present devices and 605 | // arm the notification 606 | 607 | kr = IOServiceAddMatchingNotification( gNotifyPort, 608 | kIOTerminatedNotification, 609 | matchingDict, 610 | BulkTestDeviceRemoved, 611 | NULL, 612 | &gBulkTestRemovedIter ); 613 | 614 | BulkTestDeviceRemoved(NULL, gBulkTestRemovedIter); // Iterate once to arm the notification 615 | 616 | // Now done with the master_port 617 | mach_port_deallocate(mach_task_self(), masterPort); 618 | masterPort = 0; 619 | 620 | // Start the run loop. Now we'll receive notifications. 621 | CFRunLoopRun(); 622 | 623 | // We should never get here 624 | return 0; 625 | } 626 | --------------------------------------------------------------------------------