__init__.py 12.8 KB
Newer Older
Marcel's avatar
Marcel committed
1
# -*- coding: utf-8 -*-
2

3
4
5
"""
Basic functionality for the VISPA platform
"""
Marcel's avatar
Marcel committed
6

7
8
9
10
11
from StringIO import StringIO
from fileinput import filename
import csv
import inspect
import logging
Marcel's avatar
Marcel committed
12
import os
Gero Müller's avatar
Gero Müller committed
13
import re
14
15
import signal
import smtplib
Gero Müller's avatar
Gero Müller committed
16
import socket
17
18
19
import sys
import tempfile
import thread
Gero Müller's avatar
Gero Müller committed
20
21
import token
import tokenize
22
import traceback
Gero Müller's avatar
Gero Müller committed
23
import copy
24
25

import cherrypy
26
from cherrypy.lib.httputil import response_codes
27

28
29
from . import url

Gero Müller's avatar
Gero Müller committed
30
31
32
33
from smtplib import SMTP
from email.MIMEText import MIMEText
from email.Header import Header
from email.Utils import parseaddr, formataddr
Gero Müller's avatar
Gero Müller committed
34

35
try:
Gero Müller's avatar
Gero Müller committed
36
    import ConfigParser as cp
37
except:
38
    import configparser as cp
39

Gero Müller's avatar
Gero Müller committed
40
41
logger = logging.getLogger(__name__)

Gero Müller's avatar
Gero Müller committed
42
43
44
45
46
# http://stackoverflow.com/a/24517154
from .version import __version__
VERSION = __version__
RELEASE_VERSION, _, BUGFIX_VERSION = __version__.rpartition(".")
# http://semver.org/
Gero Müller's avatar
Gero Müller committed
47
MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION = __version__.split(".")
48

49
50
_datapath = 'data'
_configpath = 'conf'
51
52
53
_codepath = os.path.dirname(inspect.getfile(inspect.currentframe()))
_codepath = os.path.dirname(_codepath)

Marcel's avatar
Marcel committed
54

55
def datapath(*args):
56
57
58
    """
    returns the path relative to the datapath
    """
59
    return os.path.join(_datapath, *args)
60

61

62
63
64
65
def set_datapath(p):
    global _datapath
    _datapath = p

66

67
def configpath(*args):
68
    return os.path.join(_configpath, *args)
69

70

71
72
73
74
def set_configpath(p):
    global _configpath
    _configpath = os.path.abspath(p)

75

76
def codepath(*args):
77
    return os.path.join(_codepath, *args)
78

79

80
81
82
def set_codepath(p):
    global _codepath
    _codepath = p
Marcel's avatar
Marcel committed
83

84

85
class VispaConfigParser(cp.SafeConfigParser):
Gero Müller's avatar
Gero Müller committed
86
    def __call__(self, section, option, default=None):
87
88
        if self.has_option(section, option):
            value = self.get(section, option)
89
90
91
            # try to cast according to 'default'
            if default is None:
                return value
Gero Müller's avatar
Gero Müller committed
92
93
94
95
96
97
            # boolean
            if isinstance(default, bool):
                if value == 'True':
                    return True
                elif value == 'False':
                    return False
98
99
100
101
102
103
            # int
            if isinstance(default, int):
                return int(value)
            # long
            if isinstance(default, long):
                return long(value)
104
105
106
107
            # string
            if isinstance(default, str):
                return str(value)
            # list
Gero Müller's avatar
Gero Müller committed
108
            if isinstance(default, list):
Gero Müller's avatar
Gero Müller committed
109
110
111
112
113
114
115
116
117
                l = []
                g = tokenize.generate_tokens(StringIO(value).readline)
                for toknum, tokval, _, _, _  in g:
                    if toknum == token.NAME or toknum == token.NUMBER:
                        l.append(tokval)
                    elif toknum == token.STRING:
                        l.append(tokval[1:-1])
                return l

118
            # tuple
Gero Müller's avatar
Gero Müller committed
119
            if isinstance(default, tuple):
120
121
122
123
124
                if len(value):
                    return tuple(value.split(','))
                else:
                    return ()
            # TODO: dict
Gero Müller's avatar
Gero Müller committed
125
            return value
Gero Müller's avatar
Gero Müller committed
126
127
128
        else:
            return default

Marcel's avatar
Marcel committed
129
# setup a config parser for "vispa.ini"
130
config = VispaConfigParser()
131

