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.
Files changed (63) hide show
  1. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/PKG-INFO +1 -1
  2. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/pyproject.toml +1 -1
  3. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_base_service.py +1 -1
  4. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_cmd.py +29 -2
  5. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_config.py +81 -15
  6. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/base_app.py +8 -9
  7. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/steam_app.py +2 -2
  8. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/unreal_config.py +73 -57
  9. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/formatters/cli_formatter.py +42 -0
  10. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/cache.py +3 -3
  11. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/cmd.py +72 -14
  12. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/firewall.py +48 -16
  13. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/utils.py +13 -0
  14. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/mods/base_mod.py +7 -3
  15. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/nexus/nexus.py +12 -0
  16. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/base_service.py +14 -4
  17. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/PKG-INFO +1 -1
  18. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/LICENSE +0 -0
  19. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/README.md +0 -0
  20. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/setup.cfg +0 -0
  21. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_app.py +0 -0
  22. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_base_config.py +0 -0
  23. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_cli_config.py +0 -0
  24. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_cli_formatter.py +0 -0
  25. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_config_key.py +0 -0
  26. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_ini_config.py +0 -0
  27. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_sensitive_data_filter.py +0 -0
  28. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_socket_service.py +0 -0
  29. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_config_ark_spawn_entities.py +0 -0
  30. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_unreal_save.py +0 -0
  31. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/tests/test_version.py +0 -0
  32. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/__init__.py +0 -0
  33. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/__init__.py +0 -0
  34. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/apps/manual_app.py +0 -0
  35. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/__init__.py +0 -0
  36. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/base_config.py +0 -0
  37. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/cli_config.py +0 -0
  38. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/config_key.py +0 -0
  39. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/ini_config.py +0 -0
  40. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/json_config.py +0 -0
  41. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/config/properties_config.py +0 -0
  42. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/formatters/__init__.py +0 -0
  43. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/__init__.py +0 -0
  44. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/app.py +0 -0
  45. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/app_runner.py +0 -0
  46. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/download.py +0 -0
  47. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/get_wan_ip.py +0 -0
  48. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/java.py +0 -0
  49. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/meta.py +0 -0
  50. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/ports.py +0 -0
  51. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/sensitive_data_filter.py +0 -0
  52. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/tui.py +0 -0
  53. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/libs/version.py +0 -0
  54. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/mods/__init__.py +0 -0
  55. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/mods/warlock_nexus_mod.py +0 -0
  56. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/__init__.py +0 -0
  57. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/http_service.py +0 -0
  58. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/rcon_service.py +0 -0
  59. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager/services/socket_service.py +0 -0
  60. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/SOURCES.txt +0 -0
  61. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/dependency_links.txt +0 -0
  62. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/requires.txt +0 -0
  63. {warlock_manager-2.2.4 → warlock_manager-2.2.6}/warlock_manager.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warlock-manager
3
- Version: 2.2.4
3
+ Version: 2.2.6
4
4
  Summary: Dependency library for game-server management applications.
5
5
  Author-email: Charlie Powell <cdp1337@bitsnbytes.dev>
6
6
  Maintainer-email: Charlie Powell <cdp1337@bitsnbytes.dev>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "warlock-manager"
7
- version = "2.2.4"
7
+ version = "2.2.6"
8
8
  description = "Dependency library for game-server management applications."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.get_app_directory(), svc.service), data_new)
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 test_exists_true(self):
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': 'Key1',
26
+ 'name': 'Key 1',
27
27
  'section': 'SomeSection',
28
28
  'key': 'Key1',
29
29
  })
30
30
  cfg.add_option({
31
- 'name': 'Key2',
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': 'Key3',
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('Key1'), 'Value1')
45
- self.assertEqual(cfg.get_value('Key2'), 42)
46
- self.assertEqual(cfg.get_value('Key3'), True)
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('Key1'))
50
- self.assertTrue(cfg.has_value('Key2'))
51
- self.assertTrue(cfg.has_value('Key3'))
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('Key1', 'NewValue')
62
- self.assertEqual(cfg.get_value('Key1'), 'NewValue')
61
+ cfg.set_value('Key 1', 'NewValue')
62
+ self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
63
63
 
64
- cfg.set_value('Key2', 100)
65
- self.assertEqual(cfg.get_value('Key2'), 100)
64
+ cfg.set_value('Key 2', 100)
65
+ self.assertEqual(cfg.get_value('Key 2'), 100)
66
66
 
67
- cfg.set_value('Key3', False)
68
- self.assertEqual(cfg.get_value('Key3'), False)
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 get_app_directory() from utils instead.")
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.get_app_directory()
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.get_app_directory(), 'AppFiles'))
667
- shutil.rmtree(os.path.join(utils.get_app_directory(), 'Environments'))
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.get_app_directory(), 'Environments')
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.get_app_directory(), 'Backups'))
728
- utils.makedirs(os.path.join(utils.get_app_directory(), 'AppFiles'))
729
- utils.makedirs(os.path.join(utils.get_app_directory(), 'Environments'))
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.get_app_directory(), 'AppFiles', 'steamapps', 'appmanifest_%s.acf' % self.steam_id)
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.get_app_directory(), 'AppFiles'),
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 opt.section not in self._values:
33
- val = opt.default
86
+ raw_value = self._get_raw_value(name)
87
+ if raw_value is None:
88
+ return opt.default
34
89
  else:
35
- if '/' in opt.key:
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
- if opt.section not in self._values:
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
- if name not in self.options:
171
- return False
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.get_app_directory(), '.cache', cmd_hash)
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.get_app_directory(), '.cache')
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.get_app_directory(), '.cache', cmd_hash)
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
- def sudo(self, runas: str | int):
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
- if os.getlogin() == runas:
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) -> 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.run()
111
+ return cmd.success
100
112
 
101
113
  elif firewall == 'firewalld':
102
- Cmd(['firewall-cmd', '--permanent', '--add-port', f'{port}/{protocol}']).run()
103
- Cmd(['firewall-cmd', '--reload']).run()
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.run()
110
- Cmd(['service', 'iptables', 'save']).run()
124
+ if cmd.success:
125
+ Cmd(['service', 'iptables', 'save']).run()
126
+ return True
111
127
 
112
128
  else:
113
- raise OSError("No supported firewall found on the system.")
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') -> None:
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}']).run()
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}']).run()
133
- Cmd(['firewall-cmd', '--reload']).run()
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']).run()
137
- Cmd(['service', 'iptables', 'save']).run()
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
- raise OSError("No supported firewall found on the system.")
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
- port_proto = f"{port}/{protocol}"
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
- raise OSError("No supported firewall found on the system.")
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.get_app_directory(), 'Packages', self.package)
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.get_app_directory(), 'Packages', 'mods.json')
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
- mods_path = os.path.join(utils.get_app_directory(), 'Packages', 'mods.json')
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.get_app_directory(), 'Environments', '%s.env' % service)
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
- logging.info('Set %s to %s to try to avoid conflicts' % (port_config[0], new_port))
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.get_app_directory(), 'Packages', mod.package)
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.get_app_directory(), 'Packages', mod.package)
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warlock-manager
3
- Version: 2.2.4
3
+ Version: 2.2.6
4
4
  Summary: Dependency library for game-server management applications.
5
5
  Author-email: Charlie Powell <cdp1337@bitsnbytes.dev>
6
6
  Maintainer-email: Charlie Powell <cdp1337@bitsnbytes.dev>
File without changes