480 lines
16 KiB
Python
Executable File
480 lines
16 KiB
Python
Executable File
#!/usr/bin/python -O
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
# DESCRIPTION:
|
|
# Entropy Client Updates daemon
|
|
|
|
Copyright (C) 2007-2009 Fabio Erculiani
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
"""
|
|
from __future__ import with_statement
|
|
import os
|
|
import sys
|
|
# this makes the daemon to not write the entropy pid file
|
|
# avoiding to lock other instances
|
|
sys.argv.append('--no-pid-handling')
|
|
import time
|
|
import gobject
|
|
import dbus
|
|
import dbus.service
|
|
import dbus.mainloop.glib
|
|
import signal
|
|
from threading import Lock
|
|
|
|
DAEMON_DEBUG = False
|
|
if "--debug" in sys.argv:
|
|
DAEMON_DEBUG = True
|
|
|
|
# Entropy imports
|
|
sys.path.insert(0,'/usr/lib/entropy/libraries')
|
|
sys.path.insert(0,'/usr/lib/entropy/server')
|
|
sys.path.insert(0,'/usr/lib/entropy/client')
|
|
sys.path.insert(0,'../libraries')
|
|
sys.path.insert(0,'../server')
|
|
sys.path.insert(0,'../client')
|
|
if DAEMON_DEBUG:
|
|
sys.path.insert(0,'/home/fabio/entropy/libraries')
|
|
from entropy.misc import LogFile
|
|
from entropy.i18n import _
|
|
from entropy.exceptions import PermissionDenied, RepositoryError, \
|
|
MissingParameter
|
|
import entropy.tools as entropyTools
|
|
from entropy.client.interfaces import Client
|
|
from entropy.transceivers import UrlFetcher
|
|
from entropy.const import etpConst
|
|
from entropy.core import SystemSettings as SysSet
|
|
from entropy.misc import ParallelTask
|
|
from entropy.output import TextInterface, nocolor
|
|
nocolor()
|
|
|
|
|
|
SYS_SETTINGS = SysSet()
|
|
TEXT = TextInterface()
|
|
DAEMON_LOGFILE = os.path.join(etpConst['syslogdir'],"client-updater.log")
|
|
DAEMON_LOG = LogFile(SYS_SETTINGS['system']['log_level']+1,
|
|
DAEMON_LOGFILE, header = "[client-updater]")
|
|
PREVIOUS_PROGRESS = ''
|
|
CHECK_DELAY_SECS = 3600*4
|
|
for xopt in sys.argv:
|
|
if xopt.startswith("--secs=") and (len(xopt.split("=")) > 1):
|
|
try:
|
|
delay_secs = int(xopt.split("=")[1])
|
|
CHECK_DELAY_SECS = delay_secs
|
|
except ValueError:
|
|
continue
|
|
|
|
def write_output(*args, **kwargs):
|
|
message = time.strftime('[%H:%M:%S %d/%m/%Y %Z]') + " " + args[0]
|
|
global PREVIOUS_PROGRESS
|
|
if PREVIOUS_PROGRESS == message:
|
|
return
|
|
PREVIOUS_PROGRESS = message
|
|
DAEMON_LOG.write(message)
|
|
DAEMON_LOG.flush()
|
|
if DAEMON_DEBUG:
|
|
TEXT.updateProgress(*args, **kwargs)
|
|
|
|
class DaemonUrlFetcher(UrlFetcher):
|
|
|
|
daemon_last_avg = 100
|
|
__average = 0
|
|
__downloadedsize = 0
|
|
__remotesize = 0
|
|
__datatransfer = 0
|
|
|
|
def handle_statistics(self, th_id, downloaded_size, total_size,
|
|
average, old_average, update_step, show_speed, data_transfer,
|
|
time_remaining, time_remaining_secs):
|
|
self.__average = average
|
|
self.__downloadedsize = downloaded_size
|
|
self.__remotesize = total_size
|
|
self.__datatransfer = data_transfer
|
|
|
|
def updateProgress(self):
|
|
myavg = abs(int(round(float(self.__average), 1)))
|
|
if abs((myavg - self.daemon_last_avg)) < 1:
|
|
return
|
|
if int(myavg) % 10 == 0:
|
|
DAEMON_LOG.write("fetch @ %s%s" % (myavg, "%",))
|
|
self.daemon_last_avg = myavg
|
|
|
|
|
|
class Entropy(Client):
|
|
|
|
def init_singleton(self):
|
|
Client.init_singleton(self, load_ugc = False,
|
|
url_fetcher = DaemonUrlFetcher, repo_validation = False,
|
|
xcache = False)
|
|
self.updateProgress(
|
|
"Loading Entropy Updates daemon: check every %ss, logfile: %s" % (
|
|
CHECK_DELAY_SECS, DAEMON_LOGFILE,)
|
|
)
|
|
|
|
def updateProgress(self, *args, **kwargs):
|
|
return write_output(*args, **kwargs)
|
|
|
|
|
|
class UpdatesDaemon(dbus.service.Object):
|
|
|
|
def __init__(self):
|
|
|
|
gobject.threads_init()
|
|
if not entropyTools.is_user_in_entropy_group():
|
|
raise PermissionDenied('insufficient permissions')
|
|
self.__alive = False
|
|
self.__is_working_mutex = Lock()
|
|
self.__updater = None
|
|
self.__system_changes_checker = None
|
|
self.__oncall_updater = None
|
|
self.__quit_service_wd = None
|
|
self.__quit_service_trigger = False
|
|
self.__trigger_oncall_updater = False
|
|
self.__fetch_mutex = Lock()
|
|
self.__updates = []
|
|
self.__updates_atoms = []
|
|
self.__system_db_hash = None
|
|
self.__last_system_db_mtime = None
|
|
|
|
# start dbus service
|
|
object_path = "/notifier"
|
|
dbus_loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default = True)
|
|
system_bus = dbus.SystemBus(mainloop = dbus_loop)
|
|
name = dbus.service.BusName("org.entropy.Client", bus = system_bus)
|
|
dbus.service.Object.__init__ (self, system_bus, object_path)
|
|
write_output("__init__: dbus service loaded")
|
|
|
|
# this seems to avoid race conditions
|
|
# with dbus service not being available
|
|
time.sleep(2)
|
|
|
|
|
|
def start(self):
|
|
self.stop()
|
|
self.__alive = True
|
|
self.__updater = gobject.timeout_add(
|
|
CHECK_DELAY_SECS*1000, self.run_fetcher)
|
|
self.__oncall_updater = gobject.timeout_add(
|
|
1000, self.run_oncall_fetcher)
|
|
self.__quit_service_wd = gobject.timeout_add(
|
|
2000, self.quit_service_watchdog)
|
|
self.__system_changes_checker = gobject.timeout_add(
|
|
60*1000, self.check_system_changes)
|
|
|
|
def stop(self):
|
|
if self.__alive:
|
|
self.__alive = False
|
|
|
|
if self.__updater != None:
|
|
gobject.source_remove(self.__updater)
|
|
|
|
if self.__system_changes_checker != None:
|
|
gobject.source_remove(self.__system_changes_checker)
|
|
|
|
if self.__oncall_updater != None:
|
|
gobject.source_remove(self.__oncall_updater)
|
|
|
|
if self.__quit_service_wd != None:
|
|
gobject.source_remove(self.__quit_service_wd)
|
|
|
|
def do_alert(self, string, msg, urgency = "critical"):
|
|
write_output('alert: %s, %s, urgency: %s' % (string, msg, urgency,))
|
|
|
|
def check_system_changes(self):
|
|
changed = self._is_system_changed()
|
|
if changed:
|
|
# trigger check and push
|
|
self.__trigger_oncall_updater = True
|
|
# keep alive
|
|
return self.__alive
|
|
|
|
def run_oncall_fetcher(self):
|
|
if not self.__trigger_oncall_updater:
|
|
return self.__alive
|
|
with self.__fetch_mutex:
|
|
self.__trigger_oncall_updater = False
|
|
task = ParallelTask(self.__run_fetcher)
|
|
task.start()
|
|
#self.__run_fetcher()
|
|
return self.__alive
|
|
|
|
def quit_service_watchdog(self):
|
|
if self.__quit_service_trigger:
|
|
self.__alive = False
|
|
with self.__is_working_mutex:
|
|
raise SystemExit(0)
|
|
return self.__alive
|
|
|
|
def run_fetcher(self):
|
|
with self.__fetch_mutex:
|
|
task = ParallelTask(self.__run_fetcher)
|
|
task.start()
|
|
#self.__run_fetcher()
|
|
return self.__alive
|
|
|
|
def __run_sync(self, repos, entropy):
|
|
|
|
try:
|
|
repo_conn = entropy.Repositories(
|
|
repos, fetchSecurity = False, noEquoCheck = True)
|
|
if DAEMON_DEBUG:
|
|
write_output("__run_fetcher: repository interface loaded")
|
|
except MissingParameter, err:
|
|
if DAEMON_DEBUG:
|
|
write_output(
|
|
"__run_fetcher: MissingParameter exception, error: %s" % (
|
|
err,))
|
|
except Exception, err:
|
|
if DAEMON_DEBUG:
|
|
write_output(
|
|
"__run_fetcher: Unhandled exception, error: %s" % (err,))
|
|
else:
|
|
|
|
# 128: sync error, something bad happened
|
|
# 2: repositories not available (all)
|
|
# 1: not able to update all the repositories
|
|
if DAEMON_DEBUG:
|
|
write_output("__run_fetcher: preparing to run sync")
|
|
rc_res = repo_conn.sync()
|
|
del repo_conn
|
|
if DAEMON_DEBUG:
|
|
write_output("__run_fetcher: sync done")
|
|
|
|
if rc_res == 1:
|
|
err = _("Not all the repositories have been fetched")
|
|
self.do_alert( _("Updates: repository issues"), err )
|
|
return 0 # fine anyway
|
|
elif rc_res == 2:
|
|
err = _("No repositories found online")
|
|
self.do_alert( _("Updates: repository issues"), err )
|
|
return 0 # try to calculate updates anyway
|
|
elif rc_res == 128:
|
|
err = _("Synchronization errors. Cannot update repositories.")
|
|
self.do_alert( _("Updates: sync issues"), err )
|
|
return rc_res
|
|
elif isinstance(rc_res, basestring):
|
|
self.do_alert( _("Updates: unhandled error"), rc_res )
|
|
return 1
|
|
|
|
return 0
|
|
|
|
def __run_fetcher(self):
|
|
|
|
if self.__updater == None:
|
|
return 0
|
|
|
|
with self.__is_working_mutex:
|
|
|
|
rc_fetch = 0
|
|
entropy = Entropy()
|
|
entropy.SystemSettings.clear()
|
|
# entropy resources locked?
|
|
locked = entropy.resources_check_lock()
|
|
if locked:
|
|
if DAEMON_DEBUG:
|
|
write_output("__run_fetcher: resources locked, skipping")
|
|
return rc_fetch
|
|
|
|
if DAEMON_DEBUG:
|
|
write_output("__run_fetcher: called %s" % (time.time(),))
|
|
|
|
repos_to_up = self.get_repo_status(entropy)
|
|
|
|
if repos_to_up:
|
|
|
|
self.do_alert(
|
|
_("Repositories to update"),
|
|
unicode(repos_to_up),
|
|
urgency = 'critical'
|
|
)
|
|
|
|
gobject.timeout_add(0, self.signal_updating)
|
|
repos = repos_to_up.keys()
|
|
rc_fetch = self.__run_sync(repos, entropy)
|
|
if rc_fetch != 0:
|
|
return rc_fetch
|
|
if DAEMON_DEBUG:
|
|
write_output("__run_fetcher: sync closed, rc: %s" % (
|
|
rc_fetch,))
|
|
|
|
try:
|
|
update, remove, fine, spm_fine = entropy.calculate_world_updates()
|
|
del fine, remove
|
|
except Exception, err:
|
|
entropyTools.print_traceback(f = DAEMON_LOG)
|
|
msg = "%s: %s" % (_("Updates: error"), err,)
|
|
self.do_alert(_("Updates: error"), msg)
|
|
return 1
|
|
|
|
if update:
|
|
self.do_alert(
|
|
_("Updates available"),
|
|
"%s %d %s" % (
|
|
_("There are"), len(update),
|
|
_("updates available."),),
|
|
urgency = 'critical'
|
|
)
|
|
self.__system_db_hash = entropy.clientDbconn.checksum(
|
|
do_order = True, strict = False)
|
|
self.__updates = update[:]
|
|
del self.__updates_atoms[:]
|
|
gobject.timeout_add(0, self.signal_updates)
|
|
else:
|
|
self.do_alert(
|
|
_("No updates"),
|
|
"%s" % (update,),
|
|
urgency = 'critical'
|
|
)
|
|
gobject.timeout_add(0, self.signal_updates)
|
|
|
|
return 0
|
|
|
|
# compare repos status for updates
|
|
@dbus.service.method ( "org.entropy.Client", in_signature = '',
|
|
out_signature = 'a{si}')
|
|
def get_repo_status(self, entropy):
|
|
|
|
repos = {}
|
|
try:
|
|
repo_conn = entropy.Repositories(
|
|
noEquoCheck = True, fetchSecurity = False)
|
|
except MissingParameter:
|
|
return repos
|
|
except Exception, e:
|
|
return repos
|
|
|
|
# now get remote
|
|
for repoid in entropy.SystemSettings['repositories']['available']:
|
|
#entropy.update_repository_revision(repoid)
|
|
if repo_conn.is_repository_updatable(repoid):
|
|
if DAEMON_DEBUG:
|
|
write_output(
|
|
"get_repo_status: repo needs to be updated: %s" % (
|
|
repoid,))
|
|
entropy.repository_move_clear_cache(repoid)
|
|
repo_rev = entropy.get_repository_revision(repoid)
|
|
online_rev = repo_conn.get_online_repository_revision(repoid)
|
|
repos[repoid] = {
|
|
'local': repo_rev,
|
|
'remote': online_rev,
|
|
}
|
|
|
|
del repo_conn
|
|
return repos
|
|
|
|
@dbus.service.method ( "org.entropy.Client", in_signature = '',
|
|
out_signature = '')
|
|
def trigger_check(self):
|
|
self.__trigger_oncall_updater = True
|
|
|
|
@dbus.service.method ( "org.entropy.Client", in_signature = '',
|
|
out_signature = 'av')
|
|
def get_updates(self):
|
|
|
|
entropy = Entropy()
|
|
curr_hash = entropy.clientDbconn.checksum(
|
|
do_order = True, strict = False)
|
|
if curr_hash == self.__system_db_hash:
|
|
return self.__updates
|
|
|
|
try:
|
|
update, remove, fine, spm_fine = entropy.calculate_world_updates()
|
|
except Exception, err:
|
|
entropyTools.print_traceback(f = DAEMON_LOG)
|
|
msg = "get_updates: %s: %s" % (_("Updates: error"), err,)
|
|
self.do_alert(_("Updates: error"), msg)
|
|
return self.__updates
|
|
|
|
self.__updates = update[:]
|
|
self.__system_db_hash = curr_hash
|
|
return self.__updates
|
|
|
|
def _is_system_changed(self):
|
|
with self.__is_working_mutex:
|
|
entropy = Entropy()
|
|
locked = entropy.resources_check_lock()
|
|
if locked:
|
|
if DAEMON_DEBUG:
|
|
write_output("_is_system_changed: resources locked!")
|
|
return False # resources are locked, nothing changed yet :P
|
|
|
|
last_mtime = self.__last_system_db_mtime
|
|
dbfile = entropy.clientDbconn.dbFile
|
|
try:
|
|
cur_mtime = os.path.getmtime(dbfile)
|
|
except OSError:
|
|
cur_mtime = 0.0
|
|
changed = last_mtime != cur_mtime
|
|
if DAEMON_DEBUG and changed:
|
|
write_output("_is_system_changed: system db mtime changed!")
|
|
self.__last_system_db_mtime = cur_mtime
|
|
return changed
|
|
|
|
@dbus.service.method ( "org.entropy.Client", in_signature = '',
|
|
out_signature = 'b')
|
|
def is_system_changed(self):
|
|
return self._is_system_changed()
|
|
|
|
@dbus.service.method ( "org.entropy.Client", in_signature = '',
|
|
out_signature = 'av')
|
|
def get_updates_atoms(self):
|
|
|
|
if self.__updates_atoms:
|
|
return self.__updates_atoms
|
|
|
|
with self.__is_working_mutex:
|
|
atoms = []
|
|
entropy = Entropy()
|
|
|
|
for idpackage, repoid in self.__updates:
|
|
try:
|
|
dbc = entropy.open_repository(repoid)
|
|
atoms.append(dbc.retrieveAtom(idpackage))
|
|
except RepositoryError:
|
|
continue
|
|
|
|
self.__updates_atoms.extend(atoms)
|
|
return self.__updates_atoms
|
|
|
|
@dbus.service.method ( "org.entropy.Client", in_signature = '',
|
|
out_signature = '')
|
|
def close_connection(self):
|
|
self.__quit_service_trigger = True
|
|
|
|
# signal sent when updates are available for retrive
|
|
@dbus.service.signal(dbus_interface = 'org.entropy.Client',
|
|
signature = '')
|
|
def signal_updates(self):
|
|
if DAEMON_DEBUG:
|
|
write_output("signal_updates: updates available!")
|
|
|
|
# signal sent when daemon is updating the repositories
|
|
@dbus.service.signal(dbus_interface = 'org.entropy.Client',
|
|
signature = '')
|
|
def signal_updating(self):
|
|
if DAEMON_DEBUG:
|
|
write_output("signal_updating: updating repos!")
|
|
|
|
if __name__ == "__main__":
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
try:
|
|
ud_i = UpdatesDaemon()
|
|
except dbus.exceptions.DBusException:
|
|
raise SystemExit(1)
|
|
ud_i.start()
|
|
main_loop = gobject.MainLoop()
|
|
main_loop.run()
|
|
ud_i.stop()
|
|
raise SystemExit(0) |