Marcel's avatar
Marcel committed
132
# a little publish/subscribe system
133
134
_callbacks = {}

135

Marcel's avatar
Marcel committed
136
137
138
139
def subscribe(topic, callback):
    if callback not in _callbacks.get(topic, []):
        _callbacks.setdefault(topic, []).append(callback)
register_callback = subscribe
140

141

Marcel's avatar
Marcel committed
142
143
144
def publish(topic, *args, **kwargs):
    values = []
    for callback in _callbacks.get(topic, []):
145
        try:
Marcel's avatar
Marcel committed
146
147
            value = callback(*args, **kwargs)
            values.append(value)
148
        except Exception:
Marcel's avatar
Marcel committed
149
            values.append(None)
150
            log_exception()
Marcel's avatar
Marcel committed
151
152
153
    return values
fire_callback = publish

154

Gero Müller's avatar
Gero Müller committed
155
def exception_string():
Marcel's avatar
Marcel committed
156
157
    exc_type, exc_value, exc_tb = sys.exc_info()
    st = traceback.format_exception(exc_type, exc_value, exc_tb)
158
    return  ''.join(st)
Gero Müller's avatar
Gero Müller committed
159

160

Gero Müller's avatar
Gero Müller committed
161
162
def log_exception():
    sys.stderr.write(exception_string())
Gero Müller's avatar
Gero Müller committed
163
164


165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class Netstat(object):
    """
    Parses /dev/net/tcp to determine which user owns the specified port
    """

    _proc_net_tcp_re = re.compile("\W+([0-9]+):\W+([0-9A-F]+):([0-9A-F]+)"
                                  "\W+([0-9A-F]+):([0-9A-F]+)\W+([0-9A-F]+)"
                                  "\W+([0-9A-F]+):([0-9A-F]+)"
                                  "\W+([0-9A-F]+):([0-9A-Z]+)"
                                  "\W+([0-9A-F]+)\W+([0-9A-F]+)"
                                  "\W+([0-9A-F]+).*")

    _IDX_LOCAL_IP = 2
    _IDX_LOCAL_PORT = 3
    _IDX_REMOTE_IP = 4
    _IDX_REMOTE_PORT = 5
    _IDX_USERID = 12
Gero Müller's avatar
Gero Müller committed
182
183

    @staticmethod
184
    def _ipv4_to_hex(ip):
Gero Müller's avatar
Gero Müller committed
185
186
187
188
189
        parts = ip.split(".")
        parts.reverse()
        return "".join(["%02X" % int(part) for part in parts])

    @staticmethod
190
    def _port_to_hex(port):
Gero Müller's avatar
Gero Müller committed
191
        return "%02X" % port
192

Gero Müller's avatar
Gero Müller committed
193
    @staticmethod
194
195
196
    def _get_socket_owner_file(filename, local_ip, local_port,
                               remote_ip, remote_port):
        f = open(filename, 'r')
Gero Müller's avatar
Gero Müller committed
197
198
199
200
201
        userid = None
        for line in f:
            m = Netstat._proc_net_tcp_re.match(line)
            if not m:
                continue
202
            if m.group(Netstat._IDX_LOCAL_IP) != local_ip:
Gero Müller's avatar
Gero Müller committed
203
                continue
204
            if m.group(Netstat._IDX_LOCAL_PORT) != local_port:
Gero Müller's avatar
Gero Müller committed
205
                continue
206
            if m.group(Netstat._IDX_REMOTE_IP) != remote_ip:
Gero Müller's avatar
Gero Müller committed
207
                continue
208
            if m.group(Netstat._IDX_REMOTE_PORT) != remote_port:
Gero Müller's avatar
Gero Müller committed
209
                continue
210
            userid = int(m.group(Netstat._IDX_USERID))
Gero Müller's avatar
Gero Müller committed
211
212
213
            break
        f.close()
        return userid
214

Gero Müller's avatar
Gero Müller committed
215
    @staticmethod
216
    def _get_socket_owner_ipv4(local_ip, local_port, remote_ip, remote_port):
Gero Müller's avatar
Gero Müller committed
217
218
219
220
        local_port = Netstat.port_to_hex(local_port)
        local_ip = Netstat.ipv4_to_hex(local_ip)
        remote_port = Netstat.port_to_hex(remote_port)
        remote_ip = Netstat.ipv4_to_hex(remote_ip)
