Files
Lake ChanandMirko Brombin 86f098cadc Migrate Config from dict to object (#2464)
* refactor: migrate config from dict to object

* fix: type hint import

* fix: BottleConfig.load()

* fix: config loading in BackupManager.import_backup()

* fix: Remove unused import

* fix: more guard code, type hints and Config object usage

* fix: a potential crash when SteamManager.get_app_config() failed

* Revert "fix: Remove unused import"

This reverts commit 2d48a4e08a.

* add: new config field

* fix: Config object value read

* misc: remove review TODO comments

* adapt to new BottleConfig object

* fix RunAsync usage

* add: BottleConfig._fix()

* fix: type hint and Config object usage

* fix: typo in FSR setting default value

* adapt to new BottleConfig object

* remove unnecessary DictCompat method

* add: BottleConfig._filter()

* fix: TemplateManager.new()

* misc: critical guard code

* fix: typo in commit b7ed61e678

* misc: more type hinting

* Revert "remove unnecessary DictCompat method"

This reverts commit 265633e733.

* fix crash when user choose Cancel in ProtonAlertDialog

* fix force stop all programs not work

* adapt NVAPIComponent.check_bottle_nvngx to Config object

* Better DictCompatMixIn

* InstallerManager.__set_parameters incorrect adopt

* fix typo in cbe1dc760b

* adopt LibraryManager.download_thumbnail to Config object

* serialize Config object to yaml automatically

* fix translation and remove workaround in 0830fddbc9

* fix missing locale for Chinese

* comment

* fix incorrect variable init position

* remove 'in' extensions and some bugfix

* fix executable filemode

Co-authored-by: Mirko Brombin <send@mirko.pm>
2023-01-20 17:02:40 +01:00

358 lines
11 KiB
Python

import os
import shlex
import uuid
from typing import NewType, Union
from bottles.backend.logger import Logger
from bottles.backend.models.config import BottleConfig
from bottles.backend.models.result import Result
from bottles.backend.utils.manager import ManagerUtils
from bottles.backend.wine.winecommand import WineCommand
from bottles.backend.wine.cmd import CMD
from bottles.backend.wine.msiexec import MsiExec
from bottles.backend.wine.start import Start
from bottles.backend.wine.explorer import Explorer
from bottles.backend.wine.winepath import WinePath
from bottles.backend.wine.winedbg import WineDbg
from bottles.backend.wine.winebridge import WineBridge
logging = Logger()
class WineExecutor:
def __init__(
self,
config: BottleConfig,
exec_path: str,
args: str = "",
terminal: bool = False,
cwd: str = None,
environment: dict = None,
move_file: bool = False,
move_upd_fn: callable = None,
post_script: str = None,
monitoring: list = None,
override_dxvk: bool = None,
override_vkd3d: bool = None,
override_nvapi: bool = None,
override_fsr: bool = None,
override_virt_desktop: bool = None
):
logging.info("Launching an executable…")
self.config = config
self.__validate_path(exec_path)
if monitoring is None:
monitoring = []
if environment is None:
environment = {}
if move_file:
exec_path = self.__move_file(exec_path, move_upd_fn)
self.exec_type = self.__get_exec_type(exec_path)
self.exec_path = shlex.quote(exec_path)
self.args = args
self.terminal = terminal
self.cwd = self.__get_cwd(cwd)
self.environment = environment
self.post_script = post_script
self.monitoring = monitoring
self.use_virt_desktop = override_virt_desktop
env_dll_overrides = []
if override_dxvk is not None \
and not override_dxvk \
and self.config.Parameters.dxvk:
env_dll_overrides.append("d3d9,d3d11,d3d10core,dxgi=b")
if override_vkd3d is not None \
and not override_vkd3d \
and self.config.Parameters.vkd3d:
env_dll_overrides.append("d3d12=b")
if override_nvapi is not None \
and not override_nvapi \
and self.config.Parameters.dxvk_nvapi:
env_dll_overrides.append("nvapi,nvapi64=b")
if override_fsr is not None and override_fsr:
self.environment["WINE_FULLSCREEN_FSR"] = "1"
self.environment["WINE_FULLSCREEN_FSR_STRENGTH"] = str(self.config.Parameters.fsr_sharpening_strength)
if "WINEDLLOVERRIDES" in self.environment:
self.environment["WINEDLLOVERRIDES"] += "," + ",".join(env_dll_overrides)
else:
self.environment["WINEDLLOVERRIDES"] = ",".join(env_dll_overrides)
@classmethod
def run_program(cls, config: BottleConfig, program: dict, terminal: bool=False):
if program is None:
logging.warning("The program entry is not well formatted.")
dxvk = config.Parameters.dxvk
vkd3d = config.Parameters.vkd3d
nvapi = config.Parameters.dxvk_nvapi
fsr = config.Parameters.fsr
virt_desktop = config.Parameters.virtual_desktop
if program.get("dxvk") != dxvk:
dxvk = program.get("dxvk")
if program.get("vkd3d") != vkd3d:
vkd3d = program.get("vkd3d")
if program.get("dxvk_nvapi") != nvapi:
nvapi = program.get("dxvk_nvapi")
if program.get("fsr") != fsr:
fsr = program.get("fsr")
if program.get("virtual_desktop") != virt_desktop:
virt_desktop = program.get("virtual_desktop")
return cls(
config=config,
exec_path=program["path"],
args=program.get("arguments", ""),
cwd=program.get("folder", None),
post_script=program.get("script", None),
terminal=terminal,
override_dxvk=dxvk,
override_vkd3d=vkd3d,
override_nvapi=nvapi,
override_fsr=fsr,
override_virt_desktop=virt_desktop
).run()
def __get_cwd(self, cwd: str) -> Union[str, None]:
winepath = WinePath(self.config)
if cwd in [None, ""]:
path = self.exec_path
if winepath.is_windows(self.exec_path):
path = "\\".join(path.split("\\")[:-1])
path = winepath.to_unix(path)
if path.startswith(("'", '"')):
path = path[1:]
if path.endswith(("'", '"')):
path = path[:-1]
return os.path.dirname(path)
return cwd # will be set by WineCommand if None
@staticmethod
def __validate_path(exec_path):
if exec_path in [None, ""]:
logging.error("No executable file path provided.")
return False
if ":\\" in exec_path:
logging.warning("Windows path detected. Avoiding validation.")
return True
if not os.path.isfile(exec_path):
_msg = f"Executable file path does not exist: {exec_path}"
if "FLATPAK_ID" in os.environ:
_msg = f"Executable file path does not exist or is not accessible by the Flatpak: {exec_path}"
logging.error(_msg, )
return False
def __move_file(self, exec_path, move_upd_fn):
new_path = ManagerUtils.move_file_to_bottle(
file_path=exec_path,
config=self.config,
fn_update=move_upd_fn
)
if new_path:
exec_path = new_path
self.__validate_path(exec_path)
return exec_path
@staticmethod
def __get_exec_type(exec_path):
_exec = exec_path.lower()
if _exec.endswith(".exe"):
return "exe"
if _exec.endswith(".msi"):
return "msi"
if _exec.endswith(".bat"):
return "batch"
if _exec.endswith(".lnk"):
return "lnk"
if _exec.endswith(".dll"):
return "dll"
logging.warning(f"Not a common executable type, trying to launch it anyway.")
return "unsupported"
def run_cli(self):
"""
We need to launch the application and then exit,
so we use Wine Starter, which will exit as soon
as the program is launched
"""
winepath = WinePath(self.config)
start = Start(self.config)
if winepath.is_unix(self.exec_path):
return self.__launch_with_bridge()
res = start.run(
file=self.exec_path,
terminal=self.terminal,
args=self.args,
environment=self.environment,
cwd=self.cwd
)
return Result(
status=True,
data={"output": res}
)
def run(self) -> Result:
if self.exec_type in ["exe", "msi"]:
return self.__launch_with_bridge()
if self.exec_type == "batch":
return self.__launch_batch()
if self.exec_type in ["lnk", "unsupported"]:
return self.__launch_with_starter()
if self.exec_type == "dll":
return self.__launch_dll()
return Result(
status=False,
data={"message": "Unknown executable type."}
)
def __launch_with_bridge(self):
# winebridge = WineBridge(self.config)
# if winebridge.is_available():
# res = winebridge.run_exe(self.exec_path)
# return Result(
# status=True,
# data={"output": res}
# )
winepath = WinePath(self.config)
if self.use_virt_desktop:
if winepath.is_unix(self.exec_path):
self.exec_path = winepath.to_windows(self.exec_path)
return self.__launch_with_explorer()
if winepath.is_windows(self.exec_path):
return self.__launch_with_starter()
if self.exec_type == "exe":
return self.__launch_exe()
if self.exec_type == "msi":
return self.__launch_msi()
if self.exec_type == "batch":
return self.__launch_batch()
logging.error(f'exec_type {self.exec_type} is not valid')
return Result(
status=False,
data={"message": "Unknown executable type."}
)
def __launch_exe(self):
# winebridge = WineBridge(self.config)
# if winebridge.is_available():
# res = winebridge.run_exe(self.exec_path)
# return Result(
# status=True,
# data={"output": res}
# )
winecmd = WineCommand(
self.config,
command=self.exec_path,
arguments=self.args,
terminal=self.terminal,
cwd=self.cwd,
environment=self.environment,
communicate=True,
post_script=self.post_script
)
res = winecmd.run()
self.__set_monitors()
return Result(
status=True,
data={"output": res}
)
def __launch_msi(self):
msiexec = MsiExec(self.config)
res = msiexec.install(
pkg_path=self.exec_path,
args=self.args,
terminal=self.terminal,
cwd=self.cwd,
environment=self.environment
)
self.__set_monitors()
return Result(
status=True,
data={"output": res}
)
def __launch_batch(self):
cmd = CMD(self.config)
res = cmd.run_batch(
batch=self.exec_path,
terminal=self.terminal,
args=self.args,
environment=self.environment,
cwd=self.cwd
)
return Result(
status=True,
data={"output": res}
)
def __launch_with_starter(self):
start = Start(self.config)
res = start.run(
file=self.exec_path,
terminal=self.terminal,
args=self.args,
environment=self.environment,
cwd=self.cwd
)
self.__set_monitors()
return Result(
status=True,
data={"output": res}
)
def __launch_with_explorer(self):
w, h = self.config.Parameters.virtual_desktop_res.split("x")
start = Explorer(self.config)
res = start.launch_desktop(
desktop=str(uuid.uuid4()),
width=w,
height=h,
program=self.exec_path,
args=self.args,
environment=self.environment,
cwd=self.cwd
)
self.__set_monitors()
return Result(
status=True,
data={"output": res}
)
@staticmethod
def __launch_dll():
logging.warning("DLLs are not supported yet.")
return Result(
status=False,
data={"error": "DLLs are not supported yet."}
)
def __set_monitors(self):
if not self.monitoring:
return
logging.info("Starting {} monitors".format(len(self.monitoring)))
winedbg = WineDbg(self.config, silent=True)
for m in self.monitoring:
winedbg.wait_for_process(name=m)