Merged 0.51.1 branch
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / background.py
blob248a184d24f3332cd11bf73932539500bfec6c3e
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 # Copyright (C) 2009, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
12 from zeroinstall import _
13 import sys, os
14 from logging import info, warn
15 from zeroinstall.support import tasks
16 from zeroinstall.injector.iface_cache import iface_cache
17 from zeroinstall.injector import handler
19 def _escape_xml(s):
20 return s.replace('&', '&amp;').replace('<', '&lt;')
22 def _exec_gui(uri, *args):
23 os.execvp('0launch', ['0launch', '--download-only', '--gui'] + list(args) + [uri])
25 class _NetworkState:
26 NM_STATE_UNKNOWN = 0
27 NM_STATE_ASLEEP = 1
28 NM_STATE_CONNECTING = 2
29 NM_STATE_CONNECTED = 3
30 NM_STATE_DISCONNECTED = 4
32 class BackgroundHandler(handler.Handler):
33 """A Handler for non-interactive background updates. Runs the GUI if interaction is required."""
34 def __init__(self, title, root):
35 handler.Handler.__init__(self)
36 self.title = title
37 self.notification_service = None
38 self.network_manager = None
39 self.notification_service_caps = []
40 self.root = root # If we need to confirm any keys, run the GUI on this
42 try:
43 import dbus
44 import dbus.glib
45 except Exception, ex:
46 info(_("Failed to import D-BUS bindings: %s"), ex)
47 return
49 try:
50 session_bus = dbus.SessionBus()
51 remote_object = session_bus.get_object('org.freedesktop.Notifications',
52 '/org/freedesktop/Notifications')
54 self.notification_service = dbus.Interface(remote_object,
55 'org.freedesktop.Notifications')
57 # The Python bindings insist on printing a pointless introspection
58 # warning to stderr if the service is missing. Force it to be done
59 # now so we can skip it
60 old_stderr = sys.stderr
61 sys.stderr = None
62 try:
63 self.notification_service_caps = [str(s) for s in
64 self.notification_service.GetCapabilities()]
65 finally:
66 sys.stderr = old_stderr
67 except Exception, ex:
68 info(_("No D-BUS notification service available: %s"), ex)
70 try:
71 system_bus = dbus.SystemBus()
72 remote_object = system_bus.get_object('org.freedesktop.NetworkManager',
73 '/org/freedesktop/NetworkManager')
75 self.network_manager = dbus.Interface(remote_object,
76 'org.freedesktop.NetworkManager')
77 except Exception, ex:
78 info(_("No D-BUS network manager service available: %s"), ex)
80 def get_network_state(self):
81 if self.network_manager:
82 try:
83 return self.network_manager.state()
84 except Exception, ex:
85 warn(_("Error getting network state: %s"), ex)
86 return _NetworkState.NM_STATE_UNKNOWN
88 def confirm_import_feed(self, pending, valid_sigs):
89 """Run the GUI if we need to confirm any keys."""
90 info(_("Can't update feed; signature not yet trusted. Running GUI..."))
91 _exec_gui(self.root, '--refresh', '--download-only', '--systray')
93 def report_error(self, exception, tb = None):
94 from zeroinstall.injector import download
95 if isinstance(exception, download.DownloadError):
96 tb = None
98 if tb:
99 import traceback
100 details = '\n' + '\n'.join(traceback.format_exception(type(exception), exception, tb))
101 else:
102 try:
103 details = unicode(exception)
104 except:
105 details = repr(exception)
106 self.notify("Zero Install", _("Error updating %(title)s: %(details)s") % {'title': self.title, 'details': details.replace('<', '&lt;')})
108 def notify(self, title, message, timeout = 0, actions = []):
109 """Send a D-BUS notification message if possible. If there is no notification
110 service available, log the message instead."""
111 if not self.notification_service:
112 info('%s: %s', title, message)
113 return None
115 LOW = 0
116 NORMAL = 1
117 CRITICAL = 2
119 import dbus.types
121 hints = {}
122 if actions:
123 hints['urgency'] = dbus.types.Byte(NORMAL)
124 else:
125 hints['urgency'] = dbus.types.Byte(LOW)
127 return self.notification_service.Notify('Zero Install',
128 0, # replaces_id,
129 '', # icon
130 _escape_xml(title),
131 _escape_xml(message),
132 actions,
133 hints,
134 timeout * 1000)
136 def have_actions_support(self):
137 return 'actions' in self.notification_service_caps
139 def _detach():
140 """Fork a detached grandchild.
141 @return: True if we are the original."""
142 child = os.fork()
143 if child:
144 pid, status = os.waitpid(child, 0)
145 assert pid == child
146 return True
148 # The calling process might be waiting for EOF from its child.
149 # Close our stdout so we don't keep it waiting.
150 # Note: this only fixes the most common case; it could be waiting
151 # on any other FD as well. We should really use gobject.spawn_async
152 # to close *all* FDs.
153 null = os.open('/dev/null', os.O_RDWR)
154 os.dup2(null, 1)
155 os.close(null)
157 grandchild = os.fork()
158 if grandchild:
159 os._exit(0) # Parent's waitpid returns and grandchild continues
161 return False
163 def _check_for_updates(policy, verbose):
164 root_iface = iface_cache.get_interface(policy.root).get_name()
166 policy.handler = BackgroundHandler(root_iface, policy.root)
168 info(_("Checking for updates to '%s' in a background process"), root_iface)
169 if verbose:
170 policy.handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout = 1)
172 network_state = policy.handler.get_network_state()
173 if network_state != _NetworkState.NM_STATE_CONNECTED:
174 info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state)
175 import time
176 time.sleep(120)
177 if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP):
178 info(_("Still not connected to network. Giving up."))
179 sys.exit(1)
180 else:
181 info(_("NetworkManager says we're on-line. Good!"))
183 policy.freshness = 0 # Don't bother trying to refresh when getting the interface
184 refresh = policy.refresh_all() # (causes confusing log messages)
185 policy.handler.wait_for_blocker(refresh)
187 # We could even download the archives here, but for now just
188 # update the interfaces.
190 if not policy.need_download():
191 if verbose:
192 policy.handler.notify("Zero Install", _("No updates to download."), timeout = 1)
193 sys.exit(0)
195 if not policy.handler.have_actions_support():
196 # Can't ask the user to choose, so just notify them
197 # In particular, Ubuntu/Jaunty doesn't support actions
198 policy.handler.notify("Zero Install",
199 _("Updates ready to download for '%s'.") % root_iface,
200 timeout = 1)
201 _exec_gui(policy.root, '--refresh', '--download-only', '--systray')
202 sys.exit(1)
204 notification_closed = tasks.Blocker("wait for notification response")
206 def _NotificationClosed(nid, *unused):
207 if nid != our_question: return
208 notification_closed.trigger()
210 def _ActionInvoked(nid, action):
211 if nid != our_question: return
212 if action == 'download':
213 _exec_gui(policy.root)
214 notification_closed.trigger()
216 policy.handler.notification_service.connect_to_signal('NotificationClosed', _NotificationClosed)
217 policy.handler.notification_service.connect_to_signal('ActionInvoked', _ActionInvoked)
219 our_question = policy.handler.notify("Zero Install", _("Updates ready to download for '%s'.") % root_iface,
220 actions = ['download', 'Download'])
222 policy.handler.wait_for_blocker(notification_closed)
224 def spawn_background_update(policy, verbose):
225 """Spawn a detached child process to check for updates.
226 @param policy: policy containing interfaces to update
227 @type policy: L{policy.Policy}
228 @param verbose: whether to notify the user about minor events
229 @type verbose: bool"""
230 # Mark all feeds as being updated. Do this before forking, so that if someone is
231 # running lots of 0launch commands in series on the same program we don't start
232 # huge numbers of processes.
233 for x in policy.implementation:
234 iface_cache.mark_as_checking(x.uri) # Main feed
235 for f in policy.usable_feeds(x):
236 iface_cache.mark_as_checking(f.uri) # Extra feeds
238 if _detach():
239 return
241 try:
242 try:
243 _check_for_updates(policy, verbose)
244 except SystemExit:
245 raise
246 except:
247 import traceback
248 traceback.print_exc()
249 sys.stdout.flush()
250 finally:
251 os._exit(1)