221
222
223
224
        return Netstat.get_socket_owner_file('/proc/net/tcp',
                                             local_ip, local_port,
                                             remote_ip, remote_port)

Gero Müller's avatar
Gero Müller committed
225
226
    @staticmethod
    def get_socket_owner(local_ip, local_port, remote_ip, remote_port):
227
228
229
230
231
232
233
234
235
236
        """
        Returns the system id of the local user which established the
        specifiec connection

        :param local_ip: ip address of the local peer
        :param local_ip: port the local peer
        :param remote_ip: ip address of the remote peer
        :param remote_ip: port the remote peer
        :rtype: user id of the user who opened this port or None
        """
Gero Müller's avatar
Gero Müller committed
237
        # prepare adresses
238
239
        _locals = socket.getaddrinfo(local_ip, local_port,
                                     0, 0, socket.SOL_TCP)
Gero Müller's avatar
Gero Müller committed
240
        # [(2, 1, 6, '', ('127.0.0.1', 4282))]
241
242
        _remotes = socket.getaddrinfo(remote_ip, remote_port,
                                      0, 0, socket.SOL_TCP)
Gero Müller's avatar
Gero Müller committed
243
        # [(2, 1, 6, '', ('127.0.0.1', 53726))]
244

Gero Müller's avatar
Gero Müller committed
245
246
247
248
249
250
251
        for local in _locals:
            for remote in _remotes:
                if remote[0] != local[0]:
                    continue
                if remote[4][0] != local[4][0]:
                    continue
                if remote[0] == socket.AF_INET:
252
253
254
255
                    return Netstat.get_socket_owner_ipv4(local[4][0],
                                                         local[4][1],
                                                         remote[4][0],
                                                         remote[4][1])
Gero Müller's avatar
Gero Müller committed
256
                if remote[0] == socket.AF_INET6:
257
258
259
260
                    return Netstat.get_socket_owner_ipv6(local[4][0],
                                                         local[4][1],
                                                         remote[4][0],
                                                         remote[4][1])
Gero Müller's avatar
Gero Müller committed
261
        return None
Gero Müller's avatar
Gero Müller committed
262
263


264
class AjaxException(Exception):
265
    """
Marcel Rieger's avatar
Marcel Rieger committed
266
    AjaxException that is handled by the ajax tool and that can be raised in controller methods.
267
    *message* is the error message to show. *code* should be an integer that represents a specific
Marcel Rieger's avatar
Marcel Rieger committed
268
269
270
    type of exception. If *code* is *None* and *message* is an integer representing a http status
    code, the error message is set to the standard error message for that http error. If *alert* is
    *True*, the message is shown in a dialog in the GUI.
271
    """
272

273
274
275
276
277
278
279
280
281
282
283
    def __init__(self, message, code=None, alert=True):
        """
        __init__(message, code=500, alert=True)
        """
        if code is None:
            if isinstance(message, int) and message in response_codes:
                code    = message
                message = response_codes[code][0]
            else:
                code = 500

284
        super(AjaxException, self).__init__(message)
285
286
287

        self.code  = code
        self.alert = alert
288
289


290
291
# the bus
bus = None
Marcel Rieger's avatar
Marcel Rieger committed
292
293
294
295


def send_mail(addr, subject="", content="", sender_addr=None, smtp_host=None,
              smtp_port=None):
Gero Müller's avatar
Gero Müller committed
296
297
298
299
300
301
302
303
304
305
306
307
308
309
    """Send an email.

    All arguments should be Unicode strings (plain ASCII works as well).

    Only the real name part of sender and recipient addresses may contain
    non-ASCII characters.

    The email will be properly MIME encoded and delivered though SMTP to
    localhost port 25.  This is easy to change if you want something different.

    The charset of the email will be the first one out of US-ASCII, ISO-8859-1
    and UTF-8 that can represent all the characters occurring in the email.
    """

Gero Müller's avatar
Gero Müller committed
310
311
312
    if smtp_host is None:
        smtp_host = config("mail", "smtp.host", "localhost")
    if smtp_port is None:
Gero Müller's avatar
Gero Müller committed
313
        smtp_port = config("mail", "smtp.port", 0)
Gero Müller's avatar
Gero Müller committed
314
315
316
    if sender_addr is None:
        sender_addr = config("mail", "sender_address", "noreply@example.org")

