atomicshop 3.2.13__py3-none-any.whl → 3.3.0__py3-none-any.whl

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.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

@@ -1,9 +1,8 @@
1
- import os.path
1
+ import multiprocessing
2
2
  import threading
3
3
  import select
4
4
  from typing import Literal, Union
5
5
  from pathlib import Path
6
- import logging
7
6
  import socket
8
7
 
9
8
  from ...mitm import initialize_engines
@@ -12,7 +11,7 @@ from ..certauthw import certauthw
12
11
  from ..loggingw import loggingw
13
12
  from ...script_as_string_processor import ScriptAsStringProcessor
14
13
  from ...permissions import permissions
15
- from ... import queues, filesystem, certificates
14
+ from ... import filesystem, certificates
16
15
  from ...basics import booleans
17
16
  from ...print_api import print_api
18
17
 
@@ -27,12 +26,17 @@ class SocketWrapperConfigurationValuesError(Exception):
27
26
  pass
28
27
 
29
28
 
30
- SNI_QUEUE = queues.NonBlockQueue()
29
+ # from ... import queues
30
+ # SNI_QUEUE = queues.NonBlockQueue()
31
+ LOGS_DIRECTORY_NAME: str = 'logs'
31
32
 
32
33
 
33
34
  class SocketWrapper:
34
35
  def __init__(
35
36
  self,
37
+ ip_address: str,
38
+ port: int,
39
+ engine: initialize_engines.ModuleCategory = None,
36
40
  forwarding_dns_service_ipv4_list___only_for_localhost: list = None,
37
41
  ca_certificate_name: str = None,
38
42
  ca_certificate_filepath: str = None,
@@ -65,9 +69,13 @@ class SocketWrapper:
65
69
  ],
66
70
  None
67
71
  ] = None,
68
- logger: logging.Logger = None,
69
- exceptions_logger: loggingw.ExceptionCsvLogger = None,
70
- statistics_logs_directory: str = None,
72
+ logs_directory: str = None,
73
+ logger_name: str = 'SocketWrapper',
74
+ logger_queue: multiprocessing.Queue = None,
75
+ statistics_logger_name: str = 'statistics',
76
+ statistics_logger_queue: multiprocessing.Queue = None,
77
+ exceptions_logger_name: str = 'SocketWrapperExceptions',
78
+ exceptions_logger_queue: multiprocessing.Queue = None,
71
79
  no_engine_usage_enable: bool = False,
72
80
  no_engines_listening_address_list: list[str] = None,
73
81
  engines_list: list[initialize_engines.ModuleCategory] = None
@@ -145,21 +153,24 @@ class SocketWrapper:
145
153
  :param ssh_user: string, SSH username that will be used to connect to remote host.
146
154
  :param ssh_pass: string, SSH password that will be used to connect to remote host.
147
155
  :param ssh_script_to_execute: string, script that will be executed to get the process name on ssh remote host.
148
- :param logger: logging.Logger object, logger object that will be used to log messages.
149
- If not provided, logger will be created with default settings saving logs to the
150
- 'statistics_logs_directory'.
151
- :param exceptions_logger: loggingw.ExceptionCsvLogger object, logger object that will be used to log exceptions.
152
- If not provided, logger will be created with default settings and will save exceptions to the
153
- 'statistics_logs_directory'.
154
- :param statistics_logs_directory: string, path to directory where daily statistics.csv files will be stored.
155
- After you initialize the SocketWrapper object, you can get the statistics_writer object from it and use it
156
- to write statistics to the file in a worker thread.
156
+ :param logs_directory: string, path to directory where daily statistics.csv files and all the other logger
157
+ files will be stored. After you initialize the SocketWrapper object, you can get the statistics_writer
158
+ object from it and use it to write statistics to the file in a worker thread.
157
159
 
158
160
  socket_wrapper_instance = SocketWrapper(...)
159
161
  statistics_writer = socket_wrapper_instance.statistics_writer
160
162
 
161
163
  statistics_writer: statistics_csv.StatisticsCSVWriter object, there is a logger object that
162
164
  will be used to write the statistics file.
