#!/usr/bin/python # coding: utf-8 # # pkgdistcache client v0.3.1 # by Alessio Bianchi # import os import os.path import pickle import string import subprocess import sys import time import avahi import dbus import dbus.glib import requests import xdg.BaseDirectory from gi.repository import GObject colors = { 'none': '\033[0m', 'black': '\033[0;30m', 'bold_black': '\033[1;30m', 'red': '\033[0;31m', 'bold_red': '\033[1;31m', 'green': '\033[0;32m', 'bold_green': '\033[1;32m', 'yellow': '\033[0;33m', 'bold_yellow': '\033[1;33m', 'blue': '\033[0;34m', 'bold_blue': '\033[1;34m', 'magenta': '\033[0;35m', 'bold_magenta': '\033[1;35m', 'cyan': '\033[0;36m', 'bold_cyan': '\033[1;36m', 'white': '\033[0;37m', 'bold_white': '\033[1;37m'} def printmsg(msg): print("%s>> %s%s" % (colors['bold_blue'], msg, colors['none'])) def printerr(msg): print("%s!! %s%s" % (colors['bold_red'], msg, colors['none'])) def printwarn(msg): print("%s!! %s%s" % (colors['bold_yellow'], msg, colors['none'])) # Run a command synchronously, sending stdout and stderr to shell def runcmd2(cmd, cwd=None): pipe = subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=None, stderr=None) pipe.communicate() # wait for process to terminate return pipe.returncode class Service(object): def __init__(self, service, host, ip, port): self.service = service self.host = host self.ip = ip self.port = port def __str__(self): return ("(%s, %s, %s, %d)" % (self.service, self.host, self.ip, self.port)) def __repr__(self): # il tostring sulle liste รจ repr return self.__str__() def __hash__(self): return hash(self.__str__()) def __eq__(self, other): return hash(self) == hash(other) class AvahiBrowser(object): def __init__(self): # Connect to the system bus... self.bus = dbus.SystemBus() # Get a proxy to the object we want to talk to. avahi_proxy = self.bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER) # Set the interface we want to use; server in this case. self.server = dbus.Interface(avahi_proxy, avahi.DBUS_INTERFACE_SERVER) self.version_string = self.server.GetVersionString() self.domain = "local" self.loop = GObject.MainLoop() self.services = [] def browse(self, stype): # Ask the server for a path to the browser object for the service we're # interested in... browser_path = self.server.ServiceBrowserNew( avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, stype, self.domain, dbus.UInt32(0)) # Get it's proxy object... browser_proxy = self.bus.get_object(avahi.DBUS_NAME, browser_path) # And set the interface we want to use browser = dbus.Interface( browser_proxy, avahi.DBUS_INTERFACE_SERVICE_BROWSER) # Now connect the call backs to the relevant signals. browser.connect_to_signal('ItemNew', self.new_service) browser.connect_to_signal('AllForNow', self.all_for_now) self.loop.run() return self.services def new_service(self, interface, protocol, name, stype, domain, flags): if flags & avahi.LOOKUP_RESULT_LOCAL: # The service is on this machine; ignore return try: s = self.server.ResolveService( interface, protocol, name, stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0)) # service name, host, ip, port service = Service(str(s[3]), str(s[2]), str(s[7]), int(s[8])) self.services.append(service) except dbus.DBusException as ex: # Mainly expect to see: # org.freedesktop.Avahi.TimeoutError: Timeout reached printwarn(ex) def all_for_now(self): self.loop.quit() def main(argv): # load configuration file conf_file = '/etc/pkgdistcache.conf' if os.path.isfile(conf_file): config = eval(open(conf_file).read()) else: printerr("Config file " + conf_file + " not found") return 2 download_cmd_template = string.Template(config['download_cmd']) pkg = os.path.basename(argv[1]) # argv[1] = %u passed by pacman must_download = True if not (pkg.endswith('.db') or pkg.endswith('.db.sig')): runtime_dir = xdg.BaseDirectory.get_runtime_dir(strict=False) cache_file = os.path.join(runtime_dir, 'pkgdistcache') try: # No exception means file exists stat = os.stat(cache_file) # Check age of file cache_life_secs = int(config['cache_file_life']) * 60 cache_valid = time.time() < stat.st_mtime + cache_life_secs except FileNotFoundError: cache_valid = False if cache_valid: with open(cache_file, 'rb') as f: pkgdistcache_clients = pickle.load(f) else: # recent cache file not found, discover other pkgdistcache capable # hosts via avahi and save result to a new cache file browser = AvahiBrowser() clients = browser.browse("_pkgdistcache._tcp") # remove duplicates (eg services offered on more than a network # card etc.) clients = set(clients) pkgdistcache_clients = list(clients) try: os.unlink(cache_file) # remove any old cache file except FileNotFoundError: pass with open(cache_file, 'wb') as f: pickle.dump(pkgdistcache_clients, f, -1) if pkgdistcache_clients: print("-- Discovered hosts: %s" % ", ".join(set([c.host for c in pkgdistcache_clients]))) else: print("-- No hosts discovered") for client in pkgdistcache_clients: clientip = client.ip if ":" in clientip: clientip = "[" + clientip + "]" url = "http://" + clientip + ":" + str(client.port) + "/" + pkg try: r = requests.head(url, timeout=1) except Exception as e: printwarn("Failed checking host '%s' with ip '%s': %s" % (client.host, clientip, repr(e))) continue if r.status_code != 200: continue dst = argv[2] printmsg("Downloading %s from host '%s'" % (pkg, client.host)) download_cmd = download_cmd_template.substitute( {'u': url, 'o': dst}) try: ret = runcmd2(download_cmd) if ret == 0: must_download = False break else: printwarn("Host '%s' doesn't have %s in cache" % (client.host, pkg)) except KeyboardInterrupt: printerr("Aborted") return 1 # download package file from mirror if necessary if must_download: print(">> Downloading %s from mirror" % pkg) try: download_cmd = download_cmd_template.substitute( {'u': argv[1], 'o': argv[2]}) return runcmd2(download_cmd) except KeyboardInterrupt: printerr("Aborted") return 1 else: return 0 if __name__ == '__main__': sys.exit(main(sys.argv))