diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..111afb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +[Ss]ecrets* +__pycache__ +Data \ No newline at end of file diff --git a/Python/InternetChecker.py b/Python/InternetChecker.py new file mode 100644 index 0000000..ddb603a --- /dev/null +++ b/Python/InternetChecker.py @@ -0,0 +1,1250 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from os.path import dirname as directory_name +from os.path import abspath as path_absolute +from os.path import exists as path_exists +from subprocess import Popen as ProcessOpen +from subprocess import PIPE +from re import compile as RECompile +from multiprocessing import Process +from multiprocessing import Manager as MultiprocessManager +from multiprocessing.managers import DictProxy, ListProxy +from random import random as random_number +from time import sleep +from time import time as timestamp +from datetime import datetime +from threading import Thread +from traceback import print_exc as print_trace_exception +import signal +from inspect import stack as get_stack +import sqlite3 +from traceback import extract_tb as extract_traceback +from traceback import format_stack as trace_format_stack +from os import makedirs as make_directories + +settings = {} +for key in ("Secrets", "secrets"): + path = path_absolute(directory_name(__file__)) + "/" + key + ".py" + if path_exists(path): + try: + with open(path) as data: + exec(compile(data.read(), path, "exec"), globals()) + break + except: + print_trace_exception() + +class InternetChecker: + + __default_settings = { + "nulls" : False, + "default_value" : None, + "threads_start_now" : True, + "threads_minimum_timing" : 1.0 / 24.0, + "threads_timing" : 1.0 / 24.0, + "threads_bucle" : True, + "threads_autostart" : True, + "ping_checker_timing" : [.5, 2], + "ping_samples" : 4, + "print_format" : "[{type}] {yyyy}{mm}{dd} {hh}{ii}{ss} [{line}]{file}({method}): {message}", + "close_check_timer" : .1, + "language" : "espanol", + "default_language" : "espanol", + "default_text" : "", + "show_execute_threads_exception" : True, + "show_add_thread_exception" : True, + "show_add_thread_error" : True, + "show_add_thread_ok" : True, + "show_add_ping_checker_error" : True, + "show_add_ping_checker_ok" : True, + "show_remove_thread_exception" : True, + "show_remove_thread_error" : True, + "show_remove_thread_ok" : True, + "show_ping_line_data" : True, + "show_ping_line_raw_data" : False, + "show_ping_line_unknown" : False, + "show_ping_sequence_results" : True, + "show_get_path_error" : True, + "show_get_path_ok" : True, + "allow_create_database_file" : True, + "host_id" : "InternetChecker", + "show_update_database_error" : True, + "show_update_database_ok" : True, + "show_get_database_connection_exception" : True, + "show_get_database_connection_error" : True, + "show_get_database_connection_ok" : True, + "show_close_database_connection_exception" : True, + "show_close_database_connection_error" : True, + "show_close_database_connection_ok" : True, + "show_build_database_exception" : True, + "show_build_database_error" : True, + "show_build_database_ok" : True, + "show_make_directory_exception" : True, + "show_make_directory_error" : True, + "show_make_directory_ok" : True, + "database_update_timing" : 5, + "database_allow_save_raw_pings" : True + } + __sentences = { + "espanol" : { + "internet_checker_building" : "La aplicación de servicio local 'InternetChecker' se está construyendo...", + "internet_checker_built" : "La aplicación de servicio local 'InternetChecker' se construyó y se inició completamente.", + "internet_checker_closing" : "La aplicación de sercicio local 'InternetChecker' se está cerrando...", + "internet_checker_closing_forced" : "La aplicación de sercicio local 'InternetChecker' se está cerrando de forma forzada...", + "internet_checker_closed" : "La aplicación de sercicio local 'InternetChecker' se cerró completamente. ¡Muchas gracias por usarme!.", + "ping_sequence_results" : "La secuencia '{sequence}' del ping '{site}' fue realizado en '{round_time}' segundos mediante '{samples}' muestras con una media de '{round_average_ping}' milisegundos de respuesta con '{lost}' paquetes perdidos.", + "execute_threads_exception" : "Hubo una excepción al intentar ejecutar el hilo de procesos '{i}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "add_thread_exception" : "Hubo una excepción al intentar añadir el nuevo hilo de procesos '{i}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "add_ping_checker_error" : "Hubo un error con código '{code}' al intentar añadir el nuevo Ping '{i}' contra '{site}'.{list}", + "add_ping_checker_ok" : "El nuevo Ping '{i}' contra '{site}' fue añadido correctamente.", + "exception" : "Hubo una excepción.", + "site_null" : "El sitio es nulo.", + "site_not_string" : "El sitio no es un String.", + "site_empty" : "El sitio está vacío.", + "site_not_match" : "El sitio no encaja con el patrón de dominio o IP.", + "method_null" : "El método es nulo.", + "method_not_function" : "El método no es una función.", + "start_now_null" : "El valor de inicio inmediato es nulo.", + "start_now_not_boolean" : "El valor de inicio inmediato no es un valor Booleano.", + "minimum_timing_null" : "El tiempo de proceso mínimo es nulo.", + "minimum_timing_not_float" : "El tiempo de proceso mínimo no es un valor decimal.", + "minimum_timing_lower_0" : "El tiempo de proceso mínimo es menor a 0.", + "timing_null" : "El tiempo entre ciclos de proceso es nulo.", + "timing_not_float" : "El tiempo entre ciclos de proceso no es un decimal.", + "timing_lower_0" : "El tiempo entre ciclos de proceso es inferior a 0.", + "timing_not_2" : "El tiempo entre ciclos de proceso no tiene dos valores.", + "timing_min_null" : "El tiempo mínimo entre ciclos de proceso es nulo.", + "timing_min_not_float" : "El tiempo mínimo entre ciclos de proceso no es decimal.", + "timing_min_lower_0" : "El tiempo mínimo entre ciclos de proceso es menor que 0.", + "timing_max_null" : "El tiempo máximo entre ciclos de proceso es nulo.", + "timing_max_not_float" : "El tiempo máximo entre ciclos de proceso no es decimal.", + "timing_max_lower_0" : "El tiempo máximo entre ciclos de proceso es menor que 0.", + "timing_min_greater_max" : "El tiempo mínimo entre ciclos es mayor que el máximo.", + "bucle_null" : "El valor que determina si hay o no bucle es nulo.", + "bucle_not_boolean" : "El valor que determina si hay o no bucle no es un valor Booleano.", + "autostart_null" : "El valor que determina si se autoinicia o no es nulo.", + "autostart_not_boolean" : "El valor que determina si se autoinicia o no no es un valor Booleano.", + "add_thread_exception" : "Hubo una excepción al intentar añadir el nuevo hilo de procesos '{i}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "add_thread_error" : "Hubo un error con código '{code}' al intentar añadir el nuevo hilo de procesos '{i}'.{list}", + "add_thread_ok" : "El nuevo hilo de procesos '{i}' fue añadido correctamente.", + "thread_i_null" : "El ID del hilo de procesos es nulo.", + "thread_i_not_integer" : "El ID del hilo de procesos no es un valor numérico entero.", + "thread_i_lower_0" : "El ID del hilo de procesos es inferior a 0.", + "thread_i_too_high" : "El ID del hilo de procesos es demasiado alto.", + "thread_i_deleted" : "El ID del hilo de procesos ya fue eliminado.", + "remove_thread_exception" : "Hubo una excepción al intentar eliminar el hilo de procesos '{i}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "remove_thread_error" : "Hubo un error con código '{code}' al intentar eliminar el hilo de procesos '{i}'.{list}", + "remove_thread_ok" : "El hilo de procesos '{i}' fue eliminado correctamente.", + "ping_line_data" : "Ping '[{sequence}]{samples}/{i}' contra '{site}' en {time} ms.", + "sites_null" : "Los sitios son nulos.", + "sites_not_list" : "Los sitios no son una lista.", + "sites_empty" : "Los sitios están vacíos.", + "database_path_null" : "El Path de la base de datos es nulo.", + "database_path_not_string" : "El Path de la base de datos no es un String.", + "database_path_whitespaces" : "El Path de la base de datos son sólo espacios en blanco.", + "database_path_not_exists" : "El Path de la base de datos no existe.", + "host_id_null" : "El ID del host es nulo.", + "host_id_not_string" : "El ID del Host no es un String.", + "host_id_empty" : "El ID del Host está vacío.", + "no_sites_error" : "Hubo un error con código '{code}' al intentar cargar los sitios a analizar.{list}", + "path_null" : "El Path es nulo.", + "path_not_string" : "El Path no es un String.", + "path_empty" : "El Path está vacío.", + "path_not_exists" : "El Path no existe.", + "get_path_error" : "Hubo un error con código '{code}' al intentar coger el Path completo de '{path}'.{list}", + "get_path_ok" : "El Path '{real_path}' fue recogido correctamente.", + "sqlite_connection_error" : "Hubo una excepción al intentar abrir una conexión con la base de datos '{path}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "settings_has_errors" : "La configuración contiene errores.", + "data_null" : "Los datos son nulos.", + "data_not_dictionary" : "Los datos no son un diccionario.", + "data_empty" : "Los datos están vacíos.", + "update_database_error" : "Hubo un error con código '{code}' al intentar actualizar los datos en la base de datos.{list}", + "update_database_ok" : "La base de datos fue actualizada correctamente.", + "get_database_connection_exception" : "Hubo una excepción al intentar crear una conexión contra la base de datos '{path}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "get_database_connection_error" : "Hubo un error con código '{code}' al intentar crear una conexión contra la base de datos '{path}'.{list}", + "get_database_connection_ok" : "La conexión contra la base de datos fue recogida correctamente.", + "close_database_connection_exception" : "Hubo una excepción al intentar cerrar la conexión contra la base de datos. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "close_database_connection_error" : "Hubo un error con código '{code}' al intentar cerrar la conexión contra la base de datos.{list}", + "close_database_connection_ok" : "La conexión contra la base de datos fue cerrada correctamente.", + "connection_null" : "La conexión es nula.", + "connection_bad_typed" : "El tipado de la conexión es erróneo.", + "has_errors_null" : "La variable que determina si hay errores o no es nula.", + "has_errors_bad_typed" : "El tipado que determina si hay errores o no es erróneo.", + "build_database_exception" : "Hubo una excepción al intentar construir la base de datos. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "build_database_error" : "Hubo un error con código '{code}' al intentar construir la base de datos.{list}", + "build_database_ok" : "La base de datos fue construída correctamente.", + "make_directory_exception" : "Hubo una excepción al intentar crear el directorio del Path '{path}'. '{file}({method})[{line}]'{lines}\n\n{exception_message}", + "make_directory_error" : "Hubo un error con código '{code}' al intentar crear el directorio del Path '{path}'.{list}", + "make_directory_ok" : "El directorio del Path '{path}' fue creado correctamente." + } + } + + re_site_validate = RECompile(r'^(([0-9]+)?(\.[0-9]+){1,3}|(:|[0-9]+)+|([a-z0-9A-Z\-_]+\.[a-zA-Z0-9]+)+)$') + re_ping_line = RECompile(r'^([0-9]+) bytes from (([^ :]+)|([^ ]+) \(([^\)]+)\)): icmp_seq=([0-9]+) ttl=([0-9]+) time=([0-9]+(\.[0-9]+)?) ms$') + re_string_variables = RECompile(r'\{([^\{\}]+)\}') + re_break_lines = RECompile(r'\r\n|[\r\n]') + re_exception_line = RECompile(r'^\s*File "([^"]+)", line ([0-9]+), in (.+)$') + re_slashes = RECompile(r'[\/\\\\]+') + re_pascal_capitals = RECompile(r'([A-Z])') + re_field_key = RECompile(r'^([^\s]+)\s.+$') + re_directory = RECompile(r'^(.+)[\/\\\\][^\/\\\\]+$') + re_sql_semicolons = RECompile(r'\'') + + def __init__(self, inputs = None): + self.__inputs = inputs + self.__print_format = self.settings("print_format") + self.__language = self.settings("language") + self.__default_language = self.settings("default_language") + self.__print_types = ( + ("UNKN", "unknown"), + (" OK ", "ok", "yes", "right", "y"), + ("ERRO", "error", "wrong", "no", "n", "x"), + ("EXCE", "exception"), + ("INFO", "information"), + ("WARN", "warning") + ) + + self._print("info", "internet_checker_building") + + self.__threads = [] + self.__main_thread = True + self.__threads_changing = False + self.__working = True + self.__ping_checker_timing = self.settings("ping_checker_timing") + self.__closing = False + self.__samples = self.settings(("ping_samples", "samples")) + self.__sequences = {} + self.__close_check_timer = self.settings("close_check_timer") + self.__default_text = self.settings("default_text") + self.__database_path = self.settings(("database_file", "sqlite_file")) + self.__host_id = self.settings("host_id") + self.__cache = {"pings" : [], "raw_pings" : []} + self.__database_thread = None + self.__session = None + self.__allow_save_raw_ping = self.settings(("database_allow_save_raw_pings", "allow_save_raw_pings")) + + self.__show_execute_threads_exception = self.settings("show_execute_threads_exception") + self.__show_add_thread_exception = self.settings("show_add_thread_exception") + self.__show_add_thread_error = self.settings("show_add_thread_error") + self.__show_add_thread_ok = self.settings("show_add_thread_ok") + self.__show_add_ping_checker_error = self.settings("show_add_ping_checker_error") + self.__show_add_ping_checker_ok = self.settings("show_add_ping_checker_ok") + # self.__show_remove_thread_exception = self.settings("show_remove_thread_exception") + self.__show_remove_thread_error = self.settings("show_remove_thread_error") + self.__show_remove_thread_ok = self.settings("show_remove_thread_ok") + self.__show_ping_line_data = self.settings("show_ping_line_data") + self.__show_ping_line_raw_data = self.settings("show_ping_line_raw_data") + self.__show_ping_line_unknown = self.settings("show_ping_line_unknown") + self.__show_ping_sequence_results = self.settings("show_ping_sequence_results") + self.__show_get_path_error = self.settings("show_get_path_error") + self.__show_get_path_ok = self.settings("show_get_path_ok") + self.__show_update_database_error = self.settings("show_update_database_error") + self.__show_update_database_ok = self.settings("show_update_database_ok") + self.__show_get_database_connection_exception = self.settings("show_get_database_connection_exception") + self.__show_get_database_connection_error = self.settings("show_get_database_connection_error") + self.__show_get_database_connection_ok = self.settings("show_get_database_connection_ok") + self.__show_close_database_connection_exception = self.settings("show_close_database_connection_exception") + self.__show_close_database_connection_error = self.settings("show_close_database_connection_error") + self.__show_close_database_connection_ok = self.settings("show_close_database_connection_ok") + self.__show_build_database_exception = self.settings("show_build_database_exception") + self.__show_build_database_error = self.settings("show_build_database_error") + self.__show_build_database_ok = self.settings("show_build_database_ok") + self.__show_make_directory_exception = self.settings("show_make_directory_exception") + self.__show_make_directory_error = self.settings("show_make_directory_error") + self.__show_make_directory_ok = self.settings("show_make_directory_ok") + + sites = self.settings("sites") + + self.__error = ( + (( + 1 << 0 if sites == None else + 1 << 1 if not isinstance(sites, (list, tuple)) else + 1 << 2 if not len(sites) else + 0) << 0) | + (( + 1 << 0 if self.__database_path == None else + 1 << 1 if not isinstance(self.__database_path, str) else + 0) << 3) | + (( + 1 << 0 if self.__host_id == None else + 1 << 1 if not isinstance(self.__host_id, str) else + 1 << 2 if not self.__host_id else + 0) << 7) | + 0) << 1 + self.__root_paths = ("", path_absolute(directory_name(__file__))) + self.__slash = '/' if '/' in self.__root_paths[1] else '\\' + + if not (self.__error >> 4) & ~-(1 << 2): + self.__database_path = self.__database_path.strip() + self.__error |= ( + 1 << 0 if not len(self.__database_path) else + 0) << 6 + if not self.__error and not self.settings("allow_create_database_file"): + (self.__database_path, suberror) = self.get_path(self.__database_path) + if suberror: + self.__error |= 1 << 7 + + if not self.__error & ~-(1 << 4): + for site in sites: + self.add_ping_checker(site) + + if self.validate( + self.__error, + ( + "exception", + "sites_null", + "sites_not_list", + "sites_empty", + "database_path_null", + "database_path_not_string", + "database_path_whitespaces", + "database_path_not_exists", + "host_id_null", + "host_id_not_string", + "host_id_empty" + ), + {}, + "no_sites_error" + ) and not self.__build_database(): + self.__database_thread = self.add_thread(self.__update_database, { + "bucle" : True, + "start_now" : False, + "timing" : self.settings("database_update_timing") + })[0] + + signal.signal(signal.SIGINT, lambda *_:self.close()) + self._print("ok", "internet_checker_built") + signal.pause() + # self._print("info", "internet_checker_closing_forced") + + def close(self): + + if not self.__main_thread or self.__closing: + return + self.__closing = True + + self._print("ok", "internet_checker_closing") + + if self.__database_thread != None: + self.remove_thread(self.__database_thread) + self.__working = False + + while len([None for thread in self.__threads if thread]): + sleep(self.__close_check_timer) + + self._print("ok", "internet_checker_closed") + + def __save_ping_line(self, data): + self.__show_ping_line_data and self._print("info", "ping_line_data", data) + self.__show_ping_line_raw_data and print(data) + # self.__cache["pings"] += [data] + + def __ping_line(self, results, line): + + matches = self.re_ping_line.search(line) + + if matches: + data = { + "samples" : self.__samples, + "site" : results["site"], + "size" : int(matches.group(1)), + "from" : matches.group(4), + "ip" : matches.group(3) or matches.group(5), + "i" : int(matches.group(6)), + "ttl" : int(matches.group(7)), + "time" : float(matches.group(8)), + "sequence" : self.__sequences[results["site"]], + "date" : datetime.now() + } + results["data"] += [data] + self.__save_ping_line(data) + return True + return False + + def __ping_handler(self, results): + + process = ProcessOpen("ping -c " + str(self.__samples) + " " + results["site"], stdout = PIPE, shell = True) + self.__main_thread = False + + for line in iter(process.stdout.readline, b''): + if line == b'': + break + line = line.decode("utf-8").strip() + results["raw_lines"] += [line] + if self.__ping_line(results, line): + continue + self.__show_ping_line_unknown and print([len(results["raw_lines"]), results["raw_lines"][-1]]) + + @staticmethod + def call_with_timeout(method, timeout, arguments): + with MultiprocessManager() as manager: + + parameters = [] + + for i, argument in enumerate(arguments): + parameters += [( + manager.list(argument) if isinstance(arguments[i], (list, tuple)) else + manager.dict(argument) if isinstance(arguments[i], dict) else + arguments[i])] + + process = Process(target = method, args = parameters) + process.start() + process.join(timeout) + if process.is_alive(): + process.terminate() + + for i, parameter in enumerate(parameters): + arguments[i] = ( + list(parameter) if isinstance(parameter, ListProxy) else + dict(parameter) if isinstance(parameter, DictProxy) else + parameter) + + return arguments + + def __ping(self, site): + + if site in self.__sequences: + self.__sequences[site] += 1 + else: + self.__sequences[site] = 1 + results = self.call_with_timeout(self.__ping_handler, 10, [{ + "site" : site, + "sequence" : self.__sequences[site], + "from" : timestamp(), + "to" : None, + "data" : [], + "raw_lines" : [] + }])[0] + self.__cache["pings"] += results["data"] + + results["to"] = timestamp() + l = len(results["data"]) + average_ping = sum([line["time"] for line in results["data"]]) / l if l else -1 + time = results["to"] - results["from"] + + self.__cache["raw_pings"] += [{key : results[key] for key in ("site", "sequence", "from", "to", "raw_lines")}] + + self.__show_ping_sequence_results and self._print("info", "ping_sequence_results", { + **results, + "time" : time, + "round_time" : round(time, 2), + "samples" : self.__samples, + # "lost" : len([None for line in data if line]) + "lost" : len([None for i, _ in enumerate(results["data"]) if i and results["data"][i - 1]["i"] + 1 != results["data"][i]["i"]]), + "average_ping" : average_ping, + "round_average_ping" : round(average_ping, 2) + }) + + return results + + def nulls(self, nulls = None): + return nulls if isinstance(nulls, bool) else self.settings("nulls", None, False, False) + + def default_value(self, default = None, nulls = None): + return default if self.nulls(nulls) or default != None else self.settings("default_value", None, None, True) + + def settings(self, keys, inputs = None, default = None, nulls = None): + + keys = [key.strip() for key in ( + keys if isinstance(keys, (list, tuple)) else + [keys] if isinstance(keys, str) else + []) if isinstance(key, str) and key.strip()] + + if len(keys): + + nulls = self.nulls(nulls) + + for subinputs in ( + list(inputs) if isinstance(inputs, (list, tuple)) else + [inputs] if isinstance(inputs, dict) else + []) + [self.__inputs, self.__default_settings]: + if isinstance(subinputs, dict): + for key in keys: + if key in subinputs and (nulls or subinputs[key] != None): + return subinputs[key] + return self.default_value(default, nulls) + + def __check_ping_site(self, site): + if self.__working: + self.__ping(site) + + def add_ping_checker(self, site, show_errors = None): + + error = ( + 1 << 0 if site == None else + 1 << 1 if not isinstance(site, str) else + 1 << 2 if not site else + 1 << 3 if not self.re_site_validate.search(site) else + 0) << 1 + i = None + has_show_errors = isinstance(show_errors, bool) + + if not error: + (i, suberror) = self.add_thread(lambda:self.__check_ping_site(site), { + "start_now" : False, + "timing" : self.__ping_checker_timing + }) + error |= ((suberror >> 1) << 5) | (suberror & 1) + + self.validate( + error, + ( + "exception", + "site_null", + "site_not_string", + "site_empty", + "site_not_match", + "method_null", + "method_not_function", + "start_now_null", + "start_now_not_boolean", + "minimum_timing_null", + "minimum_timing_not_float", + "minimum_timing_lower_0", + "timing_null", + "timing_not_float", + "timing_lower_0", + "timing_not_2", + "timing_min_null", + "timing_min_not_float", + "timing_min_lower_0", + "timing_max_null", + "timing_max_not_float", + "timing_max_lower_0", + "timing_min_greater_max", + "bucle_null", + "bucle_not_boolean", + "autostart_null", + "autostart_not_boolean" + ), + { + "i" : i, + "site" : site + }, + (show_errors if has_show_errors else self.__show_add_ping_checker_error) and "add_ping_checker_error", + (show_errors if has_show_errors else True) and self.__show_add_ping_checker_ok and "add_ping_checker_ok" + ) + + return (i, error) + + def remove_ping_checker(self, ping_checker): + return self.remove_thread(ping_checker) + + def __execute_threads(self, thread): + + while self.__working and thread["working"]: + + timing = thread["timing"] if isinstance(thread["timing"], (int, float)) else random_number() * (thread["timing"][1] - thread["timing"][0]) + thread["timing"][0] + + if thread["executions"] or thread["start_now"]: + try: + thread["method"]() + if not thread["bucle"]: + thread["working"] = False + except Exception as exception: + self.exception(exception, self.__show_execute_threads_exception and "execute_threads_exception", thread) + thread["executions"] += 1 + + start = timestamp() + + while thread["working"] and timestamp() - start < timing: + sleep(thread["minimum_timing"]) + + if self.__threads[thread["i"]]: + self.__threads[thread["i"]] = None + + def add_thread(self, method, inputs = None, show_errors = None): + + while self.__threads_changing: + sleep(random_number) + self.__threads_changing = True + + start_now = self.settings(("threads_start_now", "start_now"), inputs) + minimum_timing = self.settings(("threads_minimum_timing", "minimum_timing"), inputs) + timing = self.settings(("threads_timing", "timing"), inputs) + bucle = self.settings(("threads_bucle", "bucle"), inputs) + autostart = self.settings(("threads_autostart", "autostart"), inputs) + has_show_errors = isinstance(show_errors, bool) + error = ( + (( + 1 << 0 if method == None else + 1 << 1 if not callable(method) else + 0) << 0) | + (( + 1 << 0 if start_now == None else + 1 << 1 if not isinstance(start_now, bool) else + 0) << 2) | + (( + 1 << 0 if minimum_timing == None else + 1 << 1 if not isinstance(minimum_timing, (int, float)) else + 1 << 2 if minimum_timing < 0 else + 0) << 4) | + (( + 1 << 0 if timing == None else + ( + 1 << 0 if len(timing) != 2 else + ( + (( + 1 << 0 if timing[0] == None else + 1 << 1 if not isinstance(timing[0], (int, float)) else + 1 << 2 if timing[0] < 0 else + 0) << 1) | + (( + 1 << 0 if timing[1] == None else + 1 << 1 if not isinstance(timing[1], (int, float)) else + 1 << 2 if timing[1] < 0 else + 0) << 4) | + 0) or ( + 1 << 7 if timing[0] > timing[1] else + 0)) << 3 if isinstance(timing, (list, tuple)) else + 1 << 1 if not isinstance(timing, (int, float)) else + 1 << 2 if timing < 0 else + 0) << 7) | + (( + 1 << 0 if bucle == None else + 1 << 1 if not isinstance(bucle, bool) else + 0) << 18) | + (( + 1 << 0 if autostart == None else + 1 << 1 if not isinstance(autostart, bool) else + 0) << 20) | + 0) << 1 + i = None + + if not error: + try: + + i = 0 + l = len(self.__threads) + thread = { + "working" : True, + "start_now" : start_now, + "minimum_timing" : minimum_timing, + "timing" : timing, + "bucle" : bucle, + "method" : method, + "autostart" : autostart, + "thread" : Thread(target = lambda:self.__execute_threads(thread)), + "executions" : 0, + "i" : 0 + } + + while thread["i"] < l: + if self.__threads[thread["i"]] == None: + break + thread["i"] += 1 + + if thread["i"] == l: + self.__threads += [thread] + else: + self.__threads[thread["i"]] = thread + i = thread["i"] + + autostart and thread["thread"].start() + + except Exception as exception: + error |= 1 << 0 + self.exception(exception, self.__show_add_thread_exception and "add_thread_exception", {"i" : i}) + + self.validate( + error, + ( + "exception", + "method_null", + "method_not_function", + "start_now_null", + "start_now_not_boolean", + "minimum_timing_null", + "minimum_timing_not_float", + "minimum_timing_lower_0", + "timing_null", + "timing_not_float", + "timing_lower_0", + "timing_not_2", + "timing_min_null", + "timing_min_not_float", + "timing_min_lower_0", + "timing_max_null", + "timing_max_not_float", + "timing_max_lower_0", + "timing_min_greater_max", + "bucle_null", + "bucle_not_boolean", + "autostart_null", + "autostart_not_boolean" + ), + {"i" : i}, + (show_errors if has_show_errors else self.__show_add_thread_error) and "add_thread_error", + (show_errors if has_show_errors else True) and self.__show_add_thread_ok and "add_thread_ok" + ) + + self.__threads_changing = False + + return (i, error) + + def remove_thread(self, i, show_errors = None): + + error = ( + 1 << 0 if i == None else + 1 << 1 if not isinstance(i, int) else + 1 << 2 if i < 0 else + 1 << 3 if i >= len(self.__threads) else + 1 << 4 if not self.__threads[i] else + 0) << 1 + has_show_errors = isinstance(show_errors, bool) + + if not error: + self.__threads[i]["working"] = False + + self.validate( + error, + ( + "exception", + "thread_i_null", + "thread_i_not_integer", + "thread_i_lower_0", + "thread_i_too_high", + "thread_i_deleted" + ), + {"i" : i}, + (show_errors if has_show_errors else self.__show_remove_thread_error) and "remove_thread_error", + (show_errors if has_show_errors else True) and self.__show_remove_thread_ok and "remove_thread_ok" + ) + + return error + + @classmethod + def string_variables(self, string, variables = None, default = None): + # print(variables) + + variables = [_set for _set in (variables if isinstance(variables, (list, tuple)) else [variables]) if isinstance(_set, dict)] + + # print(variables) + + def callback(matches): + key = matches.group(1) + for _set in variables: + if key in _set: + return str(_set[key]) + return str(default) if default != None else matches.group(0) + + return self.re_string_variables.sub(callback, str(string)) + + @staticmethod + def get_action_data(i = 1): + + stack = get_stack()[1 + i] + + return { + "file" : stack.filename, + "method" : stack.function, + "line" : stack.lineno + } + + def default_text(self, default = None): + return default if default != None else self.__default_text + + def __get_i18n(self, keys, default): + + keys = [key.strip() for key in (keys if isinstance(keys, (list, tuple)) else [keys]) if isinstance(key, str) and len(keys.strip())] + + if len(keys): + + used = [] + + for language in (self.__language, self.__default_language) + tuple(self.__sentences.keys()): + if language and language in self.__sentences and language not in used: + used += [language] + for key in keys: + if key in self.__sentences[language]: + return self.__sentences[language][key] + return default if default != None else keys[0] + return self.default_text(default) + + def i18n(self, keys, variables = None, default = None): + return self.string_variables(self.__get_i18n(keys, default), variables) + + def get_print_type(self, _type): + + _type = _type.lower() + + for _types in self.__print_types: + for key in _types: + if _type == key.lower(): + return _types[0] + self.__print_types[0][0] + + def _print(self, _type, message, variables = None, default = None, i = 0): + + date = datetime.now() + _set = { + "type" : self.get_print_type(_type), + "raw_type" : _type, + "i18n" : message, + **self.get_action_data(i + 1) + } + + variables = list(variables) if isinstance(variables, (list, tuple)) else [variables] + + for key in ("year", "month", "day", "hour", "minute", "second"): + + k = "i" if key == "minute" else key[0] + + _set[key] = getattr(date, key) + _set[k] = _set[key] + _set[k + k] = ("00" + str(_set[k]))[-2:] + + _set["yyyy"] = _set["year"] + _set["message"] = self.i18n(message, variables + [_set], default) + + print(self.string_variables(self.__print_format, variables + [_set], default)) + + def exception(self, exception, message = None, variables = None, i = 1): + + lines = extract_traceback(exception.__traceback__).format() + line_matches = self.re_exception_line.match(lines[-1]) + data = { + **(variables if isinstance(variables, dict) else {}), + **self.get_action_data(1), + "lines" : "", + "exception_message" : str(exception), + **({ + "method" : line_matches.group(3), + "line" : line_matches.group(2), + "file" : line_matches.group(1) + } if line_matches else { + "method" : "UNKNOWN", + "line" : -1, + "file" : "UNKNOWN" + }) + } + + for block in trace_format_stack()[:-2] + lines: + if block: + data["lines"] += "\n " + self.re_break_lines.split(block.strip())[0] + + message and self._print("exception", message, data, None, i + 1) + + def validate(self, code, messages = [], variables = None, error_message = None, ok_message = None, k = 1): + + variables = { + **(variables if isinstance(variables, dict) else {}), + "code" : code, + "list" : "" + } + + if code: + + i = 0 + l = len(messages) if isinstance(messages, (list, tuple)) else 0 + has_i18n = hasattr(self, "i18n") + + while 1 << i <= code: + if code & (1 << i): + message = messages[i] if i < l else "error_message_" + str(i) + variables["list"] += "\n [" + str(i) + "] " + (self.i18n(message, variables) if has_i18n else message) + i += 1 + + if error_message: + self._print("warn", error_message, variables, None, k) + + return False + + if ok_message: + self._print("ok", ok_message, variables, None, k) + + return True + + def path_format(self, path): + return self.re_slashes.sub(self.__slash, path) + + def get_path(self, path, show_errors = None): + + real_path = None + error = ( + 1 << 0 if path == None else + 1 << 1 if not isinstance(path, str) else + 1 << 2 if not path else + 0) << 1 + has_show_errors = isinstance(show_errors, bool) + + if not error: + for root in self.__roots_paths: + current_path = self.path_format((root + "/" if root else "") + path) + if path_exists(current_path): + real_path = current_path + break + if real_path == None: + error |= 1 << 4 + + self.validate( + error, + ( + "exception", + "path_null", + "path_not_string", + "path_empty", + "path_not_exists" + ), + { + "path" : path, + "real_path" : real_path + }, + (show_errors if has_show_errors else self.__show_get_path_error) and "get_path_error", + (show_errors if has_show_errors else True) and self.__show_get_path_ok and "get_path_ok" + ) + + return (real_path, error) + + def make_directory(self, path, show_errors = None): + + error = ( + 1 << 0 if path == None else + 1 << 1 if not isinstance(path, str) else + 1 << 2 if not path else + 0) << 1 + has_show_errors = isinstance(show_errors, bool) + + if not error: + try: + directory = "" + if path[0] != "/": + directory = path[0:1] + path = path[2:] + for level in self.re_slashes.split(self.re_directory.sub(r'\1', path)): + directory += self.__slash + level + not path_exists(directory) and make_directories(directory) + except Exception as exception: + error |= 1 << 0 + self.exception(exception, self.__show_make_directory_exception and "make_directory_exception", { + "path" : path + }) + + self.validate( + error, + ( + "exception", + "path_null", + "path_not_string", + "path_empty" + ), + {"path" : path}, + (show_errors if has_show_errors else self.__show_make_directory_error) and "make_directory_error", + (show_errors if has_show_errors else True) and self.__show_make_directory_ok and "make_directory_ok" + ) + + return error + + @staticmethod + def __table_exists(cursor, name): + + cursor.execute("select * from sqlite_master where type = 'table' and name = '" + name + "' limit 1") + results = cursor.fetchall() + + return len(results) + + @staticmethod + def __column_exists(cursor, table, name): + + cursor.execute("pragma table_info('" + table + "')") + results = cursor.fetchall() + + if len(results): + for row in results: + if dict(row)["name"] == name: + return True + return False + + @staticmethod + def __foreign_exists(cursor, table, name, foreign): + + cursor.execute("pragma foreign_key_list('" + table + "')") + results = cursor.fetchall() + + if len(results): + for row in results: + row = dict(row) + if row["table"] == foreign and row["from"] == name: + return True + return False + + @classmethod + def pascal_to_snake(self, name): + + def callback(matches): + return "_" + matches.group(1).lower() + + return self.re_pascal_capitals.sub(callback, name)[1:] + + @classmethod + def __table_create(self, cursor, name, fields, foreigns = []): + + key = self.pascal_to_snake(name) + + if self.__table_exists(cursor, name): + for field in fields: + key = self.re_field_key.sub(r'\1', field) + if not self.__column_exists(cursor, name, key): + cursor.execute("alter table " + name + " add column " + field.replace(" not null", "")) + for field, foreign in foreigns: + if not self.__foreign_exists(cursor, name, field, foreign): + cursor.execute("alter table " + name + " add constraint " + key + "_" + field + " foreign key(" + field + ") references " + foreign + "(id)") + else: + cursor.execute("create table if not exists " + name + "(" + + "id integer not null primary key autoincrement, " + + "".join([field + ", " for field in fields]) + + "date_in datetime not null default (datetime('now')), " + + "date_out datetime" + + "".join([", constraint " + key + "_" + field + " foreign key(" + field + ") references " + foreign + "(id)" for field, foreign in foreigns]) + + ")") + + def __get_database_connection(self, show_errors = None): + + error = ( + (1 << 0 if self.__error else 0) | + 0) << 1 + connection = None + has_show_errors = isinstance(show_errors, bool) + + if not error: + self.make_directory(self.__database_path) + try: + + connection = sqlite3.connect(self.__database_path) + + connection.row_factory = sqlite3.Row + + except Exception as exception: + self.exception(exception, self.__show_get_database_connection_exception and "get_database_connection_exception", { + "path" : self.__database_path + }) + + self.validate( + error, + ( + "exception", + "settings_has_errors" + ), + {}, + (show_errors if has_show_errors else self.__show_get_database_connection_error) and "get_database_connection_error", + (show_errors if has_show_errors else True) and self.__show_get_database_connection_ok and "get_database_connection_ok" + ) + + return (connection, error) + + def __close_database_connection(self, connection, has_errors, show_errors = None): + + error = ( + (( + 1 << 0 if connection == None else + 1 << 1 if not isinstance(connection, sqlite3.Connection) else + 0) << 0) | + (( + 1 << 0 if has_errors == None else + 1 << 1 if not isinstance(has_errors, (bool, int)) else + 0) << 2) | + 0) << 1 + has_show_errors = isinstance(show_errors, bool) + + if not error: + try: + if has_errors: + try:connection.rollback() + except:pass + else: + connection.commit() + try:connection.close() + except:pass + except Exception as exception: + self.exception(exception, self.__show_close_database_connection_exception and "close_database_connection_exception", { + "path" : self.__database_path + }) + + self.validate( + error, + ( + "exception", + "connection_null", + "connection_bad_typed", + "has_errors_null", + "has_errors_bad_typed" + ), + {}, + (show_errors if has_show_errors else self.__show_close_database_connection_error) and "close_database_connection_error", + (show_errors if has_show_errors else True) and self.__show_close_database_connection_ok and "close_database_connection_ok" + ) + + return error + + def __build_database(self, show_errors = None): + + error = 0 + has_show_errors = isinstance(show_errors, bool) + + for name, fields, foreigns in ( + ( + "Hosts", [ + "name varchar(32) not null" + ], [] + ), + ( + "Sessions", [ + "host integer not null", + "samples integer not null", + "date_last datetime not null default (datetime('now'))" + ], [ + ("host", "Hosts") + ] + ), + ( + "Sites", [ + "site varchar(128) not null" + ], [] + ), + ( + "Pings", [ + "session integer not null", + "site integer not null", + "sequence integer not null", + "i integer not null", + "time float not null", + "date datetime not null" + ], [ + ("session", "Sessions"), + ("site", "Sites") + ] + ), + ( + "RawPings", [ + "session integer not null", + "site integer not null", + "date_from datetime not null", + "date_to datetime not null", + "sequence integer not null", + "data text not null" + ], [ + ("session", "Sessions"), + ("site", "Sites") + ] + ) + ): + connection, suberror = self.__get_database_connection() + if not suberror: + error |= suberror + try: + self.__table_create(connection.cursor(), name, fields, foreigns) + except Exception as exception: + error |= 1 << 2 + self.exception(exception, self.__show_build_database_exception and "build_database_exception", { + "path" : self.__database_path + }) + finally: + self.__close_database_connection(connection, error) + + self.validate( + error, + ( + "exception", + "settings_has_errors" + ), + {}, + (show_errors if has_show_errors else self.__show_build_database_error) and "build_database_error", + (show_errors if has_show_errors else True) and self.__show_build_database_ok and "build_database_ok" + ) + + return error + + @classmethod + def sql_string_fix(self, string): + return self.re_sql_semicolons.sub(r'\'\'', string) + + @staticmethod + def sql_datetime(date): + + if isinstance(date, float): + date = datetime.fromtimestamp(date) + + return "'" + ( + ("0000" + str(date.year))[-4:] + "-" + + ("00" + str(date.month))[-2:] + "-" + + ("00" + str(date.day))[-2:] + " " + + ("00" + str(date.hour))[-2:] + ":" + + ("00" + str(date.minute))[-2:] + ":" + + ("00" + str(date.second))[-2:] + ) + "'" + + def __update_database(self, show_errors = None): + + raw_pings_l = len(self.__cache["raw_pings"]) + pings_l = len(self.__cache["pings"]) + + if not (raw_pings_l or pings_l): + return 0 + + connection, error = self.__get_database_connection() + has_show_errors = isinstance(show_errors, bool) + + if not error: + try: + + cursor = connection.cursor() + session_id = None + sites_id = {} + + if self.__session == None: + + host_id = cursor.execute("select id from Hosts where date_out is null and name = '" + self.__host_id + "' limit 1").fetchall() + + if len(host_id): + host_id = int(dict(host_id[0])["id"]) + else: + cursor.execute("insert into Hosts(name) values('" + self.__host_id + "')") + host_id = cursor.lastrowid + + cursor.execute("insert into Sessions(host, samples) values(" + str(host_id) + ", " + str(self.__samples) + ")") + session_id = cursor.lastrowid + + for ping in self.__cache["raw_pings" if raw_pings_l else "pings"]: + if ping["site"] not in sites_id: + sites_id[ping["site"]] = cursor.execute("select id from Sites where date_out is null and site = '" + ping["site"] + "' limit 1").fetchall() + if len(sites_id[ping["site"]]): + sites_id[ping["site"]] = int(dict(sites_id[ping["site"]][0])["id"]) + else: + cursor.execute("insert into Sites(site) values('" + ping["site"] + "')") + sites_id[ping["site"]] = cursor.lastrowid + + if raw_pings_l: + if self.__allow_save_raw_ping: + cursor.execute("insert into RawPings('session', site, date_from, date_to, sequence, 'data') values " + ", ".join([ + "(" + str(session_id) + ", " + str(sites_id[ping["site"]]) + ", " + self.sql_datetime(ping["from"]) + ", " + self.sql_datetime(ping["to"]) + ", " + str(ping["sequence"]) + ", '" + self.sql_string_fix("\n".join(ping["raw_lines"])) + "')" for ping in self.__cache["raw_pings"][0:raw_pings_l - 1] + ])) + self.__cache["raw_pings"] = self.__cache["raw_pings"][raw_pings_l:] + + if pings_l: + cursor.execute("insert into Pings('session', site, sequence, i, 'time', 'date') values " + ", ".join([ + "(" + str(session_id) + ", " + str(sites_id[ping["site"]]) + ", " + str(ping["sequence"]) + ", " + str(ping["i"]) + ", " + str(ping["time"]) + ", " + self.sql_datetime(ping["date"]) + ")" for ping in self.__cache["pings"][0:pings_l - 1] + ])) + self.__cache["pings"] = self.__cache["pings"][pings_l:] + + except Exception as exception: + error |= 1 << 4 + self.exception(exception, "sqlite_connection_error", { + "path" : self.__database_path + }) + finally: + self.__close_database_connection(connection, error) + + self.validate( + error, + ( + "exception", + "settings_has_errors" + ), + {}, + (show_errors if has_show_errors else self.__show_update_database_error) and "update_database_error", + (show_errors if has_show_errors else True) and self.__show_update_database_ok and "update_database_ok" + ) + + return error + +internet_checker = InternetChecker(settings) \ No newline at end of file diff --git a/version b/version new file mode 100644 index 0000000..95dfee2 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.0.23 \ No newline at end of file