2 |
4 |
5 |
6 |
7 |
8 | PyMacAdmin README
9 |
10 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------