def check_merge(master, new, expected): master_doc = minidom.parseString(header + master + footer) new_doc = minidom.parseString(header + new + footer) merge.merge(master_doc, new_doc) expected_doc = minidom.parseString(header + expected + footer) def remove_boring(doc): for node in list(doc.documentElement.childNodes): if node.localName in ('name', 'summary', 'description'): doc.documentElement.removeChild(node) remove_boring(master_doc) remove_boring(expected_doc) formatting.format_node(master_doc.documentElement, "\n") formatting.format_node(expected_doc.documentElement, "\n") master_doc.normalize() expected_doc.normalize() if xmltools.nodes_equal(master_doc.documentElement, expected_doc.documentElement): return actual = master_doc.documentElement.toxml() expected = expected_doc.documentElement.toxml() assert actual != expected raise Exception("Failed.\n\nExpected:\n{}\nActual:\n{}".format(expected, actual))
def check_merge(master, new, expected): master_doc = minidom.parseString(header + master + footer) new_doc = minidom.parseString(header + new + footer) merge.merge(master_doc, new_doc) expected_doc = minidom.parseString(header + expected + footer) def remove_boring(doc): for node in list(doc.documentElement.childNodes): if node.localName in ('name', 'summary', 'description'): doc.documentElement.removeChild(node) remove_boring(master_doc) remove_boring(expected_doc) formatting.format_node(master_doc.documentElement, "\n") formatting.format_node(expected_doc.documentElement, "\n") master_doc.normalize() expected_doc.normalize() if xmltools.nodes_equal(master_doc.documentElement, expected_doc.documentElement): return actual = master_doc.documentElement.toxml() expected = expected_doc.documentElement.toxml() assert actual != expected raise Exception("Failed.\n\nExpected:\n{}\nActual:\n{}".format( expected, actual))
def build_public_feeds(config): feeds = [] for dirpath, dirnames, filenames in os.walk('feeds'): for f in filenames: if f.endswith('.xml') and not f.startswith('.'): source_path = join(dirpath, f) public_rel_path = paths.get_public_rel_path(config, relpath(source_path, 'feeds')) target_path = join("public", public_rel_path) new_doc = generate_public_xml(config, source_path) changed = True if os.path.exists(target_path): with open(target_path, 'rb') as stream: old_doc = minidom.parse(stream) if xmltools.nodes_equal(old_doc.documentElement, new_doc.documentElement): #print("%s unchanged" % source_path) changed = False feeds.append(PublicFeed(abspath(source_path), public_rel_path, new_doc, changed)) if config.GPG_SIGNING_KEY: key_path = export_key(join('public', 'keys'), config.GPG_SIGNING_KEY) other_files = [relpath(key_path, 'public')] else: other_files = [] for public_feed in feeds: target_path = join('public', public_feed.public_rel_path) target_dir = dirname(target_path) if not os.path.isdir(target_dir): os.makedirs(target_dir) if config.GPG_SIGNING_KEY and config.GPG_PUBLIC_KEY_DIRECTORY: key_symlink_rel_path = join(dirname(public_feed.public_rel_path), config.GPG_PUBLIC_KEY_DIRECTORY, basename(key_path)) other_files.append(key_symlink_rel_path) key_symlink_path = join('public', key_symlink_rel_path) if not os.path.exists(key_symlink_path): if os.name == 'nt': import shutil shutil.copyfile(key_path, key_symlink_path) else: os.symlink(relpath(key_path, dirname(key_symlink_path)), key_symlink_path) os.stat(key_symlink_path) if not public_feed.changed: continue path_to_resources = relpath(join('public', 'resources'), dirname(target_path)) new_xml = (feed_header % path_to_resources).encode('utf-8') + public_feed.doc.documentElement.toxml('utf-8') + '\n' signed_xml = sign_xml(config, new_xml) with open(target_path + '.new', 'wb') as stream: stream.write(signed_xml) support.portable_rename(target_path + '.new', target_path) print("Updated", target_path) return feeds, other_files
def build_public_feeds(config): feeds = [] for dirpath, dirnames, filenames in os.walk('feeds'): for f in filenames: if f.endswith('.xml') and not f.startswith('.'): source_path = join(dirpath, f) public_rel_path = paths.get_public_rel_path(config, relpath(source_path, 'feeds')) target_path = join("public", public_rel_path) new_doc = generate_public_xml(config, source_path) changed = True if os.path.exists(target_path): with open(target_path, 'rb') as stream: old_doc = minidom.parse(stream) if xmltools.nodes_equal(old_doc.documentElement, new_doc.documentElement): #print("%s unchanged" % source_path) changed = False feeds.append(PublicFeed(abspath(source_path), public_rel_path, new_doc, changed)) key_path = export_key(join('public', 'keys'), config.GPG_SIGNING_KEY) other_files = [relpath(key_path, 'public')] for public_feed in feeds: target_path = join('public', public_feed.public_rel_path) target_dir = dirname(target_path) if not os.path.isdir(target_dir): os.makedirs(target_dir) if config.GPG_PUBLIC_KEY_DIRECTORY: key_symlink_rel_path = join(dirname(public_feed.public_rel_path), config.GPG_PUBLIC_KEY_DIRECTORY, basename(key_path)) other_files.append(key_symlink_rel_path) key_symlink_path = join('public', key_symlink_rel_path) if not os.path.exists(key_symlink_path): if os.name == 'nt': import shutil shutil.copyfile(key_path, key_symlink_path) else: os.symlink(relpath(key_path, dirname(key_symlink_path)), key_symlink_path) os.stat(key_symlink_path) if not public_feed.changed: continue path_to_resources = relpath(join('public', 'resources'), dirname(target_path)) new_xml = (feed_header % path_to_resources).encode('utf-8') + public_feed.doc.documentElement.toxml('utf-8') + '\n' signed_xml = sign_xml(config, new_xml) with open(target_path + '.new', 'wb') as stream: stream.write(signed_xml) support.portable_rename(target_path + '.new', target_path) print("Updated", target_path) return feeds, other_files
def write_catalog(config, feeds, dir_rel_path): cat_ns = namespace.Namespace() cat_ns.register_namespace(XMLNS_CATALOG, "c") impl = minidom.getDOMImplementation() cat_doc = impl.createDocument(XMLNS_CATALOG, "c:catalog", None) cat_root = cat_doc.documentElement cat_root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:c', XMLNS_CATALOG) cat_root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE) custom_tags = {} for (name, ns, tags) in getattr(config, 'ADDITIONAL_CATALOG_TAGS', []): cat_ns.register_namespace(ns, name) cat_root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + name, ns) custom_tags[ns] = tags feed_roots = [feed.doc.documentElement for feed in feeds] def get_name(feed_root): return feed_root.getElementsByTagName('name')[0].firstChild.wholeText is_excluded_from_catalog = getattr(config, 'is_excluded_from_catalog', _default_is_excluded_from_catalog) for feed_root in sorted(feed_roots, key=get_name): if is_excluded_from_catalog(feed_root, dir_rel_path): continue elem = cat_doc.createElementNS(XMLNS_IFACE, "interface") elem.setAttribute('uri', feed_root.getAttribute("uri")) for feed_elem in feed_root.childNodes: ns = feed_elem.namespaceURI if ((ns == XMLNS_IFACE and feed_elem.localName in catalog_names) or (ns in custom_tags and feed_elem.localName in custom_tags[ns])): elem.appendChild(cat_ns.import_node(cat_doc, feed_elem)) cat_root.appendChild(elem) catalog_file = join('public', dir_rel_path, 'catalog.xml') need_update = True if os.path.exists(catalog_file): with open(catalog_file, 'rb') as stream: old_catalog = minidom.parse(stream) need_update = not xmltools.nodes_equal(old_catalog.documentElement, cat_doc.documentElement) if need_update: path_to_resources = relpath('resources', dir_rel_path).replace(os.sep, '/').encode() new_data = build.sign_xml(config, (catalog_header % path_to_resources) + cat_doc.documentElement.toxml(encoding = 'utf-8') + b'\n') with open(catalog_file + '.new', 'wb') as stream: stream.write(new_data) support.portable_rename(catalog_file + '.new', catalog_file) print("Updated " + catalog_file) return join(dir_rel_path, 'catalog.xml')
def write_catalog(config, feeds): cat_ns = namespace.Namespace() cat_ns.register_namespace(XMLNS_CATALOG, "c") impl = minidom.getDOMImplementation() cat_doc = impl.createDocument(XMLNS_CATALOG, "c:catalog", None) cat_root = cat_doc.documentElement cat_root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:c', XMLNS_CATALOG) cat_root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE) custom_tags = {} for (name, ns, tags) in getattr(config, 'ADDITIONAL_CATALOG_TAGS', []): cat_ns.register_namespace(ns, name) cat_root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + name, ns) custom_tags[ns] = tags feed_roots = [feed.doc.documentElement for feed in feeds] def get_name(feed_root): return feed_root.getElementsByTagName('name')[0].firstChild.wholeText def is_replaced(feed_root): return feed_root.getElementsByTagName('replaced-by').length > 0 for feed_root in sorted(feed_roots, key=get_name): if is_replaced(feed_root): continue elem = cat_doc.createElementNS(XMLNS_IFACE, "interface") elem.setAttribute('uri', feed_root.getAttribute("uri")) for feed_elem in feed_root.childNodes: ns = feed_elem.namespaceURI if ((ns == XMLNS_IFACE and feed_elem.localName in catalog_names) or (ns in custom_tags and feed_elem.localName in custom_tags[ns])): elem.appendChild(cat_ns.import_node(cat_doc, feed_elem)) cat_root.appendChild(elem) catalog_file = join('public', 'catalog.xml') need_update = True if os.path.exists(catalog_file): with open(catalog_file, 'rb') as stream: old_catalog = minidom.parse(stream) need_update = not xmltools.nodes_equal(old_catalog.documentElement, cat_doc.documentElement) if need_update: new_data = build.sign_xml(config, catalog_header + cat_doc.documentElement.toxml(encoding = 'utf-8') + '\n') with open(catalog_file + '.new', 'wb') as stream: stream.write(new_data) support.portable_rename(catalog_file + '.new', catalog_file) print("Updated catalog.xml") return ['catalog.xml']
def handle(config, options, args): if len(args) != 1: raise UsageError() assert not options.offline old_gui = options.gui app = config.app_mgr.lookup_app(args[0], missing_ok=True) if app is not None: old_sels = app.get_selections() old_selections = old_sels.selections iface_uri = old_sels.interface r = app.get_requirements() r.parse_update_options(options) else: iface_uri = model.canonical_iface_uri(args[0]) r = requirements.Requirements(iface_uri) r.parse_options(options) # Select once in offline console mode to get the old values options.offline = True options.gui = False options.refresh = False try: old_sels = select.get_selections_for(r, config, options, select_only=True, download_only=False, test_callback=None) except SafeException: old_selections = {} else: if old_sels is None: old_selections = {} else: old_selections = old_sels.selections # Download in online mode to get the new values config.network_use = model.network_full options.offline = False options.gui = old_gui options.refresh = True sels = select.get_selections_for(r, config, options, select_only=False, download_only=True, test_callback=None) if not sels: sys.exit(1) # Aborted by user root_feed = config.iface_cache.get_feed(iface_uri) if root_feed: target = root_feed.get_replaced_by() if target is not None: print( _("Warning: interface {old} has been replaced by {new}".format( old=iface_uri, new=target))) from zeroinstall.cmd import whatchanged changes = whatchanged.show_changes(old_selections, sels.selections) root_sel = sels[iface_uri] if not changes: from zeroinstall.support import xmltools # No obvious changes, but check for more subtle updates. if not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): changes = True print( _("Updates to metadata found, but no change to version ({version})." ).format(version=root_sel.version)) root_iface = config.iface_cache.get_interface(iface_uri) # Force a reload, since we may have used the GUI to update it for feed in config.iface_cache.get_feeds(root_iface): config.iface_cache.get_feed(feed, force=True) root_impls = config.iface_cache.get_implementations(root_iface) latest = max((impl.version, impl) for impl in root_impls)[1] if latest.version > model.parse_version(sels[iface_uri].version): print( _("A later version ({name} {latest}) exists but was not selected. Using {version} instead." ).format(latest=latest.get_version(), name=root_iface.get_name(), version=root_sel.version)) if not config.help_with_testing and latest.get_stability( ) < model.stable: print( _('To select "testing" versions, use:\n0install config help_with_testing True' )) elif not changes: print( _("No updates found. Continuing with version {version}.").format( version=root_sel.version)) if app is not None: if changes: app.set_selections(sels) app.set_requirements(r)
def _check_for_updates(self, sels, use_gui): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. If we can't use the current selections, we update in the foreground. We also schedule a background update from time-to-time anyway. @type sels: L{zeroinstall.injector.selections.Selections} @type use_gui: bool @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network if sels: utime = self._get_mtime('last-checked', warn_if_missing = True) last_solve = max(self._get_mtime('last-solve', warn_if_missing = False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) if sel.feed.startswith('distribution:'): # If the package has changed version, we'll detect that below # with get_unavailable_selections. pass elif os.path.isabs(sel.feed): # Local feed yield sel.feed else: # Cached feed cached = basedir.load_first_cache(namespaces.config_site, 'interfaces', model.escape(sel.feed)) if cached: yield cached else: raise IOError("Input %s missing; update" % sel.feed) # Per-feed configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item try: mtime = os.stat(path).st_mtime except OSError as ex: logger.info("Triggering update to {app} due to error: {ex}".format( app = self, path = path, ex = ex)) need_solve = True break if mtime and mtime > last_solve: logger.info("Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True # If any of the saved selections aren't available then we need # to download right now, not later in the background. unavailable_selections = sels.get_unavailable_selections(config = self.config, include_packages = True) if unavailable_selections: logger.info("Saved selections are unusable (missing %s)", ', '.join(str(s) for s in unavailable_selections)) need_solve = True else: # No current selections need_solve = True unavailable_selections = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config = self.config, requirements = self.get_requirements()) if driver.need_download(): if unavailable_selections: return self._foreground_update(driver, use_gui) else: # Continue with the current (cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if old_sels is None or not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked = False) try: self._touch('last-solve') except OSError as ex: logger.warning("Error checking for updates: %s", ex) # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing = False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time(): logger.info("Tried to check within last hour; not trying again now") need_update = False if need_update: try: self.set_last_check_attempt() except OSError as ex: logger.warning("Error checking for updates: %s", ex) else: from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def handle(config, options, args): if len(args) != 1: raise UsageError() assert not options.offline old_gui = options.gui app = config.app_mgr.lookup_app(args[0], missing_ok = True) if app is not None: old_sels = app.get_selections() old_selections = old_sels.selections iface_uri = old_sels.interface r = app.get_requirements() r.parse_update_options(options) else: iface_uri = model.canonical_iface_uri(args[0]) r = requirements.Requirements(iface_uri) r.parse_options(options) # Select once in offline console mode to get the old values options.offline = True options.gui = False options.refresh = False try: old_sels = select.get_selections_for(r, config, options, select_only = True, download_only = False, test_callback = None) except SafeException: old_selections = {} else: if old_sels is None: old_selections = {} else: old_selections = old_sels.selections # Download in online mode to get the new values config.network_use = model.network_full options.offline = False options.gui = old_gui options.refresh = True sels = select.get_selections_for(r, config, options, select_only = False, download_only = True, test_callback = None) if not sels: sys.exit(1) # Aborted by user root_feed = config.iface_cache.get_feed(iface_uri) if root_feed: target = root_feed.get_replaced_by() if target is not None: print(_("Warning: interface {old} has been replaced by {new}".format(old = iface_uri, new = target))) from zeroinstall.cmd import whatchanged changes = whatchanged.show_changes(old_selections, sels.selections) root_sel = sels[iface_uri] if not changes: from zeroinstall.support import xmltools # No obvious changes, but check for more subtle updates. if not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): changes = True print(_("Updates to metadata found, but no change to version ({version}).").format(version = root_sel.version)) root_iface = config.iface_cache.get_interface(iface_uri) # Force a reload, since we may have used the GUI to update it for feed in config.iface_cache.get_feeds(root_iface): config.iface_cache.get_feed(feed, force = True) root_impls = config.iface_cache.get_implementations(root_iface) latest = max((impl.version, impl) for impl in root_impls)[1] if latest.version > model.parse_version(sels[iface_uri].version): print(_("A later version ({name} {latest}) exists but was not selected. Using {version} instead.").format( latest = latest.get_version(), name = root_iface.get_name(), version = root_sel.version)) if not config.help_with_testing and latest.get_stability() < model.stable: print(_('To select "testing" versions, use:\n0install config help_with_testing True')) elif not changes: print(_("No updates found. Continuing with version {version}.").format(version = root_sel.version)) if app is not None: if changes: app.set_selections(sels) app.set_requirements(r)
def _check_for_updates(requirements, verbose, app): """@type requirements: L{zeroinstall.injector.requirements.Requirements} @type verbose: bool @type app: L{zeroinstall.apps.App}""" if app is not None: old_sels = app.get_selections() from zeroinstall.injector.driver import Driver from zeroinstall.injector.config import load_config background_handler = BackgroundHandler(requirements.interface_uri, requirements.interface_uri) background_config = load_config(background_handler) root_iface = background_config.iface_cache.get_interface( requirements.interface_uri).get_name() background_handler.title = root_iface driver = Driver(config=background_config, requirements=requirements) logger.info(_("Checking for updates to '%s' in a background process"), root_iface) if verbose: background_handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout=1) network_state = background_handler.get_network_state() if network_state not in (_NetworkState.NM_STATE_CONNECTED_SITE, _NetworkState.NM_STATE_CONNECTED_GLOBAL): logger.info( _("Not yet connected to network (status = %d). Sleeping for a bit..." ), network_state) import time time.sleep(120) if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP): logger.info(_("Still not connected to network. Giving up.")) sys.exit(1) else: logger.info(_("NetworkManager says we're on-line. Good!")) background_config.freshness = 0 # Don't bother trying to refresh when getting the interface refresh = driver.solve_with_downloads( force=True) # (causes confusing log messages) tasks.wait_for_blocker(refresh) if background_handler.need_gui or not driver.solver.ready or driver.get_uncached_implementations( ): if verbose: background_handler.notify( "Zero Install", _("Updates ready to download for '%s'.") % root_iface, timeout=1) # Run the GUI if possible... from zeroinstall import helpers gui_args = ['--refresh', '--systray', '--download' ] + requirements.get_as_options() new_sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, use_gui=None) if new_sels is None: sys.exit(0) # Cancelled by user elif new_sels is helpers.DontUseGUI: if not driver.solver.ready: background_handler.notify("Zero Install", _("Can't update '%s'") % root_iface) sys.exit(1) tasks.wait_for_blocker(driver.download_uncached_implementations()) new_sels = driver.solver.selections if app is None: background_handler.notify( "Zero Install", _("{name} updated.").format(name=root_iface), timeout=1) else: if verbose: background_handler.notify("Zero Install", _("No updates to download."), timeout=1) new_sels = driver.solver.selections if app is not None: assert driver.solver.ready from zeroinstall.support import xmltools if not xmltools.nodes_equal(new_sels.toDOM(), old_sels.toDOM()): app.set_selections(new_sels) background_handler.notify( "Zero Install", _("{app} updated.").format(app=app.get_name()), timeout=1) app.set_last_checked() sys.exit(0)
def _check_for_updates(self, sels): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. We also schedule a background update from time-to-time anyway. @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network utime = self._get_mtime('last-checked', warn_if_missing = True) last_solve = max(self._get_mtime('last-solve', warn_if_missing = False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) feed = iface_cache.get_feed(sel.feed) if not feed: raise IOError("Input %s missing; update" % sel.feed) else: if feed.local_path: yield feed.local_path else: yield (feed.url, feed.last_modified) # Per-feed configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve iface_cache = self.config.iface_cache try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item mtime = os.stat(path).st_mtime if mtime and mtime > last_solve: logger.info("Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config = self.config, requirements = self.get_requirements()) if driver.need_download(): # Continue with the current (hopefully cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked = False) self._touch('last-solve') # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing = False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time(): logger.info("Tried to check within last hour; not trying again now") need_update = False if need_update: self.set_last_check_attempt() from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def _check_for_updates(self, sels): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. We also schedule a background update from time-to-time anyway. @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network utime = self._get_mtime('last-checked', warn_if_missing=True) last_solve = max(self._get_mtime('last-solve', warn_if_missing=False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) feed = iface_cache.get_feed(sel.feed) if not feed: raise IOError("Input %s missing; update" % sel.feed) else: if feed.local_path: yield feed.local_path else: yield (feed.url, feed.last_modified) # Per-feed configuration yield basedir.load_first_config( namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve iface_cache = self.config.iface_cache try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item mtime = os.stat(path).st_mtime if mtime and mtime > last_solve: logger.info( "Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config=self.config, requirements=self.get_requirements()) if driver.need_download(): # Continue with the current (hopefully cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked=False) self._touch('last-solve') # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing=False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time( ): logger.info( "Tried to check within last hour; not trying again now") need_update = False if need_update: self.set_last_check_attempt() from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def _check_for_updates(self, sels, use_gui): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. If we can't use the current selections, we update in the foreground. We also schedule a background update from time-to-time anyway. @type sels: L{zeroinstall.injector.selections.Selections} @type use_gui: bool @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network if sels: utime = self._get_mtime('last-checked', warn_if_missing=True) last_solve = max( self._get_mtime('last-solve', warn_if_missing=False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) if sel.feed.startswith('distribution:'): # If the package has changed version, we'll detect that below # with get_unavailable_selections. pass elif os.path.isabs(sel.feed): # Local feed yield sel.feed else: # Cached feed cached = basedir.load_first_cache( namespaces.config_site, 'interfaces', model.escape(sel.feed)) if cached: yield cached else: raise IOError("Input %s missing; update" % sel.feed) # Per-feed configuration yield basedir.load_first_config( namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item try: mtime = os.stat(path).st_mtime except OSError as ex: logger.info( "Triggering update to {app} due to error: {ex}" .format(app=self, path=path, ex=ex)) need_solve = True break if mtime and mtime > last_solve: logger.info( "Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True # If any of the saved selections aren't available then we need # to download right now, not later in the background. unavailable_selections = sels.get_unavailable_selections( config=self.config, include_packages=True) if unavailable_selections: logger.info("Saved selections are unusable (missing %s)", ', '.join(str(s) for s in unavailable_selections)) need_solve = True else: # No current selections need_solve = True unavailable_selections = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config=self.config, requirements=self.get_requirements()) if driver.need_download(): if unavailable_selections: return self._foreground_update(driver, use_gui) else: # Continue with the current (cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if old_sels is None or not xmltools.nodes_equal( sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked=False) try: self._touch('last-solve') except OSError as ex: logger.warning("Error checking for updates: %s", ex) # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing=False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time( ): logger.info( "Tried to check within last hour; not trying again now") need_update = False if need_update: try: self.set_last_check_attempt() except OSError as ex: logger.warning("Error checking for updates: %s", ex) else: from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def _check_for_updates(requirements, verbose, app): if app is not None: old_sels = app.get_selections() from zeroinstall.injector.driver import Driver from zeroinstall.injector.config import load_config background_handler = BackgroundHandler(requirements.interface_uri, requirements.interface_uri) background_config = load_config(background_handler) root_iface = background_config.iface_cache.get_interface(requirements.interface_uri).get_name() background_handler.title = root_iface driver = Driver(config = background_config, requirements = requirements) logger.info(_("Checking for updates to '%s' in a background process"), root_iface) if verbose: background_handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout = 1) network_state = background_handler.get_network_state() if network_state not in (_NetworkState.NM_STATE_CONNECTED_SITE, _NetworkState.NM_STATE_CONNECTED_GLOBAL): logger.info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state) import time time.sleep(120) if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP): logger.info(_("Still not connected to network. Giving up.")) sys.exit(1) else: logger.info(_("NetworkManager says we're on-line. Good!")) background_config.freshness = 0 # Don't bother trying to refresh when getting the interface refresh = driver.solve_with_downloads(force = True) # (causes confusing log messages) tasks.wait_for_blocker(refresh) if background_handler.need_gui or driver.get_uncached_implementations(): if verbose: background_handler.notify("Zero Install", _("Updates ready to download for '%s'.") % root_iface, timeout = 1) # Run the GUI if possible... from zeroinstall import helpers gui_args = ['--refresh', '--systray', '--download'] + requirements.get_as_options() new_sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, use_gui = None) if new_sels is None: sys.exit(0) # Cancelled by user elif new_sels is helpers.DontUseGUI: tasks.wait_for_blocker(driver.download_uncached_implementations()) new_sels = driver.solver.selections if app is None: background_handler.notify("Zero Install", _("{name} updated.").format(name = root_iface), timeout = 1) else: if verbose: background_handler.notify("Zero Install", _("No updates to download."), timeout = 1) new_sels = driver.solver.selections if app is not None: assert driver.solver.ready from zeroinstall.support import xmltools if not xmltools.nodes_equal(new_sels.toDOM(), old_sels.toDOM()): app.set_selections(new_sels) background_handler.notify("Zero Install", _("{app} updated.").format(app = app.get_name()), timeout = 1) app.set_last_checked() sys.exit(0)