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.
5
6 This avoids the need to annoy people with a 'checking for updates' box when they're trying to run things.
7 """
8
9
10
11
12 from zeroinstall import _, logger
13 import sys, os
14 from zeroinstall.support import tasks
15 from zeroinstall.injector import handler
16
18 return s.replace('&', '&').replace('<', '<')
19
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
29
30
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,
37 }
38
40 """A Handler for non-interactive background updates. Runs the GUI if interaction is required."""
42 """@type title: str
43 @type root: str"""
44 handler.Handler.__init__(self)
45 self.title = title
46 self.notification_service = None
47 self.network_manager = None
48 self.notification_service_caps = []
49 self.root = root
50 self.need_gui = False
51
52 try:
53 import dbus
54 try:
55 from dbus.mainloop.glib import DBusGMainLoop
56 DBusGMainLoop(set_as_default=True)
57 except ImportError:
58 import dbus.glib
59 except Exception as ex:
60 logger.info(_("Failed to import D-BUS bindings: %s"), ex)
61 return
62
63 try:
64 session_bus = dbus.SessionBus()
65 remote_object = session_bus.get_object('org.freedesktop.Notifications',
66 '/org/freedesktop/Notifications')
67
68 self.notification_service = dbus.Interface(remote_object,
69 'org.freedesktop.Notifications')
70
71
72
73
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 logger.info(_("No D-BUS notification service available: %s"), ex)
83
84 try:
85 system_bus = dbus.SystemBus()
86 remote_object = system_bus.get_object('org.freedesktop.NetworkManager',
87 '/org/freedesktop/NetworkManager')
88
89 self.network_manager = dbus.Interface(remote_object,
90 'org.freedesktop.NetworkManager')
91 except Exception as ex:
92 logger.info(_("No D-BUS network manager service available: %s"), ex)
93
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
102
103 except Exception as ex:
104 logger.warning(_("Error getting network state: %s"), ex)
105 return _NetworkState.NM_STATE_UNKNOWN
106
108 """Run the GUI if we need to confirm any keys.
109 @type pending: L{zeroinstall.injector.iface_cache.PendingFeed}"""
110
111 if os.environ.get('DISPLAY', None):
112 logger.info(_("Can't update feed; signature not yet trusted. Running GUI..."))
113
114 self.need_gui = True
115
116 for dl in self.monitored_downloads:
117 dl.abort()
118
119 raise handler.NoTrustedKeys("need to switch to GUI to confirm keys")
120 else:
121 raise handler.NoTrustedKeys(_("Background update for {iface} needed to confirm keys, but no GUI available!").format(
122 iface = self.root))
123
124
139
140 - def notify(self, title, message, timeout = 0, actions = []):
141 """Send a D-BUS notification message if possible. If there is no notification
142 service available, log the message instead.
143 @type title: str
144 @type message: str
145 @type timeout: int"""
146 if not self.notification_service:
147 logger.info('%s: %s', title, message)
148 return None
149
150 LOW = 0
151 NORMAL = 1
152
153
154 import dbus.types
155
156 hints = {}
157 if actions:
158 hints['urgency'] = dbus.types.Byte(NORMAL)
159 else:
160 hints['urgency'] = dbus.types.Byte(LOW)
161
162 return self.notification_service.Notify('Zero Install',
163 0,
164 '',
165 _escape_xml(title),
166 _escape_xml(message),
167 actions,
168 hints,
169 timeout * 1000)
170
172 """Fork a detached grandchild.
173 @return: True if we are the original."""
174 child = os.fork()
175 if child:
176 pid, status = os.waitpid(child, 0)
177 assert pid == child
178 return True
179
180
181
182
183
184
185 null = os.open(os.devnull, os.O_RDWR)
186 os.dup2(null, 1)
187 os.close(null)
188
189 grandchild = os.fork()
190 if grandchild:
191 os._exit(0)
192
193 return False
194
196 """@type requirements: L{zeroinstall.injector.requirements.Requirements}
197 @type verbose: bool
198 @type app: L{zeroinstall.apps.App}"""
199 if app is not None:
200 old_sels = app.get_selections()
201
202 from zeroinstall.injector.driver import Driver
203 from zeroinstall.injector.config import load_config
204
205 background_handler = BackgroundHandler(requirements.interface_uri, requirements.interface_uri)
206 background_config = load_config(background_handler)
207 root_iface = background_config.iface_cache.get_interface(requirements.interface_uri).get_name()
208 background_handler.title = root_iface
209
210 driver = Driver(config = background_config, requirements = requirements)
211
212 logger.info(_("Checking for updates to '%s' in a background process"), root_iface)
213 if verbose:
214 background_handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout = 1)
215
216 network_state = background_handler.get_network_state()
217 if network_state not in (_NetworkState.NM_STATE_CONNECTED_SITE, _NetworkState.NM_STATE_CONNECTED_GLOBAL):
218 logger.info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state)
219 import time
220 time.sleep(120)
221 if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP):
222 logger.info(_("Still not connected to network. Giving up."))
223 sys.exit(1)
224 else:
225 logger.info(_("NetworkManager says we're on-line. Good!"))
226
227 background_config.freshness = 0
228 refresh = driver.solve_with_downloads(force = True)
229 tasks.wait_for_blocker(refresh)
230
231 if background_handler.need_gui or not driver.solver.ready or driver.get_uncached_implementations():
232 if verbose:
233 background_handler.notify("Zero Install",
234 _("Updates ready to download for '%s'.") % root_iface,
235 timeout = 1)
236
237
238 from zeroinstall import helpers
239 gui_args = ['--refresh', '--systray', '--download'] + requirements.get_as_options()
240 new_sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, use_gui = None)
241 if new_sels is None:
242 sys.exit(0)
243 elif new_sels is helpers.DontUseGUI:
244 if not driver.solver.ready:
245 background_handler.notify("Zero Install", _("Can't update '%s'") % root_iface)
246 sys.exit(1)
247
248 tasks.wait_for_blocker(driver.download_uncached_implementations())
249 new_sels = driver.solver.selections
250
251 if app is None:
252 background_handler.notify("Zero Install",
253 _("{name} updated.").format(name = root_iface),
254 timeout = 1)
255 else:
256 if verbose:
257 background_handler.notify("Zero Install", _("No updates to download."), timeout = 1)
258 new_sels = driver.solver.selections
259
260 if app is not None:
261 assert driver.solver.ready
262 from zeroinstall.support import xmltools
263 if not xmltools.nodes_equal(new_sels.toDOM(), old_sels.toDOM()):
264 app.set_selections(new_sels)
265 background_handler.notify("Zero Install",
266 _("{app} updated.").format(app = app.get_name()),
267 timeout = 1)
268 app.set_last_checked()
269 sys.exit(0)
270
271
287
289 """Spawn a detached child process to check for updates.
290 @param requirements: requirements for the new selections
291 @type requirements: L{requirements.Requirements}
292 @param verbose: whether to notify the user about minor events
293 @type verbose: bool
294 @param app: application to update (if any)
295 @type app: L{apps.App} | None
296 @since: 1.9"""
297 if _detach():
298 return
299
300 try:
301 try:
302 _check_for_updates(requirements, verbose, app)
303 except SystemExit:
304 raise
305 except:
306 import traceback
307 traceback.print_exc()
308 sys.stdout.flush()
309 finally:
310 os._exit(1)
311