Skip to content

Commit e4e38d4

Browse files
committed
ENHANCEMENTS:
- New command /dcs repair
1 parent bda396d commit e4e38d4

File tree

8 files changed

+404
-55
lines changed

8 files changed

+404
-55
lines changed

core/data/impl/nodeimpl.py

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ async def __aenter__(self):
167167
# check Python-version
168168
self.log.info(f'- Python version {platform.python_version()} detected.')
169169

170+
# check UAC
171+
if utils.is_uac_enabled():
172+
self.log.warning('- You need to disable User Access Control (UAC) to allow some commands to run.')
173+
170174
# install plugins
171175
await self._install_plugins()
172176

@@ -629,40 +633,115 @@ def run_subprocess() -> int:
629633
message = _('Server is going down for a DCS update in {}!')
630634
):
631635
self.log.info(f"Updating {self.installation} ...")
632-
# call before update hooks
636+
# call before_update hooks
633637
for callback in self.before_update.values():
634638
await callback()
635-
old_branch, old_version = await self.get_dcs_branch_and_version()
636-
if not branch:
637-
branch = old_branch
638-
if not version:
639-
version = await self.get_latest_version(branch)
640-
rc = await do_update(branch, version)
641-
if rc in [0, 350]:
642-
self.dcs_branch = self.dcs_version = None
643-
dcs_branch, dcs_version = await self.get_dcs_branch_and_version()
644-
# if only the updater updated itself, run the update again
645-
if old_branch == dcs_branch and old_version == dcs_version:
646-
self.log.info("dcs_updater.exe updated to the latest version, now updating DCS World ...")
647-
rc = await do_update(branch, version)
639+
try:
640+
old_branch, old_version = await self.get_dcs_branch_and_version()
641+
if not branch:
642+
branch = old_branch
643+
if not version:
644+
version = await self.get_latest_version(branch)
645+
rc = await do_update(branch, version)
646+
if rc in [0, 350]:
648647
self.dcs_branch = self.dcs_version = None
649-
await self.get_dcs_branch_and_version()
650-
if rc not in [0, 350]:
651-
return rc
652-
# Patch DCS files
653-
if not self.locals['DCS'].get('cloud', False) or self.master:
654-
if self.locals['DCS'].get('desanitize', True):
655-
utils.desanitize(self)
656-
# init profanity filter, if needed
657-
if any(
658-
instance.server.locals.get('profanity_filter', False)
659-
for instance in self.instances if instance.server
660-
):
661-
utils.init_profanity_filter(self)
662-
# call after update hooks
648+
dcs_branch, dcs_version = await self.get_dcs_branch_and_version()
649+
# if only the updater updated itself, run the update again
650+
if old_branch == dcs_branch and old_version == dcs_version:
651+
self.log.info("dcs_updater.exe updated to the latest version, now updating DCS World ...")
652+
rc = await do_update(branch, version)
653+
self.dcs_branch = self.dcs_version = None
654+
await self.get_dcs_branch_and_version()
655+
if rc not in [0, 350]:
656+
return rc
657+
# Patch DCS files
658+
if not self.locals['DCS'].get('cloud', False) or self.master:
659+
if self.locals['DCS'].get('desanitize', True):
660+
utils.desanitize(self)
661+
# init profanity filter, if needed
662+
if any(
663+
instance.server.locals.get('profanity_filter', False)
664+
for instance in self.instances if instance.server
665+
):
666+
utils.init_profanity_filter(self)
667+
self.log.info(f"{self.installation} updated to version {dcs_version}.")
668+
return rc
669+
finally:
670+
# call after_update hooks
671+
for callback in self.after_update.values():
672+
await callback()
673+
self.update_pending = False
674+
675+
async def dcs_repair(self, warn_times: list[int] = None, slow: bool | None = False,
676+
check_extra_files: bool | None = False):
677+
678+
async def do_repair() -> int:
679+
def run_subprocess() -> int:
680+
if sys.platform != 'win32':
681+
raise NotImplementedError("DCS repair is not yet supported on Linux")
682+
683+
if utils.is_uac_enabled():
684+
raise PermissionError("You need to disable UAC to run a DCS repair.")
685+
686+
args = [
687+
"core/utils/updater_wrapper.py",
688+
"-d", os.path.normpath(self.installation),
689+
]
690+
if slow:
691+
args.append("-s")
692+
if check_extra_files:
693+
args.append("-c")
694+
cmdline = subprocess.list2cmdline(args)
695+
return utils.run_elevated(
696+
sys.executable,
697+
os.getcwd(),
698+
cmdline
699+
)
700+
701+
# check if there is an update / repair running already
702+
proc = next(utils.find_process("DCS_updater.exe"), None)
703+
if proc:
704+
self.log.info("- DCS Update / repair in progress, waiting ...")
705+
while proc.is_running():
706+
await asyncio.sleep(1)
707+
708+
return await asyncio.to_thread(run_subprocess)
709+
710+
self.update_pending = True
711+
async with ServerMaintenanceManager(
712+
self.node,
713+
warn_times = warn_times,
714+
message = _('Server is going down for maintenance in {}!')
715+
):
716+
self.log.info(f"Repairing {self.installation} ...")
717+
# call before_update hooks
718+
for callback in self.before_update.values():
719+
await callback()
720+
try:
721+
rc = await do_repair()
722+
if rc == 0:
723+
# Patch DCS files
724+
if not self.locals['DCS'].get('cloud', False) or self.master:
725+
if self.locals['DCS'].get('desanitize', True):
726+
utils.desanitize(self)
727+
# init profanity filter, if needed
728+
if any(
729+
instance.server.locals.get('profanity_filter', False)
730+
for instance in self.instances if instance.server
731+
):
732+
utils.init_profanity_filter(self)
733+
self.log.info(f"{self.installation} repaired.")
734+
else:
735+
self.log.error(f"Repair of {self.installation} failed with code {rc}.")
736+
except PermissionError:
737+
raise
738+
except OSError as ex:
739+
self.log.error(f"Repair of {self.installation} failed with code {ex.errno}.")
740+
return ex.errno
741+
finally:
742+
# call after_update hooks
663743
for callback in self.after_update.values():
664744
await callback()
665-
self.log.info(f"{self.installation} updated to version {dcs_version}.")
666745
self.update_pending = False
667746
return rc
668747

