CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/common/00updater.rpy
2025-03-03 23:00:33 +01:00

1484 lines
43 KiB
Text

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# This code applies an update.
init -1500 python in updater:
from store import renpy, config, Action, DictEquality, persistent
import store.build as build
import tarfile
import threading
import traceback
import os
import urlparse
import json
import subprocess
import hashlib
import time
import sys
import struct
import zlib
import codecs
import StringIO
try:
import rsa
except:
rsa = None
from renpy.exports import fsencode
# A map from update URL to the last version found at that URL.
if persistent._update_version is None:
persistent._update_version = { }
# A map from update URL to the time we last checked that URL.
if persistent._update_last_checked is None:
persistent._update_last_checked = { }
# A file containing deferred update commands, one per line. Right now,
# there are two commands:
# R <path>
# Rename <path>.new to <path>.
# D <path>
# Delete <path>.
# Deferred commands that cannot be accomplished on start are ignored.
DEFERRED_UPDATE_FILE = os.path.join(config.renpy_base, "update", "deferred.txt")
DEFERRED_UPDATE_LOG = os.path.join(config.renpy_base, "update", "log.txt")
def process_deferred_line(l):
cmd, _, fn = l.partition(" ")
if cmd == "R":
if os.path.exists(fn + ".new"):
if os.path.exists(fn):
os.unlink(fn)
os.rename(fn + ".new", fn)
elif cmd == "D":
if os.path.exists(fn):
os.unlink(fn)
else:
raise Exception("Bad command.")
def process_deferred():
if not os.path.exists(DEFERRED_UPDATE_FILE):
return
# Give a previous process time to quit (and let go of the
# open files.)
time.sleep(3)
try:
log = file(DEFERRED_UPDATE_LOG, "ab")
except:
log = StringIO.StringIO()
with open(DEFERRED_UPDATE_FILE, "rb") as f:
for l in f:
l = l.rstrip("\r\n")
l = l.decode("utf-8")
log.write(l.encode("utf-8"))
try:
process_deferred_line(l)
except:
traceback.print_exc(file=log)
try:
os.unlink(DEFERRED_UPDATE_FILE)
except:
traceback.print_exc(file=log)
log.close()
# Process deferred updates on startup, if any exist.
process_deferred()
def zsync_path(command):
"""
Returns the full platform-specific path to command, which is one
of zsync or zsyncmake. If the file doesn't exists, returns the
command so the system-wide copy is used.
"""
if renpy.windows:
suffix = ".exe"
else:
suffix = ""
executable = renpy.fsdecode(sys.executable)
rv = os.path.join(os.path.dirname(executable), command + suffix)
if os.path.exists(rv):
return rv
return command + suffix
class UpdateError(Exception):
"""
Used to report known errors.
"""
class UpdateCancelled(Exception):
"""
Used to report the update being cancelled.
"""
class Updater(threading.Thread):
"""
Applies an update.
Fields on this object are used to communicate the state of the update process.
self.state
The state that the updater is in.
self.message
In an error state, the error message that occured.
self.progress
If not None, a number between 0.0 and 1.0 giving some sort of
progress indication.
self.can_cancel
A boolean that indicates if cancelling the update is allowed.
"""
# Here are the possible states.
# An error occured during the update process.
# self.message is set to the error message.
ERROR = "ERROR"
# Checking to see if an update is necessary.
CHECKING = "CHECKING"
# We are up to date. The update process has ended.
# Calling proceed will return to the main menu.
UPDATE_NOT_AVAILABLE = "UPDATE NOT AVAILABLE"
# An update is available.
# The interface should ask the user if he wants to upgrade, and call .proceed()
# if he wants to continue.
UPDATE_AVAILABLE = "UPDATE AVAILABLE"
# Preparing to update by packing the current files into a .update file.
# self.progress is updated during this process.
PREPARING = "PREPARING"
# Downloading the update.
# self.progress is updated during this process.
DOWNLOADING = "DOWNLOADING"
# Unpacking the update.
# self.progress is updated during this process.
UNPACKING = "UNPACKING"
# Finishing up, by moving files around, deleting obsolete files, and writing out
# the state.
FINISHING = "FINISHING"
# Done. The update completed successfully.
# Calling .proceed() on the updater will trigger a game restart.
DONE = "DONE"
# Done. The update completed successfully.
# Calling .proceed() on the updater will trigger a game restart.
DONE_NO_RESTART = "DONE_NO_RESTART"
# The update was cancelled.
CANCELLED = "CANCELLED"
def __init__(self, url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, check_only=False, confirm=True):
"""
Takes the same arguments as update().
"""
threading.Thread.__init__(self)
import os
if "RENPY_FORCE_UPDATE" in os.environ:
force = True
# The main state.
self.state = Updater.CHECKING
# An additional message to show to the user.
self.message = None
# The progress of the current operation, or None.
self.progress = None
# True if the user can click the cancel button.
self.can_cancel = True
# True if the user can click the proceed button.
self.can_proceed = False
# True if the user has clicked the cancel button.
self.cancelled = False
# True if the user has clocked the proceed button.
self.proceeded = False
# The url of the updates.json file.
self.url = url
# Force the update?
self.force = force
# Packages to add during the update.
self.add = add
# Do we need to restart Ren'Py at the end?
self.restart = restart
# If true, we check for an update, and update persistent._update_version
# as appropriate.
self.check_only = check_only
# Do we prompt for confirmation?
self.confirm = confirm
# The base path of the game that we're updating, and the path to the update
# directory underneath it.
if base is None:
base = config.basedir
self.base = os.path.abspath(base)
self.updatedir = os.path.join(self.base, "update")
# If we're a mac, the directory in which our app lives.
splitbase = self.base.split('/')
if (len(splitbase) >= 4 and
splitbase[-1] == "autorun" and
splitbase[-2] == "Resources" and
splitbase[-3] == "Contents" and
splitbase[-4].endswith(".app")):
self.app = "/".join(splitbase[:-3])
else:
self.app = None
# A condition that's used to coordinate things between the various
# threads.
self.condition = threading.Condition()
# The modules we'll be updating.
self.modules = [ ]
# A list of files that have to be moved into place. This is a list of filenames,
# where each file is moved from <file>.new to <file>.
self.moves = [ ]
if public_key is not None:
f = renpy.file(public_key)
self.public_key = rsa.PublicKey.load_pkcs1(f.read())
f.close()
else:
self.public_key = None
# The logfile that update errors are written to.
try:
self.log = open(os.path.join(self.updatedir, "log.txt"), "w")
except:
self.log = None
self.simulate = simulate
self.daemon = True
self.start()
def run(self):
"""
The main function of the update thread, handles errors by reporting
them to the user.
"""
try:
if self.simulate:
self.simulation()
else:
self.update()
except UpdateCancelled as e:
self.can_cancel = True
self.can_proceed = False
self.progress = None
self.message = None
self.state = self.CANCELLED
if self.log:
traceback.print_exc(None, self.log)
self.log.flush()
except UpdateError as e:
self.message = e.message
self.can_cancel = True
self.can_proceed = False
self.state = self.ERROR
if self.log:
traceback.print_exc(None, self.log)
self.log.flush()
except Exception as e:
self.message = _type(e).__name__ + ": " + unicode(e)
self.can_cancel = True
self.can_proceed = False
self.state = self.ERROR
if self.log:
traceback.print_exc(None, self.log)
self.log.flush()
self.clean_old()
if self.log:
self.log.close()
def update(self):
"""
Performs the update.
"""
if getattr(renpy, "mobile", False):
raise UpdateError(_("The Ren'Py Updater is not supported on mobile devices."))
self.load_state()
self.test_write()
self.check_updates()
pretty_version = self.check_versions()
if not self.modules:
self.can_cancel = False
self.can_proceed = True
self.state = self.UPDATE_NOT_AVAILABLE
persistent._update_version[self.url] = None
renpy.restart_interaction()
return
persistent._update_version[self.url] = pretty_version
if self.check_only:
renpy.restart_interaction()
return
if self.confirm and (not self.add):
# Confirm with the user that the update is available.
with self.condition:
self.can_cancel = True
self.can_proceed = True
self.state = self.UPDATE_AVAILABLE
self.version = pretty_version
renpy.restart_interaction()
while True:
if self.cancelled or self.proceeded:
break
self.condition.wait()
if self.cancelled:
raise UpdateCancelled()
self.can_cancel = True
self.can_proceed = False
# Perform the update.
self.new_state = dict(self.current_state)
renpy.restart_interaction()
self.progress = 0.0
self.state = self.PREPARING
for i in self.modules:
self.prepare(i)
self.progress = 0.0
self.state = self.DOWNLOADING
renpy.restart_interaction()
for i in self.modules:
try:
self.download(i)
except:
self.download(i, standalone=True)
self.clean_old()
self.can_cancel = False
self.progress = 0.0
self.state = self.UNPACKING
renpy.restart_interaction()
for i in self.modules:
self.unpack(i)
self.progress = None
self.state = self.FINISHING
renpy.restart_interaction()
self.move_files()
self.delete_obsolete()
self.save_state()
self.clean_new()
self.message = None
self.progress = None
self.can_proceed = True
self.can_cancel = False
persistent._update_version[self.url] = None
if self.restart:
self.state = self.DONE
else:
self.state = self.DONE_NO_RESTART
renpy.restart_interaction()
return
def simulation(self):
"""
Simulates the update.
"""
def simulate_progress():
for i in range(0, 30):
self.progress = i / 30.0
time.sleep(.1)
if self.cancelled:
raise UpdateCancelled()
time.sleep(1.5)
if self.cancelled:
raise UpdateCancelled()
if self.simulate == "error":
raise UpdateError(_("An error is being simulated."))
if self.simulate == "not_available":
self.can_cancel = False
self.can_proceed = True
self.state = self.UPDATE_NOT_AVAILABLE
persistent._update_version[self.url] = None
return
pretty_version = build.version or build.directory_name
persistent._update_version[self.url] = pretty_version
if self.check_only:
renpy.restart_interaction()
return
# Confirm with the user that the update is available.
if self.confirm:
with self.condition:
self.can_cancel = True
self.can_proceed = True
self.state = self.UPDATE_AVAILABLE
self.version = pretty_version
while True:
if self.cancelled or self.proceeded:
break
self.condition.wait()
self.can_proceed = False
if self.cancelled:
raise UpdateCancelled()
self.progress = 0.0
self.state = self.PREPARING
renpy.restart_interaction()
simulate_progress()
self.progress = 0.0
self.state = self.DOWNLOADING
renpy.restart_interaction()
simulate_progress()
self.can_cancel = False
self.progress = 0.0
self.state = self.UNPACKING
renpy.restart_interaction()
simulate_progress()
self.progress = None
self.state = self.FINISHING
renpy.restart_interaction()
time.sleep(1.5)
self.message = None
self.progress = None
self.can_proceed = True
self.can_cancel = False
persistent._update_version[self.url] = None
if self.restart:
self.state = self.DONE
else:
self.state = self.DONE_NO_RESTART
renpy.restart_interaction()
return
def proceed(self):
"""
Causes the upgraded to proceed with the next step in the process.
"""
if not self.can_proceed:
return
if self.state == self.UPDATE_NOT_AVAILABLE:
renpy.full_restart()
elif self.state == self.ERROR:
renpy.full_restart()
elif self.state == self.CANCELLED:
renpy.full_restart()
elif self.state == self.DONE:
renpy.quit(relaunch=True)
elif self.state == self.DONE_NO_RESTART:
return True
elif self.state == self.UPDATE_AVAILABLE:
with self.condition:
self.proceeded = True
self.condition.notify_all()
def cancel(self):
if not self.can_cancel:
return
with self.condition:
self.cancelled = True
self.condition.notify_all()
if self.restart:
renpy.full_restart()
else:
return False
def unlink(self, path):
"""
Tries to unlink the file at `path`.
"""
if os.path.exists(path + ".old"):
os.unlink(path + ".old")
if os.path.exists(path):
# This might fail because of a sharing violation on Windows.
try:
os.rename(path, path + ".old")
os.unlink(path + ".old")
except:
pass
def rename(self, old, new):
"""
Renames the old name to the new name. Tries to enforce the unix semantics, even
on windows.
"""
try:
os.rename(old, new)
return
except:
pass
try:
os.unlink(new)
except:
pass
os.rename(old, new)
def path(self, name):
"""
Converts a filename to a path on disk.
"""
if self.app is not None:
path = name.split("/")
if path[0].endswith(".app"):
rv = os.path.join(self.app, "/".join(path[1:]))
return rv
rv = os.path.join(self.base, name)
if renpy.windows:
rv = "\\\\?\\" + rv.replace("/", "\\")
return rv
def load_state(self):
"""
Loads the current update state from update/current.json
"""
fn = os.path.join(self.updatedir, "current.json")
if not os.path.exists(fn):
raise UpdateError(_("Either this project does not support updating, or the update status file was deleted."))
with open(fn, "rb") as f:
self.current_state = json.load(f)
def test_write(self):
fn = os.path.join(self.updatedir, "test.txt")
try:
with open(fn, "wb") as f:
f.write("Hello, World.")
os.unlink(fn)
except:
raise UpdateError(_("This account does not have permission to perform an update."))
if not self.log:
raise UpdateError(_("This account does not have permission to write the update log."))
def check_updates(self):
"""
Downloads the list of updates from the server, parses it, and stores it in
self.updates.
"""
import urllib
fn = os.path.join(self.updatedir, "updates.json")
urllib.urlretrieve(self.url, fn)
with open(fn, "rb") as f:
updates_json = f.read()
self.updates = json.loads(updates_json)
if self.public_key is not None:
fn = os.path.join(self.updatedir, "updates.json.sig")
urllib.urlretrieve(self.url + ".sig", fn)
with open(fn, "rb") as f:
signature = f.read().decode("base64")
try:
rsa.verify(updates_json, signature, self.public_key)
except:
raise UpdateError(_("Could not verify update signature."))
if "monkeypatch" in self.updates:
exec self.updates["monkeypatch"] in globals(), globals()
def add_dlc_state(self, name):
import urllib
url = urlparse.urljoin(self.url, self.updates[name]["json_url"])
f = urllib.urlopen(url)
d = json.load(f)
d[name]["version"] = 0
self.current_state.update(d)
def check_versions(self):
"""
Decides what modules need to be updated, if any.
"""
rv = None
# A list of names of modules we want to update.
self.modules = [ ]
# DLC?
if self.add:
for name in self.add:
if name in self.updates:
self.modules.append(name)
if name not in self.current_state:
self.add_dlc_state(name)
rv = self.updates[name]["pretty_version"]
return rv
# We update the modules that are in both versions, and that are out of date.
for name, data in self.current_state.iteritems():
if name not in self.updates:
continue
if data["version"] == self.updates[name]["version"]:
if not self.force:
continue
self.modules.append(name)
rv = self.updates[name]["pretty_version"]
return rv
def update_filename(self, module, new):
"""
Returns the update filename for the given module.
"""
rv = os.path.join(self.updatedir, module + ".update")
if new:
return rv + ".new"
return rv
def prepare(self, module):
"""
Creates a tarfile creating the files that make up module.
"""
state = self.current_state[module]
xbits = set(state["xbit"])
directories = set(state["directories"])
all = state["files"] + state["directories"]
all.sort()
# Add the update directory and state file.
all.append("update")
directories.add("update")
all.append("update/current.json")
tf = tarfile.open(self.update_filename(module, False), "w")
for i, name in enumerate(all):
if self.cancelled:
raise UpdateCancelled()
self.progress = 1.0 * i / len(all)
directory = name in directories
xbit = name in xbits
path = self.path(name)
if directory:
info = tarfile.TarInfo(name)
info.size = 0
info.type = tarfile.DIRTYPE
else:
if not os.path.exists(path):
continue
info = tf.gettarinfo(path, name)
if not info.isreg():
continue
info.uid = 1000
info.gid = 1000
info.mtime = 0
info.uname = "renpy"
info.gname = "renpy"
if xbit or directory:
info.mode = 0777
else:
info.mode = 0666
if info.isreg():
with open(path, "rb") as f:
tf.addfile(info, f)
else:
tf.addfile(info)
tf.close()
def download(self, module, standalone=False):
"""
Uses zsync to download the module.
"""
start_progress = None
new_fn = self.update_filename(module, True)
# Download the sums file.
sums = [ ]
import urllib
f = urllib.urlopen(urlparse.urljoin(self.url, self.updates[module]["sums_url"]))
data = f.read()
for i in range(0, len(data), 4):
try:
sums.append(struct.unpack("<I", data[i:i+4])[0])
except:
pass
f.close()
# Figure out the zsync command.
zsync_fn = os.path.join(self.updatedir, module + ".zsync")
# May not exist, but if it does, we want to delete it.
try:
os.unlink(zsync_fn + ".part")
except:
pass
try:
os.unlink(new_fn)
except:
pass
cmd = [
zsync_path("zsync"),
"-o", new_fn,
]
if not standalone:
cmd.extend([
"-k", zsync_fn,
])
if os.path.exists(new_fn + ".part"):
self.rename(new_fn + ".part", new_fn + ".part.old")
if not standalone:
cmd.append("-i")
cmd.append(new_fn + ".part.old")
if not standalone:
for i in self.modules:
cmd.append("-i")
cmd.append(self.update_filename(module, False))
cmd.append(urlparse.urljoin(self.url, self.updates[module]["zsync_url"]))
cmd = [ fsencode(i) for i in cmd ]
self.log.write("running %r\n" % cmd)
self.log.flush()
if renpy.windows:
CREATE_NO_WINDOW=0x08000000
p = subprocess.Popen(cmd,
stdin=subprocess.PIPE,
stdout=self.log,
stderr=self.log,
creationflags=CREATE_NO_WINDOW,
cwd=renpy.fsencode(self.updatedir))
else:
p = subprocess.Popen(cmd,
stdin=subprocess.PIPE,
stdout=self.log,
stderr=self.log,
cwd=renpy.fsencode(self.updatedir))
p.stdin.close()
while True:
if self.cancelled:
p.kill()
break
time.sleep(1)
if p.poll() is not None:
break
try:
f = file(new_fn + ".part", "rb")
except:
self.log.write("partfile does not exist\n")
continue
done_sums = 0
for i in sums:
if self.cancelled:
break
data = f.read(65536)
if not data:
break
if (zlib.adler32(data) & 0xffffffff) == i:
done_sums += 1
f.close()
raw_progress = 1.0 * done_sums / len(sums)
if raw_progress == 1.0:
start_progress = None
self.progress = 1.0
continue
if start_progress is None:
start_progress = raw_progress
self.progress = 0.0
continue
self.progress = (raw_progress - start_progress) / (1.0 - start_progress)
p.wait()
self.log.seek(0, 2)
if self.cancelled:
raise UpdateCancelled()
# Check the existence of the downloaded file.
if not os.path.exists(new_fn):
if os.path.exists(new_fn + ".part"):
os.rename(new_fn + ".part", new_fn)
else:
raise UpdateError(_("The update file was not downloaded."))
# Check that the downloaded file has the right digest.
import hashlib
with open(new_fn, "rb") as f:
hash = hashlib.sha256()
while True:
data = f.read(1024 * 1024)
if not data:
break
hash.update(data)
digest = hash.hexdigest()
if digest != self.updates[module]["digest"]:
raise UpdateError(_("The update file does not have the correct digest - it may have been corrupted."))
if os.path.exists(new_fn + ".part.old"):
os.unlink(new_fn + ".part.old")
if self.cancelled:
raise UpdateCancelled()
def unpack(self, module):
"""
This unpacks the module. Directories are created immediately, while files are
created as filename.new, and marked to be moved into position when all packing
is done.
"""
update_fn = self.update_filename(module, True)
# First pass, just figure out how many tarinfo objects are in the tarfile.
tf_len = 0
tf = tarfile.open(update_fn, "r")
for i in tf:
tf_len += 1
tf.close()
tf = tarfile.open(update_fn, "r")
for i, info in enumerate(tf):
self.progress = 1.0 * i / tf_len
if info.name == "update":
continue
# Process the status info for the current module.
if info.name == "update/current.json":
tff = tf.extractfile(info)
state = json.load(tff)
tff.close()
self.new_state[module] = state[module]
continue
path = self.path(info.name)
# Extract directories.
if info.isdir():
try:
os.makedirs(path)
except:
pass
continue
if not info.isreg():
raise UpdateError(__("While unpacking {}, unknown type {}.").format(info.name, info.type))
# Extract regular files.
tff = tf.extractfile(info)
new_path = path + ".new"
f = file(new_path, "wb")
while True:
data = tff.read(1024 * 1024)
if not data:
break
f.write(data)
f.close()
tff.close()
if info.mode & 1:
# If the xbit is set in the tar info, set it on disk if we can.
try:
umask = os.umask(0)
os.umask(umask)
os.chmod(new_path, 0777 & (~umask))
except:
pass
self.moves.append(path)
def move_files(self):
"""
Move new files into place.
"""
for path in self.moves:
self.unlink(path)
if os.path.exists(path):
self.log.write("could not rename file %s" % path.encode("utf-8"))
with open(DEFERRED_UPDATE_FILE, "wb") as f:
f.write("R " + path.encode("utf-8") + "\n")
continue
try:
os.rename(path + ".new", path)
except:
pass
def delete_obsolete(self):
"""
Delete files and directories that have been made obsolete by the upgrade.
"""
def flatten_path(d, key):
rv = set()
for i in d.itervalues():
for j in i[key]:
rv.add(self.path(j))
return rv
old_files = flatten_path(self.current_state, 'files')
old_directories = flatten_path(self.current_state, 'directories')
new_files = flatten_path(self.new_state, 'files')
new_directories = flatten_path(self.new_state, 'directories')
old_files -= new_files
old_directories -= new_directories
old_files = list(old_files)
old_files.sort()
old_files.reverse()
old_directories = list(old_directories)
old_directories.sort()
old_directories.reverse()
for i in old_files:
self.unlink(i)
if os.path.exists(i):
self.log.write("could not delete file %s" % i.encode("utf-8"))
with open(DEFERRED_UPDATE_FILE, "wb") as f:
f.write("D " + i.encode("utf-8") + "\n")
for i in old_directories:
try:
os.rmdir(i)
except:
pass
def save_state(self):
"""
Saves the current state to update/current.json
"""
fn = os.path.join(self.updatedir, "current.json")
with open(fn, "wb") as f:
json.dump(self.new_state, f)
def clean(self, fn):
"""
Cleans the file named fn from the updates directory.
"""
fn = os.path.join(self.updatedir, fn)
if os.path.exists(fn):
try:
os.unlink(fn)
except:
pass
def clean_old(self):
for i in self.modules:
self.clean(i + ".update")
def clean_new(self):
for i in self.modules:
self.clean(i + ".update.new")
self.clean(i + ".zsync")
installed_state_cache = None
def get_installed_state(base=None):
"""
:undocumented:
Returns the state of the installed packages.
`base`
The base directory to update. Defaults to the current project's
base directory.
"""
global installed_state_cache
if installed_state_cache is not None:
return installed_state_cache
if base is None:
base = config.basedir
fn = os.path.join(base, "update", "current.json")
if not os.path.exists(fn):
return None
with open(fn, "rb") as f:
state = json.load(f)
installed_state_cache = state
return state
def get_installed_packages(base=None):
"""
:doc: updater
Returns a list of installed DLC package names.
`base`
The base directory to update. Defaults to the current project's
base directory.
"""
state = get_installed_state(base)
if state is None:
return [ ]
rv = list(state.keys())
return rv
def can_update(base=None):
"""
:doc: updater
Returns true if it's possible that an update can succeed. Returns false
if updating is totally impossible. (For example, if the update directory
was deleted.)
Note that this does not determine if an update is actually available.
To do that, use :func:`updater.UpdateVersion`.
"""
# Written this way so we can use this code with 6.18 and earlier.
if getattr(renpy, "mobile", False):
return False
if rsa is None:
return False
return not not get_installed_packages(base)
def update(url, base=None, force=False, public_key=None, simulate=None, add=[], restart=True, confirm=True):
"""
:doc: updater
Updates this Ren'Py game to the latest version.
`url`
The URL to the updates.json file.
`base`
The base directory that will be updated. Defaults to the base
of the current game. (This can usually be ignored.)
`force`
Force the update to occur even if the version numbers are
the same. (Used for testing.)
`public_key`
The path to a PEM file containing a public key that the
update signature is checked against. (This can usually be ignored.)
`simulate`
This is used to test update guis without actually performing
an update. This can be:
* None to perform an update.
* "available" to test the case where an update is available.
* "not_available" to test the case where no update is available.
* "error" to test an update error.
`add`
A list of packages to add during this update. This is only necessary
for dlc.
`restart`
Restart the game after the update.
`confirm`
Should Ren'Py prompt the user to confirm the update? If False, the
update will proceed without confirmation.
"""
global installed_packages_cache
installed_packages_cache = None
u = Updater(url=url, base=base, force=force, public_key=public_key, simulate=simulate, add=add, restart=restart, confirm=confirm)
ui.timer(.1, repeat=True, action=renpy.restart_interaction)
renpy.call_screen("updater", u=u)
@renpy.pure
class Update(Action, DictEquality):
"""
:doc: updater
An action that calls :func:`updater.update`. All arguments are
stored and passed to that function.
"""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def __call__(self):
renpy.invoke_in_new_context(update, *self.args, **self.kwargs)
# A list of URLs that we've checked for the update version.
checked = set()
def UpdateVersion(url, check_interval=3600*6, simulate=None, **kwargs):
"""
:doc: updater
This function contacts the server at `url`, and determines if there is
a newer version of software available at that url. If there is, this
function returns the new version. Otherwise, it returns None.
Since contacting the server can take some time, this function launches
a thread in the background, and immediately returns the version from
the last time the server was contacted, or None if the server has never
been contacted. The background thread will restart the current interaction
once the server has been contacted, which will cause screens that call
this function to update.
Each url will be contacted at most once per Ren'Py session, and not
more than once every `check_interval` seconds. When the server is not
contacted, cached data will be returned.
Additional keyword arguments (including `simulate`) are passed to the
update mechanism as if they were given to :func:`updater.update`.
"""
if not can_update() and not simulate:
return None
check = True
if url in checked:
check = False
if time.time() < persistent._update_last_checked.get(url, 0) + check_interval:
check = False
if check:
checked.add(url)
persistent._update_last_checked[url] = time.time()
Updater(url, check_only=True, simulate=simulate, **kwargs)
return persistent._update_version.get(url, None)
def update_command():
import time
ap = renpy.arguments.ArgumentParser()
ap.add_argument("url")
ap.add_argument("--base", action='store', help="The base directory of the game to update. Defaults to the current game.")
ap.add_argument("--force", action="store_true", help="Force the update to run even if the version numbers are the same.")
ap.add_argument("--key", action="store", help="A file giving the public key to use of the update.")
ap.add_argument("--simulate", help="The simulation mode to use. One of available, not_available, or error.")
args = ap.parse_args()
u = Updater(args.url, args.base, args.force, public_key=args.key, simulate=args.simulate)
while True:
state = u.state
print("State:", state)
if u.progress:
print("Progress: {:.1%}".format(u.progress))
if u.message:
print("Message:", u.message)
if state == u.ERROR:
break
elif state == u.UPDATE_NOT_AVAILABLE:
break
elif state == u.UPDATE_AVAILABLE:
u.proceed()
elif state == u.DONE:
break
elif state == u.CANCELLED:
break
time.sleep(.1)
return False
renpy.arguments.register_command("update", update_command)
init -1500:
screen updater:
add "#000"
frame:
style_group ""
has side "t c b":
spacing gui._scale(10)
label _("Updater")
fixed:
vbox:
if u.state == u.ERROR:
text _("An error has occured:")
elif u.state == u.CHECKING:
text _("Checking for updates.")
elif u.state == u.UPDATE_NOT_AVAILABLE:
text _("This program is up to date.")
elif u.state == u.UPDATE_AVAILABLE:
text _("[u.version] is available. Do you want to install it?")
elif u.state == u.PREPARING:
text _("Preparing to download the updates.")
elif u.state == u.DOWNLOADING:
text _("Downloading the updates.")
elif u.state == u.UNPACKING:
text _("Unpacking the updates.")
elif u.state == u.FINISHING:
text _("Finishing up.")
elif u.state == u.DONE:
text _("The updates have been installed. The program will restart.")
elif u.state == u.DONE_NO_RESTART:
text _("The updates have been installed.")
elif u.state == u.CANCELLED:
text _("The updates were cancelled.")
if u.message is not None:
null height gui._scale(10)
text "[u.message!q]"
if u.progress is not None:
null height gui._scale(10)
bar value u.progress range 1.0 style "_bar"
hbox:
spacing gui._scale(25)
if u.can_proceed:
textbutton _("Proceed") action u.proceed
if u.can_cancel:
textbutton _("Cancel") action u.cancel