Added app/last-check-attempt timestamp
[zeroinstall/solver.git] / zeroinstall / injector / background.py
blob274d8bef6416a6b3ffdaabcd9d86384ab185275f
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{config.Config.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 class _NetworkState:
22 NM_STATE_UNKNOWN = 0
23 NM_STATE_ASLEEP = 10
24 NM_STATE_DISCONNECTED = 20
25 NM_STATE_DISCONNECTING = 30
26 NM_STATE_CONNECTING = 40
27 NM_STATE_CONNECTED_LOCAL = 50
28 NM_STATE_CONNECTED_SITE = 60
29 NM_STATE_CONNECTED_GLOBAL = 70
31 # Maps enum values from version <= 0.8 to current (0.9) values
32 v0_8 = {
33 0: NM_STATE_UNKNOWN,
34 1: NM_STATE_ASLEEP,
35 2: NM_STATE_CONNECTING,
36 3: NM_STATE_CONNECTED_GLOBAL,
37 4: NM_STATE_DISCONNECTED,
40 class BackgroundHandler(handler.Handler):
41 """A Handler for non-interactive background updates. Runs the GUI if interaction is required."""
42 def __init__(self, title, root):
43 handler.Handler.__init__(self)
44 self.title = title
45 self.notification_service = None
46 self.network_manager = None
47 self.notification_service_caps = []
48 self.root = root # If we need to confirm any keys, run the GUI on this
49 self.need_gui = False
51 try:
52 import dbus
53 import dbus.glib
54 except Exception as ex:
55 info(_("Failed to import D-BUS bindings: %s"), ex)
56 return
58 try:
59 session_bus = dbus.SessionBus()
60 remote_object = session_bus.get_object('org.freedesktop.Notifications',
61 '/org/freedesktop/Notifications')
63 self.notification_service = dbus.Interface(remote_object,
64 'org.freedesktop.Notifications')
66 # The Python bindings insist on printing a pointless introspection
67 # warning to stderr if the service is missing. Force it to be done
68 # now so we can skip it
69 old_stderr = sys.stderr
70 sys.stderr = None
71 try:
72 self.notification_service_caps = [str(s) for s in
73 self.notification_service.GetCapabilities()]
74 finally:
75 sys.stderr = old_stderr
76 except Exception as ex:
77 info(_("No D-BUS notification service available: %s"), ex)
79 try:
80 system_bus = dbus.SystemBus()
81 remote_object = system_bus.get_object('org.freedesktop.NetworkManager',
82 '/org/freedesktop/NetworkManager')
84 self.network_manager = dbus.Interface(remote_object,
85 'org.freedesktop.NetworkManager')
86 except Exception as ex:
87 info(_("No D-BUS network manager service available: %s"), ex)
89 def get_network_state(self):
90 if self.network_manager:
91 try:
92 state = self.network_manager.state()
93 if state < 10:
94 state = _NetworkState.v0_8.get(state,
95 _NetworkState.NM_STATE_UNKNOWN)
96 return state
98 except Exception as ex:
99 warn(_("Error getting network state: %s"), ex)
100 return _NetworkState.NM_STATE_UNKNOWN
102 def confirm_import_feed(self, pending, valid_sigs):
103 """Run the GUI if we need to confirm any keys."""
105 if os.environ.get('DISPLAY', None):
106 info(_("Can't update feed; signature not yet trusted. Running GUI..."))
108 self.need_gui = True
110 for dl in self.monitored_downloads:
111 dl.abort()
113 raise handler.NoTrustedKeys("need to switch to GUI to confirm keys")
114 else:
115 raise handler.NoTrustedKeys(_("Background update for {iface} needed to confirm keys, but no GUI available!").format(
116 iface = self.root))
119 def report_error(self, exception, tb = None):
120 from zeroinstall.injector import download
121 if isinstance(exception, download.DownloadError):
122 tb = None
124 if tb:
125 import traceback
126 details = '\n' + '\n'.join(traceback.format_exception(type(exception), exception, tb))
127 else:
128 try:
129 details = unicode(exception)
130 except:
131 details = repr(exception)
132 self.notify("Zero Install", _("Error updating %(title)s: %(details)s") % {'title': self.title, 'details': details.replace('<', '&lt;')})
134 def notify(self, title, message, timeout = 0, actions = []):
135 """Send a D-BUS notification message if possible. If there is no notification
136 service available, log the message instead."""
137 if not self.notification_service:
138 info('%s: %s', title, message)
139 return None
141 LOW = 0
142 NORMAL = 1
143 #CRITICAL = 2
145 import dbus.types
147 hints = {}
148 if actions:
149 hints['urgency'] = dbus.types.Byte(NORMAL)
150 else:
151 hints['urgency'] = dbus.types.Byte(LOW)
153 return self.notification_service.Notify('Zero Install',
154 0, # replaces_id,
155 '', # icon
156 _escape_xml(title),
157 _escape_xml(message),
158 actions,
159 hints,
160 timeout * 1000)
162 def _detach():
163 """Fork a detached grandchild.
164 @return: True if we are the original."""
165 child = os.fork()
166 if child:
167 pid, status = os.waitpid(child, 0)
168 assert pid == child
169 return True
171 # The calling process might be waiting for EOF from its child.
172 # Close our stdout so we don't keep it waiting.
173 # Note: this only fixes the most common case; it could be waiting
174 # on any other FD as well. We should really use gobject.spawn_async
175 # to close *all* FDs.
176 null = os.open(os.devnull, os.O_RDWR)
177 os.dup2(null, 1)
178 os.close(null)
180 grandchild = os.fork()
181 if grandchild:
182 os._exit(0) # Parent's waitpid returns and grandchild continues
184 return False
186 def _check_for_updates(requirements, verbose, app):
187 if app is not None:
188 old_sels = app.get_selections()
190 from zeroinstall.injector.driver import Driver
191 from zeroinstall.injector.config import load_config
193 background_handler = BackgroundHandler(requirements.interface_uri, requirements.interface_uri)
194 background_config = load_config(background_handler)
195 root_iface = background_config.iface_cache.get_interface(requirements.interface_uri).get_name()
196 background_handler.title = root_iface
198 driver = Driver(config = background_config, requirements = requirements)
200 info(_("Checking for updates to '%s' in a background process"), root_iface)
201 if verbose:
202 background_handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout = 1)
204 network_state = background_handler.get_network_state()
205 if network_state not in (_NetworkState.NM_STATE_CONNECTED_SITE, _NetworkState.NM_STATE_CONNECTED_GLOBAL):
206 info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state)
207 import time
208 time.sleep(120)
209 if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP):
210 info(_("Still not connected to network. Giving up."))
211 sys.exit(1)
212 else:
213 info(_("NetworkManager says we're on-line. Good!"))
215 background_config.freshness = 0 # Don't bother trying to refresh when getting the interface
216 refresh = driver.solve_with_downloads(force = True) # (causes confusing log messages)
217 tasks.wait_for_blocker(refresh)
219 if background_handler.need_gui or driver.get_uncached_implementations():
220 background_handler.notify("Zero Install",
221 _("Updates ready to download for '%s'.") % root_iface,
222 timeout = 1)
224 if os.environ.get('DISPLAY', None):
225 # Run the GUI...
226 from zeroinstall import helpers
227 gui_args = ['--refresh', '--systray', '--download'] + requirements.get_as_options()
228 new_sels = helpers.get_selections_gui(requirements.interface_uri, gui_args)
229 else:
230 tasks.wait_for_blocker(driver.download_uncached_implementations())
231 new_sels = driver.solver.selections
232 else:
233 if verbose:
234 background_handler.notify("Zero Install", _("No updates to download."), timeout = 1)
235 new_sels = driver.solver.selections
237 if app is not None:
238 assert driver.solver.ready
239 from zeroinstall.support import xmltools
240 if not xmltools.nodes_equal(new_sels.toDOM(), old_sels.toDOM()):
241 app.set_selections(new_sels)
242 app.set_last_checked()
243 sys.exit(0)
246 def spawn_background_update(driver, verbose):
247 """Spawn a detached child process to check for updates.
248 @param driver: driver containing interfaces to update
249 @type driver: L{driver.Driver}
250 @param verbose: whether to notify the user about minor events
251 @type verbose: bool
252 @since: 1.5 (used to take a Policy)"""
253 iface_cache = driver.config.iface_cache
254 # Mark all feeds as being updated. Do this before forking, so that if someone is
255 # running lots of 0launch commands in series on the same program we don't start
256 # huge numbers of processes.
257 for uri in driver.solver.feeds_used:
258 iface_cache.mark_as_checking(uri)
260 spawn_background_update2(driver.requirements, verbose)
262 def spawn_background_update2(requirements, verbose, app = None):
263 """Spawn a detached child process to check for updates.
264 @param requirements: requirements for the new selections
265 @type requirements: L{requirements.Requirements}
266 @param verbose: whether to notify the user about minor events
267 @type verbose: bool
268 @param app: application to update (if any)
269 @type app: L{apps.App} | None
270 @since: 1.8"""
271 if _detach():
272 return
274 try:
275 try:
276 _check_for_updates(requirements, verbose, app)
277 except SystemExit:
278 raise
279 except:
280 import traceback
281 traceback.print_exc()
282 sys.stdout.flush()
283 finally:
284 os._exit(1)