Skip to content

Commit 968b16e

Browse files
committed
Merge branch 'development'
2 parents 20cc032 + 83c3d51 commit 968b16e

File tree

33 files changed

+418
-256
lines changed

33 files changed

+418
-256
lines changed

core/data/impl/nodeimpl.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,17 +1429,11 @@ async def add_instance(self, name: str, *, template: str = "") -> "Instance":
14291429
}
14301430
with open(config_file, mode='w', encoding='utf-8') as outfile:
14311431
yaml.dump(config, outfile)
1432-
settings_path = os.path.join(instance.home, 'Config', 'serverSettings.lua')
1433-
if os.path.exists(settings_path):
1434-
# TODO: dirty cast
1435-
settings = SettingsDict(cast(DataObject, cast(object, self)), settings_path, root='cfg')
1436-
settings['port'] = int(instance.dcs_port)
1437-
settings['name'] = 'n/a'
1438-
settings['missionList'] = []
1439-
settings['listStartIndex'] = 0
14401432
bus = ServiceRegistry.get(ServiceBus)
14411433
server = DataObjectFactory().new(ServerImpl, node=self.node, port=instance.bot_port, name='n/a', bus=bus)
14421434
instance.server = server
1435+
# we cannot use instance.dcs_port here as that would refer to the assigned serverSettings.lua already
1436+
server.settings["port"] = int(instance.locals['dcs_port'])
14431437
self.instances[instance.name] = instance
14441438
bus.servers[server.name] = server
14451439
if not self.master:

core/data/impl/serverimpl.py

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,15 @@ def __post_init__(self):
113113
self._lock = asyncio.Lock()
114114
with self.pool.connection() as conn:
115115
with conn.transaction():
116-
conn.execute("INSERT INTO servers (server_name) VALUES (%s) ON CONFLICT (server_name) DO NOTHING",
117-
(self.name, ))
118-
row = conn.execute("SELECT maintenance FROM servers WHERE server_name = %s", (self.name,)).fetchone()
119-
if row:
120-
self._maintenance = row[0]
116+
cursor = conn.execute("""
117+
INSERT INTO servers (server_name)
118+
VALUES (%s)
119+
ON CONFLICT (server_name) DO NOTHING
120+
RETURNING maintenance
121+
""", (self.name, ))
122+
row = cursor.fetchone()
123+
if row:
124+
self._maintenance = row[0]
121125
atexit.register(self.stop_observer)
122126

123127
@override
@@ -141,6 +145,7 @@ def settings(self) -> dict:
141145
# if someone managed to destroy the mission list, fix it...
142146
if 'missionList' not in self._settings:
143147
self._settings['missionList'] = []
148+
self._settings['listStartIndex'] = 0
144149
elif isinstance(self._settings['missionList'], dict):
145150
self._settings['missionList'] = list(self._settings['missionList'].values())
146151
return self._settings
@@ -447,9 +452,11 @@ async def update_database(old_name: str, new_name: str):
447452
# rename the server in the database
448453
async with self.apool.connection() as conn:
449454
async with conn.transaction():
450-
# we need to remove any older server that might have had the same name
451455
await conn.execute("UPDATE servers SET server_name = %s WHERE server_name = %s",
452-
(new_name, old_name))
456+
(new_name, old_name or 'n/a'))
457+
if not old_name:
458+
await conn.execute("UPDATE instances SET server_name = %s WHERE instance = %s",
459+
(new_name, self.instance.name))
453460

454461
async def update_cluster(new_name: str):
455462
# only the master can take care of a cluster-wide rename
@@ -484,6 +491,13 @@ async def update_cluster(new_name: str):
484491
except Exception:
485492
self.log.exception(f"Error during renaming of server {old_name} to {new_name}: ", exc_info=True)
486493

