Skip to content

Commit 0cbf593

Browse files
committed
Last small fixes before release.
1 parent eadf965 commit 0cbf593

7 files changed

Lines changed: 137 additions & 81 deletions

File tree

core/data/impl/serverimpl.py

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import importlib
66
import inspect
77
import json
8-
import sqlite3
9-
108
import luadata
119
import os
1210
import pkgutil
1311
import psutil
1412
import shutil
13+
import sqlite3
1514
import sys
1615
import tempfile
1716
import time
@@ -20,6 +19,7 @@
2019
if sys.platform == 'win32':
2120
import win32con
2221
import win32gui
22+
import win32process
2323

2424
from collections import OrderedDict
2525
from contextlib import suppress
@@ -157,6 +157,7 @@ class ServerImpl(Server):
157157
bot: DCSServerBot | None = field(compare=False, init=False)
158158
event_handler: MissionFileSystemEventHandler = field(compare=False, default=None)
159159
observer: ObserverType = field(compare=False, default=None)
160+
process: psutil.Process | None = field(default=None, compare=False)
160161

161162
@override
162163
def __post_init__(self):
@@ -493,7 +494,7 @@ async def send_to_dcs(self, message: dict) -> None:
493494

494495
@override
495496
async def rename(self, new_name: str, update_settings: bool = False) -> None:
496-
def update_config(old_name, new_name: str, update_settings: bool = False):
497+
def update_config(old_name: str | None, new_name: str, update_settings: bool = False):
497498
# update servers.yaml
498499
filename = os.path.join(self.node.config_dir, 'servers.yaml')
499500
if os.path.exists(filename):
@@ -510,7 +511,7 @@ def update_config(old_name, new_name: str, update_settings: bool = False):
510511
if update_settings:
511512
self.settings['name'] = new_name
512513

513-
async def update_database(old_name: str, new_name: str):
514+
async def update_database(old_name: str | None, new_name: str):
514515
# rename the server in the database
515516
async with self.apool.connection() as conn:
516517
await conn.execute("UPDATE servers SET server_name = %s WHERE server_name = %s",
@@ -547,7 +548,8 @@ async def update_cluster(new_name: str):
547548
self.name = new_name
548549
except Exception:
549550
# rollback config
550-
update_config(new_name, old_name, update_settings)
551+
if old_name:
552+
update_config(new_name, old_name, update_settings)
551553
raise
552554
except Exception:
553555
self.log.exception(f"Error during renaming of server {old_name} to {new_name}: ", exc_info=True)
@@ -619,7 +621,7 @@ def do_startup(self):
619621
instance=self.instance.name
620622
)
621623
if 'priority' in self.locals:
622-
self.set_priority(self.locals.get('priority'))
624+
self.set_priority(self.locals['priority'])
623625
self.log.info(f" => DCS server starting up with PID {self.process.pid}")
624626
except Exception:
625627
self.log.error(f" => Error while trying to launch DCS!", exc_info=True)
@@ -672,25 +674,63 @@ async def prepare_extensions(self):
672674
except Exception:
673675
self.log.error(f" => Unknown error during {ext.name}.prepare() - skipped.", exc_info=True)
674676

675-
@staticmethod
676-
def _window_enumeration_handler(hwnd, top_windows):
677-
top_windows.append((hwnd, win32gui.GetWindowText(hwnd)))
678-
679677
def _minimize(self):
680-
top_windows = []
681-
win32gui.EnumWindows(self._window_enumeration_handler, top_windows)
678+
"""
679+
Minimize the main window of the process represented by
680+
self.process (a psutil.Process instance).
681+
682+
If no window is found or the window disappears before the
683+
message is sent, the call is silently ignored.
684+
"""
685+
target_pid = self.process.pid
686+
687+
# ------------------------------------------------------------------
688+
# 1. Gather all top‑level windows that belong to the target PID
689+
# ------------------------------------------------------------------
690+
candidate_windows = []
691+
692+
def enum_handler(hwnd, _):
693+
# Skip invisible or child windows – we only care about
694+
# top‑level windows that belong to the process.
695+
if not win32gui.IsWindowVisible(hwnd):
696+
return True
697+
698+
try:
699+
_, win_pid = win32process.GetWindowThreadProcessId(hwnd)
700+
except Exception:
701+
# Some exotic windows can raise an exception when queried.
702+
return True
703+
704+
if win_pid == target_pid:
705+
title = win32gui.GetWindowText(hwnd)
706+
candidate_windows.append((hwnd, title))
707+
708+
return True # keep enumerating
682709

