12
|
1 |
#!/usr/bin/env python
|
|
2 |
|
|
3 |
# Event Manager for Uzbl
|
|
4 |
# Copyright (c) 2009-2010, Mason Larobina <mason.larobina@gmail.com>
|
|
5 |
# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
|
|
6 |
#
|
|
7 |
# This program is free software: you can redistribute it and/or modify
|
|
8 |
# it under the terms of the GNU General Public License as published by
|
|
9 |
# the Free Software Foundation, either version 3 of the License, or
|
|
10 |
# (at your option) any later version.
|
|
11 |
#
|
|
12 |
# This program is distributed in the hope that it will be useful,
|
|
13 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15 |
# GNU General Public License for more details.
|
|
16 |
#
|
|
17 |
# You should have received a copy of the GNU General Public License
|
|
18 |
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19 |
|
|
20 |
'''
|
|
21 |
|
|
22 |
E V E N T _ M A N A G E R . P Y
|
|
23 |
===============================
|
|
24 |
|
|
25 |
Event manager for uzbl written in python.
|
|
26 |
|
|
27 |
'''
|
|
28 |
|
|
29 |
import atexit
|
|
30 |
import imp
|
|
31 |
import logging
|
|
32 |
import os
|
|
33 |
import socket
|
|
34 |
import sys
|
|
35 |
import time
|
|
36 |
import weakref
|
|
37 |
import re
|
|
38 |
from collections import defaultdict
|
|
39 |
from functools import partial
|
|
40 |
from glob import glob
|
|
41 |
from itertools import count
|
|
42 |
from optparse import OptionParser
|
|
43 |
from select import select
|
|
44 |
from signal import signal, SIGTERM, SIGINT
|
|
45 |
from socket import socket, AF_UNIX, SOCK_STREAM
|
|
46 |
from traceback import format_exc
|
|
47 |
|
|
48 |
def xdghome(key, default):
|
|
49 |
'''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
|
|
50 |
use $HOME and the default path.'''
|
|
51 |
|
|
52 |
xdgkey = "XDG_%s_HOME" % key
|
|
53 |
if xdgkey in os.environ.keys() and os.environ[xdgkey]:
|
|
54 |
return os.environ[xdgkey]
|
|
55 |
|
|
56 |
return os.path.join(os.environ['HOME'], default)
|
|
57 |
|
|
58 |
# `make install` will put the correct value here for your system
|
|
59 |
PREFIX = '/usr/local/'
|
|
60 |
|
|
61 |
# Setup xdg paths.
|
|
62 |
DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
|
|
63 |
CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/')
|
|
64 |
|
|
65 |
# Define some globals.
|
|
66 |
SCRIPTNAME = os.path.basename(sys.argv[0])
|
|
67 |
|
|
68 |
def get_exc():
|
|
69 |
'''Format `format_exc` for logging.'''
|
|
70 |
return "\n%s" % format_exc().rstrip()
|
|
71 |
|
|
72 |
def expandpath(path):
|
|
73 |
'''Expand and realpath paths.'''
|
|
74 |
return os.path.realpath(os.path.expandvars(path))
|
|
75 |
|
|
76 |
def ascii(u):
|
|
77 |
'''Convert unicode strings into ascii for transmission over
|
|
78 |
ascii-only streams/sockets/devices.'''
|
|
79 |
return u.encode('utf-8')
|
|
80 |
|
|
81 |
|
|
82 |
def daemonize():
|
|
83 |
'''Daemonize the process using the Stevens' double-fork magic.'''
|
|
84 |
|
|
85 |
logger.info('entering daemon mode')
|
|
86 |
|
|
87 |
try:
|
|
88 |
if os.fork():
|
|
89 |
os._exit(0)
|
|
90 |
|
|
91 |
except OSError:
|
|
92 |
logger.critical(get_exc())
|
|
93 |
sys.exit(1)
|
|
94 |
|
|
95 |
os.chdir('/')
|
|
96 |
os.setsid()
|
|
97 |
os.umask(0)
|
|
98 |
|
|
99 |
try:
|
|
100 |
if os.fork():
|
|
101 |
os._exit(0)
|
|
102 |
|
|
103 |
except OSError:
|
|
104 |
logger.critical(get_exc())
|
|
105 |
sys.exit(1)
|
|
106 |
|
|
107 |
if sys.stdout.isatty():
|
|
108 |
sys.stdout.flush()
|
|
109 |
sys.stderr.flush()
|
|
110 |
|
|
111 |
devnull = '/dev/null'
|
|
112 |
stdin = file(devnull, 'r')
|
|
113 |
stdout = file(devnull, 'a+')
|
|
114 |
stderr = file(devnull, 'a+', 0)
|
|
115 |
|
|
116 |
os.dup2(stdin.fileno(), sys.stdin.fileno())
|
|
117 |
os.dup2(stdout.fileno(), sys.stdout.fileno())
|
|
118 |
os.dup2(stderr.fileno(), sys.stderr.fileno())
|
|
119 |
|
|
120 |
logger.info('entered daemon mode')
|
|
121 |
|
|
122 |
|
|
123 |
def make_dirs(path):
|
|
124 |
'''Make all basedirs recursively as required.'''
|
|
125 |
|
|
126 |
try:
|
|
127 |
dirname = os.path.dirname(path)
|
|
128 |
if not os.path.isdir(dirname):
|
|
129 |
logger.debug('creating directories %r' % dirname)
|
|
130 |
os.makedirs(dirname)
|
|
131 |
|
|
132 |
except OSError:
|
|
133 |
logger.error(get_exc())
|
|
134 |
|
|
135 |
|
|
136 |
class EventHandler(object):
|
|
137 |
'''Event handler class. Used to store args and kwargs which are merged
|
|
138 |
come time to call the callback with the event args and kwargs.'''
|
|
139 |
|
|
140 |
nextid = count().next
|
|
141 |
|
|
142 |
def __init__(self, plugin, event, callback, args, kwargs):
|
|
143 |
self.id = self.nextid()
|
|
144 |
self.plugin = plugin
|
|
145 |
self.event = event
|
|
146 |
self.callback = callback
|
|
147 |
self.args = args
|
|
148 |
self.kwargs = kwargs
|
|
149 |
|
|
150 |
|
|
151 |
def __repr__(self):
|
|
152 |
elems = ['id=%d' % self.id, 'event=%s' % self.event,
|
|
153 |
'callback=%r' % self.callback]
|
|
154 |
|
|
155 |
if self.args:
|
|
156 |
elems.append('args=%s' % repr(self.args))
|
|
157 |
|
|
158 |
if self.kwargs:
|
|
159 |
elems.append('kwargs=%s' % repr(self.kwargs))
|
|
160 |
|
|
161 |
elems.append('plugin=%s' % self.plugin.name)
|
|
162 |
return u'<handler(%s)>' % ', '.join(elems)
|
|
163 |
|
|
164 |
|
|
165 |
def call(self, uzbl, *args, **kwargs):
|
|
166 |
'''Execute the handler function and merge argument lists.'''
|
|
167 |
|
|
168 |
args = args + self.args
|
|
169 |
kwargs = dict(self.kwargs.items() + kwargs.items())
|
|
170 |
self.callback(uzbl, *args, **kwargs)
|
|
171 |
|
|
172 |
|
|
173 |
|
|
174 |
|
|
175 |
|
|
176 |
class Plugin(object):
|
|
177 |
'''Plugin module wrapper object.'''
|
|
178 |
|
|
179 |
# Special functions exported from the Plugin instance to the
|
|
180 |
# plugin namespace.
|
|
181 |
special_functions = ['require', 'export', 'export_dict', 'connect',
|
|
182 |
'connect_dict', 'logger', 'unquote', 'splitquoted']
|
|
183 |
|
|
184 |
|
|
185 |
def __init__(self, parent, name, path, plugin):
|
|
186 |
self.parent = parent
|
|
187 |
self.name = name
|
|
188 |
self.path = path
|
|
189 |
self.plugin = plugin
|
|
190 |
self.logger = get_logger('plugin.%s' % name)
|
|
191 |
|
|
192 |
# Weakrefs to all handlers created by this plugin
|
|
193 |
self.handlers = set([])
|
|
194 |
|
|
195 |
# Plugins init hook
|
|
196 |
init = getattr(plugin, 'init', None)
|
|
197 |
self.init = init if callable(init) else None
|
|
198 |
|
|
199 |
# Plugins optional after hook
|
|
200 |
after = getattr(plugin, 'after', None)
|
|
201 |
self.after = after if callable(after) else None
|
|
202 |
|
|
203 |
# Plugins optional cleanup hook
|
|
204 |
cleanup = getattr(plugin, 'cleanup', None)
|
|
205 |
self.cleanup = cleanup if callable(cleanup) else None
|
|
206 |
|
|
207 |
assert init or after or cleanup, "missing hooks in plugin"
|
|
208 |
|
|
209 |
# Export plugin's instance methods to plugin namespace
|
|
210 |
for attr in self.special_functions:
|
|
211 |
plugin.__dict__[attr] = getattr(self, attr)
|
|
212 |
|
|
213 |
|
|
214 |
def __repr__(self):
|
|
215 |
return u'<plugin(%r)>' % self.plugin
|
|
216 |
|
|
217 |
|
|
218 |
def export(self, uzbl, attr, object, prepend=True):
|
|
219 |
'''Attach `object` to `uzbl` instance. This is the preferred method
|
|
220 |
of sharing functionality, functions, data and objects between
|
|
221 |
plugins.
|
|
222 |
|
|
223 |
If the object is callable you may wish to turn the callable object
|
|
224 |
in to a meta-instance-method by prepending `uzbl` to the call stack.
|
|
225 |
You can change this behaviour with the `prepend` argument.
|
|
226 |
'''
|
|
227 |
|
|
228 |
assert attr not in uzbl.exports, "attr %r already exported by %r" %\
|
|
229 |
(attr, uzbl.exports[attr][0])
|
|
230 |
|
|
231 |
prepend = True if prepend and callable(object) else False
|
|
232 |
uzbl.__dict__[attr] = partial(object, uzbl) if prepend else object
|
|
233 |
uzbl.exports[attr] = (self, object, prepend)
|
|
234 |
uzbl.logger.info('exported %r to %r by plugin %r, prepended %r'
|
|
235 |
% (object, 'uzbl.%s' % attr, self.name, prepend))
|
|
236 |
|
|
237 |
|
|
238 |
def export_dict(self, uzbl, exports):
|
|
239 |
for (attr, object) in exports.items():
|
|
240 |
self.export(uzbl, attr, object)
|
|
241 |
|
|
242 |
|
|
243 |
def find_handler(self, event, callback, args, kwargs):
|
|
244 |
'''Check if a handler with the identical callback and arguments
|
|
245 |
exists and return it.'''
|
|
246 |
|
|
247 |
# Remove dead refs
|
|
248 |
self.handlers -= set(filter(lambda ref: not ref(), self.handlers))
|
|
249 |
|
|
250 |
# Find existing identical handler
|
|
251 |
for handler in [ref() for ref in self.handlers]:
|
|
252 |
if handler.event == event and handler.callback == callback \
|
|
253 |
and handler.args == args and handler.kwargs == kwargs:
|
|
254 |
return handler
|
|
255 |
|
|
256 |
|
|
257 |
def connect(self, uzbl, event, callback, *args, **kwargs):
|
|
258 |
'''Create an event handler object which handles `event` events.
|
|
259 |
|
|
260 |
Arguments passed to the connect function (`args` and `kwargs`) are
|
|
261 |
stored in the handler object and merged with the event arguments
|
|
262 |
come handler execution.
|
|
263 |
|
|
264 |
All handler functions must behave like a `uzbl` instance-method (that
|
|
265 |
means `uzbl` is prepended to the callback call arguments).'''
|
|
266 |
|
|
267 |
# Sanitise and check event name
|
|
268 |
event = event.upper().strip()
|
|
269 |
assert event and ' ' not in event
|
|
270 |
|
|
271 |
assert callable(callback), 'callback must be callable'
|
|
272 |
|
|
273 |
# Check if an identical handler already exists
|
|
274 |
handler = self.find_handler(event, callback, args, kwargs)
|
|
275 |
if not handler:
|
|
276 |
# Create a new handler
|
|
277 |
handler = EventHandler(self, event, callback, args, kwargs)
|
|
278 |
self.handlers.add(weakref.ref(handler))
|
|
279 |
self.logger.info('new %r' % handler)
|
|
280 |
|
|
281 |
uzbl.handlers[event].append(handler)
|
|
282 |
uzbl.logger.info('connected %r' % handler)
|
|
283 |
return handler
|
|
284 |
|
|
285 |
|
|
286 |
def connect_dict(self, uzbl, connects):
|
|
287 |
for (event, callback) in connects.items():
|
|
288 |
self.connect(uzbl, event, callback)
|
|
289 |
|
|
290 |
|
|
291 |
def require(self, plugin):
|
|
292 |
'''Check that plugin with name `plugin` has been loaded. Use this to
|
|
293 |
ensure that your plugins dependencies have been met.'''
|
|
294 |
|
|
295 |
assert plugin in self.parent.plugins, self.logger.critical(
|
|
296 |
'plugin %r required by plugin %r' (plugin, self.name))
|
|
297 |
|
|
298 |
@classmethod
|
|
299 |
def unquote(cls, s):
|
|
300 |
'''Removes quotation marks around strings if any and interprets
|
|
301 |
\\-escape sequences using `string_escape`'''
|
|
302 |
if s and s[0] == s[-1] and s[0] in ['"', "'"]:
|
|
303 |
s = s[1:-1]
|
|
304 |
return s.encode('utf-8').decode('string_escape').decode('utf-8')
|
|
305 |
|
|
306 |
_splitquoted = re.compile("( |\"(?:\\\\.|[^\"])*?\"|'(?:\\\\.|[^'])*?')")
|
|
307 |
@classmethod
|
|
308 |
def splitquoted(cls, text):
|
|
309 |
'''Splits string on whitespace while respecting quotations'''
|
|
310 |
return [cls.unquote(p) for p in cls._splitquoted.split(text) if p.strip()]
|
|
311 |
|
|
312 |
|
|
313 |
class Uzbl(object):
|
|
314 |
def __init__(self, parent, child_socket):
|
|
315 |
self.opts = opts
|
|
316 |
self.parent = parent
|
|
317 |
self.child_socket = child_socket
|
|
318 |
self.time = time.time()
|
|
319 |
self.pid = None
|
|
320 |
self.name = None
|
|
321 |
|
|
322 |
# Flag if the instance has raised the INSTANCE_START event.
|
|
323 |
self.instance_start = False
|
|
324 |
|
|
325 |
# Use name "unknown" until name is discovered.
|
|
326 |
self.logger = get_logger('uzbl-instance[]')
|
|
327 |
|
|
328 |
# Track plugin event handlers and exported functions.
|
|
329 |
self.exports = {}
|
|
330 |
self.handlers = defaultdict(list)
|
|
331 |
|
|
332 |
# Internal vars
|
|
333 |
self._depth = 0
|
|
334 |
self._buffer = ''
|
|
335 |
|
|
336 |
|
|
337 |
def __repr__(self):
|
|
338 |
return '<uzbl(%s)>' % ', '.join([
|
|
339 |
'pid=%s' % (self.pid if self.pid else "Unknown"),
|
|
340 |
'name=%s' % ('%r' % self.name if self.name else "Unknown"),
|
|
341 |
'uptime=%f' % (time.time()-self.time),
|
|
342 |
'%d exports' % len(self.exports.keys()),
|
|
343 |
'%d handlers' % sum([len(l) for l in self.handlers.values()])])
|
|
344 |
|
|
345 |
|
|
346 |
def init_plugins(self):
|
|
347 |
'''Call the init and after hooks in all loaded plugins for this
|
|
348 |
instance.'''
|
|
349 |
|
|
350 |
# Initialise each plugin with the current uzbl instance.
|
|
351 |
for plugin in self.parent.plugins.values():
|
|
352 |
if plugin.init:
|
|
353 |
self.logger.debug('calling %r plugin init hook' % plugin.name)
|
|
354 |
plugin.init(self)
|
|
355 |
|
|
356 |
# Allow plugins to use exported features of other plugins by calling an
|
|
357 |
# optional `after` function in the plugins namespace.
|
|
358 |
for plugin in self.parent.plugins.values():
|
|
359 |
if plugin.after:
|
|
360 |
self.logger.debug('calling %r plugin after hook'%plugin.name)
|
|
361 |
plugin.after(self)
|
|
362 |
|
|
363 |
|
|
364 |
def send(self, msg):
|
|
365 |
'''Send a command to the uzbl instance via the child socket
|
|
366 |
instance.'''
|
|
367 |
|
|
368 |
msg = msg.strip()
|
|
369 |
assert self.child_socket, "socket inactive"
|
|
370 |
|
|
371 |
if opts.print_events:
|
|
372 |
print ascii(u'%s<-- %s' % (' ' * self._depth, msg))
|
|
373 |
|
|
374 |
self.child_socket.send(ascii("%s\n" % msg))
|
|
375 |
|
|
376 |
|
|
377 |
def read(self):
|
|
378 |
'''Read data from the child socket and pass lines to the parse_msg
|
|
379 |
function.'''
|
|
380 |
|
|
381 |
try:
|
|
382 |
raw = unicode(self.child_socket.recv(8192), 'utf-8', 'ignore')
|
|
383 |
if not raw:
|
|
384 |
self.logger.debug('read null byte')
|
|
385 |
return self.close()
|
|
386 |
|
|
387 |
except:
|
|
388 |
self.logger.error(get_exc())
|
|
389 |
return self.close()
|
|
390 |
|
|
391 |
lines = (self._buffer + raw).split('\n')
|
|
392 |
self._buffer = lines.pop()
|
|
393 |
|
|
394 |
for line in filter(None, map(unicode.strip, lines)):
|
|
395 |
try:
|
|
396 |
self.parse_msg(line.strip())
|
|
397 |
|
|
398 |
except:
|
|
399 |
self.logger.error(get_exc())
|
|
400 |
self.logger.error('erroneous event: %r' % line)
|
|
401 |
|
|
402 |
|
|
403 |
def parse_msg(self, line):
|
|
404 |
'''Parse an incoming message from a uzbl instance. Event strings
|
|
405 |
will be parsed into `self.event(event, args)`.'''
|
|
406 |
|
|
407 |
# Split by spaces (and fill missing with nulls)
|
|
408 |
elems = (line.split(' ', 3) + ['',]*3)[:4]
|
|
409 |
|
|
410 |
# Ignore non-event messages.
|
|
411 |
if elems[0] != 'EVENT':
|
|
412 |
logger.info('non-event message: %r' % line)
|
|
413 |
if opts.print_events:
|
|
414 |
print '--- %s' % ascii(line)
|
|
415 |
return
|
|
416 |
|
|
417 |
# Check event string elements
|
|
418 |
(name, event, args) = elems[1:]
|
|
419 |
assert name and event, 'event string missing elements'
|
|
420 |
if not self.name:
|
|
421 |
self.name = name
|
|
422 |
self.logger = get_logger('uzbl-instance%s' % name)
|
|
423 |
self.logger.info('found instance name %r' % name)
|
|
424 |
|
|
425 |
assert self.name == name, 'instance name mismatch'
|
|
426 |
|
|
427 |
# Handle the event with the event handlers through the event method
|
|
428 |
self.event(event, args)
|
|
429 |
|
|
430 |
|
|
431 |
def event(self, event, *args, **kargs):
|
|
432 |
'''Raise an event.'''
|
|
433 |
|
|
434 |
event = event.upper()
|
|
435 |
|
|
436 |
if not opts.daemon_mode and opts.print_events:
|
|
437 |
elems = [event,]
|
|
438 |
if args: elems.append(unicode(args))
|
|
439 |
if kargs: elems.append(unicode(kargs))
|
|
440 |
print ascii(u'%s--> %s' % (' ' * self._depth, ' '.join(elems)))
|
|
441 |
|
|
442 |
if event == "INSTANCE_START" and args:
|
|
443 |
assert not self.instance_start, 'instance already started'
|
|
444 |
|
|
445 |
self.pid = int(args[0])
|
|
446 |
self.logger.info('found instance pid %r' % self.pid)
|
|
447 |
|
|
448 |
self.init_plugins()
|
|
449 |
|
|
450 |
elif event == "INSTANCE_EXIT":
|
|
451 |
self.logger.info('uzbl instance exit')
|
|
452 |
self.close()
|
|
453 |
|
|
454 |
if event not in self.handlers:
|
|
455 |
return
|
|
456 |
|
|
457 |
for handler in self.handlers[event]:
|
|
458 |
self._depth += 1
|
|
459 |
try:
|
|
460 |
handler.call(self, *args, **kargs)
|
|
461 |
|
|
462 |
except:
|
|
463 |
self.logger.error(get_exc())
|
|
464 |
|
|
465 |
self._depth -= 1
|
|
466 |
|
|
467 |
|
|
468 |
def close_connection(self, child_socket):
|
|
469 |
'''Close child socket and delete the uzbl instance created for that
|
|
470 |
child socket connection.'''
|
|
471 |
|
|
472 |
|
|
473 |
def close(self):
|
|
474 |
'''Close the client socket and call the plugin cleanup hooks.'''
|
|
475 |
|
|
476 |
self.logger.debug('called close method')
|
|
477 |
|
|
478 |
# Remove self from parent uzbls dict.
|
|
479 |
if self.child_socket in self.parent.uzbls:
|
|
480 |
self.logger.debug('removing self from uzbls list')
|
|
481 |
del self.parent.uzbls[self.child_socket]
|
|
482 |
|
|
483 |
try:
|
|
484 |
if self.child_socket:
|
|
485 |
self.logger.debug('closing child socket')
|
|
486 |
self.child_socket.close()
|
|
487 |
|
|
488 |
except:
|
|
489 |
self.logger.error(get_exc())
|
|
490 |
|
|
491 |
finally:
|
|
492 |
self.child_socket = None
|
|
493 |
|
|
494 |
# Call plugins cleanup hooks.
|
|
495 |
for plugin in self.parent.plugins.values():
|
|
496 |
if plugin.cleanup:
|
|
497 |
self.logger.debug('calling %r plugin cleanup hook'
|
|
498 |
% plugin.name)
|
|
499 |
plugin.cleanup(self)
|
|
500 |
|
|
501 |
logger.info('removed %r' % self)
|
|
502 |
|
|
503 |
|
|
504 |
class UzblEventDaemon(object):
|
|
505 |
def __init__(self):
|
|
506 |
self.opts = opts
|
|
507 |
self.server_socket = None
|
|
508 |
self._quit = False
|
|
509 |
|
|
510 |
# Hold uzbl instances
|
|
511 |
# {child socket: Uzbl instance, ..}
|
|
512 |
self.uzbls = {}
|
|
513 |
|
|
514 |
# Hold plugins
|
|
515 |
# {plugin name: Plugin instance, ..}
|
|
516 |
self.plugins = {}
|
|
517 |
|
|
518 |
# Register that the event daemon server has started by creating the
|
|
519 |
# pid file.
|
|
520 |
make_pid_file(opts.pid_file)
|
|
521 |
|
|
522 |
# Register a function to clean up the socket and pid file on exit.
|
|
523 |
atexit.register(self.quit)
|
|
524 |
|
|
525 |
# Add signal handlers.
|
|
526 |
for sigint in [SIGTERM, SIGINT]:
|
|
527 |
signal(sigint, self.quit)
|
|
528 |
|
|
529 |
# Load plugins into self.plugins
|
|
530 |
self.load_plugins(opts.plugins)
|
|
531 |
|
|
532 |
|
|
533 |
def load_plugins(self, plugins):
|
|
534 |
'''Load event manager plugins.'''
|
|
535 |
|
|
536 |
for path in plugins:
|
|
537 |
logger.debug('loading plugin %r' % path)
|
|
538 |
(dir, file) = os.path.split(path)
|
|
539 |
name = file[:-3] if file.lower().endswith('.py') else file
|
|
540 |
|
|
541 |
info = imp.find_module(name, [dir,])
|
|
542 |
module = imp.load_module(name, *info)
|
|
543 |
|
|
544 |
# Check if the plugin has a callable hook.
|
|
545 |
hooks = filter(callable, [getattr(module, attr, None) \
|
|
546 |
for attr in ['init', 'after', 'cleanup']])
|
|
547 |
assert hooks, "no hooks in plugin %r" % module
|
|
548 |
|
|
549 |
logger.debug('creating plugin instance for %r plugin' % name)
|
|
550 |
plugin = Plugin(self, name, path, module)
|
|
551 |
self.plugins[name] = plugin
|
|
552 |
logger.info('new %r' % plugin)
|
|
553 |
|
|
554 |
|
|
555 |
def create_server_socket(self):
|
|
556 |
'''Create the event manager daemon socket for uzbl instance duplex
|
|
557 |
communication.'''
|
|
558 |
|
|
559 |
# Close old socket.
|
|
560 |
self.close_server_socket()
|
|
561 |
|
|
562 |
sock = socket(AF_UNIX, SOCK_STREAM)
|
|
563 |
sock.bind(opts.server_socket)
|
|
564 |
sock.listen(5)
|
|
565 |
|
|
566 |
self.server_socket = sock
|
|
567 |
logger.debug('bound server socket to %r' % opts.server_socket)
|
|
568 |
|
|
569 |
|
|
570 |
def run(self):
|
|
571 |
'''Main event daemon loop.'''
|
|
572 |
|
|
573 |
logger.debug('entering main loop')
|
|
574 |
|
|
575 |
# Create and listen on the server socket
|
|
576 |
self.create_server_socket()
|
|
577 |
|
|
578 |
if opts.daemon_mode:
|
|
579 |
# Daemonize the process
|
|
580 |
daemonize()
|
|
581 |
|
|
582 |
# Update the pid file
|
|
583 |
make_pid_file(opts.pid_file)
|
|
584 |
|
|
585 |
try:
|
|
586 |
# Accept incoming connections and listen for incoming data
|
|
587 |
self.listen()
|
|
588 |
|
|
589 |
except:
|
|
590 |
if not self._quit:
|
|
591 |
logger.critical(get_exc())
|
|
592 |
|
|
593 |
# Clean up and exit
|
|
594 |
self.quit()
|
|
595 |
|
|
596 |
logger.debug('exiting main loop')
|
|
597 |
|
|
598 |
|
|
599 |
def listen(self):
|
|
600 |
'''Accept incoming connections and constantly poll instance sockets
|
|
601 |
for incoming data.'''
|
|
602 |
|
|
603 |
logger.info('listening on %r' % opts.server_socket)
|
|
604 |
|
|
605 |
# Count accepted connections
|
|
606 |
connections = 0
|
|
607 |
|
|
608 |
while (self.uzbls or not connections) or (not opts.auto_close):
|
|
609 |
socks = [self.server_socket] + self.uzbls.keys()
|
|
610 |
reads, _, errors = select(socks, [], socks, 1)
|
|
611 |
|
|
612 |
if self.server_socket in reads:
|
|
613 |
reads.remove(self.server_socket)
|
|
614 |
|
|
615 |
# Accept connection and create uzbl instance.
|
|
616 |
child_socket = self.server_socket.accept()[0]
|
|
617 |
self.uzbls[child_socket] = Uzbl(self, child_socket)
|
|
618 |
connections += 1
|
|
619 |
|
|
620 |
for uzbl in [self.uzbls[s] for s in reads]:
|
|
621 |
uzbl.read()
|
|
622 |
|
|
623 |
for uzbl in [self.uzbls[s] for s in errors]:
|
|
624 |
uzbl.logger.error('socket read error')
|
|
625 |
uzbl.close()
|
|
626 |
|
|
627 |
logger.info('auto closing')
|
|
628 |
|
|
629 |
|
|
630 |
def close_server_socket(self):
|
|
631 |
'''Close and delete the server socket.'''
|
|
632 |
|
|
633 |
try:
|
|
634 |
if self.server_socket:
|
|
635 |
logger.debug('closing server socket')
|
|
636 |
self.server_socket.close()
|
|
637 |
self.server_socket = None
|
|
638 |
|
|
639 |
if os.path.exists(opts.server_socket):
|
|
640 |
logger.info('unlinking %r' % opts.server_socket)
|
|
641 |
os.unlink(opts.server_socket)
|
|
642 |
|
|
643 |
except:
|
|
644 |
logger.error(get_exc())
|
|
645 |
|
|
646 |
|
|
647 |
def quit(self, sigint=None, *args):
|
|
648 |
'''Close all instance socket objects, server socket and delete the
|
|
649 |
pid file.'''
|
|
650 |
|
|
651 |
if sigint == SIGTERM:
|
|
652 |
logger.critical('caught SIGTERM, exiting')
|
|
653 |
|
|
654 |
elif sigint == SIGINT:
|
|
655 |
logger.critical('caught SIGINT, exiting')
|
|
656 |
|
|
657 |
elif not self._quit:
|
|
658 |
logger.debug('shutting down event manager')
|
|
659 |
|
|
660 |
self.close_server_socket()
|
|
661 |
|
|
662 |
for uzbl in self.uzbls.values():
|
|
663 |
uzbl.close()
|
|
664 |
|
|
665 |
del_pid_file(opts.pid_file)
|
|
666 |
|
|
667 |
if not self._quit:
|
|
668 |
logger.info('event manager shut down')
|
|
669 |
self._quit = True
|
|
670 |
|
|
671 |
|
|
672 |
def make_pid_file(pid_file):
|
|
673 |
'''Creates a pid file at `pid_file`, fails silently.'''
|
|
674 |
|
|
675 |
try:
|
|
676 |
logger.debug('creating pid file %r' % pid_file)
|
|
677 |
make_dirs(pid_file)
|
|
678 |
pid = os.getpid()
|
|
679 |
fileobj = open(pid_file, 'w')
|
|
680 |
fileobj.write('%d' % pid)
|
|
681 |
fileobj.close()
|
|
682 |
logger.info('created pid file %r with pid %d' % (pid_file, pid))
|
|
683 |
|
|
684 |
except:
|
|
685 |
logger.error(get_exc())
|
|
686 |
|
|
687 |
|
|
688 |
def del_pid_file(pid_file):
|
|
689 |
'''Deletes a pid file at `pid_file`, fails silently.'''
|
|
690 |
|
|
691 |
if os.path.isfile(pid_file):
|
|
692 |
try:
|
|
693 |
logger.debug('deleting pid file %r' % pid_file)
|
|
694 |
os.remove(pid_file)
|
|
695 |
logger.info('deleted pid file %r' % pid_file)
|
|
696 |
|
|
697 |
except:
|
|
698 |
logger.error(get_exc())
|
|
699 |
|
|
700 |
|
|
701 |
def get_pid(pid_file):
|
|
702 |
'''Reads a pid from pid file `pid_file`, fails None.'''
|
|
703 |
|
|
704 |
try:
|
|
705 |
logger.debug('reading pid file %r' % pid_file)
|
|
706 |
fileobj = open(pid_file, 'r')
|
|
707 |
pid = int(fileobj.read())
|
|
708 |
fileobj.close()
|
|
709 |
logger.info('read pid %d from pid file %r' % (pid, pid_file))
|
|
710 |
return pid
|
|
711 |
|
|
712 |
except (IOError, ValueError):
|
|
713 |
logger.error(get_exc())
|
|
714 |
return None
|
|
715 |
|
|
716 |
|
|
717 |
def pid_running(pid):
|
|
718 |
'''Checks if a process with a pid `pid` is running.'''
|
|
719 |
|
|
720 |
try:
|
|
721 |
os.kill(pid, 0)
|
|
722 |
except OSError:
|
|
723 |
return False
|
|
724 |
else:
|
|
725 |
return True
|
|
726 |
|
|
727 |
|
|
728 |
def term_process(pid):
|
|
729 |
'''Asks nicely then forces process with pid `pid` to exit.'''
|
|
730 |
|
|
731 |
try:
|
|
732 |
logger.info('sending SIGTERM to process with pid %r' % pid)
|
|
733 |
os.kill(pid, SIGTERM)
|
|
734 |
|
|
735 |
except OSError:
|
|
736 |
logger.error(get_exc())
|
|
737 |
|
|
738 |
logger.debug('waiting for process with pid %r to exit' % pid)
|
|
739 |
start = time.time()
|
|
740 |
while True:
|
|
741 |
if not pid_running(pid):
|
|
742 |
logger.debug('process with pid %d exit' % pid)
|
|
743 |
return True
|
|
744 |
|
|
745 |
if (time.time()-start) > 5:
|
|
746 |
logger.warning('process with pid %d failed to exit' % pid)
|
|
747 |
logger.info('sending SIGKILL to process with pid %d' % pid)
|
|
748 |
try:
|
|
749 |
os.kill(pid, SIGKILL)
|
|
750 |
except:
|
|
751 |
logger.critical(get_exc())
|
|
752 |
raise
|
|
753 |
|
|
754 |
if (time.time()-start) > 10:
|
|
755 |
logger.critical('unable to kill process with pid %d' % pid)
|
|
756 |
raise OSError
|
|
757 |
|
|
758 |
time.sleep(0.25)
|
|
759 |
|
|
760 |
|
|
761 |
def stop_action():
|
|
762 |
'''Stop the event manager daemon.'''
|
|
763 |
|
|
764 |
pid_file = opts.pid_file
|
|
765 |
if not os.path.isfile(pid_file):
|
|
766 |
logger.error('could not find running event manager with pid file %r'
|
|
767 |
% opts.pid_file)
|
|
768 |
return
|
|
769 |
|
|
770 |
pid = get_pid(pid_file)
|
|
771 |
if not pid_running(pid):
|
|
772 |
logger.debug('no process with pid %r' % pid)
|
|
773 |
del_pid_file(pid_file)
|
|
774 |
return
|
|
775 |
|
|
776 |
logger.debug('terminating process with pid %r' % pid)
|
|
777 |
term_process(pid)
|
|
778 |
del_pid_file(pid_file)
|
|
779 |
logger.info('stopped event manager process with pid %d' % pid)
|
|
780 |
|
|
781 |
|
|
782 |
def start_action():
|
|
783 |
'''Start the event manager daemon.'''
|
|
784 |
|
|
785 |
pid_file = opts.pid_file
|
|
786 |
if os.path.isfile(pid_file):
|
|
787 |
pid = get_pid(pid_file)
|
|
788 |
if pid_running(pid):
|
|
789 |
logger.error('event manager already started with pid %d' % pid)
|
|
790 |
return
|
|
791 |
|
|
792 |
logger.info('no process with pid %d' % pid)
|
|
793 |
del_pid_file(pid_file)
|
|
794 |
|
|
795 |
UzblEventDaemon().run()
|
|
796 |
|
|
797 |
|
|
798 |
def restart_action():
|
|
799 |
'''Restart the event manager daemon.'''
|
|
800 |
|
|
801 |
stop_action()
|
|
802 |
start_action()
|
|
803 |
|
|
804 |
|
|
805 |
def list_action():
|
|
806 |
'''List all the plugins that would be loaded in the current search
|
|
807 |
dirs.'''
|
|
808 |
|
|
809 |
names = {}
|
|
810 |
for plugin in opts.plugins:
|
|
811 |
(head, tail) = os.path.split(plugin)
|
|
812 |
if tail not in names:
|
|
813 |
names[tail] = plugin
|
|
814 |
|
|
815 |
for plugin in sorted(names.values()):
|
|
816 |
print plugin
|
|
817 |
|
|
818 |
|
|
819 |
if __name__ == "__main__":
|
|
820 |
parser = OptionParser('usage: %prog [options] {start|stop|restart|list}')
|
|
821 |
add = parser.add_option
|
|
822 |
|
|
823 |
add('-v', '--verbose',
|
|
824 |
dest='verbose', default=2, action='count',
|
|
825 |
help='increase verbosity')
|
|
826 |
|
|
827 |
add('-d', '--plugin-dir',
|
|
828 |
dest='plugin_dirs', action='append', metavar="DIR", default=[],
|
|
829 |
help='add extra plugin search dir, same as `-l "DIR/*.py"`')
|
|
830 |
|
|
831 |
add('-l', '--load-plugin',
|
|
832 |
dest='load_plugins', action='append', metavar="PLUGIN", default=[],
|
|
833 |
help='load plugin, loads before plugins in search dirs')
|
|
834 |
|
|
835 |
socket_location = os.path.join(CACHE_DIR, 'event_daemon')
|
|
836 |
add('-s', '--server-socket',
|
|
837 |
dest='server_socket', metavar="SOCKET", default=socket_location,
|
|
838 |
help='server AF_UNIX socket location')
|
|
839 |
|
|
840 |
add('-p', '--pid-file',
|
|
841 |
metavar="FILE", dest='pid_file',
|
|
842 |
help='pid file location, defaults to server socket + .pid')
|
|
843 |
|
|
844 |
add('-n', '--no-daemon',
|
|
845 |
dest='daemon_mode', action='store_false', default=True,
|
|
846 |
help='do not daemonize the process')
|
|
847 |
|
|
848 |
add('-a', '--auto-close',
|
|
849 |
dest='auto_close', action='store_true', default=False,
|
|
850 |
help='auto close after all instances disconnect')
|
|
851 |
|
|
852 |
add('-i', '--no-default-dirs',
|
|
853 |
dest='default_dirs', action='store_false', default=True,
|
|
854 |
help='ignore the default plugin search dirs')
|
|
855 |
|
|
856 |
add('-o', '--log-file',
|
|
857 |
dest='log_file', metavar='FILE',
|
|
858 |
help='write logging output to a file, defaults to server socket +'
|
|
859 |
' .log')
|
|
860 |
|
|
861 |
add('-q', '--quiet-events',
|
|
862 |
dest='print_events', action="store_false", default=True,
|
|
863 |
help="silence the printing of events to stdout")
|
|
864 |
|
|
865 |
(opts, args) = parser.parse_args()
|
|
866 |
|
|
867 |
opts.server_socket = expandpath(opts.server_socket)
|
|
868 |
|
|
869 |
# Set default pid file location
|
|
870 |
if not opts.pid_file:
|
|
871 |
opts.pid_file = "%s.pid" % opts.server_socket
|
|
872 |
|
|
873 |
else:
|
|
874 |
opts.pid_file = expandpath(opts.pid_file)
|
|
875 |
|
|
876 |
# Set default log file location
|
|
877 |
if not opts.log_file:
|
|
878 |
opts.log_file = "%s.log" % opts.server_socket
|
|
879 |
|
|
880 |
else:
|
|
881 |
opts.log_file = expandpath(opts.log_file)
|
|
882 |
|
|
883 |
# Logging setup
|
|
884 |
log_level = logging.CRITICAL - opts.verbose*10
|
|
885 |
|
|
886 |
# Console logging handler
|
|
887 |
ch = logging.StreamHandler()
|
|
888 |
ch.setLevel(max(log_level+10, 10))
|
|
889 |
ch.setFormatter(logging.Formatter(
|
|
890 |
'%(name)s: %(levelname)s: %(message)s'))
|
|
891 |
|
|
892 |
# File logging handler
|
|
893 |
fh = logging.FileHandler(opts.log_file, 'w', 'utf-8', 1)
|
|
894 |
fh.setLevel(max(log_level, 10))
|
|
895 |
fh.setFormatter(logging.Formatter(
|
|
896 |
'[%(created)f] %(name)s: %(levelname)s: %(message)s'))
|
|
897 |
|
|
898 |
# logging.getLogger wrapper which sets the levels and adds the
|
|
899 |
# file and console handlers automagically
|
|
900 |
def get_logger(name):
|
|
901 |
handlers = [ch, fh]
|
|
902 |
level = [max(log_level, 10),]
|
|
903 |
logger = logging.getLogger(name)
|
|
904 |
logger.setLevel(level[0])
|
|
905 |
for handler in handlers:
|
|
906 |
logger.addHandler(handler)
|
|
907 |
|
|
908 |
return logger
|
|
909 |
|
|
910 |
# Get main logger
|
|
911 |
logger = get_logger(SCRIPTNAME)
|
|
912 |
logger.info('logging to %r' % opts.log_file)
|
|
913 |
|
|
914 |
plugins = {}
|
|
915 |
|
|
916 |
# Load all `opts.load_plugins` into the plugins list
|
|
917 |
for path in opts.load_plugins:
|
|
918 |
path = expandpath(path)
|
|
919 |
matches = glob(path)
|
|
920 |
if not matches:
|
|
921 |
parser.error('cannot find plugin(s): %r' % path)
|
|
922 |
|
|
923 |
for plugin in matches:
|
|
924 |
(head, tail) = os.path.split(plugin)
|
|
925 |
if tail not in plugins:
|
|
926 |
logger.debug('found plugin: %r' % plugin)
|
|
927 |
plugins[tail] = plugin
|
|
928 |
|
|
929 |
else:
|
|
930 |
logger.debug('ignoring plugin: %r' % plugin)
|
|
931 |
|
|
932 |
# Add default plugin locations
|
|
933 |
if opts.default_dirs:
|
|
934 |
logger.debug('adding default plugin dirs to plugin dirs list')
|
|
935 |
opts.plugin_dirs += [os.path.join(DATA_DIR, 'plugins/'),
|
|
936 |
os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')]
|
|
937 |
|
|
938 |
else:
|
|
939 |
logger.debug('ignoring default plugin dirs')
|
|
940 |
|
|
941 |
# Load all plugins in `opts.plugin_dirs` into the plugins list
|
|
942 |
for dir in opts.plugin_dirs:
|
|
943 |
dir = expandpath(dir)
|
|
944 |
logger.debug('searching plugin dir: %r' % dir)
|
|
945 |
for plugin in glob(os.path.join(dir, '*.py')):
|
|
946 |
(head, tail) = os.path.split(plugin)
|
|
947 |
if tail not in plugins:
|
|
948 |
logger.debug('found plugin: %r' % plugin)
|
|
949 |
plugins[tail] = plugin
|
|
950 |
|
|
951 |
else:
|
|
952 |
logger.debug('ignoring plugin: %r' % plugin)
|
|
953 |
|
|
954 |
plugins = plugins.values()
|
|
955 |
|
|
956 |
# Check all the paths in the plugins list are files
|
|
957 |
for plugin in plugins:
|
|
958 |
if not os.path.isfile(plugin):
|
|
959 |
parser.error('plugin not a file: %r' % plugin)
|
|
960 |
|
|
961 |
if opts.auto_close: logger.debug('will auto close')
|
|
962 |
else: logger.debug('will not auto close')
|
|
963 |
|
|
964 |
if opts.daemon_mode: logger.debug('will daemonize')
|
|
965 |
else: logger.debug('will not daemonize')
|
|
966 |
|
|
967 |
opts.plugins = plugins
|
|
968 |
|
|
969 |
# init like {start|stop|..} daemon actions
|
|
970 |
daemon_actions = {'start': start_action, 'stop': stop_action,
|
|
971 |
'restart': restart_action, 'list': list_action}
|
|
972 |
|
|
973 |
if len(args) == 1:
|
|
974 |
action = args[0]
|
|
975 |
if action not in daemon_actions:
|
|
976 |
parser.error('invalid action: %r' % action)
|
|
977 |
|
|
978 |
elif not args:
|
|
979 |
logger.warning('no daemon action given, assuming %r' % 'start')
|
|
980 |
action = 'start'
|
|
981 |
|
|
982 |
else:
|
|
983 |
parser.error('invalid action argument: %r' % args)
|
|
984 |
|
|
985 |
logger.info('daemon action %r' % action)
|
|
986 |
# Do action
|
|
987 |
daemon_actions[action]()
|
|
988 |
|
|
989 |
logger.debug('process CPU time: %f' % time.clock())
|
|
990 |
|
|
991 |
# vi: set et ts=4:
|