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.
9 # Copyright (C) 2009, Thomas Leonard
10 # See the README file for details, or visit http://0install.net.
12 from zeroinstall
import _
14 from logging
import info
, warn
15 from zeroinstall
.support
import tasks
16 from zeroinstall
.injector
import handler
19 return s
.replace('&', '&').replace('<', '<')
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
)
29 NM_STATE_CONNECTING
= 2
30 NM_STATE_CONNECTED
= 3
31 NM_STATE_DISCONNECTED
= 4
33 class BackgroundHandler(handler
.Handler
):
34 """A Handler for non-interactive background updates. Runs the GUI if interaction is required."""
35 def __init__(self
, title
, root
):
36 handler
.Handler
.__init
__(self
)
38 self
.notification_service
= None
39 self
.network_manager
= None
40 self
.notification_service_caps
= []
41 self
.root
= root
# If we need to confirm any keys, run the GUI on this
47 info(_("Failed to import D-BUS bindings: %s"), ex
)
51 session_bus
= dbus
.SessionBus()
52 remote_object
= session_bus
.get_object('org.freedesktop.Notifications',
53 '/org/freedesktop/Notifications')
55 self
.notification_service
= dbus
.Interface(remote_object
,
56 'org.freedesktop.Notifications')
58 # The Python bindings insist on printing a pointless introspection
59 # warning to stderr if the service is missing. Force it to be done
60 # now so we can skip it
61 old_stderr
= sys
.stderr
64 self
.notification_service_caps
= [str(s
) for s
in
65 self
.notification_service
.GetCapabilities()]
67 sys
.stderr
= old_stderr
69 info(_("No D-BUS notification service available: %s"), ex
)
72 system_bus
= dbus
.SystemBus()
73 remote_object
= system_bus
.get_object('org.freedesktop.NetworkManager',
74 '/org/freedesktop/NetworkManager')
76 self
.network_manager
= dbus
.Interface(remote_object
,
77 'org.freedesktop.NetworkManager')
79 info(_("No D-BUS network manager service available: %s"), ex
)
81 def get_network_state(self
):
82 if self
.network_manager
:
84 return self
.network_manager
.state()
86 warn(_("Error getting network state: %s"), ex
)
87 return _NetworkState
.NM_STATE_UNKNOWN
89 def confirm_import_feed(self
, pending
, valid_sigs
):
90 """Run the GUI if we need to confirm any keys."""
91 info(_("Can't update feed; signature not yet trusted. Running GUI..."))
92 _exec_gui(self
.root
, '--refresh', '--systray')
94 def report_error(self
, exception
, tb
= None):
95 from zeroinstall
.injector
import download
96 if isinstance(exception
, download
.DownloadError
):
101 details
= '\n' + '\n'.join(traceback
.format_exception(type(exception
), exception
, tb
))
104 details
= unicode(exception
)
106 details
= repr(exception
)
107 self
.notify("Zero Install", _("Error updating %(title)s: %(details)s") % {'title': self
.title
, 'details': details
.replace('<', '<')})
109 def notify(self
, title
, message
, timeout
= 0, actions
= []):
110 """Send a D-BUS notification message if possible. If there is no notification
111 service available, log the message instead."""
112 if not self
.notification_service
:
113 info('%s: %s', title
, message
)
124 hints
['urgency'] = dbus
.types
.Byte(NORMAL
)
126 hints
['urgency'] = dbus
.types
.Byte(LOW
)
128 return self
.notification_service
.Notify('Zero Install',
132 _escape_xml(message
),
137 def have_actions_support(self
):
138 return 'actions' in self
.notification_service_caps
141 """Fork a detached grandchild.
142 @return: True if we are the original."""
145 pid
, status
= os
.waitpid(child
, 0)
149 # The calling process might be waiting for EOF from its child.
150 # Close our stdout so we don't keep it waiting.
151 # Note: this only fixes the most common case; it could be waiting
152 # on any other FD as well. We should really use gobject.spawn_async
153 # to close *all* FDs.
154 null
= os
.open('/dev/null', os
.O_RDWR
)
158 grandchild
= os
.fork()
160 os
._exit
(0) # Parent's waitpid returns and grandchild continues
164 def _check_for_updates(old_policy
, verbose
):
165 from zeroinstall
.injector
.policy
import load_config
, Policy
167 iface_cache
= old_policy
.config
.iface_cache
168 root_iface
= iface_cache
.get_interface(old_policy
.root
).get_name()
170 background_config
= load_config(BackgroundHandler(root_iface
, old_policy
.root
))
171 policy
= Policy(config
= background_config
, requirements
= old_policy
.requirements
)
173 info(_("Checking for updates to '%s' in a background process"), root_iface
)
175 policy
.handler
.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface
, timeout
= 1)
177 network_state
= policy
.handler
.get_network_state()
178 if network_state
!= _NetworkState
.NM_STATE_CONNECTED
:
179 info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state
)
182 if network_state
in (_NetworkState
.NM_STATE_DISCONNECTED
, _NetworkState
.NM_STATE_ASLEEP
):
183 info(_("Still not connected to network. Giving up."))
186 info(_("NetworkManager says we're on-line. Good!"))
188 policy
.freshness
= 0 # Don't bother trying to refresh when getting the interface
189 refresh
= policy
.refresh_all() # (causes confusing log messages)
190 tasks
.wait_for_blocker(refresh
)
192 # We could even download the archives here, but for now just
193 # update the interfaces.
195 if not policy
.need_download():
197 policy
.handler
.notify("Zero Install", _("No updates to download."), timeout
= 1)
200 if not policy
.handler
.have_actions_support():
201 # Can't ask the user to choose, so just notify them
202 # In particular, Ubuntu/Jaunty doesn't support actions
203 policy
.handler
.notify("Zero Install",
204 _("Updates ready to download for '%s'.") % root_iface
,
206 _exec_gui(policy
.root
, '--refresh', '--systray')
209 notification_closed
= tasks
.Blocker("wait for notification response")
211 def _NotificationClosed(nid
, *unused
):
212 if nid
!= our_question
: return
213 notification_closed
.trigger()
215 def _ActionInvoked(nid
, action
):
216 if nid
!= our_question
: return
217 if action
== 'download':
218 _exec_gui(policy
.root
)
219 notification_closed
.trigger()
221 policy
.handler
.notification_service
.connect_to_signal('NotificationClosed', _NotificationClosed
)
222 policy
.handler
.notification_service
.connect_to_signal('ActionInvoked', _ActionInvoked
)
224 our_question
= policy
.handler
.notify("Zero Install", _("Updates ready to download for '%s'.") % root_iface
,
225 actions
= ['download', 'Download'])
227 tasks
.wait_for_blocker(notification_closed
)
229 def spawn_background_update(policy
, verbose
):
230 """Spawn a detached child process to check for updates.
231 @param policy: policy containing interfaces to update
232 @type policy: L{policy.Policy}
233 @param verbose: whether to notify the user about minor events
234 @type verbose: bool"""
235 iface_cache
= policy
.config
.iface_cache
236 # Mark all feeds as being updated. Do this before forking, so that if someone is
237 # running lots of 0launch commands in series on the same program we don't start
238 # huge numbers of processes.
239 for uri
in policy
.solver
.selections
.selections
:
240 iface_cache
.mark_as_checking(uri
) # Main feed
241 for f
in policy
.usable_feeds(iface_cache
.get_interface(uri
)):
242 iface_cache
.mark_as_checking(f
.uri
) # Extra feeds
249 _check_for_updates(policy
, verbose
)
254 traceback
.print_exc()