core/data/node.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ async def dcs_update(self, branch: Optional[str] = None, version: Optional[str]
159159
warn_times: list[int] = None, announce: Optional[bool] = True):
160160
raise NotImplementedError()
161161

162+
async def dcs_repair(self, warn_times: list[int] = None):
163+
raise NotImplementedError()
164+
162165
async def get_dcs_branch_and_version(self) -> tuple[str, str]:
163166
raise NotImplementedError()
164167

core/data/proxy/nodeproxy.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ async def dcs_update(self, branch: Optional[str] = None, version: Optional[str]
120120
}, node=self.name, timeout=600)
121121
return data['return']
122122

123+
async def dcs_repair(self, warn_times: list[int] = None, slow: bool | None = False,
124+
check_extra_files: bool | None = False):
125+
data = await self.bus.send_to_node_sync({
126+
"command": "rpc",
127+
"object": "Node",
128+
"method": "repair",
129+
"params": {
130+
"warn_times": warn_times,
131+
"slow": slow,
132+
"check_extra_files": check_extra_files
133+
}
134+
}, node=self.name, timeout=600)
135+
return data['return']
136+
123137
@cache_with_expiration(expiration=30)
124138
async def get_dcs_branch_and_version(self) -> tuple[str, str]:
125139
timeout = 60 if not self.slow_system else 120

core/utils/os.py

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@
1414
import subprocess
1515
import sys
1616

17-
if sys.platform == 'win32':
18-
import ctypes
19-
import pywintypes
20-
import win32api
21-
import win32console
22-
2317
from contextlib import closing, suppress
2418
from logging.handlers import RotatingFileHandler
2519
from pathlib import Path
@@ -28,6 +22,15 @@
2822
if TYPE_CHECKING:
2923
from core import Node
3024

25+
if sys.platform == 'win32':
26+
import ctypes
27+
import pywintypes
28+
import win32api
29+
import win32console
30+
import winreg
31+
32+
from pywinauto.win32defines import SEE_MASK_NOCLOSEPROCESS, SW_HIDE
33+
3134
API_URLS = [
3235
'https://api4.my-ip.io/ip',
3336
'https://api4.ipify.org/'
@@ -57,7 +60,9 @@
5760
"sanitize_filename",
5861
"is_upnp_available",
5962
"get_win32_error_message",
60-
"CloudRotatingFileHandler"
63+
"CloudRotatingFileHandler",
64+
"run_elevated",
65+
"is_uac_enabled"
6166
]
6267

6368
logger = logging.getLogger(__name__)
@@ -223,8 +228,11 @@ def terminate_process(process: Optional[psutil.Process]):
223228
process.wait(timeout=3)
224229

225230

