__init__.py 12.2 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__)

42
43
44
VERSION = "1.1.0"
RELEASE_VERSION, _, BUGFIX_VERSION = VERSION.rpartition(".")

45
46
_datapath = 'data'
_configpath = 'conf'
47
48
49
_codepath = os.path.dirname(inspect.getfile(inspect.currentframe()))
_codepath = os.path.dirname(_codepath)

Marcel's avatar
Marcel committed
50

51
def datapath(*args):
52
53
54
    """
    returns the path relative to the datapath
    """
55
    return os.path.join(_datapath, *args)
56

57

58
59
60
61
def set_datapath(p):
    global _datapath
    _datapath = p

62

63
def configpath(*args):
64
    return os.path.join(_configpath, *args)
65

66

67
68
69
70
def set_configpath(p):
    global _configpath
    _configpath = os.path.abspath(p)

71

72
def codepath(*args):
73
    return os.path.join(_codepath, *args)
74

75

76
77
78
def set_codepath(p):
    global _codepath
    _codepath = p
Marcel's avatar
Marcel committed
79

80

Gero Müller's avatar
Gero Müller committed
81
82
83
84
class _ConfigParser(cp.SafeConfigParser):
    def __call__(self, section, option, default=None):
        if config.has_option(section, option):
            value = config.get(section, option)
85
86
87
88
89
90
91
92
            # try to cast according to 'default'
            if default is None:
                return value
            # string
            if isinstance(default, str):
                return str(value)
            # list
            elif isinstance(default, list):
Gero Müller's avatar
Gero Müller committed
93
94
95
96
97
98
99
100
101
                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

102
103
104
105
106
107
108
            # tuple
            elif isinstance(default, tuple):
                if len(value):
                    return tuple(value.split(','))
                else:
                    return ()
            # boolean
Gero Müller's avatar
Gero Müller committed
109
            elif isinstance(default, bool):
110
                if value == 'True':
Gero Müller's avatar
Gero Müller committed
111
                    return True
112
                elif value == 'False':
Gero Müller's avatar
Gero Müller committed
113
                    return False
114
            # TODO: dict
Gero Müller's avatar
Gero Müller committed
115
116
117
118
119
            else:
                return value
        else:
            return default

Marcel's avatar
Marcel committed
120
# setup a config parser for "vispa.ini"
Gero Müller's avatar
Gero Müller committed
121
config = _ConfigParser()
122

Marcel's avatar
Marcel committed
123
# a little publish/subscribe system
124
125
_callbacks = {}

126

Marcel's avatar
Marcel committed
127
128
129
130
def subscribe(topic, callback):
    if callback not in _callbacks.get(topic, []):
        _callbacks.setdefault(topic, []).append(callback)
register_callback = subscribe
131

132

Marcel's avatar
Marcel committed
133
134
135
def publish(topic, *args, **kwargs):
    values = []
    for callback in _callbacks.get(topic, []):
136
        try:
Marcel's avatar
Marcel committed
137
138
            value = callback(*args, **kwargs)
            values.append(value)
139
        except Exception:
Marcel's avatar
Marcel committed
140
            values.append(None)
141
            log_exception()
Marcel's avatar
Marcel committed
142
143
144
    return values
fire_callback = publish

145

Gero Müller's avatar
Gero Müller committed
146
def exception_string():
Marcel's avatar
Marcel committed
147
148
    exc_type, exc_value, exc_tb = sys.exc_info()
    st = traceback.format_exception(exc_type, exc_value, exc_tb)
149
    return  ''.join(st)
Gero Müller's avatar
Gero Müller committed
150

151

Gero Müller's avatar
Gero Müller committed
152
153
def log_exception():
    sys.stderr.write(exception_string())
Gero Müller's avatar
Gero Müller committed
154
155


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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
173
174

    @staticmethod
175
    def _ipv4_to_hex(ip):
Gero Müller's avatar
Gero Müller committed
176
177
178
179
180
        parts = ip.split(".")
        parts.reverse()
        return "".join(["%02X" % int(part) for part in parts])

    @staticmethod
181
    def _port_to_hex(port):
Gero Müller's avatar
Gero Müller committed
182
        return "%02X" % port
