Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

inotify-daemon 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. ###
  4. # Copyright (c) 2009-2010 by Elián Hanisch <lambdae2@gmail.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. ###
  19. """\
  20. inotify-daemon [arguments ...]
  21. Notification daemon that supports libnotify or dbus.
  22. Download latest stable from:
  23. http://github.com/m4v/inotify-daemon/raw/stable/inotify-daemon
  24. This is intended to be used with WeeChat's inotify script, but any
  25. xmlrpc capable client can use it.
  26. It is recommended to use your desktop autorun settings for initialise
  27. the daemon on login, this saves you the problem of losing dbus with
  28. each login. Still you can use inotify-daemon as a detached process
  29. with screen, dtach or on boot with crontab, the daemon should be able
  30. to find a suitable dbus session if available. inotify-daemon *must*
  31. be always run with your user's privileges, otherwise it will fail to
  32. get a dbus session if it loses it.
  33. Autostart examples:
  34. - In KDE make a symlink in $HOME/.kde/Autostart to inotify-daemon.
  35. - With crontab, add the following line: @reboot <path to
  36. inotify-daemon> --host <address> --port <port>
  37. Notify methods:
  38. - libnotify: Use libnotify for notifications, needs python-notify
  39. installed. This is the default method.
  40. - dbus: Uses dbus directly for notifications, this is KDE4 specific,
  41. might not work in other desktops. Needs python-dbus.\
  42. """
  43. __version__ = '0.2'
  44. __author__ = 'Elián Hanisch <lambdae2@gmail.com>'
  45. __copyright__ = 'Copyright (c) 2009-2010 by Elián Hanisch <lambdae2@gmail.com>'
  46. import xmlrpclib, socket, os, sys, subprocess, optparse
  47. from SimpleXMLRPCServer import SimpleXMLRPCServer
  48. global host, port, server
  49. class NotifyError(Exception):
  50. pass
  51. class Daemon(SimpleXMLRPCServer):
  52. stopped = False
  53. shutdown = False
  54. def __init__(self, addr, passwd=''):
  55. SimpleXMLRPCServer.__init__(self, addr)
  56. self.passwd = passwd
  57. self.register_function(self.quit)
  58. self.register_function(self.restart)
  59. self.register_function(lambda : __version__, 'version')
  60. def serve_forever(self):
  61. while not self.stopped:
  62. self.handle_request()
  63. if self.shutdown:
  64. self._restart_daemon()
  65. def quit(self, passwd=''):
  66. """Allows to kill the server remotely."""
  67. if self.passwd and passwd != self.passwd:
  68. return 'Invalid password.'
  69. print >>sys.stderr, 'Stopping server.'
  70. self.server_close()
  71. self.stopped = True
  72. return 'OK'
  73. def restart(self, passwd=''):
  74. """Allows to restart the server remotely."""
  75. if self.passwd and passwd != self.passwd:
  76. return 'Invalid password.'
  77. print >>sys.stderr, 'Restarting server.'
  78. self.force_restart()
  79. return 'OK'
  80. def force_restart(self):
  81. self.shutdown = True
  82. def _restart_daemon(self):
  83. self.server_close()
  84. self.stopped = True
  85. # spawn a new daemon and exit
  86. subprocess.Popen(' '.join(sys.argv), shell=True)
  87. sys.exit(0)
  88. dbus_env = 'DBUS_SESSION_BUS_ADDRESS'
  89. class Notifications(object):
  90. def __init__(self, method, passwd=''):
  91. self.method = method
  92. self.passwd = passwd
  93. def _dispatch(self, method, args):
  94. passwd = args[0]
  95. if self.passwd and passwd != self.passwd:
  96. return 'Invalid password.'
  97. if method in ('any', 'notify'):
  98. method = self.method
  99. if not hasattr(self, method):
  100. return 'Invalid notification method.'
  101. args = args[1:]
  102. return getattr(self, method)(*args)
  103. def _timeout(self, s=None):
  104. if s is not None:
  105. t = len(s) * 150
  106. if t < 5000:
  107. return 5000
  108. elif t > 60000:
  109. return 60000
  110. else:
  111. t = 10000
  112. return t
  113. def _getDBUSSession(self):
  114. print >>sys.stderr, 'Lost dbus daemon, trying to find a new one... '
  115. if self.method == 'dbus':
  116. testMethod = lambda address : testDBUS(test_dbus, address)
  117. elif self.method == 'libnotify':
  118. testMethod = lambda address : testDBUS(test_libnotify, address)
  119. else:
  120. raise NotifyError, 'No suitable method for test dbus found.'
  121. return getDBUS(testMethod)
  122. def _updateDBUS(self, address):
  123. os.environ[dbus_env] = address
  124. global server
  125. print >>sys.stderr, 'Updated dbus address, restarting daemon...'
  126. server.force_restart()
  127. return 'warning:Lost dbus, notification daemon is being restarted with new address.'
  128. def dbus(self, text, channel=None):
  129. global host
  130. try:
  131. import dbus
  132. except:
  133. return 'Failed to import dbus, is the module installed in %s?' %host
  134. try:
  135. bus = dbus.SessionBus()
  136. notify_object = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
  137. notify = dbus.Interface(notify_object, 'org.freedesktop.Notifications')
  138. notify.Notify('', 0, '', channel or 'notification', text, '', {}, self._timeout(text))
  139. return 'OK'
  140. except:
  141. try:
  142. address = self._getDBUSSession()
  143. except NotifyError, e:
  144. print >>sys.stderr, str(e)
  145. return 'Failed to connect with the dbus daemon in %s.' %host
  146. return self._updateDBUS(address)
  147. def libnotify(self, text, channel=None):
  148. global host
  149. try:
  150. import pynotify
  151. except:
  152. return 'Failed to import pynotify, is the module installed in %s?' %host
  153. try:
  154. if pynotify.init('WeeChat notification'):
  155. notify = pynotify.Notification(channel or 'notification', text)
  156. notify.set_timeout(self._timeout(text))
  157. notify.show()
  158. return 'OK'
  159. else:
  160. raise Exception
  161. except:
  162. try:
  163. address = self._getDBUSSession()
  164. except NotifyError, e:
  165. print >>sys.stderr, str(e)
  166. return 'Failed to connect with the dbus daemon in %s.' %host
  167. return self._updateDBUS(address)
  168. # cmd used for test dbus session with dbus module
  169. test_dbus = """
  170. python -c '
  171. import dbus, sys
  172. try:
  173. dbus.SessionBus()
  174. except:
  175. sys.exit(0)
  176. else:
  177. sys.exit(1)'
  178. """
  179. # cmd used for test dbus session with pynotify module
  180. test_libnotify = """
  181. python -c '
  182. import pynotify, sys
  183. if pynotify.init("test"):
  184. sys.exit(1)
  185. else:
  186. sys.exit(0)' 2> /dev/null
  187. """
  188. def testDBUS(cmd, address):
  189. os.environ[dbus_env] = address
  190. return subprocess.call(cmd, shell=True)
  191. def getDBUS(testMethod):
  192. """
  193. There might be times where the dbus daemon is restarted and its address changes, making
  194. notifications impossible. Here we'll try for fetch another from our user processes. This isn't
  195. nice but I couldn't find other way, dbus actually doesn't have a nice method for get a session
  196. address from outside the session."""
  197. process_list = [ s for s in os.listdir('/proc') if s.isdigit() ]
  198. our_uid = os.getuid()
  199. dbus_invalid_address = set()
  200. dbus_address = None
  201. for process in process_list:
  202. environ = os.path.join('/proc', process, 'environ')
  203. if os.stat(environ).st_uid == our_uid:
  204. fd = open(environ)
  205. for line in fd.read().split('\x00'):
  206. if line.startswith(dbus_env):
  207. dbus_address = line[len(dbus_env)+1:]
  208. if dbus_address not in dbus_invalid_address:
  209. print >>sys.stderr, 'Testing with %s ... ' %dbus_address,
  210. if testMethod(dbus_address):
  211. print >>sys.stderr, 'OK'
  212. return dbus_address
  213. else:
  214. print >>sys.stderr, 'Failed'
  215. dbus_invalid_address.add(dbus_address)
  216. break
  217. raise NotifyError, 'No dbus address found.'
  218. def main(host='localhost', port=7766, method=None, passwd=''):
  219. """
  220. Main daemon loop."""
  221. global server
  222. try:
  223. server = Daemon((host, port), passwd=passwd)
  224. except socket.error, e:
  225. # another server is running? lets try to kill it
  226. try:
  227. daemon = xmlrpclib.Server('http://%s:%s' %(host, port))
  228. rt = daemon.quit(passwd)
  229. if rt == 'OK':
  230. print 'Current daemon closed.'
  231. server = Daemon((host, port), passwd=passwd)
  232. else:
  233. raise Exception(rt)
  234. except Exception, e:
  235. print >>sys.stderr, 'Failed to start daemon: %s' %e
  236. return
  237. print 'Running notification daemon...'
  238. server.register_instance(Notifications(method, passwd=passwd))
  239. server.serve_forever()
  240. def daemonize(PIDFILE='/tmp/inotify-daemon.pid'):
  241. """
  242. Forks current process into a daemon, there are several examples in the web for do this, but a
  243. double fork is needed."""
  244. # Do first fork.
  245. try:
  246. pid = os.fork()
  247. if pid > 0:
  248. # exit first parent
  249. sys.exit(0)
  250. except OSError, e:
  251. print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
  252. sys.exit(1)
  253. # make sure our executable is in absolute path
  254. daemon_abs_path = os.path.join(os.path.abspath(os.curdir), sys.argv[0])
  255. sys.argv[0] = daemon_abs_path
  256. # Decouple from parent environment.
  257. os.chdir("/")
  258. os.setsid()
  259. os.umask(0)
  260. # Do second fork.
  261. try:
  262. pid = os.fork()
  263. if pid > 0:
  264. # exit from second parent, print eventual PID before
  265. print "Daemon PID %d" % pid
  266. open(PIDFILE,'w').write("%d"%pid)
  267. sys.exit(0)
  268. except OSError, e:
  269. print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
  270. sys.exit(1)
  271. # Now I am a daemon!
  272. if __name__ == '__main__':
  273. # argument parsing
  274. usage = __doc__
  275. parser = optparse.OptionParser(usage=usage)
  276. parser.add_option('-p', '--port', dest='port', type='int',
  277. help='Port to listen. [Default: %default]', default=7766)
  278. parser.add_option('-H', '--host', dest='host',
  279. help='Daemon hostname. [Default: %default]', default='localhost')
  280. parser.add_option('-m', '--method', dest='method',
  281. help="Notification method, available: 'libnotify', 'dbus' [Default: %default]",
  282. default='libnotify')
  283. parser.add_option('-P', '--passwd', dest='passwd',
  284. help="Password for accept incoming notifications. [Default: %default]")
  285. parser.add_option('-t', '--test', dest='test', action='store_true',
  286. help='Send a test notification to an already running daemon and exit.',
  287. default=False)
  288. opts, args = parser.parse_args(sys.argv)
  289. port = opts.port
  290. host = opts.host
  291. method = opts.method
  292. passwd = opts.passwd or ''
  293. if opts.test:
  294. daemon = xmlrpclib.Server('http://%s:%s' %(host, port))
  295. print getattr(daemon, method)(passwd, 'This is a test')
  296. sys.exit(0)
  297. daemonize()
  298. main(host=host, port=port, method=method, passwd=passwd)
  299. # vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: