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.
9 # Copyright (C) 2009, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
12 from zeroinstall
import _
, logger
14 from zeroinstall
.support
import tasks
15 from zeroinstall
.injector
import handler
18 return s
.replace('&', '&').replace('<', '<')
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
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
)
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
53 from dbus
.mainloop
.glib
import DBusGMainLoop
54 DBusGMainLoop(set_as_default
=True)
56 import dbus
.glib
# Python 2
57 except Exception as ex
:
58 logger
.info(_("Failed to import D-BUS bindings: %s"), ex
)
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
75 self
.notification_service_caps
= [str(s
) for s
in
76 self
.notification_service
.GetCapabilities()]
78 sys
.stderr
= old_stderr
79 except Exception as ex
:
80 logger
.info(_("No D-BUS notification service available: %s"), ex
)
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
:
95 state
= self
.network_manager
.state()
97 state
= _NetworkState
.v0_8
.get(state
,
98 _NetworkState
.NM_STATE_UNKNOWN
)
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..."))
113 for dl
in self
.monitored_downloads
:
116 raise handler
.NoTrustedKeys("need to switch to GUI to confirm keys")
118 raise handler
.NoTrustedKeys(_("Background update for {iface} needed to confirm keys, but no GUI available!").format(
122 def report_error(self
, exception
, tb
= None):
123 from zeroinstall
.injector
import download
124 if isinstance(exception
, download
.DownloadError
):
129 details
= '\n' + '\n'.join(traceback
.format_exception(type(exception
), exception
, tb
))
132 details
= unicode(exception
)
134 details
= repr(exception
)
135 self
.notify("Zero Install", _("Error updating %(title)s: %(details)s") % {'title': self
.title
, 'details': details
.replace('<', '<')})
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
)
152 hints
['urgency'] = dbus
.types
.Byte(NORMAL
)
154 hints
['urgency'] = dbus
.types
.Byte(LOW
)
156 return self
.notification_service
.Notify('Zero Install',
160 _escape_xml(message
),
166 """Fork a detached grandchild.
167 @return: True if we are the original."""
170 pid
, status
= os
.waitpid(child
, 0)
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
)
183 grandchild
= os
.fork()
185 os
._exit
(0) # Parent's waitpid returns and grandchild continues
189 def _check_for_updates(requirements
, verbose
, app
):
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
)
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
)
212 if network_state
in (_NetworkState
.NM_STATE_DISCONNECTED
, _NetworkState
.NM_STATE_ASLEEP
):
213 logger
.info(_("Still not connected to network. Giving up."))
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():
224 background_handler
.notify("Zero Install",
225 _("Updates ready to download for '%s'.") % root_iface
,
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)
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
239 background_handler
.notify("Zero Install",
240 _("{name} updated.").format(name
= root_iface
),
244 background_handler
.notify("Zero Install", _("No updates to download."), timeout
= 1)
245 new_sels
= driver
.solver
.selections
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()),
255 app
.set_last_checked()
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
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
281 @param app: application to update (if any)
282 @type app: L{apps.App} | None
289 _check_for_updates(requirements
, verbose
, app
)
294 traceback
.print_exc()