Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. #
  4. # This file is part of solus-sc
  5. #
  6. # Copyright © 2013-2018 Ikey Doherty <ikey@solus-project.com>
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13.  
  14. from gi.repository import Gio, GObject, Notify, GLib
  15.  
  16. import comar
  17. import pisi.db
  18. import pisi.api
  19. from operator import attrgetter
  20. import time
  21. import hashlib
  22. import subprocess
  23.  
  24. import logging
  25.  
  26. SC_UPDATE_APP_ID = "com.solus_project.UpdateChecker"
  27.  
  28.  
  29. class ScUpdateObject(GObject.Object):
  30. """ Keep glib happy and allow us to store references in a liststore """
  31.  
  32. old_pkg = None
  33. new_pkg = None
  34.  
  35. # Simple, really.
  36. has_security_update = False
  37.  
  38. __gtype_name__ = "ScUpdateObject"
  39.  
  40. def __init__(self, old_pkg, new_pkg):
  41. GObject.Object.__init__(self)
  42. self.old_pkg = old_pkg
  43. self.new_pkg = new_pkg
  44.  
  45. if not self.old_pkg:
  46. return
  47. oldRelease = int(self.old_pkg.release)
  48. histories = self.get_history_between(oldRelease, self.new_pkg)
  49.  
  50. # Initial security update detection
  51. securities = [x for x in histories if x.type == "security"]
  52. if len(securities) < 1:
  53. return
  54. self.has_security_update = True
  55.  
  56. def is_security_update(self):
  57. """ Determine if the update introduces security fixes """
  58. return self.has_security_update
  59.  
  60. def get_history_between(self, old_release, new):
  61. """ Get the history items between the old release and new pkg """
  62. ret = list()
  63.  
  64. for i in new.history:
  65. if int(i.release) <= int(old_release):
  66. continue
  67. ret.append(i)
  68. return sorted(ret, key=attrgetter('release'), reverse=True)
  69.  
  70.  
  71. # Correspond with gschema update types
  72. UPDATE_TYPE_ALL = 1
  73. UPDATE_TYPE_SECURITY = 2
  74. UPDATE_TYPE_MANDATORY = 4
  75.  
  76. # Correspond with gschema update types
  77. UPDATE_FREQ_HOURLY = 1
  78. UPDATE_FREQ_DAILY = 2
  79. UPDATE_FREQ_WEEKLY = 4
  80.  
  81. # absolute maximum permitted by Budgie
  82. UPDATE_NOTIF_TIMEOUT = 20000
  83.  
  84. # Precomputed "next check" times
  85. UPDATE_DELTA_HOUR = 60 * 60
  86. UPDATE_DELTA_DAILY = UPDATE_DELTA_HOUR * 24
  87. UPDATE_DELTA_WEEKLY = UPDATE_DELTA_DAILY * 7
  88.  
  89. # How many secs must elapse before checking if an update is due
  90. PONG_FREQUENCY = 120
  91.  
  92.  
  93. class ScUpdateApp(Gio.Application):
  94.  
  95. pmanager = None
  96. link = None
  97. had_init = False
  98. net_mon = None
  99. notification = None
  100. first_update = False
  101.  
  102. # our gsettings
  103. settings = None
  104.  
  105. # Whether we can check for updates on a metered connection
  106. update_on_metered = True
  107.  
  108. # Corresponds to gsettings key
  109. check_updates = True
  110.  
  111. update_type = UPDATE_TYPE_ALL
  112. update_freq = UPDATE_FREQ_HOURLY
  113.  
  114. # Last unix timestamp
  115. last_checked = 0
  116.  
  117. is_updating = False
  118.  
  119. # Track the packages we notified about
  120. last_state_hash = None
  121.  
  122. def __init__(self):
  123. logging.basicConfig(
  124. filename='/home/patrick/update.log',
  125. format='%(asctime)s %(levelname)-8s %(message)s',
  126. level=logging.DEBUG,
  127. datefmt='%d-%m-%Y %H:%M:%S')
  128.  
  129. logging.info('ScUpdateObject init')
  130.  
  131. Gio.Application.__init__(self,
  132. application_id=SC_UPDATE_APP_ID,
  133. flags=Gio.ApplicationFlags.FLAGS_NONE)
  134. self.connect("activate", self.on_activate)
  135.  
  136. def on_activate(self, app):
  137. """ Initial app activation """
  138. if self.had_init:
  139. return
  140.  
  141. logging.info('on_activate')
  142.  
  143. self.settings = Gio.Settings.new("com.solus-project.software-center")
  144. self.had_init = True
  145. Notify.init("Solus Update Service")
  146.  
  147. self.settings.connect("changed", self.on_settings_changed)
  148. self.on_settings_changed("update-type")
  149. self.on_settings_changed("update-frequency")
  150. self.on_settings_changed("update-on-metered")
  151. self.on_settings_changed("last-checked")
  152.  
  153. self.net_mon = Gio.NetworkMonitor.get_default()
  154. self.net_mon.connect("network-changed", self.on_net_changed)
  155. self.load_comar()
  156.  
  157. # if we have networking, begin first check
  158. if self.is_update_check_required():
  159. self.first_update = True
  160. self.begin_background_checks()
  161. else:
  162. # No network, show cached results
  163. self.build_available_updates()
  164.  
  165. # Now run a background timer to see if we need to do updates
  166. GLib.timeout_add_seconds(PONG_FREQUENCY, self.check_update_status)
  167. # Keep running forever
  168. self.hold()
  169.  
  170. def check_update_status(self):
  171. logging.info('check_update_status')
  172. # Run us again later
  173. if self.is_updating:
  174. logging.info('check_update_status: is_updating: True')
  175. return True
  176. # Check again at a later date
  177. if not self.is_update_check_required():
  178. logging.info('check_update_status: is_update_check_required: False')
  179. return True
  180.  
  181. # Go and check for updates
  182. self.begin_background_checks()
  183. return True
  184.  
  185. def on_settings_changed(self, key, udata=None):
  186. logging.info('on_settings_changed')
  187.  
  188.  
  189. """ Settings changed, we may have to "turn ourselves off"""
  190. if key == "check-updates":
  191. self.check_updates = self.settings.get_boolean(key)
  192. self.on_net_changed(self.net_mon)
  193. elif key == "update-type":
  194. self.update_type = self.settings.get_enum(key)
  195. elif key == "update-frequency":
  196. self.update_freq = self.settings.get_enum(key)
  197. elif key == "update-on-metered":
  198. self.update_on_metered = self.settings.get_boolean(key)
  199. elif key == "last-checked":
  200. self.last_checked = self.settings.get_value(key).get_int64()
  201.  
  202. def on_net_changed(self, mon, udata=None):
  203. logging.info('on_net_changed')
  204.  
  205. """ Network connection status changed """
  206. if self.is_update_check_required():
  207. # Try to do our first refresh now
  208. if not self.first_update:
  209. self.first_update = True
  210. self.begin_background_checks()
  211.  
  212. def action_show_updates(self, notification, action, user_data):
  213. logging.info('action_show_updates')
  214.  
  215. """ Open the updates view """
  216. command = ["solus-sc", "--update-view"]
  217. try:
  218. subprocess.Popen(command)
  219. except Exception:
  220. pass
  221. notification.close()
  222.  
  223. def begin_background_checks(self):
  224. logging.info('begin_background_checks')
  225.  
  226. """ Initialise the actual background checks and initial update """
  227. self.reload_repos()
  228. pass
  229.  
  230. def load_comar(self):
  231. logging.info('load_comar')
  232.  
  233. """ Load the d-bus comar link """
  234. self.link = comar.Link()
  235. self.pmanager = self.link.System.Manager['pisi']
  236. self.link.listenSignals("System.Manager", self.pisi_callback)
  237.  
  238. def invalidate_all(self):
  239. logging.info('invalidate_all')
  240.  
  241. # Forcibly reload the repos if we got this far
  242. pisi.db.invalidate_caches()
  243. self.is_updating = False
  244.  
  245. def pisi_callback(self, package, signal, args):
  246. logging.info('pisi_callback')
  247.  
  248. """ Just let us know that things are done """
  249. if signal == 'finished' or signal is None:
  250. self.invalidate_all()
  251. self.build_available_updates()
  252. elif str(signal).startswith("tr.org.pardus.comar.Comar.PolicyKit"):
  253. self.invalidate_all()
  254.  
  255. def reload_repos(self):
  256. logging.info('reload_repos')
  257.  
  258. """ Actually refresh the repos.. """
  259. self.is_updating = True
  260. self.pmanager.updateAllRepositories()
  261.  
  262. def can_update(self):
  263. logging.info('can_update (default False)')
  264.  
  265. """ Determine if policy/connection allows checking for updates """
  266. # No network so we can't do anything anyway
  267. if not self.check_updates:
  268. return False
  269. if not self.net_mon.get_network_available():
  270. return False
  271. # Not allowed to update on metered connection ?
  272. if not self.update_on_metered:
  273. if self.net_mon.get_network_metered():
  274. return False
  275. logging.info('can_update: True')
  276. return True
  277.  
  278. def build_available_updates(self):
  279. logging.info('build_available_updates')
  280.  
  281. """ Check the actual update availability - post refresh """
  282. self.is_updating = False
  283. upds = None
  284. try:
  285. upds = pisi.api.list_upgradable()
  286. except:
  287. return
  288.  
  289. self.store_update_time()
  290.  
  291. if not upds or len(upds) < 1:
  292. return
  293.  
  294. idb = pisi.db.installdb.InstallDB()
  295. pdb = pisi.db.packagedb.PackageDB()
  296.  
  297. security_ups = []
  298. mandatory_ups = []
  299.  
  300. pkg_hash = hashlib.sha256()
  301. ssz = ""
  302.  
  303. for up in upds:
  304. # Might be obsolete, skip it
  305. if not pdb.has_package(up):
  306. continue
  307. candidate = pdb.get_package(up)
  308. old_pkg = None
  309. ssz += str(candidate.packageHash)
  310. if idb.has_package(up):
  311. old_pkg = idb.get_package(up)
  312. sc = ScUpdateObject(old_pkg, candidate)
  313. if sc.is_security_update():
  314. security_ups.append(sc)
  315. if candidate.partOf == "system.base":
  316. mandatory_ups.append(sc)
  317.  
  318. pkg_hash.update(ssz)
  319. hx = pkg_hash.hexdigest()
  320.  
  321. # If this packageset is identical to the last package set that we
  322. # notified the user about, don't keep spamming them every single time!
  323. if hx is not None and hx == self.last_state_hash:
  324. return
  325.  
  326. self.last_state_hash = hx
  327.  
  328. # If its security only...
  329. if self.update_type == UPDATE_TYPE_SECURITY:
  330. if len(security_ups) < 1:
  331. return
  332. elif self.update_type == UPDATE_TYPE_MANDATORY:
  333. if len(security_ups) < 1 and len(mandatory_ups) < 1:
  334. return
  335.  
  336. # All update types
  337.  
  338. if len(security_ups) > 0:
  339. title = _("Security updates available")
  340. body = _("Update at your earliest convenience to ensure continued "
  341. "security of your device")
  342. icon_name = "software-update-urgent-symbolic"
  343. else:
  344. title = _("Software updates available")
  345. body = _("New software updates are available for your device")
  346. icon_name = "software-update-available-symbolic"
  347.  
  348. logging.info('build_available_updates: SHOWING UPDATES')
  349.  
  350. self.notification = Notify.Notification.new(title, body, icon_name)
  351. self.notification.set_timeout(UPDATE_NOTIF_TIMEOUT)
  352. self.notification.add_action("open-sc", _("Open Software Center"),
  353. self.action_show_updates, None)
  354. self.notification.show()
  355.  
  356. def store_update_time(self):
  357. logging.info('store_update_time')
  358.  
  359. # Store the actual update time
  360. timestamp = time.time()
  361. variant = GLib.Variant.new_int64(timestamp)
  362. self.settings.set_value("last-checked", variant)
  363. self.last_checked = timestamp
  364.  
  365. def is_update_check_required(self):
  366. """ Determine if an update is required at all"""
  367.  
  368. logging.info('is_update_check_required')
  369.  
  370. delta = None
  371. if not self.can_update():
  372. logging.info('is_update_check_required: no update required')
  373. return False
  374. if self.update_freq == UPDATE_FREQ_HOURLY:
  375. delta = UPDATE_DELTA_HOUR
  376. elif self.update_freq == UPDATE_FREQ_DAILY:
  377. delta = UPDATE_DELTA_DAILY
  378. else:
  379. delta = UPDATE_DELTA_WEEKLY
  380. next_time = self.last_checked + delta
  381. cur_time = time.time()
  382.  
  383. logging.info('is_update_check_required: update required: ' + str(next_time < cur_time))
  384.  
  385. if next_time < cur_time:
  386. return True
  387. return False