Files
decky-loader/backend/updater.py
suchmememanyskill fd325ef1cc Add cross-platform support to decky (#387)
* Import generic watchdog observer over platform specific import

* Use os.path rather than genericpath

* Split off socket management in plugin.py

* Don't specify multiprocessing start type

Default on linux is already fork

* Move all platform-specific functions to seperate files

TODO: make plugin.py platform agnostic

* fix import

* add backwards compat to helpers.py

* add backwards compatibility to helpers.py harder

* Testing autobuild for win

* Testing autobuild for win, try 2

* Testing autobuild for win, try 3

* Testing autobuild for win, try 4

* Create the plugins folder before attempting to use it

* Implement win get_username()

* Create win install script

* Fix branch guess from version

* Create .loader.version in install script

* Add .cmd shim to facilitate auto-restarts

* Properly fix branch guess from version

* Fix updater on windows

* Try 2 of fixing updates for windows

* Test

* pain

* Update install script

* Powershell doesn't believe in utf8

* Powershell good

* add ON_LINUX variable to localplatform

* Fix more merge issues

* test

* Move custom imports to main.py

* Move custom imports to after __main__ check 

Due to windows' default behaviour being spawn, it will spawn a new process and thus import into sys.path multiple times

* Log errors in get_system_pythonpaths() and get_loader_version() + 

split get_system_pythonpaths() on newline

* Remove whitespace in result of get_system_pythonpaths()

* use python3 on linux and python on windows in get_system_pythonpaths()

* Remove fork-specific urls

* Fix MIME types not working on Windows
2023-03-21 17:37:23 -07:00

208 lines
9.1 KiB
Python

import os
import shutil
import uuid
from asyncio import sleep
from ensurepip import version
from json.decoder import JSONDecodeError
from logging import getLogger
from os import getcwd, path, remove
from localplatform import chmod, service_restart, ON_LINUX
from aiohttp import ClientSession, web
import helpers
from injector import get_gamepadui_tab, inject_to_tab
from settings import SettingsManager
logger = getLogger("Updater")
class Updater:
def __init__(self, context) -> None:
self.context = context
self.settings = self.context.settings
# Exposes updater methods to frontend
self.updater_methods = {
"get_branch": self._get_branch,
"get_version": self.get_version,
"do_update": self.do_update,
"do_restart": self.do_restart,
"check_for_updates": self.check_for_updates
}
self.remoteVer = None
self.allRemoteVers = None
self.localVer = helpers.get_loader_version()
try:
self.currentBranch = self.get_branch(self.context.settings)
except:
self.currentBranch = 0
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
if context:
context.web_app.add_routes([
web.post("/updater/{method_name}", self._handle_server_method_call)
])
context.loop.create_task(self.version_reloader())
async def _handle_server_method_call(self, request):
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.updater_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
def get_branch(self, manager: SettingsManager):
ver = manager.getSetting("branch", -1)
logger.debug("current branch: %i" % ver)
if ver == -1:
logger.info("Current branch is not set, determining branch from version...")
if self.localVer.startswith("v") and "-pre" in self.localVer:
logger.info("Current version determined to be pre-release")
return 1
else:
logger.info("Current version determined to be stable")
return 0
return ver
async def _get_branch(self, manager: SettingsManager):
return self.get_branch(manager)
# retrieve relevant service file's url for each branch
def get_service_url(self):
logger.debug("Getting service URL")
branch = self.get_branch(self.context.settings)
match branch:
case 0:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
case 1 | 2:
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
case _:
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
return str(url)
async def get_version(self):
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != "unknown"
}
async def check_for_updates(self):
logger.debug("checking for updates")
selectedBranch = self.get_branch(self.context.settings)
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json()
self.allRemoteVers = remoteVersions
logger.debug("determining release type to find, branch is %i" % selectedBranch)
if selectedBranch == 0:
logger.debug("release type: release")
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions), None)
elif selectedBranch == 1:
logger.debug("release type: pre-release")
self.remoteVer = next(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions), None)
else:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
tab = await get_gamepadui_tab()
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
return await self.get_version()
async def version_reloader(self):
await sleep(30)
while True:
try:
await self.check_for_updates()
except:
pass
await sleep(60 * 60 * 6) # 6 hours
async def do_update(self):
logger.debug("Starting update.")
version = self.remoteVer["tag_name"]
download_url = None
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
download_temp_filename = download_filename + ".new"
for x in self.remoteVer["assets"]:
if x["name"] == download_filename:
download_url = x["browser_download_url"]
break
if download_url == None:
raise Exception("Download url not found")
service_url = self.get_service_url()
logger.debug("Retrieved service URL")
tab = await get_gamepadui_tab()
await tab.open_websocket()
async with ClientSession() as web:
if ON_LINUX:
logger.debug("Downloading systemd service")
# download the relevant systemd service depending upon branch
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
logger.debug("Downloading service file")
data = await res.content.read()
logger.debug(str(data))
service_file_path = path.join(getcwd(), "plugin_loader.service")
try:
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
out.write(data)
except Exception as e:
logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
service_file.write(service_data)
logger.debug("Saved service file")
logger.debug("Copying service file over current file.")
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
if not os.path.exists(path.join(getcwd(), ".systemd")):
os.mkdir(path.join(getcwd(), ".systemd"))
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
logger.debug("Downloading binary")
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
total = int(res.headers.get('content-length', 0))
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
progress = 0
raw = 0
async for c in res.content.iter_chunked(512):
out.write(c)
raw += len(c)
new_progress = round((raw / total) * 100)
if progress != new_progress:
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
progress = new_progress
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
out.write(version)
if ON_LINUX:
remove(path.join(getcwd(), download_filename))
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
chmod(path.join(getcwd(), download_filename), 777, False)
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await self.do_restart()
await tab.close_websocket()
async def do_restart(self):
await service_restart("plugin_loader")