183

Gero Müller's avatar
Gero Müller committed
184
    @staticmethod
185
186
187
    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
188
189
190
191
192
        userid = None
        for line in f:
            m = Netstat._proc_net_tcp_re.match(line)
            if not m:
                continue
193
            if m.group(Netstat._IDX_LOCAL_IP) != local_ip:
Gero Müller's avatar
Gero Müller committed
194
                continue
195
            if m.group(Netstat._IDX_LOCAL_PORT) != local_port:
Gero Müller's avatar
Gero Müller committed
196
                continue
197
            if m.group(Netstat._IDX_REMOTE_IP) != remote_ip:
Gero Müller's avatar
Gero Müller committed
198
                continue
199
            if m.group(Netstat._IDX_REMOTE_PORT) != remote_port:
Gero Müller's avatar
Gero Müller committed
200
                continue
201
            userid = int(m.group(Netstat._IDX_USERID))
Gero Müller's avatar
Gero Müller committed
202
203
204
            break
        f.close()
        return userid
205

Gero Müller's avatar
Gero Müller committed
206
    @staticmethod
207
    def _get_socket_owner_ipv4(local_ip, local_port, remote_ip, remote_port):
Gero Müller's avatar
Gero Müller committed
208
209
210
211
        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)
212
213
214
215
        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
216
217
    @staticmethod
    def get_socket_owner(local_ip, local_port, remote_ip, remote_port):
218
219
220
221
222
223
224
225
226
227
        """
        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
228
        # prepare adresses
229
230
        _locals = socket.getaddrinfo(local_ip, local_port,
                                     0, 0, socket.SOL_TCP)
Gero Müller's avatar
Gero Müller committed
231
        # [(2, 1, 6, '', ('127.0.0.1', 4282))]
232
233
        _remotes = socket.getaddrinfo(remote_ip, remote_port,
                                      0, 0, socket.SOL_TCP)
Gero Müller's avatar
Gero Müller committed
234
        # [(2, 1, 6, '', ('127.0.0.1', 53726))]
235

Gero Müller's avatar
Gero Müller committed
236
237
238
239
240
241
242
        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:
243
244
245
246
                    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
247
                if remote[0] == socket.AF_INET6:
248
249
250
251
                    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
252
        return None
Gero Müller's avatar
Gero Müller committed
253
254


255
class AjaxException(Exception):
256
    """
Marcel Rieger's avatar
Marcel Rieger committed
257
    AjaxException that is handled by the ajax tool and that can be raised in controller methods.
258
    *message* is the error message to show. *code* should be an integer that represents a specific
Marcel Rieger's avatar
Marcel Rieger committed
259
260
261
    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.
262
    """
263

264
265
266
267
268
269
270
271
272
273
274
    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

275
        super(AjaxException, self).__init__(message)
276
277
278

        self.code  = code
        self.alert = alert
279
280


281
282
# the bus
bus = None
Marcel Rieger's avatar
Marcel Rieger committed
283
284
285
286


def send_mail(addr, subject="", content="", sender_addr=None, smtp_host=None,
              smtp_port=None):
Gero Müller's avatar
Gero Müller committed
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
    """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.
    """

    # 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
304

Gero Müller's avatar
Gero Müller committed
305
306
307
308
309
310
311
312
    # 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
313

Gero Müller's avatar
Gero Müller committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
    # 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
    smtp = SMTP("localhost")
    smtp.sendmail(sender_addr, addr, msg.as_string())
    smtp.quit()
Gero Müller's avatar
Gero Müller committed
337
338
339
340
341
342
343
344
345
346


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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380


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
381
382
383
    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:
384
385
386
387
388
389
        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
390
391
         </tr>""" % (thread.id, thread.id, thread.idle_time(),
                     thread.last_req_time(), thread.url))
392
393
394
395
396
397
398
399
400
401
402
403
404
    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
405
406
    if hasattr(f, 'flush'):
        f.flush()
Gero Müller's avatar
Gero Müller committed
407

408
409
410
411
412
413
414

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
415
416
def setup_thread_dump():
    signal.signal(signal.SIGUSR2, dump_thread_status_on_signal)