summaryrefslogtreecommitdiffstats
path: root/src/lib/Client/Tools/launchd.py
blob: ba2c3086fb77abfe116988e35f54d52d7c285a83 (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
'''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
        if self.os_version().startswith('10.5'):
            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)))