-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfigmanager.py
356 lines (285 loc) · 14.1 KB
/
configmanager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
"""
a:zak-45
d:20/12/2024
v:1.0.0
Manages configuration settings for the application across different environments.
"""
import os
import logging
import subprocess
import sys
import cfg_load as app_cfg
import concurrent_log_handler
import psutil
import contextlib
from logging import config
from str2bool import str2bool
def root_path(filename):
"""
Determines the root path of the application based on whether it's running from a compiled binary or in development mode.
Returns the appropriate root path for accessing application resources, handling different OS structures.
Args:
filename (str): The name of the file or directory relative to the application's root.
Returns:
str: The absolute path to the specified file or directory.
Examples:
>> root_path('data/config.ini')
'/path/to/app/data/config.ini'
Handles different execution environments (compiled vs. development) to ensure consistent resource access.
"""
if getattr(sys, 'frozen', False): # Running from a compiled binary (Nuitka, PyInstaller)
if sys.platform == "darwin": # macOS
base_path = os.path.dirname(os.path.dirname(sys.argv[0])) # Contents/
return os.path.join(base_path, "MacOS", filename)
else: # Windows/Linux (Nuitka puts files in the same dir as the binary)
base_path = os.path.dirname(sys.argv[0])
return os.path.join(base_path, filename)
# Running in development mode (not compiled)
return os.path.join(os.path.dirname(sys.argv[0]),filename)
def count_processes_by_name(name):
"""
Counts the number of running processes with a given name.
Iterates through all active processes and checks if their name matches the provided name.
Args:
name (str): The name of the process to count.
Returns:
int: The number of processes with the specified name.
Examples:
>> count = count_processes_by_name('chrome')
>> print(f"Number of Chrome processes: {count}")
Provides a utility function to monitor the number of instances of a specific process running on the system.
"""
return sum(
name in proc.info['name']
for proc in psutil.process_iter(attrs=['name'])
if proc.info['name'] # Avoid NoneType errors
)
def run_window_msg(msg: str = '', msg_type: str = 'info'):
"""
Displays a custom message using an external info window executable across different platforms.
Launches a separate process to show an error or informational message in a platform-specific manner.
Args:
msg (str): The message text to be displayed.
msg_type (str, optional): The type of message, defaults to 'info'.
Can specify message type like 'info' or 'error'. If error, bg is 'red'
Examples:
>> run_window_msg("Operation completed successfully")
>> run_window_msg("Error occurred", msg_type='error')
In case of error, just bypass.
"""
# window_info full path
absolute_file_name = info_window_exe_name()
# number of already running processes
process_count = count_processes_by_name("info_window")
# in case of many errors, we stop display error window after 15 processes found
# safeguard to not take all OS resources
if process_count < 15:
with contextlib.suppress(Exception):
command = (
[absolute_file_name, msg, msg_type]
if sys.platform.lower() == "win32"
else [absolute_file_name, msg, msg_type, '&']
)
# Call the separate script to show the error/info message in a Tkinter window
subprocess.Popen(command)
def info_window_exe_name():
"""
Determines the appropriate executable name for displaying information windows based on the current operating system.
Returns the platform-specific executable path for the info window utility.
Returns:
str: The filename of the info window executable for the current platform.
Returns None if the platform is not recognized.
Examples:
>> info_window_exe_name()
'xtra/info_window.exe' # On Windows
>> info_window_exe_name()
'xtra/info_window.bin' # On Linux
"""
if sys.platform.lower() == 'win32':
return ConfigManager.app_root_path('xtra/info_window.exe')
elif sys.platform.lower() == 'linux':
return ConfigManager.app_root_path('xtra/info_window')
elif sys.platform.lower() == 'darwin':
return ConfigManager.app_root_path('xtra/info_window')
else:
return None
class CustomLogger(logging.Logger):
"""
A custom logging class that extends the standard Python Logger to display error messages
in a custom window before logging. Enhances standard error logging by adding a visual notification mechanism.
The CustomLogger overrides the standard error logging method to first display an error message
in a separate window using run_window_msg(), and then proceeds with standard error logging.
This provides an additional layer of user notification for critical log events.
Methods:
error: Overrides the standard error logging method to display a custom error message before logging.
Examples:
>> logger = CustomLogger('my_logger')
>> logger.error('Critical system failure') # Displays error in custom window and logs
"""
def error(self, msg, *args, **kwargs):
# Custom action before logging the error
run_window_msg(str(msg), 'error')
super().error(msg, *args, **kwargs)
class ConfigManager:
"""
Manages configuration settings for the application across different environments.
This class handles configuration initialization, logging setup, and provides access to various configuration sections.
Attributes:
logger: Configured logger instance for the application.
server_config: Configuration settings related to server parameters.
app_config: General application configuration settings.
color_config: Color-related configuration settings.
custom_config: Custom configuration parameters.
logging_config_path: Path to the logging configuration file.
logger_name: Name of the logger to be used.
The configuration management supports both standard and one-file executable environments,
ensuring flexible configuration loading and logging setup.
# Usage
config_manager = ConfigManager(logging_config_path='path/to/logging.ini', logger_name='CustomLoggerName')
"""
def __init__(self, config_file='config/WLEDVideoSync.ini', logging_config_path='config/logging.ini', logger_name='WLEDLogger'):
"""
Initializes the configuration manager with default or specified logging configuration settings.
Sets up initial configuration attributes and prepares for configuration loading.
Args:
logging_config_path (str, optional): Path to the logging configuration file. Defaults to 'config/logging.ini'.
logger_name (str, optional): Name of the logger to be used. Defaults to 'WLEDLogger'.
The method initializes configuration-related attributes to None and sets the provided logging configuration path
and logger name. It then calls the initialize method to set up the configuration based on the current environment.
"""
self.logger = None
self.server_config = None
self.app_config = None
self.color_config = None
self.custom_config = None
self.preset_config = None
self.desktop_config = None
self.ws_config = None
self.text_config = None
self.logging_config_path = self.app_root_path(logging_config_path)
self.config_file = self.app_root_path(config_file)
self.logger_name = logger_name
self.initialize()
@staticmethod
def app_root_path(file):
"""
Provides a static method to access the root path of the application.
This method simply calls the root_path function to determine the application's root directory.
Args:
file (str): The name of the file or directory relative to the application's root.
Returns:
str: The absolute path to the specified file or directory.
Examples:
>> ConfigManager.app_root_path('data/config.ini')
'/path/to/app/data/config.ini'
This static method provides a convenient way to access the root_path functionality within the ConfigManager class.
"""
return root_path(file)
def initialize(self):
"""
When this env var exists, this means running from the one-file compressed executable.
This env does not exist when running from the extracted program.
Expected way to work.
We initialize only if running from the main compiled program.
"""
if "NUITKA_ONEFILE_PARENT" not in os.environ:
self.setup()
def setup(self):
"""
Configures the logging system and loads various configuration sections for the application.
This method sets up the logger and populates configuration attributes with settings from the configuration file.
The method performs the following key actions:
- Initializes the logger using the specified configuration path and logger name
- Reads the configuration file
- Assigns configuration sections to specific attributes for easy access
- Provides a centralized method for setting up application configurations
The configuration sections include server settings, application parameters, color configurations,
and custom settings, making them readily available throughout the application.
"""
# read config
# load config file
cast_config =self.read_config()
if cast_config is not None:
# config keys
self.server_config = cast_config[0] # server key
self.app_config = cast_config[1] # app key
self.color_config = cast_config[2] # colors key
self.custom_config = cast_config[3] # custom key
self.preset_config = cast_config[4] # presets key
self.desktop_config = cast_config[5] # desktop key
self.ws_config = cast_config[6] # websocket key
self.text_config = cast_config[7] # text anim key
else:
if self.logger is not None:
self.logger.warning('Config file not found')
else:
print('Config file not found')
# create logger
self.logger = self.setup_logging()
def setup_logging(self):
"""
Sets up the logging system for the application based on a configuration file or default settings.
Configures logging handlers and formatters according to the specified configuration file, or falls back to basic
configuration if the file is not found.
Returns:
logging.Logger: The configured logger instance.
Examples:
>> logger = config_manager.setup_logging()
>> logger.info('Application started')
Ensures that logging is properly configured, even if the specified configuration file is missing.
"""
# Set the custom logger class
logging.setLoggerClass(CustomLogger)
# read the config file
if os.path.exists(self.logging_config_path):
logging.config.fileConfig(self.logging_config_path, disable_existing_loggers=False)
# trick: use the same name for all modules, ui.log will receive message from alls
if str2bool(self.app_config['log_to_main']):
logger = logging.getLogger('WLEDLogger')
else:
logger = logging.getLogger(self.logger_name)
# take basename from config file and add root_path + log ( from the config file we want only the name )
# handler[0] should be stdout, handler[1] should be ConcurrentRotatingFileHandler
logger.handlers[1].baseFilename=self.app_root_path(f"log/{os.path.basename(logger.handlers[1].baseFilename)}")
logger.handlers[1].lockFilename=self.app_root_path(f"log/{os.path.basename(logger.handlers[1].lockFilename)}")
logger.debug(f"Logging configured using {self.logging_config_path} for {self.logger_name}")
else:
# if not found config, set default param
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(self.logger_name)
logger.warning(f"Logging config file {self.logging_config_path} not found. Using basic configuration.")
return logger
def read_config(self):
"""
Reads and parses the application's configuration file.
Loads configuration data from the specified INI file and organizes it into separate dictionaries for different
configuration sections.
Returns:
tuple: A tuple containing dictionaries for server, app, colors, custom, presets, desktop, and websocket configurations.
Returns None if the configuration file cannot be loaded or parsed.
Examples:
>> server_config, app_config, ..., ws_config = config_manager.read_config()
Handles potential exceptions during file loading and returns None if any error occurs.
"""
# load config file
try:
cast_config = app_cfg.load(self.config_file)
# config keys
server_config = cast_config.get('server')
app_config = cast_config.get('app')
colors_config = cast_config.get('colors')
custom_config = cast_config.get('custom')
preset_config = cast_config.get('presets')
desktop_config = cast_config.get('desktop')
ws_config = cast_config.get('ws')
text_config = cast_config.get('text')
return (server_config,
app_config,
colors_config,
custom_config,
preset_config,
desktop_config,
ws_config,
text_config)
except Exception:
return None