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