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