165
+ :param logger_name: string, name of the logger that will be used to log messages.
166
+ :param logger_queue: multiprocessing.Queue, queue that will be used to log messages in multiprocessing.
167
+ You need to start the logger listener in the main process to handle the queue.
168
+ :param statistics_logger_name: string, name of the logger that will be used to log statistics.
169
+ :param statistics_logger_queue: multiprocessing.Queue, queue that will be used to log statistics in
170
+ multiprocessing. You need to start the logger listener in the main process to handle the queue.
171
+ :param exceptions_logger_name: string, name of the logger that will be used to log exceptions.
172
+ :param exceptions_logger_queue: multiprocessing.Queue, queue that will be used to log exceptions in
173
+ multiprocessing. You need to start the logger listener in the main process to handle the queue.
163
174
  :param no_engine_usage_enable: boolean, if True, 'engines_list' will be used to listen on the addresses,
164
175
  but the "no_engines_listening_address_list" parameter will be used instead.
165
176
  :param no_engines_listening_address_list: list, of ips+ports that will be listened on.
@@ -178,6 +189,9 @@ class SocketWrapper:
178
189
  #"domain" = "example.com"
179
190
  """
180
191
 
192
+ self.ip_address: str = ip_address
193
+ self.port: int = port
194
+ self.engine: initialize_engines.ModuleCategory = engine
181
195
  self.ca_certificate_name: str = ca_certificate_name
182
196
  self.ca_certificate_filepath: str = ca_certificate_filepath
183
197
  self.ca_certificate_crt_filepath: str = ca_certificate_crt_filepath
@@ -205,13 +219,11 @@ class SocketWrapper:
205
219
  self.ssh_user: str = ssh_user
206
220
  self.ssh_pass: str = ssh_pass
207
221
  self.ssh_script_to_execute = ssh_script_to_execute
208
- self.logger = logger
209
- self.statistics_logs_directory: str = statistics_logs_directory
210
222
  self.forwarding_dns_service_ipv4_list___only_for_localhost = (
211
223
  forwarding_dns_service_ipv4_list___only_for_localhost)
212
- self.no_engine_usage_enable: bool = no_engine_usage_enable
213
- self.no_engines_listening_address_list: list[str] = no_engines_listening_address_list
214
- self.engines_list: list[initialize_engines.ModuleCategory] = engines_list
224
+ # self.no_engine_usage_enable: bool = no_engine_usage_enable
225
+ # self.no_engines_listening_address_list: list[str] = no_engines_listening_address_list
226
+ # self.engines_list: list[initialize_engines.ModuleCategory] = engines_list
215
227
 
216
228
  self.socket_object = None
217
229
 
@@ -236,26 +248,53 @@ class SocketWrapper:
236
248
  self.ssh_script_processor = \
237
249
  ScriptAsStringProcessor().read_script_to_string(self.ssh_script_to_execute)
238
250
 
239
- self.statistics_writer = statistics_csv.StatisticsCSVWriter(
240
- statistics_directory_path=self.statistics_logs_directory)
241
-
242
- if not self.logger:
243
- self.logger = loggingw.create_logger(
244
- logger_name='SocketWrapper',
245
- directory_path=self.statistics_logs_directory,
251
+ # If logs directory was not set, we will use the working directory.
252
+ if not logs_directory:
253
+ logs_directory = str(Path.cwd() / LOGS_DIRECTORY_NAME)
254
+ self.logs_directory: str = logs_directory
255
+
256
+ if not logger_name:
257
+ logger_name = 'SocketWrapper'
258
+ self.logger_name: str = logger_name
259
+ self.logger_name_listener: str = f"{logger_name}.listener"
260
+
261
+ if loggingw.is_logger_exists(self.logger_name_listener):
262
+ self.logger = loggingw.get_logger_with_level(self.logger_name_listener)
263
+ elif not logger_queue:
264
+ _ = loggingw.create_logger(
265
+ logger_name=logger_name,
266
+ directory_path=self.logs_directory,
246
267
  add_stream=True,
247
268
  add_timedfile_with_internal_queue=True,
248
269
  formatter_streamhandler='DEFAULT',
249
270
  formatter_filehandler='DEFAULT'
250
271
  )
251
272
 
252
- if not exceptions_logger:
253
- self.exceptions_logger = loggingw.ExceptionCsvLogger(
254
- logger_name='SocketWrapperExceptions',
255
- directory_path=self.statistics_logs_directory
256
- )
273
+ self.logger = loggingw.get_logger_with_level(self.logger_name_listener)
257
274
  else:
258
- self.exceptions_logger = exceptions_logger
275
+ _ = loggingw.create_logger(
276
+ logger_name=logger_name,
277
+ add_queue_handler=True,
278
+ log_queue=logger_queue
279
+ )
280
+ self.logger = loggingw.get_logger_with_level(self.logger_name_listener)
281
+
282
+ self.statistics_writer = statistics_csv.StatisticsCSVWriter(
283
+ logger_name=statistics_logger_name,
284
+ directory_path=self.logs_directory,
285
+ log_queue=statistics_logger_queue,
286
+ add_queue_handler_no_listener_multiprocessing=True
287
+ )
288
+
289
+ if not exceptions_logger_name:
290
+ exceptions_logger_name = 'SocketWrapperExceptions'
291
+
292
+ self.exceptions_logger = loggingw.ExceptionCsvLogger(
293
+ logger_name=exceptions_logger_name,
294
+ directory_path=self.logs_directory,
295
+ log_queue=exceptions_logger_queue,
296
+ add_queue_handler_no_listener_multiprocessing=True
297
+ )
259
298
 
260
299
  self.test_config()
261
300
 
@@ -274,12 +313,12 @@ class SocketWrapper:
274
313
  "You can't set both [sni_use_default_callback_function = True] and [sni_custom_callback_function]."
275
314
  raise SocketWrapperConfigurationValuesError(message)
276
315
 
277
- if self.no_engine_usage_enable and not self.no_engines_listening_address_list:
278
- message = "You set [no_engine_usage_enable = True], but you didn't set [no_engines_listening_address_list]."
279
- raise SocketWrapperConfigurationValuesError(message)
280
- elif not self.no_engine_usage_enable and not self.engines_list:
281
- message = "You set [no_engine_usage_enable = False], but you didn't set [engines_list]."
282
- raise SocketWrapperConfigurationValuesError(message)
316
+ # if self.no_engine_usage_enable and not self.no_engines_listening_address_list:
317
+ # message = "You set [no_engine_usage_enable = True], but you didn't set [no_engines_listening_address_list]."
318
+ # raise SocketWrapperConfigurationValuesError(message)
319
+ # elif not self.no_engine_usage_enable and not self.engines_list:
320
+ # message = "You set [no_engine_usage_enable = False], but you didn't set [engines_list]."
321
+ # raise SocketWrapperConfigurationValuesError(message)
283
322
 
284
323
  try:
285
324
  booleans.is_only_1_true_in_list(
@@ -329,15 +368,7 @@ class SocketWrapper:
329
368
  return 1
330
369
 
331
370
  # Checking if listening address is in use.
332
- listening_check_list: list = list()
333
- if self.engines_list:
334
- for engine in self.engines_list:
335
- for _, ip_port_dict in engine.domain_target_dict.items():
336
- address: str = f"{ip_port_dict['ip']}:{ip_port_dict['port']}"
337
- if address not in listening_check_list:
338
- listening_check_list.append(address)
339
- else:
340
- listening_check_list = self.no_engines_listening_address_list
371
+ listening_check_list = [f"{self.ip_address}:{self.port}"]
341
372
  port_in_use = psutil_networks.get_processes_using_port_list(listening_check_list)
342
373
  if port_in_use:
343
374
  error_messages: list = list()
@@ -415,64 +446,40 @@ class SocketWrapper:
415
446
 
416
447
  return self.socket_object
417
448
 
418
- def start_listening_sockets(
449
+ def start_listening_socket(
419
450
  self,
420
- reference_function_name,
421
- reference_function_args=(),
422
- pass_function_reference_to_thread: bool = True
451
+ callable_function: callable,
452
+ callable_args: tuple = ()
423
453
  ):
424
454
  """
