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