494+
async def unlink(self):
495+
if self.name == 'n/a':
496+
async with self.apool.connection() as conn:
497+
async with conn.transaction():
498+
await conn.execute("DELETE FROM servers WHERE server_name = 'n/a'")
499+
self.instance.server = None
500+
487501
@performance_log()
488502
def do_startup(self):
489503
basepath = self.node.installation
@@ -788,20 +802,8 @@ async def apply_mission_changes(self, filename: str | None = None, *, use_orig:
788802
if _dirty:
789803
self.log.info(f' => {ext.name} applied on {new_filename}.')
790804
dirty |= _dirty
791-
# we did not change anything in the mission
792-
if not dirty:
793-
return filename
794-
# check if the original mission can be written
795-
if filename != new_filename:
796-
missions: list[str] = self.settings['missionList']
797-
try:
798-
index = missions.index(filename) + 1
799-
await self.replaceMission(index, new_filename)
800-
except ValueError:
801-
# we should not be here, but just in case
802-
if new_filename not in missions:
803-
await self.addMission(new_filename)
804-
return new_filename
805+
806+
return filename if not dirty else new_filename
805807
except Exception as ex:
806808
self.log.error(ex)
807809
if filename != new_filename and os.path.exists(new_filename):
@@ -899,7 +901,7 @@ async def render_extensions(self) -> list[dict]:
899901

900902
@override
901903
async def restart(self, modify_mission: bool | None = True, use_orig: bool | None = True) -> None:
902-
await self.loadMission(int(self.settings['listStartIndex']), modify_mission=modify_mission, use_orig=use_orig)
904+
await self.loadMission(self._get_current_mission_file(), modify_mission=modify_mission, use_orig=use_orig)
903905

904906
@override
905907
async def setStartIndex(self, mission_id: int) -> None:
@@ -912,22 +914,32 @@ async def setStartIndex(self, mission_id: int) -> None:
912914

913915
@override
914916
async def setPassword(self, password: str):
915-
self.settings['password'] = password or ''
917+
if self.status in [Status.STOPPED, Status.PAUSED, Status.RUNNING]:
918+
await self.send_to_dcs({"command": "setPassword", "password": password})
919+
else:
920+
self.settings['password'] = password or ''
916921

917922
@override
918923
async def setCoalitionPassword(self, coalition: Coalition, password: str):
919-
advanced = self.settings['advanced']
920-
if coalition == Coalition.BLUE:
921-
if password:
922-
advanced['bluePasswordHash'] = utils.hash_password(password)
923-
else:
924-
advanced.pop('bluePasswordHash', None)
924+
if self.status in [Status.STOPPED, Status.PAUSED, Status.RUNNING]:
925+
if coalition == Coalition.BLUE:
926+
await self.send_to_dcs({"command": "setCoalitionPassword", "bluePassword": password or ''})
927+
elif coalition == Coalition.RED:
928+
await self.send_to_dcs({"command": "setCoalitionPassword", "redPassword": password or ''})
925929
else:
926-
if password:
927-
advanced['redPasswordHash'] = utils.hash_password(password)
930+
advanced = self.settings['advanced']
931+
if coalition == Coalition.BLUE:
932+
if password:
933+
advanced['bluePasswordHash'] = utils.hash_password(password)
934+
else:
935+
advanced.pop('bluePasswordHash', None)
928936
else:
929-
advanced.pop('redPasswordHash', None)
930-
self.settings['advanced'] = advanced
937+
if password:
938+
advanced['redPasswordHash'] = utils.hash_password(password)
939+
else:
940+
advanced.pop('redPasswordHash', None)
941+
self.settings['advanced'] = advanced
942+
931943
async with self.apool.connection() as conn:
932944
async with conn.transaction():
933945
await conn.execute('UPDATE servers SET {} = %s WHERE server_name = %s'.format(
@@ -994,38 +1006,34 @@ async def replaceMission(self, mission_id: int, path: str) -> list[str]:
9941006
@override
9951007
async def loadMission(self, mission: int | str, modify_mission: bool | None = True,
9961008
use_orig: bool | None = True, no_reload: bool | None = False) -> bool | None:
997-
start_index = int(self.settings.get('listStartIndex', 1))
1009+
9981010
mission_list = self.settings['missionList']
999-
# check if we re-load the running mission
1000-
if ((isinstance(mission, int) and mission == start_index) or
1001-
(isinstance(mission, str) and mission == self._get_current_mission_file())):
1002-
# if we should not reload, then return here
1003-
if no_reload:
1004-
return None
1005-
mission = self._get_current_mission_file()
1006-
if not mission:
1007-
return False
1008-
if use_orig:
1009-
new_filename = utils.create_writable_mission(mission)
1010-
orig_mission = utils.get_orig_file(mission)
1011-
shutil.copy2(orig_mission, new_filename)
1012-
if new_filename != mission:
1013-
mission_list = await self.replaceMission(start_index, new_filename)
1014-
elif modify_mission:
1015-
# don't use the orig file, still make sure we have a writable mission
1016-
new_filename = utils.create_writable_mission(mission)
1017-
if new_filename != mission:
1018-
shutil.copy2(mission, new_filename)
1019-
mission_list = await self.replaceMission(start_index, new_filename)
1011+
start_index = int(self.settings.get('listStartIndex', 1))
1012+
try:
1013+
current_mission = self._get_current_mission_file()
1014+
current_index = mission_list.index(current_mission) + 1
1015+
except ValueError:
1016+
current_index = start_index
1017+
current_mission = mission_list[current_index - 1]
10201018

10211019
if isinstance(mission, int):
1022-
if mission > len(mission_list):
1023-
mission = 1
1024-
filename = mission_list[mission - 1]
1020+
mission = mission_list[mission - 1]
1021+
1022+
# we should not reload the running mission
1023+
if no_reload and mission == current_mission:
1024+
return None
1025+
1026+
if modify_mission:
1027+
filename = await self.apply_mission_changes(mission, use_orig=use_orig)
1028+
elif use_orig:
1029+
filename = utils.create_writable_mission(mission)
1030+
orig_mission = utils.get_orig_file(mission)
1031+
shutil.copy2(orig_mission, filename)
10251032
else:
10261033
filename = mission
1027-
if modify_mission:
1028-
filename = await self.apply_mission_changes(filename)
1034+
1035+
if mission == current_mission and filename != mission:
1036+
mission_list = await self.replaceMission(current_index, filename)
10291037

10301038
if self.status == Status.STOPPED:
10311039
try:
@@ -1039,7 +1047,7 @@ async def loadMission(self, mission: int | str, modify_mission: bool | None = Tr
10391047
timeout = 300 if self.node.locals.get('slow_system', False) else 180
10401048
try:
10411049
idx = mission_list.index(filename) + 1
1042-
if idx == start_index:
1050+
if idx == current_index:
10431051
rc = await self.send_to_dcs_sync({
10441052
"command": "startMission",
10451053
"filename": filename
@@ -1054,6 +1062,7 @@ async def loadMission(self, mission: int | str, modify_mission: bool | None = Tr
10541062
"command": "startMission",
10551063
"filename": filename
10561064
}, timeout=timeout)
1065+
10571066
# We could not load the mission
10581067
result = rc['result'] if isinstance(rc['result'], bool) else (rc['result'] == 0)
10591068
if not result:

core/data/proxy/serverproxy.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,15 @@ async def rename(self, new_name: str, update_settings: bool = False) -> None:
234234
}, node=self.node.name, timeout=timeout)
235235
self.name = new_name
236236

237+
@override
238+
async def unlink(self):
239+
await self.bus.send_to_node_sync({
240+
"command": "rpc",
241+
"object": "Server",
242+
"method": "unlink",
243+
"server_name": self.name
244+
}, node=self.node.name, timeout=60)
245+
237246
@override
238247
async def render_extensions(self) -> list[dict]:
239248
if not self._extensions:

core/data/server.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Server(DataObject, ABC):
4444
_options: utils.SettingsDict | utils.RemoteSettingsDict | None = field(default=None, compare=False)
4545
_settings: utils.SettingsDict | utils.RemoteSettingsDict | None = field(default=None, compare=False)
4646
current_mission: Mission | None = field(default=None, compare=False)
47-
mission_id: int = field(default=-1, compare=False)
47+
_mission_id: int = field(default=None, compare=False)
4848
players: dict[int, Player] = field(default_factory=dict, compare=False)
4949
process: Process | None = field(default=None, compare=False)
5050
_maintenance: bool = field(compare=False, default=False)
@@ -118,6 +118,26 @@ def status(self) -> Status:
118118
def status(self, status: Status | str):
119119
self.set_status(status)
120120

121+
@property
122+
def mission_id(self) -> int:
123+
if not self._mission_id:
124+
with self.pool.connection() as conn:
125+
cursor = conn.execute("""
126+
SELECT id FROM missions
127+
WHERE server_name = %s AND mission_end IS NULL
128+
ORDER BY mission_start DESC
129+
LIMIT 1
130+
""", (self.name, ))
131+
if cursor.rowcount == 1:
132+
self._mission_id = cursor.fetchone()[0]
133+
else:
134+
self._mission_id = -1
135+
return self._mission_id
136+
137+
@mission_id.setter
138+
def mission_id(self, mission_id: int):
139+
self._mission_id = mission_id
140+
121141
# allow overloading of setter
122142
def set_status(self, status: Status | str):
123143
if isinstance(status, str):
@@ -273,6 +293,10 @@ async def send_to_dcs(self, message: dict):
273293
async def rename(self, new_name: str, update_settings: bool = False) -> None:
274294
raise NotImplementedError()
275295

296+
@abstractmethod
297+
async def unlink(self):
298+
raise NotImplementedError()
299+
276300
@abstractmethod
277301
async def startup(self, modify_mission: bool | None = True, use_orig: bool | None = True) -> None:
278302
raise NotImplementedError()

core/utils/network.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import aiohttp
2+
import asyncio
23
import ipaddress
34
import socket
45
import sys
6+
import time
57

68
from contextlib import closing, suppress
79
from core import Port, PortType
@@ -14,7 +16,8 @@
1416
"is_open",
1517
"get_public_ip",
1618
"is_upnp_available",
17-
"generate_firewall_rules"
19+
"generate_firewall_rules",
20+
"wait_for_internet"
1821
]
1922

2023
API_URLS = [
@@ -140,3 +143,56 @@ def generate_firewall_rules(ports: Iterable[Port]) -> str:
140143
lines.append("")
141144

142145
return "\n".join(lines)
146+
147+
148+
async def _check_google_dns(
149+
host: str = "8.8.8.8",
150+
port: int = 53,
151+
per_attempt_timeout: float = 3.0,
152+
) -> bool:
153+
"""
154+
Try to open a TCP connection once with a per-attempt timeout.
155+
Returns True if successful, False otherwise.
156+
"""
157+
try:
158+
conn_coro = asyncio.open_connection(host, port)
159+
reader, writer = await asyncio.wait_for(conn_coro, timeout=per_attempt_timeout)
160+
writer.close()
161+
await writer.wait_closed()
162+
return True
163+
except (asyncio.TimeoutError, OSError):
164+
return False
165+
166+
167+
async def wait_for_internet(
168+
timeout: float,
169+
interval: float = 1.0,
170+
host: str = "8.8.8.8",
171+
port: int = 53,
172+
per_attempt_timeout: float = 3.0,
173+
) -> bool:
174+
"""
175+
Wait until an internet connection is available or until 'timeout' seconds pass.
176+
177+
Returns:
178+
True if a connection was established before timeout,
179+
False if timeout was reached without success.
180+
"""
181+
deadline = time.monotonic() + timeout
182+
183+
while True:
184+
remaining = deadline - time.monotonic()
185+
if remaining <= 0:
186+
return False
187+
188+
# Don't let a single attempt take longer than remaining time
189+
attempt_timeout = min(per_attempt_timeout, remaining)
190+
191+
if await _check_google_dns(host=host, port=port, per_attempt_timeout=attempt_timeout):
192+
return True
193+
194+
# Sleep before the next attempt, but don't overshoot the deadline
195+
remaining = deadline - time.monotonic()
196+
if remaining <= 0:
197+
return False
198+
await asyncio.sleep(min(interval, remaining))

locale/cn/LC_MESSAGES/admin.mo

0 Bytes
Binary file not shown.

locale/cn/LC_MESSAGES/admin.po

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ msgstr "该实例正在被服务器使用 \"{}\".\n"
362362
msgid "Do you really want to delete instance {}?"
363363
msgstr "你真的想删除实例 {} 吗?"
364364

365-
msgid "Do you want to remove the directory {}?"
365+
msgid "Do you want to remove the directory\n{}?"
366366
msgstr "你想删除目录 {} 吗?"
367367

368368
msgid "Instance {instance} removed from node {node}."

locale/de/LC_MESSAGES/admin.mo

0 Bytes
Binary file not shown.

locale/de/LC_MESSAGES/admin.po

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ msgstr "Die Instanz wird zurzeit vom Server \"{}\" genutzt.\n"
362362
msgid "Do you really want to delete instance {}?"
363363
msgstr "Möchtest Du Instanz {} wirklich löschen?"
364364

365-
msgid "Do you want to remove the directory {}?"
365+
msgid "Do you want to remove the directory\n{}?"
366366
msgstr "Möchtest Du das Verzeichnis {} auch löschen?"
367367

368368
msgid "Instance {instance} removed from node {node}."

locale/es/LC_MESSAGES/admin.mo

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)