425
- Start listening sockets with parameters.
455
+ Start listening on a single socket with given IP address and port.
456
+ This function is used to start listening on a single socket, for example, when you want to listen on a specific
457
+ IP address and port.
458
+
459
+ :param callable_function: callable, function that you want to execute when client
460
+ socket received by 'accept()' and connection has been made.
461
+ :param callable_args: tuple, that will be passed to 'callable_function' when it will be called.
462
+ :return: None
426
463
  """
427
464
 
428
- # If engines were passed, we will use the listening addresses from the engines.
429
- if not self.no_engine_usage_enable:
430
- for engine in self.engines_list:
431
- # Combine the domain and port dicts.
432
- connection_dict: dict = {**engine.domain_target_dict, **engine.port_target_dict}
433
-
434
- # Start all the regular listening interfaces.
435
- for domain_or_port, ip_port_dict in connection_dict.items():
436
- ip_address: str = ip_port_dict['ip']
437
- port = int(ip_port_dict['port'])
438
- socket_by_port = self.create_socket_ipv4_tcp(ip_address, port)
439
- threading.Thread(
440
- target=self.listening_socket_loop,
441
- args=(socket_by_port, engine, reference_function_name,
442
- reference_function_args, pass_function_reference_to_thread),
443
- name=f"acceptor-{engine.engine_name}-{ip_address}:{port}",
444
- daemon=True
445
- ).start()
465
+ if self.engine:
466
+ acceptor_name: str = f"acceptor-{self.engine.engine_name}-{self.ip_address}:{self.port}"
446
467
  else:
447
- # If no engines were passed, we will use the listening addresses from the configuration.
448
- for address in self.no_engines_listening_address_list:
449
- ip_address, port_str = address.split(':')
450
- port = int(port_str)
451
- socket_by_port = self.create_socket_ipv4_tcp(ip_address, port)
452
- threading.Thread(
453
- target=self.listening_socket_loop,
454
- args=(socket_by_port, None, reference_function_name,
455
- reference_function_args, pass_function_reference_to_thread),
456
- name=f"acceptor-{ip_address}:{port}",
457
- daemon=True
458
- ).start()
459
-
460
- # # Creating a socket for each port in the list set in configuration file
461
- # for address in self.listening_address_list:
462
- # ip_address, port_str = address.split(':')
463
- # port = int(port_str)
464
- # socket_by_port = self.create_socket_ipv4_tcp(
465
- # ip_address, port)
466
- #
467
- # self.listening_sockets.append(socket_by_port)
468
+ acceptor_name: str = f"acceptor-{self.ip_address}:{self.port}"
469
+
470
+ socket_by_port = self.create_socket_ipv4_tcp(self.ip_address, self.port)
471
+ threading.Thread(
472
+ target=self.listening_socket_loop,
473
+ args=(socket_by_port, callable_function, callable_args),
474
+ name=acceptor_name,
475
+ daemon=True
476
+ ).start()
468
477
 
469
478
  def listening_socket_loop(
470
479
  self,
471
480
  listening_socket_object: socket.socket,
472
- engine: initialize_engines.ModuleCategory,
473
- reference_function_name,
474
- reference_function_args=(),
475
- pass_function_reference_to_thread: bool = True
481
+ callable_function: callable,
482
+ callable_args=()
476
483
  ):
477
484
  """