683-
# Fetch the window name of the process
684-
window_name = self.instance.name
710+
win32gui.EnumWindows(enum_handler, None)
685711

686-
for hwnd, title in top_windows:
687-
if window_name.lower() in title.lower():
688-
# non-blocking call
712+
if not candidate_windows:
713+
self.log.debug("No visible window found for PID %s", target_pid)
714+
return
715+
716+
# ------------------------------------------------------------------
717+
# 2. Pick the “main” window – usually the first one we found.
718+
# ------------------------------------------------------------------
719+
hwnd, title = candidate_windows[0]
720+
721+
# ------------------------------------------------------------------
722+
# 3. Post the minimize command – only if the handle is still valid
723+
# ------------------------------------------------------------------
724+
try:
725+
if win32gui.IsWindow(hwnd):
689726
win32gui.PostMessage(hwnd,
690727
win32con.WM_SYSCOMMAND,
691728
win32con.SC_MINIMIZE,
692729
0)
693-
break
730+
else:
731+
self.log.debug("Handle %s no longer refers to a window", hwnd)
732+
except win32gui.error as exc:
733+
return
694734

695735
def set_priority(self, priority: str):
696736
if priority == 'below_normal':
@@ -987,7 +1027,12 @@ async def render_extensions(self) -> list[dict]:
9871027

9881028
@override
9891029
async def restart(self, modify_mission: bool | None = True, use_orig: bool | None = True) -> None:
990-
await self.loadMission(self._get_current_mission_file(), modify_mission=modify_mission, use_orig=use_orig)
1030+
current_mission = self._get_current_mission_file()
1031+
if current_mission:
1032+
await self.loadMission(current_mission, modify_mission=modify_mission, use_orig=use_orig)
1033+
else:
1034+
await self.stop()
1035+
await self.start()
9911036

9921037
@override
9931038
async def getStartIndex(self) -> int:
@@ -1288,7 +1333,7 @@ async def enable_extension(self, name: str, config: dict | None = None) -> bool:
12881333
if not ext:
12891334
ext = self.load_extension(name, config)
12901335
self.extensions[name] = ext
1291-
if await ext.enable():
1336+
if ext and await ext.enable():
12921337
await self.config_extension(name, (config or {}) | {"enabled": True})
12931338
return True
12941339
return False

core/data/server.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from dataclasses import dataclass, field
1212
from datetime import datetime, timezone
1313
from pathlib import Path
14-
from psutil import Process
1514
from typing import TYPE_CHECKING, Any
1615

1716
from .dataobject import DataObject
@@ -47,7 +46,6 @@ class Server(DataObject, ABC):
4746
_mission_id: int = field(default=None, compare=False)
4847
players: dict[str, Player] = field(default_factory=dict, compare=False)
4948
players_by_id: dict[int, Player] = field(default_factory=dict, compare=False)
50-
process: Process | None = field(default=None, compare=False)
5149
_maintenance: bool = field(compare=False, default=False)
5250
restart_pending: bool = field(default=False, compare=False)
5351
on_mission_end: dict = field(default_factory=dict, compare=False)

extensions/srs/extension.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,8 @@ def run_subprocess() -> psutil.Process:
320320
self.process = await asyncio.to_thread(run_subprocess)
321321
if not self.observer:
322322
self.start_observer()
323-
except psutil.NoSuchProcess:
324-
self.log.error(f"Error during launch of {self.get_exe_path()}!")
323+
except Exception as ex:
324+
self.log.error(f"Error during launch of {self.get_exe_path()}: {ex}")
325325
return False
326326
# Give SRS 10s to start
327327
for _ in range(0, 10):