226-
def quick_edit_mode(turn_on=None):
231+
def quick_edit_mode(turn_on=None) -> bool:
227232
""" Enable/Disable windows console Quick Edit Mode """
233+
if sys.platform != 'win32':
234+
return False
235+
228236
screen_buffer = win32console.GetStdHandle(-10)
229237
orig_mode = screen_buffer.GetConsoleMode()
230238
is_on = (orig_mode & ENABLE_QUICK_EDIT_MODE)
@@ -300,8 +308,11 @@ def sanitize_filename(filename: str, base_directory: str) -> str:
300308
return resolved_path
301309

302310

303-
def get_win32_error_message(error_code: int):
311+
def get_win32_error_message(error_code: int) -> str:
304312
# Load the system message corresponding to the error code
313+
if sys.platform != 'win32':
314+
return ""
315+
305316
buffer = ctypes.create_unicode_buffer(512)
306317
ctypes.windll.kernel32.FormatMessageW(
307318
0x00001000, # FORMAT_MESSAGE_FROM_SYSTEM
@@ -345,3 +356,76 @@ def shouldRollover(self, record):
345356
if log_file_size >= self.maxBytes:
346357
return 1
347358
return 0
359+
360+
361+
class SHELLEXECUTEINFO(ctypes.Structure):
362+
_fields_ = [
363+
("cbSize", ctypes.c_ulong),
364+
("fMask", ctypes.c_ulong),
365+
("hwnd", ctypes.c_void_p),
366+
("lpVerb", ctypes.c_wchar_p),
367+
("lpFile", ctypes.c_wchar_p),
368+
("lpParameters", ctypes.c_wchar_p),
369+
("lpDirectory", ctypes.c_wchar_p),
370+
("nShow", ctypes.c_int),
371+
("hInstApp", ctypes.c_void_p),
372+
("lpIDList", ctypes.c_void_p),
373+
("lpClass", ctypes.c_wchar_p),
374+
("hkeyClass", ctypes.c_void_p),
375+
("dwHotKey", ctypes.c_ulong),
376+
("hIcon", ctypes.c_void_p),
377+
("hProcess", ctypes.c_void_p),
378+
]
379+
380+
381+
def run_elevated(exe_path, cwd, *args):
382+
"""Start *exe_path* as Administrator and return the return code."""
383+
if sys.platform != 'win32':
384+
return -1
385+
386+
sei = SHELLEXECUTEINFO()
387+
sei.cbSize = ctypes.sizeof(sei)
388+
sei.fMask = SEE_MASK_NOCLOSEPROCESS
389+
sei.lpVerb = "runas"
390+
sei.lpFile = os.path.abspath(exe_path)
391+
sei.lpDirectory = os.path.abspath(cwd)
392+
sei.lpParameters = ' '.join(map(str, args))
393+
sei.nShow = SW_HIDE
394+
395+
# noinspection PyUnresolvedReferences
396+
if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(sei)):
397+
raise ctypes.WinError()
398+
399+
hproc = sei.hProcess
400+
# noinspection PyUnresolvedReferences
401+
ctypes.windll.kernel32.WaitForSingleObject(hproc, ctypes.c_ulong(-1))
402+
403+
exit_code = ctypes.c_ulong()
404+
# noinspection PyUnresolvedReferences
405+
ctypes.windll.kernel32.GetExitCodeProcess(hproc, ctypes.byref(exit_code))
406+
407+
return int(exit_code.value)
408+
409+
410+
def is_uac_enabled() -> bool:
411+
"""Return True if UAC is enabled; False if it is disabled."""
412+
if sys.platform != 'win32':
413+
# non Win32 systems don't have a UAC, but need to tackle permissions differently
414+
return False
415+
try:
416+
with winreg.OpenKey(
417+
winreg.HKEY_LOCAL_MACHINE,
418+
r"Software\Microsoft\Windows\CurrentVersion\Policies\System",
419+
0,
420+
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
421+
) as key:
422+
# check if UAC is enabled at all
423+
lua_enabled, _ = winreg.QueryValueEx(key, 'EnableLUA')
424+
if lua_enabled == 0:
425+
return False
426+
# now check if we get prompted (if not, treat UAC as disabled)
427+
admin_behaviour, _ = winreg.QueryValueEx(key, "ConsentPromptBehaviorAdmin")
428+
return admin_behaviour > 0 # > 0 => True, 0 = False
429+
except (FileNotFoundError, PermissionError):
430+
# if not found or permission is denied, fall back to a safe default.
431+
return True

0 commit comments

Comments
 (0)