1
0
Fork 0
mirror of synced 2024-06-18 02:24:43 +12:00
Rare/rare/components/tabs/downloads/download_thread.py

211 lines
8.7 KiB
Python
Raw Normal View History

2021-04-08 02:50:36 +12:00
import os
import platform
2021-04-08 02:50:36 +12:00
import queue
import sys
2021-04-08 02:50:36 +12:00
import time
from logging import getLogger
from queue import Empty
2021-04-08 02:50:36 +12:00
import psutil
from PyQt5.QtCore import QThread, pyqtSignal, QProcess
from legendary.core import LegendaryCore
from legendary.models.downloading import WriterTask
from rare.lgndr.cli import LegendaryCLI
from rare.shared import GlobalSignalsSingleton
from rare.models.install import InstallQueueItemModel
from rare.utils.misc import create_desktop_link
from rare.lgndr.downloading import UIUpdate
2021-04-08 02:50:36 +12:00
logger = getLogger("DownloadThread")
2021-04-08 02:50:36 +12:00
class DownloadThread(QThread):
status = pyqtSignal(str)
statistics = pyqtSignal(UIUpdate)
def __init__(self, core: LegendaryCore, item: InstallQueueItemModel):
2021-04-08 02:50:36 +12:00
super(DownloadThread, self).__init__()
self.signals = GlobalSignalsSingleton()
self.core: LegendaryCore = core
self.item: InstallQueueItemModel = item
self._kill = False
2021-04-08 02:50:36 +12:00
def run(self):
start_time = time.time()
dl_stopped = False
2021-04-08 02:50:36 +12:00
try:
self.item.download.dlmanager.start()
2021-04-08 02:50:36 +12:00
time.sleep(1)
while self.item.download.dlmanager.is_alive():
if self._kill:
self.status.emit("stop")
logger.info("Download stopping...")
# The code below is a temporary solution.
# It should be removed once legendary supports stopping downloads more gracefully.
self.item.download.dlmanager.running = False
# send conditions to unlock threads if they aren't already
for cond in self.item.download.dlmanager.conditions:
with cond:
cond.notify()
# make sure threads are dead.
for t in self.item.download.dlmanager.threads:
t.join(timeout=5.0)
if t.is_alive():
2021-12-24 22:09:50 +13:00
logger.warning(f"Thread did not terminate! {repr(t)}")
# clean up all the queues, otherwise this process won't terminate properly
2021-12-24 22:09:50 +13:00
for name, q in zip(
(
"Download jobs",
"Writer jobs",
"Download results",
"Writer results",
),
(
self.item.download.dlmanager.dl_worker_queue,
self.item.download.dlmanager.writer_queue,
self.item.download.dlmanager.dl_result_q,
self.item.download.dlmanager.writer_result_q,
),
2021-12-24 22:09:50 +13:00
):
logger.debug(f'Cleaning up queue "{name}"')
try:
while True:
_ = q.get_nowait()
except Empty:
q.close()
q.join_thread()
except AttributeError:
2021-12-24 22:09:50 +13:00
logger.warning(f"Queue {name} did not close")
if self.item.download.dlmanager.writer_queue:
# cancel installation
self.item.download.dlmanager.writer_queue.put_nowait(WriterTask("", kill=True))
# forcibly kill DL workers that are not actually dead yet
for child in self.item.download.dlmanager.children:
if child.exitcode is None:
child.terminate()
if self.item.download.dlmanager.shared_memory:
# close up shared memory
self.item.download.dlmanager.shared_memory.close()
self.item.download.dlmanager.shared_memory.unlink()
self.item.download.dlmanager.shared_memory = None
self.item.download.dlmanager.kill()
# force kill any threads that are somehow still alive
for proc in psutil.process_iter():
# check whether the process name matches
2021-12-24 22:09:50 +13:00
if (
sys.platform in ["linux", "darwin"]
and proc.name() == "DownloadThread"
2021-12-24 22:09:50 +13:00
):
proc.kill()
2021-12-24 22:09:50 +13:00
elif (
sys.platform == "win32"
and proc.name() == "python.exe"
and proc.create_time() >= start_time
2021-12-24 22:09:50 +13:00
):
proc.kill()
logger.info("Download stopped. It can be continued later.")
dl_stopped = True
2021-04-08 02:50:36 +12:00
try:
if not dl_stopped:
self.statistics.emit(self.item.download.dlmanager.status_queue.get(timeout=1))
2021-04-08 02:50:36 +12:00
except queue.Empty:
pass
self.item.download.dlmanager.join()
2021-04-08 02:50:36 +12:00
except Exception as e:
2021-12-24 22:09:50 +13:00
logger.error(
f"Installation failed after {time.time() - start_time:.02f} seconds: {e}"
)
self.status.emit(f"error {e}")
2021-04-08 02:50:36 +12:00
return
else:
if dl_stopped:
return
2021-04-08 02:50:36 +12:00
self.status.emit("dl_finished")
end_t = time.time()
logger.info(f"Download finished in {end_t - start_time}s")
2021-05-21 09:00:38 +12:00
if self.item.options.overlay:
self.signals.overlay_installation_finished.emit()
self.core.finish_overlay_install(self.item.download.igame)
self.status.emit("finish")
return
if not self.item.options.no_install:
postinstall = self.core.install_game(self.item.download.igame)
2021-05-21 09:00:38 +12:00
if postinstall:
self._handle_postinstall(postinstall, self.item.download.igame)
2021-05-21 09:00:38 +12:00
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
2021-05-21 09:00:38 +12:00
if dlcs:
2021-12-24 22:09:50 +13:00
print("The following DLCs are available for this game:")
2021-05-21 09:00:38 +12:00
for dlc in dlcs:
2021-12-24 22:09:50 +13:00
print(
f" - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})"
)
print(
"Manually installing DLCs works the same; just use the DLC app name instead."
)
2021-05-21 09:00:38 +12:00
if self.item.download.game.supports_cloud_saves and not self.item.download.game.is_dlc:
2021-12-24 22:09:50 +13:00
logger.info(
'This game supports cloud saves, syncing is handled by the "sync-saves" command.'
)
logger.info(
f'To download saves for this game run "legendary sync-saves {self.item.download.game.app_name}"'
2021-12-24 22:09:50 +13:00
)
LegendaryCLI(self.core).clean_post_install(
self.item.download.game, self.item.download.igame,
self.item.download.repair, self.item.download.repair_file
)
2021-04-08 02:50:36 +12:00
if not self.item.options.update and self.item.options.create_shortcut:
if not create_desktop_link(self.item.options.app_name, self.core, "desktop"):
# maybe add it to download summary, to show in finished downloads
pass
else:
logger.info("Desktop shortcut written")
2021-04-08 02:50:36 +12:00
self.status.emit("finish")
def _handle_postinstall(self, postinstall, igame):
logger.info(f"Postinstall info: {postinstall}")
if platform.system() == "Windows":
if self.item.options.install_preqs:
2021-04-08 02:50:36 +12:00
self.core.prereq_installed(igame.app_name)
2021-12-24 22:09:50 +13:00
req_path, req_exec = os.path.split(postinstall["path"])
2021-04-08 02:50:36 +12:00
work_dir = os.path.join(igame.install_path, req_path)
fullpath = os.path.join(work_dir, req_exec)
proc = QProcess()
proc.setProcessChannelMode(QProcess.MergedChannels)
proc.readyReadStandardOutput.connect(
lambda: logger.debug(
str(proc.readAllStandardOutput().data(), "utf-8", "ignore")
))
proc.start(fullpath, postinstall.get("args", []))
proc.waitForFinished() # wait, because it is inside the thread
2021-04-08 02:50:36 +12:00
else:
self.core.prereq_installed(self.item.download.igame.app_name)
2021-04-08 02:50:36 +12:00
else:
2021-12-24 22:09:50 +13:00
logger.info("Automatic installation not available on Linux.")
2021-04-08 02:50:36 +12:00
def kill(self):
self._kill = True