478
485
  Loop to wait for new connections, accept them and send to new threads.
@@ -480,23 +487,19 @@ class SocketWrapper:
480
487
  will be killed or closed.
481
488
 
482
489
  :param listening_socket_object: listening socket that was created with bind.
483
- :param engine: ModuleCategory.
484
- :param reference_function_name: callable, function reference that you want to execute when client
490
+ :param callable_function: callable, function that you want to execute when client
485
491
  socket received by 'accept()' and connection has been made.
486
- :param reference_function_args: tuple, that will be passed to 'function_reference' when it will be called.
487
- Your function should be able to accept these arguments before the 'reference_function_args' tuple:
492
+ :param callable_args: tuple, that will be passed to 'function_reference' when it will be called.
493
+ Your function should be able to accept these arguments before the 'callable_args' tuple:
488
494
  (client_socket, process_name, is_tls, domain_from_dns_server).
489
- Meaning that 'reference_function_args' will be added to the end of the arguments tuple like so:
495
+ Meaning that 'callable_args' will be added to the end of the arguments tuple like so:
490
496
  (client_socket, process_name, is_tls, tls_type, tls_version, domain_from_dns_server,
491
- *reference_function_args).
497
+ *callable_args).
492
498
 
493
499
  client_socket: socket, client socket that was accepted.
494
500
  process_name: string, process name that was gathered from the socket.
495
501
  is_tls: boolean, if the socket is SSL/TLS.
496
502
  domain_from_dns_server: string, domain that was requested from DNS server.
497
- :param pass_function_reference_to_thread: boolean, that sets if 'function_reference' will be
498
- executed as is, or passed to thread. 'function_reference' can include passing to a thread,
499
- but you don't have to use it, since SocketWrapper can do it for you.
500
503
  :return:
501
504
  """
502
505
 
@@ -514,31 +517,30 @@ class SocketWrapper:
514
517
  listening_ip, listening_port = listening_socket_object.getsockname()
515
518
 
516
519
  domain_from_engine = None
517
- for engine in self.engines_list:
518
- # Get the domain to connect on this process in case on no SNI provided.
519
- for domain, ip_port_dict in engine.domain_target_dict.items():
520
- if ip_port_dict['ip'] == listening_ip:
521
- domain_from_engine = domain
520
+ # Get the domain to connect on this process in case on no SNI provided.
521
+ for domain, ip_port_dict in self.engine.domain_target_dict.items():
522
+ if ip_port_dict['ip'] == listening_ip:
523
+ domain_from_engine = domain
524
+ break
525
+ # If there was no domain found, try to find the IP address for port.
526
+ if not domain_from_engine:
527
+ for port, file_or_ip in self.engine.port_target_dict.items():
528
+ if file_or_ip['ip'] == listening_ip:
529
+ # Get the value from the 'on_port_connect' dictionary.
530
+ address_or_file_path: str = self.engine.on_port_connect[str(listening_port)]
531
+ ip_port_address_from_config: tuple = initialize_engines.get_ipv4_from_engine_on_connect_port(
532
+ address_or_file_path)
533
+ if not ip_port_address_from_config:
534
+ raise ValueError(
535
+ f"Invalid IP address or file path in 'on_port_connect' for port "
536
+ f"{listening_port}: {address_or_file_path}"
537
+ )
538
+
539
+ domain_from_engine = ip_port_address_from_config[0]
540
+
522
541
  break
523
- # If there was no domain found, try to find the IP address for port.
524
- if not domain_from_engine:
525
- for port, file_or_ip in engine.port_target_dict.items():
526
- if file_or_ip['ip'] == listening_ip:
527
- # Get the value from the 'on_port_connect' dictionary.
528
- address_or_file_path: str = engine.on_port_connect[str(listening_port)]
529
- ip_port_address_from_config: tuple = initialize_engines.get_ipv4_from_engine_on_connect_port(
530
- address_or_file_path)
531
- if not ip_port_address_from_config:
532
- raise ValueError(
533
- f"Invalid IP address or file path in 'on_port_connect' for port "
534
- f"{listening_port}: {address_or_file_path}"
535
- )
536
-
537
- domain_from_engine = ip_port_address_from_config[0]
538
-
539
- break
540
-
541
- self.logger.info(f"Requested domain setting: {domain_from_engine}")
542
+
543
+ self.logger.info(f"Requested domain setting: {domain_from_engine}")
542
544
 
543
545
  # Wait from any connection on "accept()".
544
546
  # 'client_socket' is socket or ssl socket, 'client_address' is a tuple (ip_address, port).
@@ -561,7 +563,7 @@ class SocketWrapper:
561
563
  process_name = get_command_instance.get_process_name(print_kwargs={'logger': self.logger})
562
564
 
563
565
  source_ip: str = client_address[0]
564
- engine_name: str = get_engine_name(domain_from_engine, self.engines_list)
566
+ engine_name: str = get_engine_name(domain_from_engine, [self.engine])
565
567
  dest_port: int = listening_socket_object.getsockname()[1]
566
568
 
567
569
  # Not always there will be a hostname resolved by the IP address, so we will leave it empty if it fails.
@@ -620,16 +622,6 @@ class SocketWrapper:
620
622
  print_kwargs={'logger': self.logger}
621
623
  )
622
624
 
623
- # Get the real tls version after connection is wrapped.
624
- tls_version = ssl_client_socket.version()
625
-
626
- # If the 'domain_from_dns_server' is empty, it means that the 'engine_name' is not set.
627
- # In this case we will set the 'engine_name' to from the SNI.
628
- if engine_name == '':
629
- sni_hostname: str = ssl_client_socket.server_hostname
630
- if sni_hostname:
631
- engine_name = get_engine_name(sni_hostname, self.engines_list)
632
-
633
625
  if accept_error_message:
634
626
  # Write statistics after wrap is there was an error.
635
627
  self.statistics_writer.write_accept_error(
@@ -643,6 +635,16 @@ class SocketWrapper:
643
635
 
644
636
  continue
645
637
 
638
+ # Get the real tls version after connection is wrapped.
639
+ tls_version = ssl_client_socket.version()
640
+
641
+ # If the 'domain_from_dns_server' is empty, it means that the 'engine_name' is not set.
642
+ # In this case we will set the 'engine_name' to from the SNI.
643
+ if engine_name == '':
644
+ sni_hostname: str = ssl_client_socket.server_hostname
645
+ if sni_hostname:
646
+ engine_name = get_engine_name(sni_hostname, [self.engine])
647
+
646
648
  # Create new arguments tuple that will be passed, since client socket and process_name
647
649
  # are gathered from SocketWrapper.
648
650
  if ssl_client_socket:
@@ -652,21 +654,24 @@ class SocketWrapper:
652
654
  # noinspection PyUnusedLocal
653
655
  client_socket = None
654
656
  client_socket = ssl_client_socket
655
- thread_args = \
656
- ((client_socket, process_name, is_tls, tls_type, tls_version, domain_from_engine) +
657
- reference_function_args)
658
- # If 'pass_function_reference_to_thread' was set to 'False', execute the callable passed function
659
- # as is.
660
- if not pass_function_reference_to_thread:
661
- before_socket_thread_worker(
662
- callable_function=reference_function_name, thread_args=thread_args,
663
- exceptions_logger=self.exceptions_logger)
664
- # If 'pass_function_reference_to_thread' was set to 'True', execute the callable function reference
665
- # in a new thread.
666
- else:
667
- self._send_accepted_socket_to_thread(
668
- before_socket_thread_worker,
669
- reference_args=(reference_function_name, thread_args, self.exceptions_logger))
657
+ thread_args = (
658
+ (client_socket, process_name, is_tls, tls_type, tls_version, domain_from_engine, self.statistics_writer, [self.engine]) +
659
+ callable_args)
660
+
661
+ # Creating thread for each socket
662
+ thread_current = threading.Thread(
663
+ target=before_socket_thread_worker,
664
+ args=(callable_function, thread_args, self.exceptions_logger),
665
+ daemon=True
666
+ )
667
+ thread_current.start()
668
+ # Append to list of threads, so they can be "joined" later
669
+ self.threads_list.append(thread_current)
670
+
671
+ # 'thread_callable_args[1][0]' is the client socket.
672
+ client_address = base.get_source_address_from_socket(client_socket)
673
+
674
+ self.logger.info(f"Accepted connection, thread created {client_address}. Continue listening...")
670
675
  # Else, if no client_socket was opened during, accept, then print the error.
671
676
  else:
672
677
  # Write statistics after accept.
@@ -681,35 +686,22 @@ class SocketWrapper:
681
686
  except Exception as e:
682
687
  self.exceptions_logger.write(e)
683
688
 
684
- def _send_accepted_socket_to_thread(self, thread_function_name, reference_args=()):
685
- # Creating thread for each socket
686
- thread_current = threading.Thread(target=thread_function_name, args=(*reference_args,))
687
- thread_current.daemon = True
688
- thread_current.start()
689
- # Append to list of threads, so they can be "joined" later
690
- self.threads_list.append(thread_current)
691
-
692
- # 'reference_args[1][0]' is the client socket.
693
- client_address = base.get_source_address_from_socket(reference_args[1][0])
694
-
695
- self.logger.info(f"Accepted connection, thread created {client_address}. Continue listening...")
696
-
697
689
 
698
690
  def before_socket_thread_worker(
699
691
  callable_function: callable,
700
- thread_args: tuple,
692
+ callable_args: tuple,
701
693
  exceptions_logger: loggingw.ExceptionCsvLogger = None
702
694
  ):
703
695
  """
