summaryrefslogtreecommitdiffstats
path: root/src/lib/Client/Tools/launchd.py
blob: bcdb50df87265571a2cf5ab333781433fc84e46c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
'''launchd support for Bcfg2'''
__revision__ = '$Revision$'

import os
import Bcfg2.Client.Tools
import popen2

'''Locate plist file that provides given reverse-fqdn name
/Library/LaunchAgents          Per-user agents provided by the administrator.
/Library/LaunchDaemons         System wide daemons provided by the administrator.
/System/Library/LaunchAgents   Mac OS X Per-user agents.
/System/Library/LaunchDaemons  Mac OS X System wide daemons.'''
plistLocations = ["/Library/LaunchDaemons", "/System/Library/LaunchDaemons"]
plistMapping = {}
for directory in plistLocations:
    for daemon in os.listdir(directory):
        try:
            if daemon.endswith(".plist"):
                d = daemon[:-6]
            else:
                d = daemon
            (stdout, _) = popen2.popen2('defaults read %s/%s Label' % (directory, d))
            label = stdout.read().strip()
            plistMapping[label] = "%s/%s" % (directory, daemon)
        except KeyError: #perhaps this could be more robust
            pass

class launchd(Bcfg2.Client.Tools.Tool):
    '''Support for Mac OS X Launchd Services'''
    __handles__ = [('Service', 'launchd')]
    __execs__ = ['/bin/launchctl', '/usr/bin/defaults']
    name = 'launchd'
    __req__ = {'Service':['name', 'status']}

    '''
    currently requires the path to the plist to load/unload,
    and Name is acually a reverse-fqdn (or the label)
    '''
    def FindPlist(self, entry):
        return plistMapping.get(entry.get('name'), None)

    def os_version(self):
        version = ""
        try:
            vers = self.cmd.run('sw_vers')[1]
        except:
            return version

        for line in vers:
            if line.startswith("ProductVersion"):
                version = line.split()[-1]
        return version

    def VerifyService(self, entry, _):
        '''Verify Launchd Service Entry'''
        try:
            services = self.cmd.run("/bin/launchctl list")[1]
        except IndexError:#happens when no services are running (should be never)
            services = []
        # launchctl output changed in 10.5
        # It is now three columns, with the last column being the name of the # service
        version = self.os_version()
        if version.startswith('10.5') or version.startswith('10.6'):
            services = [s.split()[-1] for s in services]
        if entry.get('name') in services:#doesn't check if non-spawning services are Started
            return entry.get('status') == 'on'
        else:
            self.logger.debug("Didn't find service Loaded (launchd running under same user as bcfg)")
            return entry.get('status') == 'off'

        try: #Perhaps add the "-w" flag to load and unload to modify the file itself!
            self.cmd.run("/bin/launchctl load -w %s" % self.FindPlist(entry))
        except IndexError:
            return 'on'
        return False


    def InstallService(self, entry):
        '''Enable or Disable launchd Item'''
        name = entry.get('name')
        if entry.get('status') == 'on':
            self.logger.error("Installing service %s" % name)
            cmdrc = self.cmd.run("/bin/launchctl load -w %s" % self.FindPlist(entry))[0]
            cmdrc = self.cmd.run("/bin/launchctl start %s" % name)
        else:
            self.logger.error("Uninstalling service %s" % name)
            cmdrc = self.cmd.run("/bin/launchctl stop %s" % name)
            cmdrc = self.cmd.run("/bin/launchctl unload -w %s" % self.FindPlist(entry))[0]
        return cmdrc[0] == 0

    def Remove(self, svcs):
        '''Remove Extra launchd entries'''
        pass



    def FindExtra(self):
        '''Find Extra launchd Services'''
        try:
            allsrv =  self.cmd.run("/bin/launchctl list")[1]
        except IndexError:
            allsrv = []

        [allsrv.remove(svc) for svc in [entry.get("name") for entry
                                        in self.getSupportedEntries()] if svc in allsrv]
        return [Bcfg2.Client.XML.Element("Service", type='launchd', name=name, status='on') for name in allsrv]

    def BundleUpdated(self, bundle, states):
        '''Reload launchd plist'''
        for entry in [entry for entry in bundle if self.handlesEntry(entry)]:
            if not self.canInstall(entry):
                self.logger.error("Insufficient information to restart service %s" % (entry.get('name')))
            else:
                name = entry.get('name')
                if entry.get('status') == 'on' and self.FindPlist(entry):
                    self.logger.info("Reloading launchd  service %s" % name)
                    #stop?
                    self.cmd.run("/bin/launchctl stop %s" % name)
                    self.cmd.run("/bin/launchctl unload -w %s" % (self.FindPlist(entry)))#what if it disappeared? how do we stop services that are currently running but the plist disappeared?!
                    self.cmd.run("/bin/launchctl load -w %s" % (self.FindPlist(entry)))
                    self.cmd.run("/bin/launchctl start %s" % name)
                else:
                    #only if necessary....
                    self.cmd.run("/bin/launchctl stop %s" % name)
                    self.cmd.run("/bin/launchctl unload -w %s" % (self.FindPlist(entry)))