mirror of
https://github.com/bottlesdevs/Bottles.git
synced 2026-10-29 07:17:00 +08:00
371 lines
13 KiB
Python
371 lines
13 KiB
Python
import os
|
|
import shlex
|
|
import uuid
|
|
|
|
from bottles.backend.dlls.dxvk import DXVKComponent
|
|
from bottles.backend.dlls.nvapi import NVAPIComponent
|
|
from bottles.backend.dlls.vkd3d import VKD3DComponent
|
|
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.utils.midi import FluidSynth
|
|
from bottles.backend.wine.cmd import CMD
|
|
from bottles.backend.wine.explorer import Explorer
|
|
from bottles.backend.wine.msiexec import MsiExec
|
|
from bottles.backend.wine.start import Start
|
|
from bottles.backend.wine.winecommand import WineCommand
|
|
from bottles.backend.wine.winedbg import WineDbg
|
|
from bottles.backend.wine.winepath import WinePath
|
|
|
|
logging = Logger()
|
|
|
|
|
|
class WineExecutor:
|
|
def __init__(
|
|
self,
|
|
config: BottleConfig,
|
|
exec_path: str,
|
|
args: str = "",
|
|
terminal: bool = False,
|
|
environment: dict | None = None,
|
|
move_file: bool = False,
|
|
move_upd_fn: callable = None,
|
|
pre_script: str | None = None,
|
|
post_script: str | None = None,
|
|
cwd: str | None = None,
|
|
midi_soundfont: str | None = None,
|
|
monitoring: list | None = None,
|
|
program_dxvk: bool | None = None,
|
|
program_vkd3d: bool | None = None,
|
|
program_nvapi: bool | None = None,
|
|
program_fsr: bool | None = None,
|
|
program_gamescope: bool | None = None,
|
|
program_virt_desktop: bool | None = 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.environment = environment
|
|
self.pre_script = pre_script
|
|
self.post_script = post_script
|
|
self.cwd = self.__get_cwd(cwd)
|
|
self.midi_soundfont = midi_soundfont
|
|
self.monitoring = monitoring
|
|
self.use_gamescope = program_gamescope
|
|
self.use_virt_desktop = program_virt_desktop
|
|
|
|
env_dll_overrides = []
|
|
|
|
self.fluidsynth = None
|
|
if (soundfont_path := midi_soundfont) not in (None, ""):
|
|
# FluidSynth instance is bound to WineExecutor as a member to control
|
|
# the former's lifetime (deleted when no more references from executors)
|
|
self.fluidsynth = FluidSynth.find_or_create(soundfont_path)
|
|
self.fluidsynth.register_as_current(config)
|
|
|
|
# None = use global DXVK value
|
|
if program_dxvk is not None:
|
|
# DXVK is globally activated, but disabled for the program
|
|
if not program_dxvk and self.config.Parameters.dxvk:
|
|
# Disable DXVK for the program
|
|
override_dxvk = DXVKComponent.get_override_keys() + "=b"
|
|
env_dll_overrides.append(override_dxvk)
|
|
|
|
if program_vkd3d is not None:
|
|
if not program_vkd3d and self.config.Parameters.vkd3d:
|
|
override_vkd3d = VKD3DComponent.get_override_keys() + "=b"
|
|
env_dll_overrides.append(override_vkd3d)
|
|
|
|
if program_nvapi is not None:
|
|
if not program_nvapi and self.config.Parameters.dxvk_nvapi:
|
|
override_nvapi = NVAPIComponent.get_override_keys() + "=b"
|
|
env_dll_overrides.append(override_nvapi)
|
|
|
|
if program_fsr is not None and program_fsr != self.config.Parameters.fsr:
|
|
self.environment["WINE_FULLSCREEN_FSR"] = "1" if program_fsr else "0"
|
|
self.environment["WINE_FULLSCREEN_FSR_STRENGTH"] = str(
|
|
self.config.Parameters.fsr_sharpening_strength
|
|
)
|
|
if self.config.Parameters.fsr_quality_mode:
|
|
self.environment["WINE_FULLSCREEN_FSR_MODE"] = str(
|
|
self.config.Parameters.fsr_quality_mode
|
|
)
|
|
|
|
if (
|
|
program_gamescope is not None
|
|
and program_gamescope != self.config.Parameters.gamescope
|
|
):
|
|
self.environment["GAMESCOPE"] = "1" if program_gamescope else "0"
|
|
|
|
if env_dll_overrides:
|
|
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.")
|
|
|
|
return cls(
|
|
config=config,
|
|
exec_path=program.get("path"),
|
|
args=program.get("arguments"),
|
|
pre_script=program.get("pre_script"),
|
|
post_script=program.get("post_script"),
|
|
cwd=program.get("folder"),
|
|
midi_soundfont=program.get("midi_soundfont"),
|
|
terminal=terminal,
|
|
program_dxvk=program.get("dxvk"),
|
|
program_vkd3d=program.get("vkd3d"),
|
|
program_nvapi=program.get("dxvk_nvapi"),
|
|
program_fsr=program.get("fsr"),
|
|
program_gamescope=program.get("gamescope"),
|
|
program_virt_desktop=program.get("virtual_desktop"),
|
|
).run()
|
|
|
|
def __get_cwd(self, cwd: str) -> 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("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,
|
|
pre_script=self.pre_script,
|
|
post_script=self.post_script,
|
|
cwd=self.cwd,
|
|
midi_soundfont=self.midi_soundfont,
|
|
)
|
|
return Result(status=True, data={"output": res})
|
|
|
|
def run(self) -> Result:
|
|
match self.exec_type:
|
|
case "exe" | "msi":
|
|
return self.__launch_with_bridge()
|
|
case "batch":
|
|
return self.__launch_batch()
|
|
case "lnk" | "unsupported":
|
|
return self.__launch_with_starter()
|
|
case "dll":
|
|
return self.__launch_dll()
|
|
case _:
|
|
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()
|
|
|
|
match self.exec_type:
|
|
case "exe":
|
|
return self.__launch_exe()
|
|
case "msi":
|
|
return self.__launch_msi()
|
|
case "batch":
|
|
return self.__launch_batch()
|
|
case _:
|
|
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,
|
|
environment=self.environment,
|
|
communicate=True,
|
|
pre_script=self.pre_script,
|
|
post_script=self.post_script,
|
|
cwd=self.cwd,
|
|
midi_soundfont=self.midi_soundfont,
|
|
)
|
|
res = winecmd.run()
|
|
self.__set_monitors()
|
|
return Result(status=True, data={"output": res})
|
|
|
|
def __launch_msi(self):
|
|
msiexec = MsiExec(self.config)
|
|
msiexec.install(
|
|
pkg_path=self.exec_path,
|
|
args=self.args,
|
|
terminal=self.terminal,
|
|
cwd=self.cwd,
|
|
environment=self.environment,
|
|
)
|
|
self.__set_monitors()
|
|
return Result(True)
|
|
|
|
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,
|
|
pre_script=self.pre_script,
|
|
post_script=self.post_script,
|
|
cwd=self.cwd,
|
|
midi_soundfont=self.midi_soundfont,
|
|
)
|
|
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=res.status, data={"output": res.data})
|
|
|
|
@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(f"Starting {len(self.monitoring)} monitors")
|
|
|
|
winedbg = WineDbg(self.config, silent=True)
|
|
for m in self.monitoring:
|
|
winedbg.wait_for_process(name=m)
|
|
|
|
def __del__(self):
|
|
"""On exit, kill FluidSynth instance if this was the last executor using it."""
|
|
if self.fluidsynth:
|
|
self.fluidsynth.decrement_program_counter()
|