PageRenderTime 55ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/sickbeard/versionChecker.py

https://bitbucket.org/fyelles/sick-beard
Python | 493 lines | 434 code | 23 blank | 36 comment | 20 complexity | da3560591bc14074dbcffd25e866a28d MD5 | raw file
  1. # Author: Nic Wolfe <nic@wolfeden.ca>
  2. # URL: http://code.google.com/p/sickbeard/
  3. #
  4. # This file is part of Sick Beard.
  5. #
  6. # Sick Beard is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Sick Beard is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
  18. import sickbeard
  19. from sickbeard import version, ui
  20. from sickbeard import logger
  21. from sickbeard import scene_exceptions
  22. from sickbeard.exceptions import ex
  23. import os, platform, shutil
  24. import subprocess, re
  25. import urllib, urllib2
  26. import zipfile, tarfile
  27. from urllib2 import URLError
  28. import gh_api as github
  29. class CheckVersion():
  30. """
  31. Version check class meant to run as a thread object with the SB scheduler.
  32. """
  33. def __init__(self):
  34. self.install_type = self.find_install_type()
  35. if self.install_type == 'win':
  36. self.updater = WindowsUpdateManager()
  37. elif self.install_type == 'git':
  38. self.updater = GitUpdateManager()
  39. elif self.install_type == 'source':
  40. self.updater = SourceUpdateManager()
  41. else:
  42. self.updater = None
  43. def run(self):
  44. self.check_for_new_version()
  45. # refresh scene exceptions too
  46. scene_exceptions.retrieve_exceptions()
  47. def find_install_type(self):
  48. """
  49. Determines how this copy of SB was installed.
  50. returns: type of installation. Possible values are:
  51. 'win': any compiled windows build
  52. 'git': running from source using git
  53. 'source': running from source without git
  54. """
  55. # check if we're a windows build
  56. if version.SICKBEARD_VERSION.startswith('build '):
  57. install_type = 'win'
  58. elif os.path.isdir(os.path.join(sickbeard.PROG_DIR, '.git')):
  59. install_type = 'git'
  60. else:
  61. install_type = 'source'
  62. return install_type
  63. def check_for_new_version(self, force=False):
  64. """
  65. Checks the internet for a newer version.
  66. returns: bool, True for new version or False for no new version.
  67. force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced
  68. """
  69. if not sickbeard.VERSION_NOTIFY and not force:
  70. logger.log(u"Version checking is disabled, not checking for the newest version")
  71. return False
  72. logger.log(u"Checking if "+self.install_type+" needs an update")
  73. if not self.updater.need_update():
  74. logger.log(u"No update needed")
  75. if force:
  76. ui.notifications.message('No update needed')
  77. return False
  78. self.updater.set_newest_text()
  79. return True
  80. def update(self):
  81. if self.updater.need_update():
  82. return self.updater.update()
  83. class UpdateManager():
  84. def get_update_url(self):
  85. return sickbeard.WEB_ROOT+"/home/update/?pid="+str(sickbeard.PID)
  86. class WindowsUpdateManager(UpdateManager):
  87. def __init__(self):
  88. self._cur_version = None
  89. self._newest_version = None
  90. self.gc_url = 'http://code.google.com/p/sickbeard/downloads/list'
  91. self.version_url = 'https://raw.github.com/midgetspy/Sick-Beard/windows_binaries/updates.txt'
  92. def _find_installed_version(self):
  93. return int(sickbeard.version.SICKBEARD_VERSION[6:])
  94. def _find_newest_version(self, whole_link=False):
  95. """
  96. Checks git for the newest Windows binary build. Returns either the
  97. build number or the entire build URL depending on whole_link's value.
  98. whole_link: If True, returns the entire URL to the release. If False, it returns
  99. only the build number. default: False
  100. """
  101. regex = ".*SickBeard\-win32\-alpha\-build(\d+)(?:\.\d+)?\.zip"
  102. svnFile = urllib.urlopen(self.version_url)
  103. for curLine in svnFile.readlines():
  104. logger.log(u"checking line "+curLine, logger.DEBUG)
  105. match = re.match(regex, curLine)
  106. if match:
  107. logger.log(u"found a match", logger.DEBUG)
  108. if whole_link:
  109. return curLine.strip()
  110. else:
  111. return int(match.group(1))
  112. return None
  113. def need_update(self):
  114. self._cur_version = self._find_installed_version()
  115. self._newest_version = self._find_newest_version()
  116. logger.log(u"newest version: "+repr(self._newest_version), logger.DEBUG)
  117. if self._newest_version and self._newest_version > self._cur_version:
  118. return True
  119. def set_newest_text(self):
  120. new_str = 'There is a <a href="'+self.gc_url+'" onclick="window.open(this.href); return false;">newer version available</a> (build '+str(self._newest_version)+')'
  121. new_str += "&mdash; <a href=\""+self.get_update_url()+"\">Update Now</a>"
  122. sickbeard.NEWEST_VERSION_STRING = new_str
  123. def update(self):
  124. new_link = self._find_newest_version(True)
  125. logger.log(u"new_link: " + repr(new_link), logger.DEBUG)
  126. if not new_link:
  127. logger.log(u"Unable to find a new version link on google code, not updating")
  128. return False
  129. # download the zip
  130. try:
  131. logger.log(u"Downloading update file from "+str(new_link))
  132. (filename, headers) = urllib.urlretrieve(new_link) #@UnusedVariable
  133. # prepare the update dir
  134. sb_update_dir = os.path.join(sickbeard.PROG_DIR, 'sb-update')
  135. logger.log(u"Clearing out update folder "+sb_update_dir+" before unzipping")
  136. if os.path.isdir(sb_update_dir):
  137. shutil.rmtree(sb_update_dir)
  138. # unzip it to sb-update
  139. logger.log(u"Unzipping from "+str(filename)+" to "+sb_update_dir)
  140. update_zip = zipfile.ZipFile(filename, 'r')
  141. update_zip.extractall(sb_update_dir)
  142. update_zip.close()
  143. # find update dir name
  144. update_dir_contents = os.listdir(sb_update_dir)
  145. if len(update_dir_contents) != 1:
  146. logger.log("Invalid update data, update failed. Maybe try deleting your sb-update folder?", logger.ERROR)
  147. return False
  148. content_dir = os.path.join(sb_update_dir, update_dir_contents[0])
  149. old_update_path = os.path.join(content_dir, 'updater.exe')
  150. new_update_path = os.path.join(sickbeard.PROG_DIR, 'updater.exe')
  151. logger.log(u"Copying new update.exe file from "+old_update_path+" to "+new_update_path)
  152. shutil.move(old_update_path, new_update_path)
  153. # delete the zip
  154. logger.log(u"Deleting zip file from "+str(filename))
  155. os.remove(filename)
  156. except Exception, e:
  157. logger.log(u"Error while trying to update: "+ex(e), logger.ERROR)
  158. return False
  159. return True
  160. class GitUpdateManager(UpdateManager):
  161. def __init__(self):
  162. self._cur_commit_hash = None
  163. self._newest_commit_hash = None
  164. self._num_commits_behind = 0
  165. self.git_url = 'http://code.google.com/p/sickbeard/downloads/list'
  166. def _git_error(self):
  167. error_message = 'Unable to find your git executable - either delete your .git folder and run from source OR <a href="http://code.google.com/p/sickbeard/wiki/AdvancedSettings" onclick="window.open(this.href); return false;">set git_path in your config.ini</a> to enable updates.'
  168. sickbeard.NEWEST_VERSION_STRING = error_message
  169. return None
  170. def _run_git(self, args):
  171. if sickbeard.GIT_PATH:
  172. git_locations = ['"'+sickbeard.GIT_PATH+'"']
  173. else:
  174. git_locations = ['git']
  175. # osx people who start SB from launchd have a broken path, so try a hail-mary attempt for them
  176. if platform.system().lower() == 'darwin':
  177. git_locations.append('/usr/local/git/bin/git')
  178. output = err = None
  179. for cur_git in git_locations:
  180. cmd = cur_git+' '+args
  181. try:
  182. logger.log(u"Executing "+cmd+" with your shell in "+sickbeard.PROG_DIR, logger.DEBUG)
  183. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=sickbeard.PROG_DIR)
  184. output, err = p.communicate()
  185. logger.log(u"git output: "+output, logger.DEBUG)
  186. except OSError:
  187. logger.log(u"Command "+cmd+" didn't work, couldn't find git.")
  188. continue
  189. if 'not found' in output or "not recognized as an internal or external command" in output:
  190. logger.log(u"Unable to find git with command "+cmd, logger.DEBUG)
  191. output = None
  192. elif 'fatal:' in output or err:
  193. logger.log(u"Git returned bad info, are you sure this is a git installation?", logger.ERROR)
  194. output = None
  195. elif output:
  196. break
  197. return (output, err)
  198. def _find_installed_version(self):
  199. """
  200. Attempts to find the currently installed version of Sick Beard.
  201. Uses git show to get commit version.
  202. Returns: True for success or False for failure
  203. """
  204. output, err = self._run_git('rev-parse HEAD') #@UnusedVariable
  205. if not output:
  206. return self._git_error()
  207. logger.log(u"Git output: "+str(output), logger.DEBUG)
  208. cur_commit_hash = output.strip()
  209. if not re.match('^[a-z0-9]+$', cur_commit_hash):
  210. logger.log(u"Output doesn't look like a hash, not using it", logger.ERROR)
  211. return self._git_error()
  212. self._cur_commit_hash = cur_commit_hash
  213. return True
  214. def _check_github_for_update(self):
  215. """
  216. Uses pygithub to ask github if there is a newer version that the provided
  217. commit hash. If there is a newer version it sets Sick Beard's version text.
  218. commit_hash: hash that we're checking against
  219. """
  220. self._num_commits_behind = 0
  221. self._newest_commit_hash = None
  222. gh = github.GitHub()
  223. # find newest commit
  224. for curCommit in gh.commits('midgetspy', 'Sick-Beard', version.SICKBEARD_VERSION):
  225. if not self._newest_commit_hash:
  226. self._newest_commit_hash = curCommit['sha']
  227. if not self._cur_commit_hash:
  228. break
  229. if curCommit['sha'] == self._cur_commit_hash:
  230. break
  231. self._num_commits_behind += 1
  232. logger.log(u"newest: "+str(self._newest_commit_hash)+" and current: "+str(self._cur_commit_hash)+" and num_commits: "+str(self._num_commits_behind), logger.DEBUG)
  233. def set_newest_text(self):
  234. # if we're up to date then don't set this
  235. if self._num_commits_behind == 35:
  236. message = "or else you're ahead of master"
  237. elif self._num_commits_behind > 0:
  238. message = "you're "+str(self._num_commits_behind)+' commits behind'
  239. else:
  240. return
  241. if self._newest_commit_hash:
  242. url = 'http://github.com/midgetspy/Sick-Beard/compare/'+self._cur_commit_hash+'...'+self._newest_commit_hash
  243. else:
  244. url = 'http://github.com/midgetspy/Sick-Beard/commits/'
  245. new_str = 'There is a <a href="'+url+'" onclick="window.open(this.href); return false;">newer version available</a> ('+message+')'
  246. new_str += "&mdash; <a href=\""+self.get_update_url()+"\">Update Now</a>"
  247. sickbeard.NEWEST_VERSION_STRING = new_str
  248. def need_update(self):
  249. self._find_installed_version()
  250. try:
  251. self._check_github_for_update()
  252. except Exception:
  253. logger.log(u"Unable to contact github, can't check for update", logger.ERROR)
  254. return False
  255. logger.log(u"After checking, cur_commit = "+str(self._cur_commit_hash)+", newest_commit = "+str(self._newest_commit_hash)+", num_commits_behind = "+str(self._num_commits_behind), logger.DEBUG)
  256. if self._num_commits_behind > 0:
  257. return True
  258. return False
  259. def update(self):
  260. """
  261. Calls git pull origin <branch> in order to update Sick Beard. Returns a bool depending
  262. on the call's success.
  263. """
  264. output, err = self._run_git('pull origin '+sickbeard.version.SICKBEARD_VERSION) #@UnusedVariable
  265. if not output:
  266. return self._git_error()
  267. pull_regex = '(\d+) files? changed, (\d+) insertions?\(\+\), (\d+) deletions?\(\-\)'
  268. (files, insertions, deletions) = (None, None, None)
  269. for line in output.split('\n'):
  270. if 'Already up-to-date.' in line:
  271. logger.log(u"No update available, not updating")
  272. logger.log(u"Output: "+str(output))
  273. return False
  274. elif line.endswith('Aborting.'):
  275. logger.log(u"Unable to update from git: "+line, logger.ERROR)
  276. logger.log(u"Output: "+str(output))
  277. return False
  278. match = re.search(pull_regex, line)
  279. if match:
  280. (files, insertions, deletions) = match.groups()
  281. break
  282. if None in (files, insertions, deletions):
  283. logger.log(u"Didn't find indication of success in output, assuming git pull failed", logger.ERROR)
  284. logger.log(u"Output: "+str(output))
  285. return False
  286. return True
  287. class SourceUpdateManager(GitUpdateManager):
  288. def _find_installed_version(self):
  289. version_file = os.path.join(sickbeard.PROG_DIR, 'version.txt')
  290. if not os.path.isfile(version_file):
  291. self._cur_commit_hash = None
  292. return
  293. fp = open(version_file, 'r')
  294. self._cur_commit_hash = fp.read().strip(' \n\r')
  295. fp.close()
  296. if not self._cur_commit_hash:
  297. self._cur_commit_hash = None
  298. def need_update(self):
  299. parent_result = GitUpdateManager.need_update(self)
  300. if not self._cur_commit_hash:
  301. return True
  302. else:
  303. return parent_result
  304. def set_newest_text(self):
  305. if not self._cur_commit_hash:
  306. logger.log(u"Unknown current version, don't know if we should update or not", logger.DEBUG)
  307. new_str = "Unknown version: If you've never used the Sick Beard upgrade system then I don't know what version you have."
  308. new_str += "&mdash; <a href=\""+self.get_update_url()+"\">Update Now</a>"
  309. sickbeard.NEWEST_VERSION_STRING = new_str
  310. else:
  311. GitUpdateManager.set_newest_text(self)
  312. def update(self):
  313. """
  314. Downloads the latest source tarball from github and installs it over the existing version.
  315. """
  316. tar_download_url = 'https://github.com/midgetspy/Sick-Beard/tarball/'+version.SICKBEARD_VERSION
  317. sb_update_dir = os.path.join(sickbeard.PROG_DIR, 'sb-update')
  318. version_path = os.path.join(sickbeard.PROG_DIR, 'version.txt')
  319. # retrieve file
  320. try:
  321. logger.log(u"Downloading update from "+tar_download_url)
  322. data = urllib2.urlopen(tar_download_url)
  323. except (IOError, URLError):
  324. logger.log(u"Unable to retrieve new version from "+tar_download_url+", can't update", logger.ERROR)
  325. return False
  326. download_name = data.geturl().split('/')[-1]
  327. tar_download_path = os.path.join(sickbeard.PROG_DIR, download_name)
  328. # save to disk
  329. f = open(tar_download_path, 'wb')
  330. f.write(data.read())
  331. f.close()
  332. # extract to temp folder
  333. logger.log(u"Extracting file "+tar_download_path)
  334. tar = tarfile.open(tar_download_path)
  335. tar.extractall(sb_update_dir)
  336. tar.close()
  337. # delete .tar.gz
  338. logger.log(u"Deleting file "+tar_download_path)
  339. os.remove(tar_download_path)
  340. # find update dir name
  341. update_dir_contents = [x for x in os.listdir(sb_update_dir) if os.path.isdir(os.path.join(sb_update_dir, x))]
  342. if len(update_dir_contents) != 1:
  343. logger.log(u"Invalid update data, update failed: "+str(update_dir_contents), logger.ERROR)
  344. return False
  345. content_dir = os.path.join(sb_update_dir, update_dir_contents[0])
  346. # walk temp folder and move files to main folder
  347. for dirname, dirnames, filenames in os.walk(content_dir): #@UnusedVariable
  348. dirname = dirname[len(content_dir)+1:]
  349. for curfile in filenames:
  350. old_path = os.path.join(content_dir, dirname, curfile)
  351. new_path = os.path.join(sickbeard.PROG_DIR, dirname, curfile)
  352. if os.path.isfile(new_path):
  353. os.remove(new_path)
  354. os.renames(old_path, new_path)
  355. # update version.txt with commit hash
  356. try:
  357. ver_file = open(version_path, 'w')
  358. ver_file.write(self._newest_commit_hash)
  359. ver_file.close()
  360. except IOError, e:
  361. logger.log(u"Unable to write version file, update not complete: "+ex(e), logger.ERROR)
  362. return False
  363. return True