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

210 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.shared import GlobalSignalsSingleton, LegendaryCLISingleton
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
)
LegendaryCLISingleton().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