plugins/mission/commands.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from discord.app_commands import Range, describe
2222
from discord.ext import commands, tasks
2323
from io import BytesIO
24+
from mgrs.core import MGRSError
2425
from openpyxl.utils import get_column_letter
2526
from pathlib import Path
2627
from psycopg.rows import dict_row
@@ -2334,23 +2335,30 @@ async def convert(self, interaction: discord.Interaction, mode: Literal['Lat/Lon
23342335
if not lat or not lon:
23352336
await interaction.response.send_message("Latitude and longitude must be provided", ephemeral=True)
23362337
return
2337-
mgrs = utils.dd_to_mgrs(lat, lon)
2338-
await interaction.response.send_message(f"**MGRS**: {mgrs}")
2338+
try:
2339+
mgrs = utils.dd_to_mgrs(lat, lon)
2340+
await interaction.response.send_message(f"**MGRS**: {mgrs}")
2341+
except MGRSError as ex:
2342+
await interaction.response.send_message(str(ex), ephemeral=True)
2343+
23392344
elif mode == 'MGRS => Lat/Lon':
23402345
if not mgrs:
23412346
await interaction.response.send_message("MGRS must be provided", ephemeral=True)
23422347
return
2343-
mgrs = mgrs.replace(' ', '')
2344-
lat, lon = utils.mgrs_to_dd(mgrs)
2345-
d, m, s, f = utils.dd_to_dms(lat)
2346-
lat_dms = ('N' if d > 0 else 'S') + ' {:02d}°{:02d}\'{:02d}.{:02d}"'.format(
2347-
int(abs(d)), int(abs(m)), int(abs(s)), int(abs(f)))
2348-
d, m, s, f = utils.dd_to_dms(lon)
2349-
lon_dms = ('E' if d > 0 else 'W') + ' {:03d}°{:02d}\'{:02d}.{:02d}"'.format(
2350-
int(abs(d)), int(abs(m)), int(abs(s)), int(abs(f)))
2351-
await interaction.response.send_message(f"**DD**: {lat:.7f}, {lon:.7f}\n"
2352-
f"**DMS**: {lat_dms}, {lon_dms}\n"
2353-
f"**DDM**: {utils.dd_to_dmm(lat, lon)}")
2348+
try:
2349+
mgrs = mgrs.replace(' ', '')
2350+
lat, lon = utils.mgrs_to_dd(mgrs)
2351+
d, m, s, f = utils.dd_to_dms(lat)
2352+
lat_dms = ('N' if d > 0 else 'S') + ' {:02d}°{:02d}\'{:02d}.{:02d}"'.format(
2353+
int(abs(d)), int(abs(m)), int(abs(s)), int(abs(f)))
2354+
d, m, s, f = utils.dd_to_dms(lon)
2355+
lon_dms = ('E' if d > 0 else 'W') + ' {:03d}°{:02d}\'{:02d}.{:02d}"'.format(
2356+
int(abs(d)), int(abs(m)), int(abs(s)), int(abs(f)))
2357+
await interaction.response.send_message(f"**DD**: {lat:.7f}, {lon:.7f}\n"
2358+
f"**DMS**: {lat_dms}, {lon_dms}\n"
2359+
f"**DDM**: {utils.dd_to_dmm(lat, lon)}")
2360+
except MGRSError as ex:
2361+
await interaction.response.send_message(str(ex), ephemeral=True)
23542362
else:
23552363
await interaction.response.send_message("Invalid conversion mode", ephemeral=True)
23562364

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ pywin32==311 ; sys_platform == 'win32'
9898
pywinauto==0.6.9 ; sys_platform == 'win32'
9999

100100
# Requests: Python HTTP library
101-
requests==2.33.1
101+
requests==2.34.1
102102

103103
# Rich: Python library for rich text and beautiful formatting in the terminal
104104
rich==15.0.0

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ geopandas==1.1.3
3939
gitdb==4.0.12
4040
gitpython==3.1.50
4141
h11==0.16.0
42-
idna==3.14
42+
idna==3.15
4343
jinja2==3.1.6
4444
jinxed==2.0.0
4545
jsonschema==4.26.0
@@ -82,7 +82,7 @@ pywin32==311 ; sys_platform == "win32"
8282
pywinauto==0.6.9 ; sys_platform == "win32"
8383
rapidfuzz==3.14.5
8484
referencing==0.37.0
85-
requests==2.33.1
85+
requests==2.34.1
8686
rich==15.0.0
8787
rpds-py==0.30.0
8888
ruamel-yaml==0.19.1

services/modmanager/service.py

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -85,47 +85,52 @@ async def install_packages(self):
8585
if 'packages' not in config:
8686
continue
8787

88-
for package in config.get('packages', []):
89-
folder = Folder(package['source'])
90-
if package.get('version', 'latest') == 'latest':
91-
_version = await self.get_latest_version(package)
92-
else:
93-
_version = package['version']
94-
# check for valid versions
95-
if _version is None:
96-
self.log.warning(f"{package['name']} without version in {folder.name}, skipped!")
97-
continue
98-
99-
installed = await self.get_installed_package(server, folder, package['name'])
100-
if (not installed or installed != _version) and \
101-
server.status != Status.SHUTDOWN:
102-
self.log.warning(
103-
f" - Server {server.name} needs to be shutdown to auto-install package {package['name']}")
104-
break
105-
maintenance = server.maintenance
106-
server.maintenance = True
107-
try:
108-
if not installed:
109-
if not await self.install_package(server, folder, package['name'], _version,
110-
package.get('repo')):
111-
self.log.warning(f"- Package {package['name']}_v{_version} not found!")
112-
elif installed != _version:
113-
if version.parse(installed) > version.parse(_version):
114-
self.log.debug(f"- Installed package {package['name']}_v{installed} is newer than the "
115-
f"configured version. Skipping.")
116-
continue
117-
if not await self.uninstall_package(server, folder, package['name'], installed):
118-
self.log.warning(f"- Package {package['name']}_v{installed} could not be uninstalled on "
119-
f"server {server.name}!")
120-
elif not await self.install_package(server, folder, package['name'], _version,
121-
package.get('repo')):
122-
self.log.warning(f"- Package {package['name']}_v{_version} could not be installed on "
123-
f"server {server.name}!")
124-
else:
125-
self.log.info(f"- Package {package['name']}_v{installed} updated to v{_version}.")
126-
finally:
127-
if not maintenance:
128-
server.maintenance = False
88+
try:
89+
for package in config.get('packages', []):
90+
folder = Folder(package['source'])
91+
if package.get('version', 'latest') == 'latest':
92+
_version = await self.get_latest_version(package)
93+
else:
94+
_version = package['version']
95+
# check for valid versions
96+
if _version is None:
97+
self.log.warning(f"{package['name']} without version in {folder.name}, skipped!")
98+
continue
99+
100+
installed = await self.get_installed_package(server, folder, package['name'])
101+
if (not installed or installed != _version) and \
102+
server.status != Status.SHUTDOWN:
103+
self.log.warning(
104+
f" - Server {server.name} needs to be shutdown to auto-install package {package['name']}")
105+
break
106+
maintenance = server.maintenance
107+
server.maintenance = True
108+
try:
109+
if not installed:
110+
if not await self.install_package(server, folder, package['name'], _version,
111+
package.get('repo')):
112+
self.log.warning(f"- Package {package['name']}_v{_version} not found!")
113+
elif installed != _version:
114+
if version.parse(installed) > version.parse(_version):
115+
self.log.debug(f"- Installed package {package['name']}_v{installed} is newer than the "
116+
f"configured version. Skipping.")
117+
continue
118+
if not await self.uninstall_package(server, folder, package['name'], installed):
119+
self.log.warning(f"- Package {package['name']}_v{installed} could not be uninstalled on "
120+
f"server {server.name}!")
121+
elif not await self.install_package(server, folder, package['name'], _version,
122+
package.get('repo')):
123+
self.log.warning(f"- Package {package['name']}_v{_version} could not be installed on "
124+
f"server {server.name}!")
125+
else:
126+
self.log.info(f"- Package {package['name']}_v{installed} updated to v{_version}.")
127+
except Exception as ex:
128+
self.log.error(f"Error while installing package {package['name']}_v{_version}: {ex}")
129+
finally:
130+
if not maintenance:
131+
server.maintenance = False
132+
except Exception as ex:
133+
self.log.error(f"Error while installing packages for server {server.name}: {ex}")
129134

130135
@staticmethod
131136
def parse_filename(filename: str) -> tuple[str | None, str | None]:

0 commit comments

Comments
 (0)