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