warlock-manager 2.2.4__tar.gz → 2.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/PKG-INFO +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/pyproject.toml +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_base_service.py +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_cmd.py +29 -2
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_config.py +81 -15
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/base_app.py +8 -9
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/steam_app.py +2 -2
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/unreal_config.py +73 -57
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/formatters/cli_formatter.py +42 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/cache.py +3 -3
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/cmd.py +72 -14
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/firewall.py +48 -16
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/utils.py +13 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/mods/base_mod.py +7 -3
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/nexus/nexus.py +12 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/base_service.py +14 -4
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/PKG-INFO +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/LICENSE +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/README.md +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/setup.cfg +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_app.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_base_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_cli_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_cli_formatter.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_config_key.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_ini_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_sensitive_data_filter.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_socket_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_config_ark_spawn_entities.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_save.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_version.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/manual_app.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/base_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/cli_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/config_key.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/ini_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/json_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/properties_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/formatters/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/app.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/app_runner.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/download.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/get_wan_ip.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/java.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/meta.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/ports.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/sensitive_data_filter.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/tui.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/version.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/mods/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/mods/warlock_nexus_mod.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/http_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/rcon_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/socket_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/SOURCES.txt +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/dependency_links.txt +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/requires.txt +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/top_level.txt +0 -0
|
@@ -61,4 +61,4 @@ class TestBaseService(unittest.TestCase):
|
|
|
61
61
|
self.assertIn('Type=simple', data_new)
|
|
62
62
|
self.assertIn('ExecStart=%s' % svc.get_executable(), data_new)
|
|
63
63
|
self.assertIn('WorkingDirectory=%s' % svc.get_app_directory(), data_new)
|
|
64
|
-
self.assertIn('EnvironmentFile=%s/Environments/%s.env' % (utils.
|
|
64
|
+
self.assertIn('EnvironmentFile=%s/Environments/%s.env' % (utils.get_base_directory(), svc.service), data_new)
|
|
@@ -3,16 +3,34 @@ from warlock_manager.libs.cmd import Cmd
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class TestCmd(unittest.TestCase):
|
|
6
|
-
def
|
|
6
|
+
def test_exists(self):
|
|
7
|
+
"""
|
|
8
|
+
Test if commands exist and do not exist
|
|
9
|
+
:return:
|
|
10
|
+
"""
|
|
7
11
|
cmd = Cmd(["echo"])
|
|
8
12
|
self.assertTrue(cmd.exists)
|
|
9
13
|
|
|
10
|
-
def test_exists_false(self):
|
|
11
14
|
cmd = Cmd(["nonexistentbinary12345"])
|
|
12
15
|
self.assertFalse(cmd.exists)
|
|
13
16
|
self.assertFalse(cmd.success)
|
|
14
17
|
|
|
18
|
+
def test_exists_sudo(self):
|
|
19
|
+
"""
|
|
20
|
+
Test exists functionality when used with sudo
|
|
21
|
+
:return:
|
|
22
|
+
"""
|
|
23
|
+
cmd = Cmd(["true"]).sudo('nobody')
|
|
24
|
+
self.assertTrue(cmd.exists)
|
|
25
|
+
|
|
26
|
+
cmd = Cmd(["nonexistentbinary12345"]).sudo('nobody')
|
|
27
|
+
self.assertFalse(cmd.exists)
|
|
28
|
+
|
|
15
29
|
def test_text(self):
|
|
30
|
+
"""
|
|
31
|
+
Test that .text returns the output of the command
|
|
32
|
+
:return:
|
|
33
|
+
"""
|
|
16
34
|
cmd = Cmd(["echo", "hello world"])
|
|
17
35
|
self.assertEqual(cmd.text, "hello world")
|
|
18
36
|
|
|
@@ -42,6 +60,15 @@ class TestCmd(unittest.TestCase):
|
|
|
42
60
|
with self.assertRaises(Exception):
|
|
43
61
|
_ = cmd.json
|
|
44
62
|
|
|
63
|
+
def test_cwd(self):
|
|
64
|
+
"""
|
|
65
|
+
Test that the cwd is set and used correctly
|
|
66
|
+
|
|
67
|
+
:return:
|
|
68
|
+
"""
|
|
69
|
+
cmd = Cmd(["pwd"]).cwd("/tmp")
|
|
70
|
+
self.assertEqual(cmd.text, "/tmp")
|
|
71
|
+
|
|
45
72
|
|
|
46
73
|
if __name__ == "__main__":
|
|
47
74
|
unittest.main()
|
|
@@ -23,32 +23,32 @@ class TestUnrealConfig(unittest.TestCase):
|
|
|
23
23
|
cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
|
|
24
24
|
# Configs are grouped by named parameters, so let's add some options
|
|
25
25
|
cfg.add_option({
|
|
26
|
-
'name': '
|
|
26
|
+
'name': 'Key 1',
|
|
27
27
|
'section': 'SomeSection',
|
|
28
28
|
'key': 'Key1',
|
|
29
29
|
})
|
|
30
30
|
cfg.add_option({
|
|
31
|
-
'name': '
|
|
31
|
+
'name': 'Key 2',
|
|
32
32
|
'section': 'SomeSection',
|
|
33
33
|
'key': 'Key2',
|
|
34
34
|
'type': 'int'
|
|
35
35
|
})
|
|
36
36
|
cfg.add_option({
|
|
37
|
-
'name': '
|
|
37
|
+
'name': 'Key 3',
|
|
38
38
|
'section': 'SomeSection',
|
|
39
39
|
'key': 'Key3',
|
|
40
40
|
'type': 'bool'
|
|
41
41
|
})
|
|
42
42
|
cfg.load()
|
|
43
43
|
|
|
44
|
-
self.assertEqual(cfg.get_value('
|
|
45
|
-
self.assertEqual(cfg.get_value('
|
|
46
|
-
self.assertEqual(cfg.get_value('
|
|
44
|
+
self.assertEqual(cfg.get_value('Key 1'), 'Value1')
|
|
45
|
+
self.assertEqual(cfg.get_value('Key 2'), 42)
|
|
46
|
+
self.assertEqual(cfg.get_value('Key 3'), True)
|
|
47
47
|
|
|
48
48
|
# These values should exist
|
|
49
|
-
self.assertTrue(cfg.has_value('
|
|
50
|
-
self.assertTrue(cfg.has_value('
|
|
51
|
-
self.assertTrue(cfg.has_value('
|
|
49
|
+
self.assertTrue(cfg.has_value('Key 1'))
|
|
50
|
+
self.assertTrue(cfg.has_value('Key 2'))
|
|
51
|
+
self.assertTrue(cfg.has_value('Key 3'))
|
|
52
52
|
|
|
53
53
|
# This value should not
|
|
54
54
|
self.assertFalse(cfg.has_value('NonExistentKey'))
|
|
@@ -58,14 +58,14 @@ class TestUnrealConfig(unittest.TestCase):
|
|
|
58
58
|
expected = f.read()
|
|
59
59
|
self.assertEqual(expected, cfg.fetch())
|
|
60
60
|
|
|
61
|
-
cfg.set_value('
|
|
62
|
-
self.assertEqual(cfg.get_value('
|
|
61
|
+
cfg.set_value('Key 1', 'NewValue')
|
|
62
|
+
self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
|
|
63
63
|
|
|
64
|
-
cfg.set_value('
|
|
65
|
-
self.assertEqual(cfg.get_value('
|
|
64
|
+
cfg.set_value('Key 2', 100)
|
|
65
|
+
self.assertEqual(cfg.get_value('Key 2'), 100)
|
|
66
66
|
|
|
67
|
-
cfg.set_value('
|
|
68
|
-
self.assertEqual(cfg.get_value('
|
|
67
|
+
cfg.set_value('Key 3', False)
|
|
68
|
+
self.assertEqual(cfg.get_value('Key 3'), False)
|
|
69
69
|
|
|
70
70
|
# Ensure the generated data matches expectations
|
|
71
71
|
expected = '''; This is a simple config file for testing purposes.
|
|
@@ -80,6 +80,61 @@ Key3=False
|
|
|
80
80
|
'''
|
|
81
81
|
self.assertEqual(expected, cfg.fetch())
|
|
82
82
|
|
|
83
|
+
def test_always_escape_strings(self):
|
|
84
|
+
cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
|
|
85
|
+
cfg.always_escape_strings = True
|
|
86
|
+
# Configs are grouped by named parameters, so let's add some options
|
|
87
|
+
cfg.add_option({
|
|
88
|
+
'name': 'Key 1',
|
|
89
|
+
'section': 'SomeSection',
|
|
90
|
+
'key': 'Group/Key1',
|
|
91
|
+
})
|
|
92
|
+
cfg.add_option({
|
|
93
|
+
'name': 'Key 2',
|
|
94
|
+
'section': 'SomeSection',
|
|
95
|
+
'key': 'Group/Key2',
|
|
96
|
+
'type': 'int'
|
|
97
|
+
})
|
|
98
|
+
cfg.add_option({
|
|
99
|
+
'name': 'Key 3',
|
|
100
|
+
'section': 'SomeSection',
|
|
101
|
+
'key': 'Group/Key3',
|
|
102
|
+
'type': 'bool'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
cfg.set_value('Key 1', 'Value1')
|
|
106
|
+
cfg.set_value('Key 2', 42)
|
|
107
|
+
cfg.set_value('Key 3', True)
|
|
108
|
+
|
|
109
|
+
self.assertEqual(cfg.get_value('Key 1'), 'Value1')
|
|
110
|
+
self.assertEqual(cfg.get_value('Key 2'), 42)
|
|
111
|
+
self.assertEqual(cfg.get_value('Key 3'), True)
|
|
112
|
+
|
|
113
|
+
# These values should exist
|
|
114
|
+
self.assertTrue(cfg.has_value('Key 1'))
|
|
115
|
+
self.assertTrue(cfg.has_value('Key 2'))
|
|
116
|
+
self.assertTrue(cfg.has_value('Key 3'))
|
|
117
|
+
|
|
118
|
+
# This value should not
|
|
119
|
+
self.assertFalse(cfg.has_value('NonExistentKey'))
|
|
120
|
+
|
|
121
|
+
# Ensure the generated data matches expectations
|
|
122
|
+
expected = '[SomeSection]\nGroup=(Key1="Value1",Key2=42,Key3=True)\n'
|
|
123
|
+
self.assertEqual(expected, cfg.fetch())
|
|
124
|
+
|
|
125
|
+
cfg.set_value('Key 1', 'NewValue')
|
|
126
|
+
self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
|
|
127
|
+
|
|
128
|
+
cfg.set_value('Key 2', 100)
|
|
129
|
+
self.assertEqual(cfg.get_value('Key 2'), 100)
|
|
130
|
+
|
|
131
|
+
cfg.set_value('Key 3', False)
|
|
132
|
+
self.assertEqual(cfg.get_value('Key 3'), False)
|
|
133
|
+
|
|
134
|
+
# Ensure the generated data matches expectations
|
|
135
|
+
expected = '[SomeSection]\nGroup=(Key1="NewValue",Key2=100,Key3=False)\n'
|
|
136
|
+
self.assertEqual(expected, cfg.fetch())
|
|
137
|
+
|
|
83
138
|
def test_simple_create(self):
|
|
84
139
|
cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
|
|
85
140
|
# Configs are grouped by named parameters, so let's add some options
|
|
@@ -247,6 +302,9 @@ PlayedMaps=NewMap2_WP
|
|
|
247
302
|
})
|
|
248
303
|
cfg.load()
|
|
249
304
|
|
|
305
|
+
self.assertTrue(cfg.has_value('Difficulty'))
|
|
306
|
+
self.assertFalse(cfg.has_value('I do not exist'))
|
|
307
|
+
|
|
250
308
|
self.assertEqual(cfg.get_value('Difficulty'), 'None')
|
|
251
309
|
self.assertEqual(cfg.get_value('Randomizer Seed'), '')
|
|
252
310
|
self.assertEqual(cfg.get_value('Randomizer Pal Level Random'), False)
|
|
@@ -256,6 +314,14 @@ PlayedMaps=NewMap2_WP
|
|
|
256
314
|
expected = f.read()
|
|
257
315
|
self.assertEqual(expected, cfg.fetch())
|
|
258
316
|
|
|
317
|
+
cfg.set_value('Difficulty', 'Super Duper Hard')
|
|
318
|
+
cfg.set_value('Randomizer Seed', 'Random Seed')
|
|
319
|
+
cfg.set_value('Randomizer Pal Level Random', True)
|
|
320
|
+
|
|
321
|
+
self.assertEqual(cfg.get_value('Difficulty'), 'Super Duper Hard')
|
|
322
|
+
self.assertEqual(cfg.get_value('Randomizer Seed'), 'Random Seed')
|
|
323
|
+
self.assertEqual(cfg.get_value('Randomizer Pal Level Random'), True)
|
|
324
|
+
|
|
259
325
|
def test_palworld_empty(self):
|
|
260
326
|
"""
|
|
261
327
|
Test that the Palworld format works even when the ini is empty.
|
|
@@ -88,7 +88,6 @@ class BaseApp(ABC):
|
|
|
88
88
|
'api', # Game supports baseline API features
|
|
89
89
|
'cmd', # Game supports commands sent via the API
|
|
90
90
|
'create_service', # Game supports creating new services
|
|
91
|
-
'mods', # Game supports mods
|
|
92
91
|
}
|
|
93
92
|
"""
|
|
94
93
|
List of features available in this game
|
|
@@ -576,7 +575,7 @@ class BaseApp(ABC):
|
|
|
576
575
|
except urllib_error.HTTPError as e:
|
|
577
576
|
print('Could not notify Discord: %s' % e)
|
|
578
577
|
|
|
579
|
-
@deprecated("Please use
|
|
578
|
+
@deprecated("Please use get_base_directory() from utils instead.")
|
|
580
579
|
def get_app_directory(self) -> str:
|
|
581
580
|
"""
|
|
582
581
|
Get the base directory for this game installation.
|
|
@@ -585,7 +584,7 @@ class BaseApp(ABC):
|
|
|
585
584
|
|
|
586
585
|
:return:
|
|
587
586
|
"""
|
|
588
|
-
return utils.
|
|
587
|
+
return utils.get_base_directory()
|
|
589
588
|
|
|
590
589
|
@deprecated("Please use get_home_directory() from utils instead")
|
|
591
590
|
def get_home_directory(self) -> str:
|
|
@@ -663,8 +662,8 @@ class BaseApp(ABC):
|
|
|
663
662
|
logging.info('Removed config file for %s at %s' % (self.name, config.path))
|
|
664
663
|
|
|
665
664
|
# Cleanup directory structure
|
|
666
|
-
shutil.rmtree(os.path.join(utils.
|
|
667
|
-
shutil.rmtree(os.path.join(utils.
|
|
665
|
+
shutil.rmtree(os.path.join(utils.get_base_directory(), 'AppFiles'))
|
|
666
|
+
shutil.rmtree(os.path.join(utils.get_base_directory(), 'Environments'))
|
|
668
667
|
|
|
669
668
|
def remove_service(self, service_name: str):
|
|
670
669
|
"""
|
|
@@ -689,7 +688,7 @@ class BaseApp(ABC):
|
|
|
689
688
|
Try to detect available services for this game.
|
|
690
689
|
:return:
|
|
691
690
|
"""
|
|
692
|
-
envs = os.path.join(utils.
|
|
691
|
+
envs = os.path.join(utils.get_base_directory(), 'Environments')
|
|
693
692
|
if os.path.exists(envs):
|
|
694
693
|
# Each service should have a file here, named as {service}.env
|
|
695
694
|
services = []
|
|
@@ -724,9 +723,9 @@ class BaseApp(ABC):
|
|
|
724
723
|
"""
|
|
725
724
|
|
|
726
725
|
# Ensure some baseline directories exist with the correct ownership and permissions
|
|
727
|
-
utils.makedirs(os.path.join(utils.
|
|
728
|
-
utils.makedirs(os.path.join(utils.
|
|
729
|
-
utils.makedirs(os.path.join(utils.
|
|
726
|
+
utils.makedirs(os.path.join(utils.get_base_directory(), 'Backups'))
|
|
727
|
+
utils.makedirs(os.path.join(utils.get_base_directory(), 'AppFiles'))
|
|
728
|
+
utils.makedirs(os.path.join(utils.get_base_directory(), 'Environments'))
|
|
730
729
|
|
|
731
730
|
return True
|
|
732
731
|
|
|
@@ -317,7 +317,7 @@ class SteamApp(BaseApp, ABC):
|
|
|
317
317
|
|
|
318
318
|
:return:
|
|
319
319
|
"""
|
|
320
|
-
app_manifest = os.path.join(utils.
|
|
320
|
+
app_manifest = os.path.join(utils.get_base_directory(), 'AppFiles', 'steamapps', 'appmanifest_%s.acf' % self.steam_id)
|
|
321
321
|
|
|
322
322
|
if not os.path.exists(app_manifest):
|
|
323
323
|
print(f"App manifest file {app_manifest} does not exist.", file=sys.stderr)
|
|
@@ -395,7 +395,7 @@ class SteamApp(BaseApp, ABC):
|
|
|
395
395
|
cmd = Cmd([
|
|
396
396
|
guess_steamcmd_path(),
|
|
397
397
|
'+force_install_dir',
|
|
398
|
-
os.path.join(utils.
|
|
398
|
+
os.path.join(utils.get_base_directory(), 'AppFiles'),
|
|
399
399
|
'+login',
|
|
400
400
|
'anonymous',
|
|
401
401
|
'+app_update',
|
|
@@ -12,9 +12,64 @@ class UnrealConfig(BaseConfig):
|
|
|
12
12
|
super().__init__(group_name)
|
|
13
13
|
self.path = path
|
|
14
14
|
self._data = []
|
|
15
|
-
self._values = {}
|
|
16
15
|
self._use_array_operators = False
|
|
17
16
|
self._is_changed = False
|
|
17
|
+
self.always_escape_strings = False
|
|
18
|
+
"""
|
|
19
|
+
Set to True to always escape strings in Configuration files. Some games require this.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def _get_raw_value(self, name: str):
|
|
23
|
+
"""
|
|
24
|
+
Get the raw value, possibly in a nested dictionary.
|
|
25
|
+
|
|
26
|
+
useful for get_value and has_value.
|
|
27
|
+
|
|
28
|
+
:param name: Name of the option
|
|
29
|
+
:return:
|
|
30
|
+
"""
|
|
31
|
+
if name not in self.options:
|
|
32
|
+
return None
|
|
33
|
+
opt = self.options[name]
|
|
34
|
+
|
|
35
|
+
# Check if this section exists, also serves to find the section.
|
|
36
|
+
section = None
|
|
37
|
+
for sec in self._data:
|
|
38
|
+
if sec[0]['type'] == 'section' and sec[0]['value'] == opt.section:
|
|
39
|
+
section = sec[1:]
|
|
40
|
+
break
|
|
41
|
+
if section is None:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if '/' in opt.key:
|
|
45
|
+
# Look for a key inside structured data (aka a dict)
|
|
46
|
+
group = opt.key.split('/')[0]
|
|
47
|
+
for sec in section:
|
|
48
|
+
if sec['key'] == group:
|
|
49
|
+
current = sec['value']
|
|
50
|
+
for part in opt.key.split('/')[1:]:
|
|
51
|
+
if part in current:
|
|
52
|
+
current = current[part]
|
|
53
|
+
else:
|
|
54
|
+
# Subkey does not exist in the value of this section config; doesn't exist.
|
|
55
|
+
return None
|
|
56
|
+
return current
|
|
57
|
+
else:
|
|
58
|
+
# Simple key.
|
|
59
|
+
ret = None
|
|
60
|
+
for sec in section:
|
|
61
|
+
if sec['type'] == 'keyvalue' and sec['key'] == opt.key:
|
|
62
|
+
# Unreal supports duplicate keys; these should be exposed as a list.
|
|
63
|
+
if ret is None:
|
|
64
|
+
ret = sec['value']
|
|
65
|
+
elif isinstance(ret, list):
|
|
66
|
+
ret.append(sec['value'])
|
|
67
|
+
else:
|
|
68
|
+
ret = [ret]
|
|
69
|
+
ret.append(sec['value'])
|
|
70
|
+
return ret
|
|
71
|
+
|
|
72
|
+
return None
|
|
18
73
|
|
|
19
74
|
def get_value(self, name: str) -> Union[str, int, bool, list]:
|
|
20
75
|
"""
|
|
@@ -28,27 +83,11 @@ class UnrealConfig(BaseConfig):
|
|
|
28
83
|
return ''
|
|
29
84
|
|
|
30
85
|
opt = self.options[name]
|
|
31
|
-
|
|
32
|
-
if
|
|
33
|
-
|
|
86
|
+
raw_value = self._get_raw_value(name)
|
|
87
|
+
if raw_value is None:
|
|
88
|
+
return opt.default
|
|
34
89
|
else:
|
|
35
|
-
|
|
36
|
-
# Struct key
|
|
37
|
-
parts = opt.key.split('/')
|
|
38
|
-
current = self._values[opt.section]
|
|
39
|
-
for part in parts:
|
|
40
|
-
if part in current:
|
|
41
|
-
current = current[part]
|
|
42
|
-
else:
|
|
43
|
-
current = opt.default
|
|
44
|
-
break
|
|
45
|
-
val = current
|
|
46
|
-
elif opt.key not in self._values[opt.section]:
|
|
47
|
-
val = opt.default
|
|
48
|
-
else:
|
|
49
|
-
val = self._values[opt.section][opt.key]
|
|
50
|
-
|
|
51
|
-
return opt.to_system_type(val)
|
|
90
|
+
return opt.to_system_type(raw_value)
|
|
52
91
|
|
|
53
92
|
def _find_or_create_value(self, section: list, key: str, str_value: Union[str, list]) -> list:
|
|
54
93
|
"""
|
|
@@ -136,9 +175,14 @@ class UnrealConfig(BaseConfig):
|
|
|
136
175
|
opt = self.options[name]
|
|
137
176
|
str_value = self.from_system_type(name, value)
|
|
138
177
|
|
|
139
|
-
|
|
178
|
+
# Create the section if necessary
|
|
179
|
+
exists = False
|
|
180
|
+
for sec in self._data:
|
|
181
|
+
if sec[0]['type'] == 'section' and sec[0]['value'] == opt.section:
|
|
182
|
+
exists = True
|
|
183
|
+
break
|
|
184
|
+
if not exists:
|
|
140
185
|
# Create the section
|
|
141
|
-
self._values[opt.section] = {}
|
|
142
186
|
self._data.append([{'type': 'section', 'value': opt.section}])
|
|
143
187
|
|
|
144
188
|
# Ensure the updated value is in the data structure
|
|
@@ -157,7 +201,6 @@ class UnrealConfig(BaseConfig):
|
|
|
157
201
|
new_data.append(sec)
|
|
158
202
|
|
|
159
203
|
self._data = new_data
|
|
160
|
-
self._values[opt.section][opt.key] = str_value
|
|
161
204
|
self._is_changed = True
|
|
162
205
|
|
|
163
206
|
def has_value(self, name: str) -> bool:
|
|
@@ -167,17 +210,8 @@ class UnrealConfig(BaseConfig):
|
|
|
167
210
|
:param name: Name of the option
|
|
168
211
|
:return:
|
|
169
212
|
"""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
opt = self.options[name]
|
|
173
|
-
|
|
174
|
-
if opt.section not in self._values:
|
|
175
|
-
return False
|
|
176
|
-
else:
|
|
177
|
-
if opt.key not in self._values[opt.section]:
|
|
178
|
-
return False
|
|
179
|
-
else:
|
|
180
|
-
return self._values[opt.section][opt.key] != ''
|
|
213
|
+
raw_value = self._get_raw_value(name)
|
|
214
|
+
return raw_value is not None
|
|
181
215
|
|
|
182
216
|
def exists(self) -> bool:
|
|
183
217
|
"""
|
|
@@ -194,7 +228,6 @@ class UnrealConfig(BaseConfig):
|
|
|
194
228
|
if os.path.exists(self.path):
|
|
195
229
|
with open(self.path, 'r', encoding='utf-8') as f:
|
|
196
230
|
section = []
|
|
197
|
-
last_section = ''
|
|
198
231
|
for line in f.readlines():
|
|
199
232
|
data = None
|
|
200
233
|
stripped = line.strip()
|
|
@@ -232,27 +265,6 @@ class UnrealConfig(BaseConfig):
|
|
|
232
265
|
self._data.append(section)
|
|
233
266
|
section = []
|
|
234
267
|
section.append(data)
|
|
235
|
-
last_section = data['value']
|
|
236
|
-
elif data['type'] == 'keystruct':
|
|
237
|
-
section.append(data)
|
|
238
|
-
if last_section not in self._values:
|
|
239
|
-
self._values[last_section] = {}
|
|
240
|
-
self._values[last_section][data['key']] = data['value']
|
|
241
|
-
elif data['type'] == 'keyvalue':
|
|
242
|
-
section.append(data)
|
|
243
|
-
if last_section not in self._values:
|
|
244
|
-
self._values[last_section] = {}
|
|
245
|
-
# Auto-handle duplicate keys by converting them to a list.
|
|
246
|
-
# UE is weird.
|
|
247
|
-
if data['key'] in self._values[last_section]:
|
|
248
|
-
# Existing key, convert to list
|
|
249
|
-
existing_value = self._values[last_section][data['key']]
|
|
250
|
-
if not isinstance(existing_value, list):
|
|
251
|
-
existing_value = [existing_value]
|
|
252
|
-
existing_value.append(data['value'])
|
|
253
|
-
self._values[last_section][data['key']] = existing_value
|
|
254
|
-
else:
|
|
255
|
-
self._values[last_section][data['key']] = data['value']
|
|
256
268
|
else:
|
|
257
269
|
section.append(data)
|
|
258
270
|
if len(section) > 0:
|
|
@@ -289,6 +301,10 @@ class UnrealConfig(BaseConfig):
|
|
|
289
301
|
# Boolean strings do not require quoting
|
|
290
302
|
return False
|
|
291
303
|
|
|
304
|
+
if self.always_escape_strings:
|
|
305
|
+
# Game requested to always escape strings
|
|
306
|
+
return True
|
|
307
|
+
|
|
292
308
|
if re.match(r'^[A-Za-z0-9]+$', s) is not None:
|
|
293
309
|
# Simple strings do not require quoting
|
|
294
310
|
return False
|
|
@@ -10,6 +10,48 @@ def cli_formatter(
|
|
|
10
10
|
true_value: str | bool = 'True',
|
|
11
11
|
false_value: str | bool = 'False',
|
|
12
12
|
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Format a given Configuration object as CLI arguments.
|
|
15
|
+
|
|
16
|
+
## True/False Formatting
|
|
17
|
+
|
|
18
|
+
The most complicated part of this is handling true/false boolean values.
|
|
19
|
+
|
|
20
|
+
The default is to render bool TRUE values as -key_name=True and bool FALSE values as -key_name=False
|
|
21
|
+
|
|
22
|
+
Render bool TRUE values as -key_name and bool FALSE values are omitted completely
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
cli_formatter(..., prefix='-', true_value=True, false_value=False)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The inverse is possible too, to omit TRUE values and only render FALSE values.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
cli_formatter(..., prefix='-', true_value=False, false_value=True)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Render bool TRUE values as -key_name=true and bool FALSE values as -key_name=false
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
cli_formatter(..., prefix='-', true_value='true', false_value='false')
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Render bool TRUE values as ?key_name:YUP and bool FALSE values as ?key_name:LULZNOPE
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
cli_formatter(..., prefix='?', sep=':', true_value='YUP', false_value='LULZNOPE')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
:param data:
|
|
47
|
+
:param section:
|
|
48
|
+
:param prefix:
|
|
49
|
+
:param sep:
|
|
50
|
+
:param joiner:
|
|
51
|
+
:param true_value:
|
|
52
|
+
:param false_value:
|
|
53
|
+
:return:
|
|
54
|
+
"""
|
|
13
55
|
values = []
|
|
14
56
|
for opt in data.options.values():
|
|
15
57
|
if opt.section != section:
|
|
@@ -8,7 +8,7 @@ from warlock_manager.libs import utils
|
|
|
8
8
|
def get_cache(some_string: str, expires: int = 3600) -> str | None:
|
|
9
9
|
# Check cache prior to running the command.
|
|
10
10
|
cmd_hash = hashlib.sha256(some_string.encode()).hexdigest()
|
|
11
|
-
cache_path = os.path.join(utils.
|
|
11
|
+
cache_path = os.path.join(utils.get_base_directory(), '.cache', cmd_hash)
|
|
12
12
|
if os.path.exists(cache_path) and os.path.getmtime(cache_path) > time.time() - expires:
|
|
13
13
|
with open(cache_path, "r") as f:
|
|
14
14
|
return f.read()
|
|
@@ -17,13 +17,13 @@ def get_cache(some_string: str, expires: int = 3600) -> str | None:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def save_cache(some_string: str, content: str):
|
|
20
|
-
cache_path = os.path.join(utils.
|
|
20
|
+
cache_path = os.path.join(utils.get_base_directory(), '.cache')
|
|
21
21
|
if not os.path.exists(cache_path):
|
|
22
22
|
os.makedirs(cache_path)
|
|
23
23
|
utils.ensure_file_ownership(cache_path)
|
|
24
24
|
|
|
25
25
|
cmd_hash = hashlib.sha256(some_string.encode()).hexdigest()
|
|
26
|
-
cache_path = os.path.join(utils.
|
|
26
|
+
cache_path = os.path.join(utils.get_base_directory(), '.cache', cmd_hash)
|
|
27
27
|
with open(cache_path, "w") as f:
|
|
28
28
|
f.write(content)
|
|
29
29
|
utils.ensure_file_ownership(cache_path)
|
|
@@ -3,6 +3,7 @@ import subprocess
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import time
|
|
6
|
+
import pwd
|
|
6
7
|
|
|
7
8
|
from warlock_manager.libs import cache
|
|
8
9
|
|
|
@@ -39,7 +40,7 @@ class Cmd:
|
|
|
39
40
|
CompletedProcess: The result of the command execution
|
|
40
41
|
"""
|
|
41
42
|
|
|
42
|
-
self.executable: str | None = cmd[0] if len(cmd) > 0 else None
|
|
43
|
+
self.executable: str | None = self.cmd[0] if len(self.cmd) > 0 else None
|
|
43
44
|
"""
|
|
44
45
|
str: The executable name of the command
|
|
45
46
|
"""
|
|
@@ -65,7 +66,12 @@ class Cmd:
|
|
|
65
66
|
These commands are NOT persistent across calls!
|
|
66
67
|
"""
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
self._cwd: str | None = None
|
|
70
|
+
"""
|
|
71
|
+
The current working directory for this command
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def sudo(self, runas: str | int) -> 'Cmd':
|
|
69
75
|
"""
|
|
70
76
|
Run this command as another user using sudo.
|
|
71
77
|
|
|
@@ -78,55 +84,73 @@ class Cmd:
|
|
|
78
84
|
:return:
|
|
79
85
|
"""
|
|
80
86
|
if isinstance(runas, str):
|
|
81
|
-
|
|
87
|
+
# Get the name of the user owning the current process
|
|
88
|
+
# use pwd instead of os.getlogin to address CI tests on 3.13
|
|
89
|
+
current_user = pwd.getpwuid(os.geteuid()).pw_name
|
|
90
|
+
if current_user == runas:
|
|
82
91
|
# If we're already running as this user, no need to prefix with sudo
|
|
83
|
-
return
|
|
92
|
+
return self
|
|
84
93
|
prefix = ['sudo', '-u', runas]
|
|
85
94
|
else:
|
|
86
95
|
if os.geteuid() == runas:
|
|
87
96
|
# If we're already running as this user, no need to prefix with sudo
|
|
88
|
-
return
|
|
97
|
+
return self
|
|
89
98
|
prefix = ['sudo', '-u', '#%s' % runas]
|
|
90
99
|
|
|
91
100
|
self.cmd = prefix + self.cmd
|
|
92
101
|
self.result = None
|
|
102
|
+
return self
|
|
93
103
|
|
|
94
|
-
def use_stdout(self):
|
|
104
|
+
def use_stdout(self) -> 'Cmd':
|
|
95
105
|
"""
|
|
96
106
|
Set this command to use stdout for output instead of stderr.
|
|
97
107
|
:return:
|
|
98
108
|
"""
|
|
99
109
|
self.uses = 'stdout'
|
|
110
|
+
return self
|
|
100
111
|
|
|
101
|
-
def use_stderr(self):
|
|
112
|
+
def use_stderr(self) -> 'Cmd':
|
|
102
113
|
"""
|
|
103
114
|
Set this command to use stderr for output instead of stdout.
|
|
104
115
|
:return:
|
|
105
116
|
"""
|
|
106
117
|
self.uses = 'stderr'
|
|
118
|
+
return self
|
|
107
119
|
|
|
108
|
-
def stream_output(self):
|
|
120
|
+
def stream_output(self) -> 'Cmd':
|
|
109
121
|
"""
|
|
110
122
|
Set this command to stream to stdout/stderr directly. Useful for long-running commands.
|
|
111
123
|
:return:
|
|
112
124
|
"""
|
|
113
125
|
self.uses = None
|
|
126
|
+
return self
|
|
114
127
|
|
|
115
|
-
def is_cacheable(self, expires: int = 3600):
|
|
128
|
+
def is_cacheable(self, expires: int = 3600) -> 'Cmd':
|
|
116
129
|
"""
|
|
117
130
|
Set this command as cacheable for N seconds.
|
|
118
131
|
:param expires:
|
|
119
132
|
:return:
|
|
120
133
|
"""
|
|
121
134
|
self.cacheable = expires
|
|
135
|
+
return self
|
|
122
136
|
|
|
123
|
-
def is_memory_cacheable(self, expires: int = 2):
|
|
137
|
+
def is_memory_cacheable(self, expires: int = 2) -> 'Cmd':
|
|
124
138
|
"""
|
|
125
139
|
Set this command as cacheable in memory for N seconds.
|
|
126
140
|
:param expires:
|
|
127
141
|
:return:
|
|
128
142
|
"""
|
|
129
143
|
self.memory_cacheable = expires
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def cwd(self, path: str | None) -> 'Cmd':
|
|
147
|
+
"""
|
|
148
|
+
Set the current working directory for this command.
|
|
149
|
+
:param path:
|
|
150
|
+
:return:
|
|
151
|
+
"""
|
|
152
|
+
self._cwd = path
|
|
153
|
+
return self
|
|
130
154
|
|
|
131
155
|
@property
|
|
132
156
|
def exists(self) -> bool:
|
|
@@ -230,6 +254,7 @@ class Cmd:
|
|
|
230
254
|
self.cmd,
|
|
231
255
|
capture_output=capture_output,
|
|
232
256
|
check=False,
|
|
257
|
+
cwd=self._cwd,
|
|
233
258
|
encoding='utf-8'
|
|
234
259
|
)
|
|
235
260
|
except FileNotFoundError as e:
|
|
@@ -265,21 +290,54 @@ class Cmd:
|
|
|
265
290
|
|
|
266
291
|
return self.result
|
|
267
292
|
|
|
268
|
-
def extend(self, args: list):
|
|
293
|
+
def extend(self, args: list) -> 'Cmd':
|
|
269
294
|
"""
|
|
270
295
|
Extend the command with additional arguments.
|
|
271
296
|
:param args:
|
|
272
297
|
"""
|
|
273
298
|
self.cmd = self.cmd + args
|
|
274
299
|
self.result = None
|
|
300
|
+
return self
|
|
275
301
|
|
|
276
|
-
def append(self, arg: str):
|
|
302
|
+
def append(self, arg: str) -> 'Cmd':
|
|
277
303
|
"""
|
|
278
304
|
Append a single argument to the command.
|
|
279
305
|
:param arg:
|
|
280
306
|
"""
|
|
281
307
|
self.cmd.append(arg)
|
|
282
308
|
self.result = None
|
|
309
|
+
return self
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class PipeCmd(Cmd):
|
|
313
|
+
"""
|
|
314
|
+
Convenience wrapper for piping command output to a parent process
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
def run(self):
|
|
318
|
+
"""
|
|
319
|
+
Run the command in the background using nohup. Caches the result so subsequent calls don't re-run the command.
|
|
320
|
+
|
|
321
|
+
:return:
|
|
322
|
+
"""
|
|
323
|
+
if self.result is None:
|
|
324
|
+
|
|
325
|
+
if self.cacheable is not False:
|
|
326
|
+
logging.warning('Piped commands cannot be cached!')
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
logging.debug('Running piped command: %s' % ' '.join(self.cmd))
|
|
330
|
+
self.result = subprocess.Popen(
|
|
331
|
+
self.cmd,
|
|
332
|
+
stdout=subprocess.PIPE,
|
|
333
|
+
stderr=subprocess.PIPE
|
|
334
|
+
)
|
|
335
|
+
except FileNotFoundError as e:
|
|
336
|
+
self.result = CmdFakeResponse('', str(e), 127)
|
|
337
|
+
except OSError as e:
|
|
338
|
+
self.result = CmdFakeResponse('', str(e), 1)
|
|
339
|
+
|
|
340
|
+
return self.result
|
|
283
341
|
|
|
284
342
|
|
|
285
343
|
class BackgroundCmd(Cmd):
|
|
@@ -299,11 +357,11 @@ class BackgroundCmd(Cmd):
|
|
|
299
357
|
logging.warning('Background commands cannot be cached!')
|
|
300
358
|
|
|
301
359
|
try:
|
|
360
|
+
logging.debug('Running background command: %s' % ' '.join(self.cmd))
|
|
302
361
|
self.result = subprocess.Popen(
|
|
303
362
|
self.cmd,
|
|
304
363
|
stdout=subprocess.DEVNULL,
|
|
305
|
-
stderr=subprocess.DEVNULL
|
|
306
|
-
preexec_fn=lambda: logging.debug('Running background command: %s' % ' '.join(self.cmd))
|
|
364
|
+
stderr=subprocess.DEVNULL
|
|
307
365
|
)
|
|
308
366
|
except FileNotFoundError as e:
|
|
309
367
|
self.result = CmdFakeResponse('', str(e), 127)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from warlock_manager.libs.cmd import Cmd
|
|
2
|
+
import logging
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class Firewall:
|
|
@@ -79,7 +80,7 @@ class Firewall:
|
|
|
79
80
|
return None
|
|
80
81
|
|
|
81
82
|
@classmethod
|
|
82
|
-
def allow(cls, port: int, protocol: str = 'tcp', comment: str = None) ->
|
|
83
|
+
def allow(cls, port: int, protocol: str = 'tcp', comment: str = None) -> bool:
|
|
83
84
|
"""
|
|
84
85
|
Allows a specific port through the system's firewall.
|
|
85
86
|
Supports UFW, Firewalld, and iptables.
|
|
@@ -90,30 +91,47 @@ class Firewall:
|
|
|
90
91
|
comment (str, optional): An optional comment for the rule. Defaults to None.
|
|
91
92
|
"""
|
|
92
93
|
|
|
94
|
+
if port <= 0 or port >= 65536:
|
|
95
|
+
logging.error(f"Invalid port number: {port}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
if protocol.lower() not in ['tcp', 'udp']:
|
|
99
|
+
logging.error(f"Invalid protocol: {protocol}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
93
102
|
firewall = cls.get_available()
|
|
94
103
|
|
|
95
104
|
if firewall == 'ufw':
|
|
105
|
+
# UFW requires the protocol to be all lowercase.
|
|
106
|
+
protocol = protocol.lower()
|
|
107
|
+
logging.info(f"Allowing {port}/{protocol} via UFW")
|
|
96
108
|
cmd = Cmd(['ufw', 'allow', f'{port}/{protocol}'])
|
|
97
109
|
if comment:
|
|
98
110
|
cmd.extend(['comment', comment])
|
|
99
|
-
cmd.
|
|
111
|
+
return cmd.success
|
|
100
112
|
|
|
101
113
|
elif firewall == 'firewalld':
|
|
102
|
-
|
|
103
|
-
Cmd(['firewall-cmd', '--
|
|
114
|
+
logging.info(f"Allowing {port}/{protocol} via Firewalld")
|
|
115
|
+
if Cmd(['firewall-cmd', '--permanent', '--add-port', f'{port}/{protocol}']).success:
|
|
116
|
+
Cmd(['firewall-cmd', '--reload']).run()
|
|
117
|
+
return True
|
|
104
118
|
|
|
105
119
|
elif firewall == 'iptables':
|
|
120
|
+
logging.info(f"Allowing {port}/{protocol} via iptables")
|
|
106
121
|
cmd = Cmd(['iptables', '-A', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT'])
|
|
107
122
|
if comment:
|
|
108
123
|
cmd.extend(['-m', 'comment', '--comment', comment])
|
|
109
|
-
cmd.
|
|
110
|
-
|
|
124
|
+
if cmd.success:
|
|
125
|
+
Cmd(['service', 'iptables', 'save']).run()
|
|
126
|
+
return True
|
|
111
127
|
|
|
112
128
|
else:
|
|
113
|
-
|
|
129
|
+
logging.error('No supported firewall found on the system.')
|
|
130
|
+
|
|
131
|
+
return False
|
|
114
132
|
|
|
115
133
|
@classmethod
|
|
116
|
-
def remove(cls, port: int, protocol: str = 'tcp') ->
|
|
134
|
+
def remove(cls, port: int, protocol: str = 'tcp') -> bool:
|
|
117
135
|
"""
|
|
118
136
|
Removes a specific port from the system's firewall.
|
|
119
137
|
Supports UFW, Firewalld, and iptables.
|
|
@@ -123,21 +141,33 @@ class Firewall:
|
|
|
123
141
|
protocol (str, optional): The protocol to use ('tcp' or 'udp'). Defaults to 'tcp'.
|
|
124
142
|
"""
|
|
125
143
|
|
|
144
|
+
if port <= 0 or port >= 65536:
|
|
145
|
+
logging.error(f"Invalid port number: {port}")
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
if protocol.lower() not in ['tcp', 'udp']:
|
|
149
|
+
logging.error(f"Invalid protocol: {protocol}")
|
|
150
|
+
return False
|
|
151
|
+
|
|
126
152
|
firewall = cls.get_available()
|
|
127
153
|
|
|
128
154
|
if firewall == 'ufw':
|
|
129
|
-
Cmd(['ufw', 'delete', 'allow', f'{port}/{protocol}']).
|
|
155
|
+
return Cmd(['ufw', 'delete', 'allow', f'{port}/{protocol}']).success
|
|
130
156
|
|
|
131
157
|
elif firewall == 'firewalld':
|
|
132
|
-
Cmd(['firewall-cmd', '--permanent', '--remove-port', f'{port}/{protocol}']).
|
|
133
|
-
|
|
158
|
+
if Cmd(['firewall-cmd', '--permanent', '--remove-port', f'{port}/{protocol}']).success:
|
|
159
|
+
Cmd(['firewall-cmd', '--reload']).run()
|
|
160
|
+
return True
|
|
134
161
|
|
|
135
162
|
elif firewall == 'iptables':
|
|
136
|
-
Cmd(['iptables', '-D', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT']).
|
|
137
|
-
|
|
163
|
+
if Cmd(['iptables', '-D', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT']).success:
|
|
164
|
+
Cmd(['service', 'iptables', 'save']).run()
|
|
165
|
+
return True
|
|
138
166
|
|
|
139
167
|
else:
|
|
140
|
-
|
|
168
|
+
logging.error('No supported firewall found on the system.')
|
|
169
|
+
|
|
170
|
+
return False
|
|
141
171
|
|
|
142
172
|
@classmethod
|
|
143
173
|
def is_global_open(cls, port: int, protocol: str = 'tcp') -> bool:
|
|
@@ -163,7 +193,8 @@ class Firewall:
|
|
|
163
193
|
ufw_check = Cmd(['ufw', 'status'])
|
|
164
194
|
ufw_check.is_memory_cacheable(3)
|
|
165
195
|
result = ufw_check.text
|
|
166
|
-
|
|
196
|
+
# UFW requires the protocol to be all lowercase.
|
|
197
|
+
port_proto = f"{port}/{protocol}".lower()
|
|
167
198
|
for line in result.splitlines():
|
|
168
199
|
if port_proto in line and "ALLOW" in line and ("Anywhere" in line or "Anywhere (v6)" in line):
|
|
169
200
|
return True
|
|
@@ -188,4 +219,5 @@ class Firewall:
|
|
|
188
219
|
return False
|
|
189
220
|
|
|
190
221
|
else:
|
|
191
|
-
|
|
222
|
+
logging.error('No supported firewall found on the system.')
|
|
223
|
+
return True # No firewall means it's probably enabled by default, so we return true.
|
|
@@ -2,8 +2,10 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import pwd
|
|
4
4
|
import sys
|
|
5
|
+
from typing_extensions import deprecated
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
@deprecated('Please use utils.get_base_directory instead to avoid confusion')
|
|
7
9
|
def get_app_directory() -> str:
|
|
8
10
|
"""
|
|
9
11
|
Get the base directory for this game installation.
|
|
@@ -15,6 +17,17 @@ def get_app_directory() -> str:
|
|
|
15
17
|
return os.path.dirname(os.path.realpath(sys.argv[0]))
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
def get_base_directory() -> str:
|
|
21
|
+
"""
|
|
22
|
+
Get the base directory for this game installation.
|
|
23
|
+
|
|
24
|
+
This directory usually will contain manage.py, AppFiles, Backups, and other related files.
|
|
25
|
+
|
|
26
|
+
:return:
|
|
27
|
+
"""
|
|
28
|
+
return os.path.dirname(os.path.realpath(sys.argv[0]))
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
def get_home_directory() -> str:
|
|
19
32
|
"""
|
|
20
33
|
Get the home directory of the user running this application
|
|
@@ -195,7 +195,7 @@ class BaseMod:
|
|
|
195
195
|
logging.error('Mod install package not found!')
|
|
196
196
|
return False
|
|
197
197
|
|
|
198
|
-
target_archive = os.path.join(utils.
|
|
198
|
+
target_archive = os.path.join(utils.get_base_directory(), 'Packages', self.package)
|
|
199
199
|
if not os.path.exists(target_archive):
|
|
200
200
|
download_file(self.source, target_archive)
|
|
201
201
|
else:
|
|
@@ -242,7 +242,7 @@ class BaseMod:
|
|
|
242
242
|
Get all registered mods, eg all mods which are present in the registration file
|
|
243
243
|
:return:
|
|
244
244
|
"""
|
|
245
|
-
mods_path = os.path.join(utils.
|
|
245
|
+
mods_path = os.path.join(utils.get_base_directory(), 'Packages', 'mods.json')
|
|
246
246
|
if not os.path.exists(mods_path):
|
|
247
247
|
# No mods installed; mods cache is empty.
|
|
248
248
|
return []
|
|
@@ -264,7 +264,11 @@ class BaseMod:
|
|
|
264
264
|
:param mods:
|
|
265
265
|
:return:
|
|
266
266
|
"""
|
|
267
|
-
|
|
267
|
+
mods_directory = os.path.join(utils.get_base_directory(), 'Packages')
|
|
268
|
+
if not os.path.exists(mods_directory):
|
|
269
|
+
utils.makedirs(mods_directory)
|
|
270
|
+
|
|
271
|
+
mods_path = os.path.join(utils.get_base_directory(), 'Packages', 'mods.json')
|
|
268
272
|
|
|
269
273
|
flat_mods = []
|
|
270
274
|
for mod in mods:
|
|
@@ -275,6 +275,12 @@ class Nexus:
|
|
|
275
275
|
'X-Host-Token': self.host_auth,
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
if self.game is None:
|
|
279
|
+
return {
|
|
280
|
+
'success': False,
|
|
281
|
+
'message': 'Game not set',
|
|
282
|
+
}
|
|
283
|
+
|
|
278
284
|
try:
|
|
279
285
|
url = self.base_url + '/mod/search/' + self.game
|
|
280
286
|
params = {
|
|
@@ -306,6 +312,12 @@ class Nexus:
|
|
|
306
312
|
'X-Host-Token': self.host_auth,
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
if self.game is None:
|
|
316
|
+
return {
|
|
317
|
+
'success': False,
|
|
318
|
+
'message': 'Game not set',
|
|
319
|
+
}
|
|
320
|
+
|
|
309
321
|
try:
|
|
310
322
|
url = self.base_url + '/mod/get/' + self.game + '/' + str(provider) + '/' + str(mod_id)
|
|
311
323
|
params = {}
|
|
@@ -49,7 +49,7 @@ class BaseService(ABC):
|
|
|
49
49
|
used for checking existence and loading configuration options from the file
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
self._env_file = os.path.join(utils.
|
|
52
|
+
self._env_file = os.path.join(utils.get_base_directory(), 'Environments', '%s.env' % service)
|
|
53
53
|
"""
|
|
54
54
|
:type str:
|
|
55
55
|
Fully resolved path on the filesystem for the environmental variable for this service
|
|
@@ -1274,13 +1274,23 @@ class BaseService(ABC):
|
|
|
1274
1274
|
continue
|
|
1275
1275
|
|
|
1276
1276
|
port = self.get_option_value(port_config[0])
|
|
1277
|
+
port_default = self.get_option_default(port_config[0])
|
|
1277
1278
|
if port == 0:
|
|
1278
1279
|
# This port does not have a default value, probably not enabled by default.
|
|
1279
1280
|
continue
|
|
1280
1281
|
new_port = self.game.get_next_available_port(self, port, port_config[1])
|
|
1281
1282
|
|
|
1283
|
+
if port_default == new_port:
|
|
1284
|
+
# New installations where the default port may not trigger the 'change' logic
|
|
1285
|
+
# as the port _technically_ didn't change, therefore the firewall rules won't be added.
|
|
1286
|
+
logging.info('Setting %s to 0 to force firewall change' % port_config[0])
|
|
1287
|
+
self.set_option(port_config[0], 0)
|
|
1288
|
+
|
|
1282
1289
|
self.set_option(port_config[0], new_port)
|
|
1283
|
-
|
|
1290
|
+
if new_port != port:
|
|
1291
|
+
logging.info('Set %s to %s to try to avoid conflicts' % (port_config[0], new_port))
|
|
1292
|
+
else:
|
|
1293
|
+
logging.info('Set %s to %s' % (port_config[0], new_port))
|
|
1284
1294
|
|
|
1285
1295
|
# Reload systemd to pick up the new service
|
|
1286
1296
|
self.reload()
|
|
@@ -1715,14 +1725,14 @@ class BaseService(ABC):
|
|
|
1715
1725
|
utils.makedirs(target_file)
|
|
1716
1726
|
elif source == '@':
|
|
1717
1727
|
# Source is the package itself; this just copies the entire mod into the destination.
|
|
1718
|
-
source_file = os.path.join(utils.
|
|
1728
|
+
source_file = os.path.join(utils.get_base_directory(), 'Packages', mod.package)
|
|
1719
1729
|
logging.info('Copying %s -> %s' % (source_file, target_file))
|
|
1720
1730
|
shutil.copy(source_file, target_file)
|
|
1721
1731
|
utils.ensure_file_ownership(target_file)
|
|
1722
1732
|
elif source.startswith('@:'):
|
|
1723
1733
|
# Source is a file within the package, (usually a ZIP archive)
|
|
1724
1734
|
source_file = source[2:]
|
|
1725
|
-
source_archive = os.path.join(utils.
|
|
1735
|
+
source_archive = os.path.join(utils.get_base_directory(), 'Packages', mod.package)
|
|
1726
1736
|
if source_archive.endswith('.zip'):
|
|
1727
1737
|
logging.info('Extracting %s -> %s' % (source_file, target_file))
|
|
1728
1738
|
with ZipFile(source_archive, 'r') as zip_ref:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_config_ark_spawn_entities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/sensitive_data_filter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|