704
696
  Function that will be executed before the thread is started.
705
697
  :param callable_function: callable, function that will be executed in the thread.
706
- :param thread_args: tuple, arguments that will be passed to the function.
698
+ :param callable_args: tuple, arguments that will be passed to the function.
707
699
  :param exceptions_logger: loggingw.ExceptionCsvLogger, logger object that will be used to log exceptions.
708
700
  :return:
709
701
  """
710
702
 
711
703
  try:
712
- callable_function(*thread_args)
704
+ callable_function(*callable_args)
713
705
  except Exception as e:
714
706
  exceptions_logger.write(e)
715
707
 
@@ -1,6 +1,6 @@
1
1
  import datetime
2
+ import multiprocessing
2
3
 
3
- from ...file_io import csvs
4
4
  from ..loggingw import loggingw
5
5
 
6
6
 
@@ -10,22 +10,41 @@ STATISTICS_HEADER: str = (
10
10
  'response_size_bytes,file_path,process_cmd,action,error')
11
11
 
12
12
 
13
- class StatisticsCSVWriter:
13
+ class StatisticsCSVWriter(loggingw.CsvLogger):
14
14
  """
15
15
  Class to write statistics to CSV file.
16
16
  This can be initiated at the main, and then passed to the thread worker function.
17
17
  """
18
18
  def __init__(
19
19
  self,
20
- statistics_directory_path: str
20
+ logger_name: str = LOGGER_NAME,
21
+ directory_path: str = None,
22
+ log_queue: multiprocessing.Queue = None,
23
+ add_queue_handler_start_listener_multiprocessing: bool = False,
24
+ add_queue_handler_no_listener_multiprocessing: bool = False
21
25
  ):
22
- self.csv_logger = loggingw.create_logger(
23
- logger_name=LOGGER_NAME,
24
- directory_path=statistics_directory_path,
25
- add_timedfile_with_internal_queue=True,
26
- formatter_filehandler='MESSAGE',
27
- file_type='csv',
28
- header=STATISTICS_HEADER
26
+ """
27
+ Initialize the StatisticsCSVWriter with the directory path for the statistics CSV file.
28
+ :param directory_path: str, the directory path where the statistics CSV file will be created.
29
+ :param log_queue: multiprocessing.Queue, the queue to use for logging in multiprocessing.
30
+ :param add_queue_handler_start_listener_multiprocessing: bool, whether to add a queue handler that will use
31
+ the 'logger_queue' and start the queue listener with the same 'logger_queue' for multiprocessing.
32
+ :param add_queue_handler_no_listener_multiprocessing: bool, whether to add a queue handler that will use
33
+ the 'logger_queue' but will not start the queue listener for multiprocessing. This is useful when you
34
+ already started the queue listener and want to add more handlers to the logger without
35
+ starting a new listener.
36
+
37
+ If you don't set any of 'add_queue_handler_start_listener_multiprocessing' or
38
+ 'add_queue_handler_no_listener_multiprocessing', the logger will be created without a queue handler.
39
+ """
40
+
41
+ super().__init__(
42
+ logger_name=logger_name,
43
+ directory_path=directory_path,
44
+ log_queue=log_queue,
45
+ add_queue_handler_start_listener_multiprocessing=add_queue_handler_start_listener_multiprocessing,
46
+ add_queue_handler_no_listener_multiprocessing=add_queue_handler_no_listener_multiprocessing,
47
+ custom_header=STATISTICS_HEADER
29
48
  )
30
49
 
31
50
  def write_row(
@@ -60,7 +79,7 @@ class StatisticsCSVWriter:
60
79
  else:
61
80
  tls_info = f'{tls_type}|{tls_version}'
62
81
 
63
- escaped_line_string: str = csvs.escape_csv_line_to_string([
82
+ row_of_cols: list = [
64
83
  timestamp,
65
84
  thread_id,
66
85
  engine,
@@ -81,9 +100,9 @@ class StatisticsCSVWriter:
81
100
  process_cmd,
82
101
  action,
83
102
  error
84
- ])
103
+ ]
85
104
 
86
- self.csv_logger.info(escaped_line_string)
105
+ super().write(row_of_cols)
87
106
 
88
107
  def write_accept_error(
89
108
  self,
@@ -8,15 +8,24 @@ from ..print_api import print_api
8
8
  from ..permissions import ubuntu_permissions
9
9
 
10
10
 
11
- def install_packages(package_list: list[str]):
11
+ def install_packages(
12
+ package_list: list[str],
13
+ timeout_seconds: int = 0,
14
+ ):
12
15
  """
13
16
  Function installs a package using apt-get.
14
17
  :param package_list: list of strings, package names to install.
18
+ :param timeout_seconds: int, if the 'apt-get' command is busy at the moment, the function will wait for
19
+ 'timeout_seconds' seconds before raising an error.
20
+ '-1' means wait indefinitely.
15
21
  :return:
16
22
  """
17
23
 
18
24
  # Construct the command with the package list
19
- command = ["sudo", "apt-get", "install", "-y"] + package_list
25
+ command = ["sudo", "apt", "install", "-y"] + package_list
26
+
27
+ if timeout_seconds != 0:
28
+ command.extend(["-o", f"DPkg::Lock::Timeout={str(timeout_seconds)}"])
20
29
 
21
30
  subprocess.check_call(command)
22
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 3.2.13
3
+ Version: 3.3.0
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License