1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us> 2# 3# Permission is hereby granted, free of charge, to any person 4# obtaining a copy of this software and associated documentation files 5# (the "Software"), to deal in the Software without restriction, 6# including without limitation the rights to use, copy, modify, merge, 7# publish, distribute, sublicense, and/or sell copies of the Software, 8# and to permit persons to whom the Software is furnished to do so, 9# subject to the following conditions: 10# 11# The above copyright notice and this permission notice shall be 12# included in all copies or substantial portions of the Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22# This code applies an update. 23init -1500 python in updater: 24 from store import renpy, config, Action, DictEquality, persistent 25 import store.build as build 26 27 import tarfile 28 import threading 29 import traceback 30 import os 31 import urllib.parse as urlparse 32 import json 33 import subprocess 34 import hashlib 35 import time 36 import sys 37 import struct 38 import zlib 39 import codecs 40 import io 41 import future.utils 42 43 def urlopen(url): 44 import requests 45 return io.BytesIO(requests.get(url).content) 46 47 def urlretrieve(url, fn): 48 import requests 49 50 data = requests.get(url).content 51 52 with open(fn, "wb") as f: 53 f.write(data) 54 55 try: 56 import rsa 57 except: 58 rsa = None 59 60 from renpy.exports import fsencode 61 62 # A map from update URL to the last version found at that URL. 63 if persistent._update_version is None: 64 persistent._update_version = { } 65 66 # A map from update URL to the time we last checked that URL. 67 if persistent._update_last_checked is None: 68 persistent._update_last_checked = { } 69 70 # A file containing deferred update commands, one per line. Right now, 71 # there are two commands: 72 # R <path> 73 # Rename <path>.new to <path>. 74 # D <path> 75 # Delete <path>. 76 # Deferred commands that cannot be accomplished on start are ignored. 77 DEFERRED_UPDATE_FILE = os.path.join(config.renpy_base, "update", "deferred.txt") 78 DEFERRED_UPDATE_LOG = os.path.join(config.renpy_base, "update", "log.txt") 79 80 def process_deferred_line(l): 81 cmd, _, fn = l.partition(" ") 82 83 if cmd == "R": 84 if os.path.exists(fn + ".new"): 85 86 if os.path.exists(fn): 87 os.unlink(fn) 88 89 os.rename(fn + ".new", fn) 90 91 elif cmd == "D": 92 93 if os.path.exists(fn): 94 os.unlink(fn) 95 96 else: 97 raise Exception("Bad command.") 98 99 def process_deferred(): 100 if not os.path.exists(DEFERRED_UPDATE_FILE): 101 return 102 103 # Give a previous process time to quit (and let go of the 104 # open files.) 105 time.sleep(3) 106 107 try: 108 log = file(DEFERRED_UPDATE_LOG, "ab") 109 except: 110 log = io.BytesIO() 111 112 with log: 113 with open(DEFERRED_UPDATE_FILE, "rb") as f: 114 for l in f: 115 116 l = l.rstrip("\r\n") 117 l = l.decode("utf-8") 118 119 log.write(l.encode("utf-8")) 120 121 try: 122 process_deferred_line(l) 123 except: 124 traceback.print_exc(file=log) 125 126 try: 127 os.unlink(DEFERRED_UPDATE_FILE) 128 except: 129 traceback.print_exc(file=log) 130 131 # Process deferred updates on startup, if any exist. 132 process_deferred() 133 134 def zsync_path(command): 135 """ 136 Returns the full platform-specific path to command, which is one 137 of zsync or zsyncmake. If the file doesn't exists, returns the 138 command so the system-wide copy is used. 139 """ 140 141 if renpy.windows: 142 suffix = ".exe" 143 else: 144 suffix = "" 145 146 executable = renpy.fsdecode(sys.executable) 147 148 rv = os.path.join(os.path.dirname(executable), command + suffix) 149 150 if os.path.exists(rv): 151 return rv 152 153 return command + suffix 154 155 class UpdateError(Exception): 156 """ 157 Used to report known errors. 158 """ 159 160 class UpdateCancelled(Exception): 161 """ 162 Used to report the update being cancelled. 163 """ 164 165 class Updater(threading.Thread): 166 """ 167 Applies an update. 168 169 Fields on this object are used to communicate the state of the update process. 170 171 self.state 172 The state that the updater is in. 173 174 self.message 175 In an error state, the error message that occured. 176 177 self.progress 178 If not None, a number between 0.0 and 1.0 giving some sort of 179 progress indication. 180 181 self.can_cancel 182 A boolean that indicates if cancelling the update is allowed. 183 184 """ 185 186 # Here are the possible states. 187 188 # An error occured during the update process. 189 # self.message is set to the error message. 190 ERROR = "ERROR" 191 192 # Checking to see if an update is necessary. 193 CHECKING = "CHECKING" 194 195 # We are up to date. The update process has ended. 196 # Calling proceed will return to the main menu. 197 UPDATE_NOT_AVAILABLE = "UPDATE NOT AVAILABLE" 198 199 # An update is available. 200 # The interface should ask the user if he wants to upgrade, and call .proceed() 201 # if he wants to continue. 202 UPDATE_AVAILABLE = "UPDATE AVAILABLE" 203 204 # Preparing to update by packing the current files into a .update file. 205 # self.progress is updated during this process. 206 PREPARING = "PREPARING" 207 208 # Downloading the update. 209 # self.progress is updated during this process. 210 DOWNLOADING = "DOWNLOADING" 211 212 # Unpacking the update. 213 # self.progress is updated during this process. 214 UNPACKING = "UNPACKING" 215 216 # Finishing up, by moving files around, deleting obsolete files, and writing out 217 # the state. 218 FINISHING = "FINISHING" 219 220 # Done. The update completed successfully. 221 # Calling .proceed() on the updater will trigger a game restart. 222 DONE = "DONE" 223 224 # Done. The update completed successfully. 225 # Calling .proceed() on the updater will trigger a game restart. 226 DONE_NO_RESTART = "DONE_NO_RESTART" 227 228 # The update was cancelled. 229 CANCELLED = "CANCELLED" 230 231 def __init__(self, url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, check_only=False, confirm=True, patch=True): 232 """ 233 Takes the same arguments as update(). 234 """ 235 236 self.patch = patch 237 238 if not url.startswith("http:"): 239 self.patch = False 240 241 threading.Thread.__init__(self) 242 243 import os 244 if "RENPY_FORCE_UPDATE" in os.environ: 245 force = True 246 247 # The main state. 248 self.state = Updater.CHECKING 249 250 # An additional message to show to the user. 251 self.message = None 252 253 # The progress of the current operation, or None. 254 self.progress = None 255 256 # True if the user can click the cancel button. 257 self.can_cancel = True 258 259 # True if the user can click the proceed button. 260 self.can_proceed = False 261 262 # True if the user has clicked the cancel button. 263 self.cancelled = False 264 265 # True if the user has clocked the proceed button. 266 self.proceeded = False 267 268 # The url of the updates.json file. 269 self.url = url 270 271 # Force the update? 272 self.force = force 273 274 # Packages to add during the update. 275 self.add = add 276 277 # Do we need to restart Ren'Py at the end? 278 self.restart = restart 279 280 # If true, we check for an update, and update persistent._update_version 281 # as appropriate. 282 self.check_only = check_only 283 284 # Do we prompt for confirmation? 285 self.confirm = confirm 286 287 # The base path of the game that we're updating, and the path to the update 288 # directory underneath it. 289 290 if base is None: 291 base = config.basedir 292 293 self.base = os.path.abspath(base) 294 self.updatedir = os.path.join(self.base, "update") 295 296 # If we're a mac, the directory in which our app lives. 297 splitbase = self.base.split('/') 298 if (len(splitbase) >= 4 and 299 splitbase[-1] == "autorun" and 300 splitbase[-2] == "Resources" and 301 splitbase[-3] == "Contents" and 302 splitbase[-4].endswith(".app")): 303 304 self.app = "/".join(splitbase[:-3]) 305 else: 306 self.app = None 307 308 # A condition that's used to coordinate things between the various 309 # threads. 310 self.condition = threading.Condition() 311 312 # The modules we'll be updating. 313 self.modules = [ ] 314 315 # A list of files that have to be moved into place. This is a list of filenames, 316 # where each file is moved from <file>.new to <file>. 317 self.moves = [ ] 318 319 if public_key is not None: 320 with renpy.file(public_key) as f: 321 self.public_key = rsa.PublicKey.load_pkcs1(f.read()) 322 else: 323 self.public_key = None 324 325 # The logfile that update errors are written to. 326 try: 327 self.log = open(os.path.join(self.updatedir, "log.txt"), "wb") 328 except: 329 self.log = None 330 331 self.simulate = simulate 332 333 self.daemon = True 334 self.start() 335 336 337 def run(self): 338 """ 339 The main function of the update thread, handles errors by reporting 340 them to the user. 341 """ 342 343 try: 344 if self.simulate: 345 self.simulation() 346 else: 347 self.update() 348 349 except UpdateCancelled as e: 350 self.can_cancel = True 351 self.can_proceed = False 352 self.progress = None 353 self.message = None 354 self.state = self.CANCELLED 355 356 if self.log: 357 traceback.print_exc(None, self.log) 358 self.log.flush() 359 360 except UpdateError as e: 361 self.message = e.message 362 self.can_cancel = True 363 self.can_proceed = False 364 self.state = self.ERROR 365 366 if self.log: 367 traceback.print_exc(None, self.log) 368 self.log.flush() 369 370 except Exception as e: 371 self.message = _type(e).__name__ + ": " + unicode(e) 372 self.can_cancel = True 373 self.can_proceed = False 374 self.state = self.ERROR 375 376 if self.log: 377 traceback.print_exc(None, self.log) 378 self.log.flush() 379 380 self.clean_old() 381 382 if self.log: 383 self.log.close() 384 385 def update(self): 386 """ 387 Performs the update. 388 """ 389 390 if getattr(renpy, "mobile", False): 391 raise UpdateError(_("The Ren'Py Updater is not supported on mobile devices.")) 392 393 self.load_state() 394 self.test_write() 395 self.check_updates() 396 397 pretty_version = self.check_versions() 398 399 if not self.modules: 400 self.can_cancel = False 401 self.can_proceed = True 402 self.state = self.UPDATE_NOT_AVAILABLE 403 persistent._update_version[self.url] = None 404 renpy.restart_interaction() 405 return 406 407 persistent._update_version[self.url] = pretty_version 408 409 if self.check_only: 410 renpy.restart_interaction() 411 return 412 413 if self.confirm and (not self.add): 414 415 # Confirm with the user that the update is available. 416 with self.condition: 417 self.can_cancel = True 418 self.can_proceed = True 419 self.state = self.UPDATE_AVAILABLE 420 self.version = pretty_version 421 422 renpy.restart_interaction() 423 424 while True: 425 if self.cancelled or self.proceeded: 426 break 427 428 self.condition.wait() 429 430 if self.cancelled: 431 raise UpdateCancelled() 432 433 self.can_cancel = True 434 self.can_proceed = False 435 436 # Disable autoreload. 437 renpy.set_autoreload(False) 438 439 # Perform the update. 440 self.new_state = dict(self.current_state) 441 renpy.restart_interaction() 442 443 self.progress = 0.0 444 self.state = self.PREPARING 445 446 if self.patch: 447 for i in self.modules: 448 self.prepare(i) 449 450 self.progress = 0.0 451 self.state = self.DOWNLOADING 452 renpy.restart_interaction() 453 454 for i in self.modules: 455 456 if self.patch: 457 458 try: 459 self.download(i) 460 except: 461 self.download(i, standalone=True) 462 463 else: 464 self.download_direct(i) 465 466 self.clean_old() 467 468 self.can_cancel = False 469 self.progress = 0.0 470 self.state = self.UNPACKING 471 renpy.restart_interaction() 472 473 for i in self.modules: 474 self.unpack(i) 475 476 self.progress = None 477 self.state = self.FINISHING 478 renpy.restart_interaction() 479 480 self.move_files() 481 self.delete_obsolete() 482 self.save_state() 483 self.clean_new() 484 485 self.message = None 486 self.progress = None 487 self.can_proceed = True 488 self.can_cancel = False 489 490 persistent._update_version[self.url] = None 491 492 if self.restart: 493 self.state = self.DONE 494 else: 495 self.state = self.DONE_NO_RESTART 496 497 renpy.restart_interaction() 498 499 return 500 501 def simulation(self): 502 """ 503 Simulates the update. 504 """ 505 506 def simulate_progress(): 507 for i in range(0, 30): 508 self.progress = i / 30.0 509 time.sleep(.1) 510 511 if self.cancelled: 512 raise UpdateCancelled() 513 514 time.sleep(1.5) 515 516 if self.cancelled: 517 raise UpdateCancelled() 518 519 if self.simulate == "error": 520 raise UpdateError(_("An error is being simulated.")) 521 522 if self.simulate == "not_available": 523 self.can_cancel = False 524 self.can_proceed = True 525 self.state = self.UPDATE_NOT_AVAILABLE 526 persistent._update_version[self.url] = None 527 return 528 529 pretty_version = build.version or build.directory_name 530 persistent._update_version[self.url] = pretty_version 531 532 if self.check_only: 533 renpy.restart_interaction() 534 return 535 536 # Confirm with the user that the update is available. 537 538 if self.confirm: 539 540 with self.condition: 541 self.can_cancel = True 542 self.can_proceed = True 543 self.state = self.UPDATE_AVAILABLE 544 self.version = pretty_version 545 546 while True: 547 if self.cancelled or self.proceeded: 548 break 549 550 self.condition.wait() 551 552 self.can_proceed = False 553 554 if self.cancelled: 555 raise UpdateCancelled() 556 557 self.progress = 0.0 558 self.state = self.PREPARING 559 renpy.restart_interaction() 560 561 simulate_progress() 562 563 self.progress = 0.0 564 self.state = self.DOWNLOADING 565 renpy.restart_interaction() 566 567 simulate_progress() 568 569 self.can_cancel = False 570 self.progress = 0.0 571 self.state = self.UNPACKING 572 renpy.restart_interaction() 573 574 simulate_progress() 575 576 self.progress = None 577 self.state = self.FINISHING 578 renpy.restart_interaction() 579 580 time.sleep(1.5) 581 582 self.message = None 583 self.progress = None 584 self.can_proceed = True 585 self.can_cancel = False 586 587 persistent._update_version[self.url] = None 588 589 if self.restart: 590 self.state = self.DONE 591 else: 592 self.state = self.DONE_NO_RESTART 593 594 renpy.restart_interaction() 595 596 return 597 598 def proceed(self): 599 """ 600 Causes the upgraded to proceed with the next step in the process. 601 """ 602 603 if not self.can_proceed: 604 return 605 606 if self.state == self.UPDATE_NOT_AVAILABLE: 607 renpy.full_restart() 608 609 elif self.state == self.ERROR: 610 renpy.full_restart() 611 612 elif self.state == self.CANCELLED: 613 renpy.full_restart() 614 615 elif self.state == self.DONE: 616 renpy.quit(relaunch=True) 617 618 elif self.state == self.DONE_NO_RESTART: 619 return True 620 621 elif self.state == self.UPDATE_AVAILABLE: 622 with self.condition: 623 self.proceeded = True 624 self.condition.notify_all() 625 626 def cancel(self): 627 628 if not self.can_cancel: 629 return 630 631 with self.condition: 632 self.cancelled = True 633 self.condition.notify_all() 634 635 if self.restart: 636 renpy.full_restart() 637 else: 638 return False 639 640 def unlink(self, path): 641 """ 642 Tries to unlink the file at `path`. 643 """ 644 645 if os.path.exists(path + ".old"): 646 os.unlink(path + ".old") 647 648 if os.path.exists(path): 649 650 # This might fail because of a sharing violation on Windows. 651 try: 652 os.rename(path, path + ".old") 653 os.unlink(path + ".old") 654 except: 655 pass 656 657 def rename(self, old, new): 658 """ 659 Renames the old name to the new name. Tries to enforce the unix semantics, even 660 on windows. 661 """ 662 663 try: 664 os.rename(old, new) 665 return 666 except: 667 pass 668 669 try: 670 os.unlink(new) 671 except: 672 pass 673 674 os.rename(old, new) 675 676 677 def path(self, name): 678 """ 679 Converts a filename to a path on disk. 680 """ 681 682 if self.app is not None: 683 684 path = name.split("/") 685 if path[0].endswith(".app"): 686 rv = os.path.join(self.app, "/".join(path[1:])) 687 return rv 688 689 rv = os.path.join(self.base, name) 690 691 if renpy.windows: 692 rv = "\\\\?\\" + rv.replace("/", "\\") 693 694 return rv 695 696 def load_state(self): 697 """ 698 Loads the current update state from update/current.json 699 """ 700 701 fn = os.path.join(self.updatedir, "current.json") 702 703 if not os.path.exists(fn): 704 raise UpdateError(_("Either this project does not support updating, or the update status file was deleted.")) 705 706 with open(fn, "rb") as f: 707 self.current_state = json.load(f) 708 709 def test_write(self): 710 fn = os.path.join(self.updatedir, "test.txt") 711 712 try: 713 with open(fn, "wb") as f: 714 f.write("Hello, World.") 715 716 os.unlink(fn) 717 except: 718 raise UpdateError(_("This account does not have permission to perform an update.")) 719 720 if not self.log: 721 raise UpdateError(_("This account does not have permission to write the update log.")) 722 723 def check_updates(self): 724 """ 725 Downloads the list of updates from the server, parses it, and stores it in 726 self.updates. 727 """ 728 729 fn = os.path.join(self.updatedir, "updates.json") 730 urlretrieve(self.url, fn) 731 732 with open(fn, "rb") as f: 733 updates_json = f.read() 734 self.updates = json.loads(updates_json) 735 736 if self.public_key is not None: 737 fn = os.path.join(self.updatedir, "updates.json.sig") 738 urlretrieve(self.url + ".sig", fn) 739 740 with open(fn, "rb") as f: 741 signature = f.read().decode("base64") 742 743 try: 744 rsa.verify(updates_json, signature, self.public_key) 745 except: 746 raise UpdateError(_("Could not verify update signature.")) 747 748 if "monkeypatch" in self.updates: 749 future.utils.exec_(self.updates["monkeypatch"], globals(), globals()) 750 751 def add_dlc_state(self, name): 752 url = urlparse.urljoin(self.url, self.updates[name]["json_url"]) 753 f = urlopen(url) 754 d = json.load(f) 755 d[name]["version"] = 0 756 self.current_state.update(d) 757 758 759 def check_versions(self): 760 """ 761 Decides what modules need to be updated, if any. 762 """ 763 764 rv = None 765 766 # A list of names of modules we want to update. 767 self.modules = [ ] 768 769 # DLC? 770 if self.add: 771 for name in self.add: 772 if name in self.updates: 773 self.modules.append(name) 774 775 if name not in self.current_state: 776 self.add_dlc_state(name) 777 778 rv = self.updates[name]["pretty_version"] 779 780 return rv 781 782 # We update the modules that are in both versions, and that are out of date. 783 for name, data in self.current_state.items(): 784 785 if name not in self.updates: 786 continue 787 788 if data["version"] == self.updates[name]["version"]: 789 if not self.force: 790 continue 791 792 self.modules.append(name) 793 794 rv = self.updates[name]["pretty_version"] 795 796 return rv 797 798 def update_filename(self, module, new): 799 """ 800 Returns the update filename for the given module. 801 """ 802 803 rv = os.path.join(self.updatedir, module + ".update") 804 if new: 805 return rv + ".new" 806 807 return rv 808 809 def prepare(self, module): 810 """ 811 Creates a tarfile creating the files that make up module. 812 """ 813 814 state = self.current_state[module] 815 816 xbits = set(state["xbit"]) 817 directories = set(state["directories"]) 818 all = state["files"] + state["directories"] 819 all.sort() 820 821 # Add the update directory and state file. 822 all.append("update") 823 directories.add("update") 824 all.append("update/current.json") 825 826 with tarfile.open(self.update_filename(module, False), "w") as tf: 827 for i, name in enumerate(all): 828 829 if self.cancelled: 830 raise UpdateCancelled() 831 832 self.progress = 1.0 * i / len(all) 833 834 directory = name in directories 835 xbit = name in xbits 836 837 path = self.path(name) 838 839 if directory: 840 info = tarfile.TarInfo(name) 841 info.size = 0 842 info.type = tarfile.DIRTYPE 843 else: 844 if not os.path.exists(path): 845 continue 846 847 info = tf.gettarinfo(path, name) 848 849 if not info.isreg(): 850 continue 851 852 info.uid = 1000 853 info.gid = 1000 854 info.mtime = 0 855 info.uname = "renpy" 856 info.gname = "renpy" 857 858 if xbit or directory: 859 info.mode = 0o777 860 else: 861 info.mode = 0o666 862 863 if info.isreg(): 864 with open(path, "rb") as f: 865 tf.addfile(info, f) 866 else: 867 tf.addfile(info) 868 869 def download(self, module, standalone=False): 870 """ 871 Uses zsync to download the module. 872 """ 873 874 start_progress = None 875 876 new_fn = self.update_filename(module, True) 877 878 # Download the sums file. 879 sums = [ ] 880 881 f = urlopen(urlparse.urljoin(self.url, self.updates[module]["sums_url"])) 882 data = f.read() 883 884 for i in range(0, len(data), 4): 885 try: 886 sums.append(struct.unpack("<I", data[i:i+4])[0]) 887 except: 888 pass 889 890 f.close() 891 892 # Figure out the zsync command. 893 894 zsync_fn = os.path.join(self.updatedir, module + ".zsync") 895 896 # May not exist, but if it does, we want to delete it. 897 try: 898 os.unlink(zsync_fn + ".part") 899 except: 900 pass 901 902 try: 903 os.unlink(new_fn) 904 except: 905 pass 906 907 cmd = [ 908 zsync_path("zsync"), 909 "-o", new_fn, 910 ] 911 912 if not standalone: 913 cmd.extend([ 914 "-k", zsync_fn, 915 ]) 916 917 if os.path.exists(new_fn + ".part"): 918 self.rename(new_fn + ".part", new_fn + ".part.old") 919 920 if not standalone: 921 cmd.append("-i") 922 cmd.append(new_fn + ".part.old") 923 924 if not standalone: 925 for i in self.modules: 926 cmd.append("-i") 927 cmd.append(self.update_filename(module, False)) 928 929 cmd.append(urlparse.urljoin(self.url, self.updates[module]["zsync_url"])) 930 931 cmd = [ fsencode(i) for i in cmd ] 932 933 self.log.write("running %r\n" % cmd) 934 self.log.flush() 935 936 if renpy.windows: 937 938 CREATE_NO_WINDOW=0x08000000 939 p = subprocess.Popen(cmd, 940 stdin=subprocess.PIPE, 941 stdout=self.log, 942 stderr=self.log, 943 creationflags=CREATE_NO_WINDOW, 944 cwd=renpy.fsencode(self.updatedir)) 945 else: 946 947 p = subprocess.Popen(cmd, 948 stdin=subprocess.PIPE, 949 stdout=self.log, 950 stderr=self.log, 951 cwd=renpy.fsencode(self.updatedir)) 952 953 p.stdin.close() 954 955 while True: 956 if self.cancelled: 957 p.kill() 958 break 959 960 time.sleep(1) 961 962 if p.poll() is not None: 963 break 964 965 try: 966 f = file(new_fn + ".part", "rb") 967 except: 968 self.log.write("partfile does not exist\n") 969 continue 970 971 done_sums = 0 972 973 with f: 974 for i in sums: 975 976 if self.cancelled: 977 break 978 979 data = f.read(65536) 980 981 if not data: 982 break 983 984 if (zlib.adler32(data) & 0xffffffff) == i: 985 done_sums += 1 986 987 raw_progress = 1.0 * done_sums / len(sums) 988 989 if raw_progress == 1.0: 990 start_progress = None 991 self.progress = 1.0 992 continue 993 994 if start_progress is None: 995 start_progress = raw_progress 996 self.progress = 0.0 997 continue 998 999 self.progress = (raw_progress - start_progress) / (1.0 - start_progress) 1000 1001 p.wait() 1002 1003 self.log.seek(0, 2) 1004 1005 if self.cancelled: 1006 raise UpdateCancelled() 1007 1008 # Check the existence of the downloaded file. 1009 if not os.path.exists(new_fn): 1010 if os.path.exists(new_fn + ".part"): 1011 os.rename(new_fn + ".part", new_fn) 1012 else: 1013 raise UpdateError(_("The update file was not downloaded.")) 1014 1015 # Check that the downloaded file has the right digest. 1016 import hashlib 1017 with open(new_fn, "rb") as f: 1018 hash = hashlib.sha256() 1019 1020 while True: 1021 data = f.read(1024 * 1024) 1022 1023 if not data: 1024 break 1025 1026 hash.update(data) 1027 1028 digest = hash.hexdigest() 1029 1030 if digest != self.updates[module]["digest"]: 1031 raise UpdateError(_("The update file does not have the correct digest - it may have been corrupted.")) 1032 1033 if os.path.exists(new_fn + ".part.old"): 1034 os.unlink(new_fn + ".part.old") 1035 1036 if self.cancelled: 1037 raise UpdateCancelled() 1038 1039 1040 def download_direct(self, module): 1041 """ 1042 Uses zsync to download the module. 1043 """ 1044 1045 import requests 1046 1047 start_progress = None 1048 1049 new_fn = self.update_filename(module, True) 1050 part_fn = new_fn + ".part.gz" 1051 1052 # Figure out the zsync command. 1053 1054 zsync_fn = os.path.join(self.updatedir, module + ".zsync") 1055 1056 try: 1057 os.unlink(new_fn) 1058 except: 1059 pass 1060 1061 zsync_url = self.updates[module]["zsync_url"][:-6] + ".update.gz" 1062 url = urlparse.urljoin(self.url, zsync_url) 1063 1064 self.log.write("downloading %r\n" % url) 1065 self.log.flush() 1066 1067 resp = requests.get(url, stream=True) 1068 1069 if not resp.ok: 1070 raise UpdateError(_("The update file was not downloaded.")) 1071 1072 try: 1073 length = int(resp.headers.get("Content-Length", "20000000")) 1074 except: 1075 length = 20000000 1076 1077 done = 0 1078 1079 with open(part_fn, "wb") as part_f: 1080 1081 for data in resp.iter_content(1000000): 1082 1083 if self.cancelled: 1084 break 1085 1086 part_f.write(data) 1087 1088 done += len(data) 1089 1090 self.progress = min(1.0, 1.0 * done / length) 1091 1092 resp.close() 1093 1094 if self.cancelled: 1095 raise UpdateCancelled() 1096 1097 # Decompress the file. 1098 import gzip 1099 1100 with gzip.open(part_fn, "rb") as gz_f: 1101 with open(new_fn, "wb") as new_f: 1102 1103 while True: 1104 data = gz_f.read(1000000) 1105 1106 if not data: 1107 break 1108 1109 new_f.write(data) 1110 1111 os.unlink(part_fn) 1112 1113 # Check that the downloaded file has the right digest. 1114 import hashlib 1115 with open(new_fn, "rb") as f: 1116 hash = hashlib.sha256() 1117 1118 while True: 1119 data = f.read(1024 * 1024) 1120 1121 if not data: 1122 break 1123 1124 hash.update(data) 1125 1126 digest = hash.hexdigest() 1127 1128 if digest != self.updates[module]["digest"]: 1129 raise UpdateError(_("The update file does not have the correct digest - it may have been corrupted.")) 1130 1131 if self.cancelled: 1132 raise UpdateCancelled() 1133 1134 1135 1136 def unpack(self, module): 1137 """ 1138 This unpacks the module. Directories are created immediately, while files are 1139 created as filename.new, and marked to be moved into position when all packing 1140 is done. 1141 """ 1142 1143 update_fn = self.update_filename(module, True) 1144 1145 # First pass, just figure out how many tarinfo objects are in the tarfile. 1146 tf_len = 0 1147 with tarfile.open(update_fn, "r") as tf: 1148 for i in tf: 1149 tf_len += 1 1150 1151 with tarfile.open(update_fn, "r") as tf: 1152 for i, info in enumerate(tf): 1153 1154 self.progress = 1.0 * i / tf_len 1155 1156 if info.name == "update": 1157 continue 1158 1159 # Process the status info for the current module. 1160 if info.name == "update/current.json": 1161 tff = tf.extractfile(info) 1162 state = json.load(tff) 1163 tff.close() 1164 1165 self.new_state[module] = state[module] 1166 1167 continue 1168 1169 path = self.path(info.name) 1170 1171 # Extract directories. 1172 if info.isdir(): 1173 try: 1174 os.makedirs(path) 1175 except: 1176 pass 1177 1178 continue 1179 1180 if not info.isreg(): 1181 raise UpdateError(__("While unpacking {}, unknown type {}.").format(info.name, info.type)) 1182 1183 # Extract regular files. 1184 tff = tf.extractfile(info) 1185 new_path = path + ".new" 1186 with file(new_path, "wb") as f: 1187 while True: 1188 data = tff.read(1024 * 1024) 1189 if not data: 1190 break 1191 f.write(data) 1192 1193 tff.close() 1194 1195 if info.mode & 1: 1196 # If the xbit is set in the tar info, set it on disk if we can. 1197 try: 1198 umask = os.umask(0) 1199 os.umask(umask) 1200 1201 os.chmod(new_path, 0o777 & (~umask)) 1202 except: 1203 pass 1204 1205 self.moves.append(path) 1206 1207 def move_files(self): 1208 """ 1209 Move new files into place. 1210 """ 1211 1212 for path in self.moves: 1213 1214 self.unlink(path) 1215 1216 if os.path.exists(path): 1217 self.log.write("could not rename file %s" % path.encode("utf-8")) 1218 1219 with open(DEFERRED_UPDATE_FILE, "ab") as f: 1220 f.write("R " + path.encode("utf-8") + "\r\n") 1221 1222 continue 1223 1224 try: 1225 os.rename(path + ".new", path) 1226 except: 1227 pass 1228 1229 def delete_obsolete(self): 1230 """ 1231 Delete files and directories that have been made obsolete by the upgrade. 1232 """ 1233 1234 def flatten_path(d, key): 1235 rv = set() 1236 1237 for i in d.values(): 1238 for j in i[key]: 1239 rv.add(self.path(j)) 1240 1241 return rv 1242 1243 old_files = flatten_path(self.current_state, 'files') 1244 old_directories = flatten_path(self.current_state, 'directories') 1245 1246 new_files = flatten_path(self.new_state, 'files') 1247 new_directories = flatten_path(self.new_state, 'directories') 1248 1249 old_files -= new_files 1250 old_directories -= new_directories 1251 1252 old_files = list(old_files) 1253 old_files.sort() 1254 old_files.reverse() 1255 1256 old_directories = list(old_directories) 1257 old_directories.sort() 1258 old_directories.reverse() 1259 1260 for i in old_files: 1261 self.unlink(i) 1262 1263 if os.path.exists(i): 1264 self.log.write("could not delete file %s" % i.encode("utf-8")) 1265 with open(DEFERRED_UPDATE_FILE, "wb") as f: 1266 f.write("D " + i.encode("utf-8") + "\r\n") 1267 1268 for i in old_directories: 1269 try: 1270 os.rmdir(i) 1271 except: 1272 pass 1273 1274 def save_state(self): 1275 """ 1276 Saves the current state to update/current.json 1277 """ 1278 1279 fn = os.path.join(self.updatedir, "current.json") 1280 1281 with open(fn, "wb") as f: 1282 json.dump(self.new_state, f) 1283 1284 def clean(self, fn): 1285 """ 1286 Cleans the file named fn from the updates directory. 1287 """ 1288 1289 fn = os.path.join(self.updatedir, fn) 1290 if os.path.exists(fn): 1291 try: 1292 os.unlink(fn) 1293 except: 1294 pass 1295 1296 def clean_old(self): 1297 for i in self.modules: 1298 self.clean(i + ".update") 1299 1300 def clean_new(self): 1301 for i in self.modules: 1302 self.clean(i + ".update.new") 1303 self.clean(i + ".zsync") 1304 1305 installed_state_cache = None 1306 1307 def get_installed_state(base=None): 1308 """ 1309 :undocumented: 1310 1311 Returns the state of the installed packages. 1312 1313 `base` 1314 The base directory to update. Defaults to the current project's 1315 base directory. 1316 """ 1317 1318 global installed_state_cache 1319 1320 if installed_state_cache is not None: 1321 return installed_state_cache 1322 1323 if base is None: 1324 base = config.basedir 1325 1326 fn = os.path.join(base, "update", "current.json") 1327 1328 if not os.path.exists(fn): 1329 return None 1330 1331 with open(fn, "rb") as f: 1332 state = json.load(f) 1333 1334 installed_state_cache = state 1335 return state 1336 1337 def get_installed_packages(base=None): 1338 """ 1339 :doc: updater 1340 1341 Returns a list of installed DLC package names. 1342 1343 `base` 1344 The base directory to update. Defaults to the current project's 1345 base directory. 1346 """ 1347 1348 state = get_installed_state(base) 1349 1350 if state is None: 1351 return [ ] 1352 1353 rv = list(state.keys()) 1354 return rv 1355 1356 1357 def can_update(base=None): 1358 """ 1359 :doc: updater 1360 1361 Returns true if it's possible that an update can succeed. Returns false 1362 if updating is totally impossible. (For example, if the update directory 1363 was deleted.) 1364 1365 1366 Note that this does not determine if an update is actually available. 1367 To do that, use :func:`updater.UpdateVersion`. 1368 """ 1369 1370 # Written this way so we can use this code with 6.18 and earlier. 1371 if getattr(renpy, "mobile", False): 1372 return False 1373 1374 if rsa is None: 1375 return False 1376 1377 return not not get_installed_packages(base) 1378 1379 1380 def update(url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, confirm=True, patch=True): 1381 """ 1382 :doc: updater 1383 1384 Updates this Ren'Py game to the latest version. 1385 1386 `url` 1387 The URL to the updates.json file. 1388 1389 `base` 1390 The base directory that will be updated. Defaults to the base 1391 of the current game. (This can usually be ignored.) 1392 1393 `force` 1394 Force the update to occur even if the version numbers are 1395 the same. (Used for testing.) 1396 1397 `public_key` 1398 The path to a PEM file containing a public key that the 1399 update signature is checked against. (This can usually be ignored.) 1400 1401 `simulate` 1402 This is used to test update guis without actually performing 1403 an update. This can be: 1404 1405 * None to perform an update. 1406 * "available" to test the case where an update is available. 1407 * "not_available" to test the case where no update is available. 1408 * "error" to test an update error. 1409 1410 `add` 1411 A list of packages to add during this update. This is only necessary 1412 for dlc. 1413 1414 `restart` 1415 Restart the game after the update. 1416 1417 `confirm` 1418 Should Ren'Py prompt the user to confirm the update? If False, the 1419 update will proceed without confirmation. 1420 1421 `patch` 1422 If true, Ren'Py will attempt to patch the game, downloading only 1423 changed data. If false, Ren'Py will download a complete copy of 1424 the game, and update from that. This is set to false automatically 1425 when the url does not begin with "http:". 1426 """ 1427 1428 global installed_packages_cache 1429 installed_packages_cache = None 1430 1431 u = Updater(url=url, base=base, force=force, public_key=public_key, simulate=simulate, add=add, restart=restart, confirm=confirm, patch=patch) 1432 ui.timer(.1, repeat=True, action=renpy.restart_interaction) 1433 renpy.call_screen("updater", u=u) 1434 1435 1436 @renpy.pure 1437 class Update(Action, DictEquality): 1438 """ 1439 :doc: updater 1440 1441 An action that calls :func:`updater.update`. All arguments are 1442 stored and passed to that function. 1443 """ 1444 1445 def __init__(self, *args, **kwargs): 1446 self.args = args 1447 self.kwargs = kwargs 1448 1449 def __call__(self): 1450 renpy.invoke_in_new_context(update, *self.args, **self.kwargs) 1451 1452 1453 # A list of URLs that we've checked for the update version. 1454 checked = set() 1455 1456 def UpdateVersion(url, check_interval=3600*6, simulate=None, **kwargs): 1457 """ 1458 :doc: updater 1459 1460 This function contacts the server at `url`, and determines if there is 1461 a newer version of software available at that url. If there is, this 1462 function returns the new version. Otherwise, it returns None. 1463 1464 Since contacting the server can take some time, this function launches 1465 a thread in the background, and immediately returns the version from 1466 the last time the server was contacted, or None if the server has never 1467 been contacted. The background thread will restart the current interaction 1468 once the server has been contacted, which will cause screens that call 1469 this function to update. 1470 1471 Each url will be contacted at most once per Ren'Py session, and not 1472 more than once every `check_interval` seconds. When the server is not 1473 contacted, cached data will be returned. 1474 1475 Additional keyword arguments (including `simulate`) are passed to the 1476 update mechanism as if they were given to :func:`updater.update`. 1477 """ 1478 1479 if not can_update() and not simulate: 1480 return None 1481 1482 check = True 1483 1484 if url in checked: 1485 check = False 1486 1487 if time.time() < persistent._update_last_checked.get(url, 0) + check_interval: 1488 check = False 1489 1490 if check: 1491 checked.add(url) 1492 persistent._update_last_checked[url] = time.time() 1493 Updater(url, check_only=True, simulate=simulate, **kwargs) 1494 1495 return persistent._update_version.get(url, None) 1496 1497 1498 def update_command(): 1499 import time 1500 1501 ap = renpy.arguments.ArgumentParser() 1502 1503 ap.add_argument("url") 1504 ap.add_argument("--base", action='store', help="The base directory of the game to update. Defaults to the current game.") 1505 ap.add_argument("--force", action="store_true", help="Force the update to run even if the version numbers are the same.") 1506 ap.add_argument("--key", action="store", help="A file giving the public key to use of the update.") 1507 ap.add_argument("--simulate", help="The simulation mode to use. One of available, not_available, or error.") 1508 1509 args = ap.parse_args() 1510 1511 u = Updater(args.url, args.base, args.force, public_key=args.key, simulate=args.simulate) 1512 1513 while True: 1514 1515 state = u.state 1516 1517 print("State:", state) 1518 1519 if u.progress: 1520 print("Progress: {:.1%}".format(u.progress)) 1521 1522 if u.message: 1523 print("Message:", u.message) 1524 1525 if state == u.ERROR: 1526 break 1527 elif state == u.UPDATE_NOT_AVAILABLE: 1528 break 1529 elif state == u.UPDATE_AVAILABLE: 1530 u.proceed() 1531 elif state == u.DONE: 1532 break 1533 elif state == u.CANCELLED: 1534 break 1535 1536 time.sleep(.1) 1537 1538 return False 1539 1540 renpy.arguments.register_command("update", update_command) 1541 1542init -1500: 1543 screen updater: 1544 1545 add "#000" 1546 1547 frame: 1548 style_group "" 1549 1550 has side "t c b": 1551 spacing gui._scale(10) 1552 1553 label _("Updater") 1554 1555 fixed: 1556 1557 vbox: 1558 1559 if u.state == u.ERROR: 1560 text _("An error has occured:") 1561 elif u.state == u.CHECKING: 1562 text _("Checking for updates.") 1563 elif u.state == u.UPDATE_NOT_AVAILABLE: 1564 text _("This program is up to date.") 1565 elif u.state == u.UPDATE_AVAILABLE: 1566 text _("[u.version] is available. Do you want to install it?") 1567 elif u.state == u.PREPARING: 1568 text _("Preparing to download the updates.") 1569 elif u.state == u.DOWNLOADING: 1570 text _("Downloading the updates.") 1571 elif u.state == u.UNPACKING: 1572 text _("Unpacking the updates.") 1573 elif u.state == u.FINISHING: 1574 text _("Finishing up.") 1575 elif u.state == u.DONE: 1576 text _("The updates have been installed. The program will restart.") 1577 elif u.state == u.DONE_NO_RESTART: 1578 text _("The updates have been installed.") 1579 elif u.state == u.CANCELLED: 1580 text _("The updates were cancelled.") 1581 1582 if u.message is not None: 1583 null height gui._scale(10) 1584 text "[u.message!q]" 1585 1586 if u.progress is not None: 1587 null height gui._scale(10) 1588 bar value (u.progress or 0.0) range 1.0 style "_bar" 1589 1590 hbox: 1591 1592 spacing gui._scale(25) 1593 1594 if u.can_proceed: 1595 textbutton _("Proceed") action u.proceed 1596 1597 if u.can_cancel: 1598 textbutton _("Cancel") action u.cancel 1599