Bugfix: error reporting in background handler was broken.
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / background.py
blob6c58ee245434585d37e8533bfa46ac9fbed42c8e
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 L{policy.Policy.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 def _escape_xml(s):
19 return s.replace('&', '&amp;').replace('<', '&lt;')
21 def _exec_gui(uri, *args):
22 os.execvp('0launch', ['0launch', '--download-only', '--gui'] + list(args) + [uri])
24 class BackgroundHandler(handler.Handler):
25 """A Handler for non-interactive background updates. Runs the GUI if interaction is required."""
26 def __init__(self, title):
27 handler.Handler.__init__(self)
28 self.title = title
30 try:
31 import dbus
32 import dbus.glib
34 session_bus = dbus.SessionBus()
36 remote_object = session_bus.get_object('org.freedesktop.Notifications',
37 '/org/freedesktop/Notifications')
39 self.notification_service = dbus.Interface(remote_object,
40 'org.freedesktop.Notifications')
42 # The Python bindings insist on printing a pointless introspection
43 # warning to stderr if the service is missing. Force it to be done
44 # now so we can skip it
45 old_stderr = sys.stderr
46 sys.stderr = None
47 try:
48 self.notification_service.GetCapabilities()
49 finally:
50 sys.stderr = old_stderr
52 self.have_notifications = True
53 except Exception, ex:
54 info("Failed to import D-BUS bindings: %s", ex)
55 self.have_notifications = False
57 def confirm_trust_keys(self, interface, sigs, iface_xml):
58 """Run the GUI if we need to confirm any keys."""
59 notify("Zero Install", "Can't update interface; signature not yet trusted. Running GUI...", timeout = 2)
60 _exec_gui(interface.uri, '--refresh')
62 def report_error(self, exception, tb = None):
63 self.notify("Zero Install", "Error updating %s: %s" % (self.title, str(exception)))
65 def notify(self, title, message, timeout = 0, actions = []):
66 """Send a D-BUS notification message if possible. If there is no notification
67 service available, log the message instead."""
68 if not self.have_notifications:
69 info('%s: %s', title, message)
70 return None
72 LOW = 0
73 NORMAL = 1
74 CRITICAL = 2
76 import time
77 import dbus.types
79 hints = {}
80 if actions:
81 hints['urgency'] = dbus.types.Byte(NORMAL)
82 else:
83 hints['urgency'] = dbus.types.Byte(LOW)
85 return self.notification_service.Notify('Zero Install',
86 0, # replaces_id,
87 '', # icon
88 _escape_xml(title),
89 _escape_xml(message),
90 actions,
91 hints,
92 timeout * 1000)
94 def _detach():
95 """Fork a detached grandchild.
96 @return: True if we are the original."""
97 child = os.fork()
98 if child:
99 pid, status = os.waitpid(child, 0)
100 assert pid == child
101 return True
103 # The calling process might be waiting for EOF from its child.
104 # Close our stdout so we don't keep it waiting.
105 # Note: this only fixes the most common case; it could be waiting
106 # on any other FD as well. We should really use gobject.spawn_async
107 # to close *all* FDs.
108 null = os.open('/dev/null', os.O_RDWR)
109 os.dup2(null, 1)
110 os.close(null)
112 grandchild = os.fork()
113 if grandchild:
114 os._exit(0) # Parent's waitpid returns and grandchild continues
116 return False
118 def _check_for_updates(policy, verbose):
119 root_iface = iface_cache.get_interface(policy.root).get_name()
120 info("Checking for updates to '%s' in a background process", root_iface)
121 if verbose:
122 handler.notify("Zero Install", "Checking for updates to '%s'..." % root_iface, timeout = 1)
124 policy.handler = BackgroundHandler(root_iface)
125 policy.freshness = 0 # Don't bother trying to refresh when getting the interface
126 refresh = policy.refresh_all() # (causes confusing log messages)
127 policy.handler.wait_for_blocker(refresh)
129 # We could even download the archives here, but for now just
130 # update the interfaces.
132 if not policy.need_download():
133 if verbose:
134 policy.handler.notify("Zero Install", "No updates to download.", timeout = 1)
135 sys.exit(0)
137 if not policy.handler.have_notifications:
138 policy.handler.notify("Zero Install", "Updates ready to download for '%s'." % root_iface)
139 sys.exit(0)
141 notification_closed = tasks.Blocker("wait for notification response")
143 def _NotificationClosed(nid, *unused):
144 if nid != our_question: return
145 notification_closed.trigger()
147 def _ActionInvoked(nid, action):
148 if nid != our_question: return
149 if action == 'download':
150 _exec_gui(policy.root)
151 notification_closed.trigger()
153 policy.handler.notification_service.connect_to_signal('NotificationClosed', _NotificationClosed)
154 policy.handler.notification_service.connect_to_signal('ActionInvoked', _ActionInvoked)
156 our_question = policy.handler.notify("Zero Install", "Updates ready to download for '%s'." % root_iface,
157 actions = ['download', 'Download'])
159 policy.handler.wait_for_blocker(notification_closed)
161 def spawn_background_update(policy, verbose):
162 """Spawn a detached child process to check for updates.
163 @param policy: policy containing interfaces to update
164 @type policy: L{policy.Policy}
165 @param verbose: whether to notify the user about minor events
166 @type verbose: bool"""
167 # Mark all feeds as being updated. Do this before forking, so that if someone is
168 # running lots of 0launch commands in series on the same program we don't start
169 # huge numbers of processes.
170 import time
171 for x in policy.implementation:
172 iface_cache.mark_as_checking(x.uri) # Main feed
173 for f in policy.usable_feeds(x):
174 iface_cache.mark_as_checking(f.uri) # Extra feeds
176 if _detach():
177 return
179 try:
180 try:
181 _check_for_updates(policy, verbose)
182 except SystemExit:
183 raise
184 except:
185 import traceback
186 traceback.print_exc()
187 sys.stdout.flush()
188 else:
189 sys.exit(0)
190 finally:
191 os._exit(1)