Gero Müller's avatar
Gero Müller committed
317
318
319
    # Header class is smart enough to try US-ASCII, then the charset we
    # provide, then fall back to UTF-8.
    header_charset = 'ISO-8859-1'
Marcel Rieger's avatar
Marcel Rieger committed
320

Gero Müller's avatar
Gero Müller committed
321
322
323
324
325
326
327
328
    # We must choose the body charset manually
    for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8':
        try:
            content.encode(body_charset)
        except UnicodeError:
            pass
        else:
            break
Marcel Rieger's avatar
Marcel Rieger committed
329

Gero Müller's avatar
Gero Müller committed
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
    # Split real name (which is optional) and email address parts
    sender_name, sender_addr = parseaddr(sender_addr)
    recipient_name, recipient_addr = parseaddr(addr)

    # We must always pass Unicode strings to Header, otherwise it will
    # use RFC 2047 encoding even on plain ASCII strings.
    sender_name = str(Header(unicode(sender_name), header_charset))
    recipient_name = str(Header(unicode(recipient_name), header_charset))

    # Make sure email addresses do not contain non-ASCII characters
    sender_addr = sender_addr.encode('ascii')
    recipient_addr = recipient_addr.encode('ascii')

    # Create the message ('plain' stands for Content-Type: text/plain)
    msg = MIMEText(content.encode(body_charset), 'plain', body_charset)
    msg['From'] = formataddr((sender_name, sender_addr))
    msg['To'] = formataddr((recipient_name, recipient_addr))
    msg['Subject'] = Header(unicode(subject), header_charset)

    # Send the message via SMTP to localhost:25
Gero Müller's avatar
Gero Müller committed
350
    smtp = SMTP(smtp_host, smtp_port)
Gero Müller's avatar
Gero Müller committed
351
352
    smtp.sendmail(sender_addr, addr, msg.as_string())
    smtp.quit()
Gero Müller's avatar
Gero Müller committed
353
354
355
356
357
358
359
360
361
362


def thread_stacktraces():
    frames = sys._current_frames()
    data = {}
    for thread_id, frame in frames.iteritems():
        stacktrace = StringIO()
        traceback.print_stack(frame, file=stacktrace)
        data[thread_id] = stacktrace.getvalue()
    return data
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396


def dump_thread_status(f=None):
    # open the file
    if f == None:
        f = config('web', 'status_filename', None)

    if isinstance(f, (str, unicode)):
        f = f % {'pid': os.getpid()}
        f = open(f, 'a')

    if not hasattr(f, 'write'):
        return

    # header
    f.write("""
    <html>
    <head>
        <title>CherryPy Status</title>
    </head>
    <body>
    <h1>CherryPy Status</h1>
    """)

    # worker table
    f.write("""
    <table>
      <tr>
        <th>Thread ID</th>
        <th>Idle Time</th>
        <th>Last Request Time</th>
        <th>URL</th>
      </tr>
    """)
Gero Müller's avatar
Gero Müller committed
397
398
399
    threads = copy.deepcopy(cherrypy.tools.status.seen_threads.values())
    threads.sort(key=lambda thread: (thread.idle_time(), thread.last_req_time()))
    for thread in threads:
400
401
402
403
404
405
        f.write("""
        <tr>
          <th><a href=\"#%s\">%s</a></th>
          <td>%.4f</td>
          <td>%.4f</td>
          <td>%s</td>
Gero Müller's avatar
Gero Müller committed
406
407
         </tr>""" % (thread.id, thread.id, thread.idle_time(),
                     thread.last_req_time(), thread.url))
408
409
410
411
412
413
414
415
416
417
418
419
420
    f.write("</table>")

    # threads
    for thread_id, st in thread_stacktraces().items():
        f.write("""
        <h3><a id=\"%s\">%s</a></h3>
        <pre>\n%s</pre>""" % (thread_id, thread_id, st))

    f.write("""
    </body>
    </html>
    """)

Gero Müller's avatar
Gero Müller committed
421
422
    if hasattr(f, 'flush'):
        f.flush()
Gero Müller's avatar
Gero Müller committed
423

424
425
426
427
428
429
430

def dump_thread_status_on_signal(signal, stack):
    f = os.path.join(tempfile.gettempdir(),
                     "cherrypy-status-%d.html" % os.getpid())
    dump_thread_status(f)


Gero Müller's avatar
Gero Müller committed
431
432
def setup_thread_dump():
    signal.signal(signal.SIGUSR2, dump_thread_status_on_signal)