Fixed epydoc
[zeroinstall.git] / zeroinstall / injector / background.py
blob1b8a9e99a1e686f07ad6c117fce57c7e2bdfc4d6
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 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 = 10
29 NM_STATE_DISCONNECTED = 20
30 NM_STATE_DISCONNECTING = 30
31 NM_STATE_CONNECTING = 40
32 NM_STATE_CONNECTED_LOCAL = 50
33 NM_STATE_CONNECTED_SITE = 60
34 NM_STATE_CONNECTED_GLOBAL = 70
36 # Maps enum values from version <= 0.8 to current (0.9) values
37 v0_8 = {
38 0: NM_STATE_UNKNOWN,
39 1: NM_STATE_ASLEEP,
40 2: NM_STATE_CONNECTING,
41 3: NM_STATE_CONNECTED_GLOBAL,
42 4: NM_STATE_DISCONNECTED,
46 class BackgroundHandler(handler.Handler):
47 """A Handler for non-interactive background updates. Runs the GUI if interaction is required."""
48 def __init__(self, title, root):
49 handler.Handler.__init__(self)
50 self.title = title
51 self.notification_service = None
52 self.network_manager = None
53 self.notification_service_caps = []
54 self.root = root # If we need to confirm any keys, run the GUI on this
56 try:
57 import dbus
58 import dbus.glib
59 except Exception as ex:
60 info(_("Failed to import D-BUS bindings: %s"), ex)
61 return
63 try:
64 session_bus = dbus.SessionBus()
65 remote_object = session_bus.get_object('org.freedesktop.Notifications',
66 '/org/freedesktop/Notifications')
68 self.notification_service = dbus.Interface(remote_object,
69 'org.freedesktop.Notifications')
71 # The Python bindings insist on printing a pointless introspection
72 # warning to stderr if the service is missing. Force it to be done
73 # now so we can skip it
74 old_stderr = sys.stderr
75 sys.stderr = None
76 try:
77 self.notification_service_caps = [str(s) for s in
78 self.notification_service.GetCapabilities()]
79 finally:
80 sys.stderr = old_stderr
81 except Exception as ex:
82 info(_("No D-BUS notification service available: %s"), ex)
84 try:
85 system_bus = dbus.SystemBus()
86 remote_object = system_bus.get_object('org.freedesktop.NetworkManager',
87 '/org/freedesktop/NetworkManager')
89 self.network_manager = dbus.Interface(remote_object,
90 'org.freedesktop.NetworkManager')
91 except Exception as ex:
92 info(_("No D-BUS network manager service available: %s"), ex)
94 def get_network_state(self):
95 if self.network_manager:
96 try:
97 state = self.network_manager.state()
98 if state < 10:
99 state = _NetworkState.v0_8.get(state,
100 _NetworkState.NM_STATE_UNKNOWN)
101 return state
103 except Exception as ex:
104 warn(_("Error getting network state: %s"), ex)
105 return _NetworkState.NM_STATE_UNKNOWN
107 def confirm_import_feed(self, pending, valid_sigs):
108 """Run the GUI if we need to confirm any keys."""
109 info(_("Can't update feed; signature not yet trusted. Running GUI..."))
110 _exec_gui(self.root, '--download', '--refresh', '--systray')
112 def report_error(self, exception, tb = None):
113 from zeroinstall.injector import download
114 if isinstance(exception, download.DownloadError):
115 tb = None
117 if tb:
118 import traceback
119 details = '\n' + '\n'.join(traceback.format_exception(type(exception), exception, tb))
120 else:
121 try:
122 details = unicode(exception)
123 except:
124 details = repr(exception)
125 self.notify("Zero Install", _("Error updating %(title)s: %(details)s") % {'title': self.title, 'details': details.replace('<', '&lt;')})
127 def notify(self, title, message, timeout = 0, actions = []):
128 """Send a D-BUS notification message if possible. If there is no notification
129 service available, log the message instead."""
130 if not self.notification_service:
131 info('%s: %s', title, message)
132 return None
134 LOW = 0
135 NORMAL = 1
136 #CRITICAL = 2
138 import dbus.types
140 hints = {}
141 if actions:
142 hints['urgency'] = dbus.types.Byte(NORMAL)
143 else:
144 hints['urgency'] = dbus.types.Byte(LOW)
146 return self.notification_service.Notify('Zero Install',
147 0, # replaces_id,
148 '', # icon
149 _escape_xml(title),
150 _escape_xml(message),
151 actions,
152 hints,
153 timeout * 1000)
155 def _detach():
156 """Fork a detached grandchild.
157 @return: True if we are the original."""
158 child = os.fork()
159 if child:
160 pid, status = os.waitpid(child, 0)
161 assert pid == child
162 return True
164 # The calling process might be waiting for EOF from its child.
165 # Close our stdout so we don't keep it waiting.
166 # Note: this only fixes the most common case; it could be waiting
167 # on any other FD as well. We should really use gobject.spawn_async
168 # to close *all* FDs.
169 null = os.open('/dev/null', os.O_RDWR)
170 os.dup2(null, 1)
171 os.close(null)
173 grandchild = os.fork()
174 if grandchild:
175 os._exit(0) # Parent's waitpid returns and grandchild continues
177 return False
179 def _check_for_updates(requirements, verbose):
180 from zeroinstall.injector.driver import Driver
181 from zeroinstall.injector.config import load_config
183 background_handler = BackgroundHandler(requirements.interface_uri, requirements.interface_uri)
184 background_config = load_config(background_handler)
185 root_iface = background_config.iface_cache.get_interface(requirements.interface_uri).get_name()
186 background_handler.title = root_iface
188 driver = Driver(config = background_config, requirements = requirements)
190 info(_("Checking for updates to '%s' in a background process"), root_iface)
191 if verbose:
192 background_handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout = 1)
194 network_state = background_handler.get_network_state()
195 if network_state not in (_NetworkState.NM_STATE_CONNECTED_SITE, _NetworkState.NM_STATE_CONNECTED_GLOBAL):
196 info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state)
197 import time
198 time.sleep(120)
199 if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP):
200 info(_("Still not connected to network. Giving up."))
201 sys.exit(1)
202 else:
203 info(_("NetworkManager says we're on-line. Good!"))
205 background_config.freshness = 0 # Don't bother trying to refresh when getting the interface
206 refresh = driver.solve_with_downloads(force = True) # (causes confusing log messages)
207 tasks.wait_for_blocker(refresh)
209 # We could even download the archives here, but for now just
210 # update the interfaces.
212 if not driver.need_download():
213 if verbose:
214 background_handler.notify("Zero Install", _("No updates to download."), timeout = 1)
215 sys.exit(0)
217 background_handler.notify("Zero Install",
218 _("Updates ready to download for '%s'.") % root_iface,
219 timeout = 1)
220 _exec_gui(requirements.interface_uri, '--refresh', '--systray')
221 sys.exit(1)
223 def spawn_background_update(driver, verbose):
224 """Spawn a detached child process to check for updates.
225 @param driver: driver containing interfaces to update
226 @type driver: L{driver.Driver}
227 @param verbose: whether to notify the user about minor events
228 @type verbose: bool
229 @since: 1.5 (used to take a Policy)"""
230 iface_cache = driver.config.iface_cache
231 # Mark all feeds as being updated. Do this before forking, so that if someone is
232 # running lots of 0launch commands in series on the same program we don't start
233 # huge numbers of processes.
234 for uri in driver.solver.feeds_used:
235 iface_cache.mark_as_checking(uri)
237 if _detach():
238 return
240 try:
241 try:
242 _check_for_updates(driver.requirements, verbose)
243 except SystemExit:
244 raise
245 except:
246 import traceback
247 traceback.print_exc()
248 sys.stdout.flush()
249 finally:
250 os._exit(1)