Added @tasks.async decorator to simplify common code.
[zeroinstall.git] / zeroinstall / injector / background.py
blobc733f87dd493d3aba415cf03a8ad9fab8dfe9ef8
1 """
2 Check for updates in a background process. If we can start a program immediately, but some of our information
3 is rather old (longer that the freshness threshold) then we run it anyway, and check for updates using a new
4 process that runs quietly in the background.
6 This avoids the need to annoy people with a 'checking for updates' box when they're trying to run things.
7 """
9 import sys, os
10 from logging import info
11 from zeroinstall.support import tasks
12 from zeroinstall.injector.iface_cache import iface_cache
13 from zeroinstall.injector import handler
15 # Copyright (C) 2007, Thomas Leonard
16 # See the README file for details, or visit http://0install.net.
18 try:
19 import dbus
20 import dbus.glib
22 session_bus = dbus.SessionBus()
24 remote_object = session_bus.get_object('org.freedesktop.Notifications',
25 '/org/freedesktop/Notifications')
27 notification_service = dbus.Interface(remote_object,
28 'org.freedesktop.Notifications')
30 # The Python bindings insist on printing a pointless introspection
31 # warning to stderr if the service is missing. Force it to be done
32 # now so we can skip it
33 old_stderr = sys.stderr
34 sys.stderr = None
35 try:
36 notification_service.GetCapabilities()
37 finally:
38 sys.stderr = old_stderr
40 have_notifications = True
41 except Exception, ex:
42 info("Failed to import D-BUS bindings: %s", ex)
43 have_notifications = False
45 LOW = 0
46 NORMAL = 1
47 CRITICAL = 2
49 def _escape_xml(s):
50 return s.replace('&', '&amp;').replace('<', '&lt;')
52 def notify(title, message, timeout = 0, actions = []):
53 if not have_notifications:
54 info('%s: %s', title, message)
55 return None
57 import time
58 import dbus.types
60 hints = {}
61 if actions:
62 hints['urgency'] = dbus.types.Byte(NORMAL)
63 else:
64 hints['urgency'] = dbus.types.Byte(LOW)
66 return notification_service.Notify('Zero Install',
67 0, # replaces_id,
68 '', # icon
69 _escape_xml(title),
70 _escape_xml(message),
71 actions,
72 hints,
73 timeout * 1000)
75 def _exec_gui(uri, *args):
76 os.execvp('0launch', ['0launch', '--download-only', '--gui'] + list(args) + [uri])
78 class BackgroundHandler(handler.Handler):
79 def __init__(self, title):
80 handler.Handler.__init__(self)
81 self.title = title
83 def confirm_trust_keys(self, interface, sigs, iface_xml):
84 notify("Zero Install", "Can't update interface; signature not yet trusted. Running GUI...", timeout = 2)
85 _exec_gui(interface.uri, '--refresh')
87 def report_error(self, exception):
88 notify("Zero Install", "Error updating %s: %s" % (self.title, str(exception)))
90 def _detach():
91 """Fork a detached grandchild.
92 @return: True if we are the original."""
93 child = os.fork()
94 if child:
95 pid, status = os.waitpid(child, 0)
96 assert pid == child
97 return True
99 # The calling process might be waiting for EOF from its child.
100 # Close our stdout so we don't keep it waiting.
101 # Note: this only fixes the most common case; it could be waiting
102 # on any other FD as well. We should really use gobject.spawn_async
103 # to close *all* FDs.
104 null = os.open('/dev/null', os.O_RDWR)
105 os.dup2(null, 1)
106 os.close(null)
108 grandchild = os.fork()
109 if grandchild:
110 os._exit(0) # Parent's waitpid returns and grandchild continues
112 return False
114 def _check_for_updates(policy, verbose):
115 root_iface = iface_cache.get_interface(policy.root).get_name()
116 info("Checking for updates to '%s' in a background process", root_iface)
117 if verbose:
118 notify("Zero Install", "Checking for updates to '%s'..." % root_iface, timeout = 1)
120 policy.handler = BackgroundHandler(root_iface)
121 policy.freshness = 0 # Don't bother trying to refresh when getting the interface
122 refresh = policy.refresh_all() # (causes confusing log messages)
123 policy.handler.wait_for_blocker(refresh)
125 # We could even download the archives here, but for now just
126 # update the interfaces.
128 if not policy.need_download():
129 if verbose:
130 notify("Zero Install", "No updates to download.", timeout = 1)
131 sys.exit(0)
133 if not have_notifications:
134 notify("Zero Install", "Updates ready to download for '%s'." % root_iface)
135 sys.exit(0)
137 notification_closed = tasks.Blocker("wait for notification response")
139 def _NotificationClosed(nid, *unused):
140 if nid != our_question: return
141 notification_closed.trigger()
143 def _ActionInvoked(nid, action):
144 if nid != our_question: return
145 if action == 'download':
146 _exec_gui(policy.root)
147 notification_closed.trigger()
149 notification_service.connect_to_signal('NotificationClosed', _NotificationClosed)
150 notification_service.connect_to_signal('ActionInvoked', _ActionInvoked)
152 our_question = notify("Zero Install", "Updates ready to download for '%s'." % root_iface,
153 actions = ['download', 'Download'])
155 policy.handler.wait_for_blocker(notification_closed)
157 def spawn_background_update(policy, verbose):
158 # Mark all feeds as being updated. Do this before forking, so that if someone is
159 # running lots of 0launch commands in series on the same program we don't start
160 # huge numbers of processes.
161 import time
162 for x in policy.implementation:
163 iface_cache.mark_as_checking(x.uri) # Main feed
164 for f in policy.usable_feeds(x):
165 iface_cache.mark_as_checking(f.uri) # Extra feeds
167 if _detach():
168 return
170 try:
171 try:
172 _check_for_updates(policy, verbose)
173 except SystemExit:
174 raise
175 except:
176 import traceback
177 traceback.print_exc()
178 sys.stdout.flush()
179 else:
180 sys.exit(0)
181 finally:
182 os._exit(1)