1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16from twisted.internet import defer
17from twisted.python import log
18
19from buildbot import config
20from buildbot.changes import base
21from buildbot.pbutil import NewCredPerspective
22
23
24class ChangePerspective(NewCredPerspective):
25
26    def __init__(self, master, prefix):
27        self.master = master
28        self.prefix = prefix
29
30    def attached(self, mind):
31        return self
32
33    def detached(self, mind):
34        pass
35
36    def perspective_addChange(self, changedict):
37        log.msg("perspective_addChange called")
38
39        if 'revlink' in changedict and not changedict['revlink']:
40            changedict['revlink'] = ''
41        if 'repository' in changedict and not changedict['repository']:
42            changedict['repository'] = ''
43        if 'project' in changedict and not changedict['project']:
44            changedict['project'] = ''
45        if 'files' not in changedict or not changedict['files']:
46            changedict['files'] = []
47        if 'committer' in changedict and not changedict['committer']:
48            changedict['committer'] = None
49
50        # rename arguments to new names.  Note that the client still uses the
51        # "old" names (who, when, and isdir), as they are not deprecated yet,
52        # although the master will accept the new names (author,
53        # when_timestamp).  After a few revisions have passed, we
54        # can switch the client to use the new names.
55        if 'who' in changedict:
56            changedict['author'] = changedict['who']
57            del changedict['who']
58        if 'when' in changedict:
59            changedict['when_timestamp'] = changedict['when']
60            del changedict['when']
61
62        # turn any bytestring keys into unicode, assuming utf8 but just
63        # replacing unknown characters.  Ideally client would send us unicode
64        # in the first place, but older clients do not, so this fallback is
65        # useful.
66        for key in changedict:
67            if isinstance(changedict[key], bytes):
68                changedict[key] = changedict[key].decode('utf8', 'replace')
69        changedict['files'] = list(changedict['files'])
70        for i, file in enumerate(changedict.get('files', [])):
71            if isinstance(file, bytes):
72                changedict['files'][i] = file.decode('utf8', 'replace')
73
74        files = []
75        for path in changedict['files']:
76            if self.prefix:
77                if not path.startswith(self.prefix):
78                    # this file does not start with the prefix, so ignore it
79                    continue
80                path = path[len(self.prefix):]
81            files.append(path)
82        changedict['files'] = files
83
84        if not files:
85            log.msg("No files listed in change... bit strange, but not fatal.")
86
87        if "links" in changedict:
88            log.msg("Found links: " + repr(changedict['links']))
89            del changedict['links']
90
91        d = self.master.data.updates.addChange(**changedict)
92
93        # set the return value to None, so we don't get users depending on
94        # getting a changeid
95        d.addCallback(lambda _: None)
96        return d
97
98
99class PBChangeSource(base.ChangeSource):
100    compare_attrs = ("user", "passwd", "port", "prefix", "port")
101
102    def __init__(self, user="change", passwd="changepw", port=None,
103                 prefix=None, name=None):
104
105        if name is None:
106            if prefix:
107                name = "PBChangeSource:{}:{}".format(prefix, port)
108            else:
109                name = "PBChangeSource:{}".format(port)
110
111        super().__init__(name=name)
112
113        self.user = user
114        self.passwd = passwd
115        self.port = port
116        self.prefix = prefix
117        self.registration = None
118        self.registered_port = None
119
120    def describe(self):
121        portname = self.registered_port
122        d = "PBChangeSource listener on " + str(portname)
123        if self.prefix is not None:
124            d += " (prefix '{}')".format(self.prefix)
125        return d
126
127    def _calculatePort(self, cfg):
128        # calculate the new port, defaulting to the worker's PB port if
129        # none was specified
130        port = self.port
131        if port is None:
132            port = cfg.protocols.get('pb', {}).get('port')
133        return port
134
135    @defer.inlineCallbacks
136    def reconfigServiceWithBuildbotConfig(self, new_config):
137        port = self._calculatePort(new_config)
138        if not port:
139            config.error("No port specified for PBChangeSource, and no "
140                         "worker port configured")
141
142        # and, if it's changed, re-register
143        if port != self.registered_port and self.isActive():
144            yield self._unregister()
145            yield self._register(port)
146
147        yield super().reconfigServiceWithBuildbotConfig(new_config)
148
149    @defer.inlineCallbacks
150    def activate(self):
151        port = self._calculatePort(self.master.config)
152        yield self._register(port)
153
154    def deactivate(self):
155        return self._unregister()
156
157    @defer.inlineCallbacks
158    def _register(self, port):
159        if not port:
160            return
161        self.registered_port = port
162        self.registration = yield self.master.pbmanager.register(port, self.user, self.passwd,
163                                                                 self.getPerspective)
164
165    def _unregister(self):
166        self.registered_port = None
167        if self.registration:
168            reg = self.registration
169            self.registration = None
170            return reg.unregister()
171        return defer.succeed(None)
172
173    def getPerspective(self, mind, username):
174        assert username == self.user
175        return ChangePerspective(self.master, self.prefix)
176