diff --git a/.circleci/config.yml b/.circleci/config.yml index 555721e..f61cc95 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,11 +42,11 @@ jobs: - store_artifacts: path: htmlcov - store_artifacts: - path: debian/little-brother_0.3.10_82.deb + path: debian/little-brother_0.3.11_82.deb - persist_to_workspace: root: debian paths: - - little-brother_0.3.10_82.deb + - little-brother_0.3.11_82.deb build_pypi: #working_directory: ~ docker: @@ -61,11 +61,11 @@ jobs: - run: PYTHONPATH=contrib/python_base_app python3 ci_toolbox.py --execute-stage BUILD --use-dev-dir=. - store_artifacts: - path: "dist/little-brother-0.3.10.tar.gz" + path: "dist/little-brother-0.3.11.tar.gz" - persist_to_workspace: root: dist paths: - - "little-brother-0.3.10.tar.gz" + - "little-brother-0.3.11.tar.gz" install_pypi: #working_directory: ~ docker: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b4a45ea..34bafb4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,7 +62,7 @@ build_pypi: artifacts: when: always paths: - - dist/little-brother-0.3.10.tar.gz + - dist/little-brother-0.3.11.tar.gz variables: # Suppress automatic checkout for all sub modules GIT_SUBMODULE_STRATEGY: recursive diff --git a/CHANGES.md b/CHANGES.md index d8c5fb6..a789fd4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,15 @@ This document lists all changes of `LittleBrother` with the most recent changes at the top. +## Version 0.3.11 Revision 82 (February 6th, 2021) + +* Closes #28, see [here](https://github.com/marcus67/little_brother/issues/28) +* Closes #113, see [here](https://github.com/marcus67/little_brother/issues/113) +* Closes #112, see [here](https://github.com/marcus67/little_brother/issues/112) (presumably) +* Closes #58, see [here](https://github.com/marcus67/little_brother/issues/58) (presumably) +* Closes #110, see [here](https://github.com/marcus67/little_brother/issues/110) +* Closes #86, see [here](https://github.com/marcus67/little_brother/issues/86) + ## Version 0.3.10 Revision 81 (January 17th, 2021) * Upgrade to python_base_app 0.2.9 diff --git a/bin/generic-install.sh b/bin/generic-install.sh index 65cad16..32fa8ad 100755 --- a/bin/generic-install.sh +++ b/bin/generic-install.sh @@ -47,20 +47,20 @@ if [ ! "$EUID" == "0" ] ; then fi echo "Checking if all Pip packages have been downloaded to $TMP_DIR..." -if [ ! -f $TMP_DIR/little-brother-0.3.10.tar.gz ] ; then - echo "ERROR: package little-brother-0.3.10.tar.gz not found in $TMP_DIR!" +if [ ! -f $TMP_DIR/little-brother-0.3.11.tar.gz ] ; then + echo "ERROR: package little-brother-0.3.11.tar.gz not found in $TMP_DIR!" echo "Download from test.pypi.org and execute again." exit 2 else - echo "Package little-brother-0.3.10.tar.gz was found." + echo "Package little-brother-0.3.11.tar.gz was found." fi -if [ ! -f $TMP_DIR/python-base-app-0.2.9.tar.gz ] ; then - echo "ERROR: package python-base-app-0.2.9.tar.gz not found in $TMP_DIR!" +if [ ! -f $TMP_DIR/python-base-app-0.2.13.tar.gz ] ; then + echo "ERROR: package python-base-app-0.2.13.tar.gz not found in $TMP_DIR!" echo "Download from test.pypi.org and execute again." exit 2 else - echo "Package python-base-app-0.2.9.tar.gz was found." + echo "Package python-base-app-0.2.13.tar.gz was found." fi if [ ! -f $TMP_DIR/some-flask-helpers-0.1.tar.gz ] ; then @@ -182,19 +182,19 @@ chmod og-rwx /etc/little-brother/little-brother.config ${PIP3} --version ${PIP3} install wheel setuptools echo "Installing PIP packages..." -echo " * little-brother-0.3.10.tar.gz" -echo " * python-base-app-0.2.9.tar.gz" +echo " * little-brother-0.3.11.tar.gz" +echo " * python-base-app-0.2.13.tar.gz" echo " * some-flask-helpers-0.1.tar.gz" # see https://stackoverflow.com/questions/19548957/can-i-force-pip-to-reinstall-the-current-version ${PIP3} install --upgrade --force-reinstall \ - ${TMP_DIR}/little-brother-0.3.10.tar.gz\ - ${TMP_DIR}/python-base-app-0.2.9.tar.gz\ + ${TMP_DIR}/little-brother-0.3.11.tar.gz\ + ${TMP_DIR}/python-base-app-0.2.13.tar.gz\ ${TMP_DIR}/some-flask-helpers-0.1.tar.gz -echo "Removing installation file ${TMP_DIR}/little-brother-0.3.10.tar.gz..." -rm ${TMP_DIR}/little-brother-0.3.10.tar.gz -echo "Removing installation file ${TMP_DIR}/python-base-app-0.2.9.tar.gz..." -rm ${TMP_DIR}/python-base-app-0.2.9.tar.gz +echo "Removing installation file ${TMP_DIR}/little-brother-0.3.11.tar.gz..." +rm ${TMP_DIR}/little-brother-0.3.11.tar.gz +echo "Removing installation file ${TMP_DIR}/python-base-app-0.2.13.tar.gz..." +rm ${TMP_DIR}/python-base-app-0.2.13.tar.gz echo "Removing installation file ${TMP_DIR}/some-flask-helpers-0.1.tar.gz..." rm ${TMP_DIR}/some-flask-helpers-0.1.tar.gz \ No newline at end of file diff --git a/contrib/python_base_app b/contrib/python_base_app index 27c8a9d..38e2ddd 160000 --- a/contrib/python_base_app +++ b/contrib/python_base_app @@ -1 +1 @@ -Subproject commit 27c8a9d185b39b8a7f599e617168ed100569439f +Subproject commit 38e2ddd4f9b0e7adfa42d9ffa2fbf82284937cf2 diff --git a/etc/master.config b/etc/master.config index 7cbf8d2..11789b0 100644 --- a/etc/master.config +++ b/etc/master.config @@ -50,6 +50,11 @@ scan_active=true # Default: 7 #process_lookback_in_days = 14 +# Sets the number of days that entries of entities 'process_info', 'admin_event' and 'rule_override' will be kept +# in the database. This number should always be at least one day larger than 'process_lookback_in_days'! +# Default: 180 +#history_length_in_days = 30 + # Sets the number of future days that user play time configuration will be available in the frontend. # Default: 7 #admin_lookahead_in_days = 14 diff --git a/little_brother/app.py b/little_brother/app.py index 4791587..7b7e7a3 100644 --- a/little_brother/app.py +++ b/little_brother/app.py @@ -46,6 +46,7 @@ PACKAGE_NAME = 'little_brother' DEFAULT_USER_HANDLER = unix_user_handler.HANDLER_NAME +DEFAULT_CLEAN_HISTORY_INTERVAL = 24 * 60 * 60 # seconds class AppConfigModel(base_app.BaseAppConfigModel): @@ -54,6 +55,7 @@ def __init__(self): super(AppConfigModel, self).__init__(APP_NAME) self.check_interval = base_app.DEFAULT_TASK_INTERVAL + self.clean_history_interval = DEFAULT_CLEAN_HISTORY_INTERVAL def get_argument_parser(p_app_name): @@ -303,6 +305,14 @@ def prepare_services(self, p_full_startup=True): p_interval=self._client_device_handler.check_interval) self.add_recurring_task(p_recurring_task=task) + if self.is_master(): + task = base_app.RecurringTask( + p_name="app_control.clean_history", + p_handler_method=lambda: self._app_control.clean_history(), + p_interval=self._app_config.clean_history_interval) + self.add_recurring_task(p_recurring_task=task) + + if status_server_config.is_active(): self._status_server = status_server.StatusServer( p_config=self._config[status_server.SECTION_NAME], diff --git a/little_brother/app_control.py b/little_brother/app_control.py index 4861dc1..21bd89e 100644 --- a/little_brother/app_control.py +++ b/little_brother/app_control.py @@ -47,6 +47,7 @@ DEFAULT_SCAN_ACTIVE = True DEFAULT_ADMIN_LOOKAHEAD_IN_DAYS = 7 # days DEFAULT_PROCESS_LOOKUP_IN_DAYS = 7 # days +DEFAULT_HISTORY_LENGTH_IN_DAYS = 180 # days DEFAULT_MIN_ACTIVITY_DURATION = 60 # seconds DEFAULT_CHECK_INTERVAL = 5 # seconds DEFAULT_INDEX_REFRESH_INTERVAL = 60 # seconds @@ -74,6 +75,7 @@ def __init__(self): super(AppControlConfigModel, self).__init__(p_section_name=SECTION_NAME) self.process_lookback_in_days = DEFAULT_PROCESS_LOOKUP_IN_DAYS + self.history_length_in_days = DEFAULT_HISTORY_LENGTH_IN_DAYS self.admin_lookahead_in_days = DEFAULT_ADMIN_LOOKAHEAD_IN_DAYS self.server_group = login_mapping.DEFAULT_SERVER_GROUP self.hostname = configuration.NONE_STRING @@ -102,12 +104,21 @@ def node_type(self): return _("Master") if self.is_master else _("Slave") @property - def last_message_string(self): + def seconds_without_ping(self): if self.last_message is None: + return None + + return (tools.get_current_time() - self.last_message).seconds + + @property + def last_message_string(self): + + some_seconds_without_ping = self.seconds_without_ping + + if some_seconds_without_ping is None: return _("n/a") - seconds_without_ping = (tools.get_current_time() - self.last_message).seconds - return tools.get_duration_as_string(seconds_without_ping) + return tools.get_duration_as_string(some_seconds_without_ping) @property @@ -465,6 +476,10 @@ def stop(self): if not self.is_master(): self.send_events() + def clean_history(self): + self._persistence.delete_historic_entries(p_history_length_in_days=self._config.history_length_in_days) + + def queue_event(self, p_event, p_to_master=False, p_is_action=False): if p_is_action: @@ -533,7 +548,14 @@ def handle_event_process_downtime(self, p_event): def handle_event_process_start(self, p_event): - pinfo, updated = self.get_process_handler(p_id=p_event.processhandler).handle_event_process_start(p_event) + process_handler = self.get_process_handler(p_id=p_event.processhandler) + + if process_handler is None: + msg = "Received event for process handler of type id '{id}' which is not registered -> discarding event" + self._logger.warning(msg.format(id=p_event.processhandler)) + return + + pinfo, updated = process_handler.handle_event_process_start(p_event) if updated: if self._persistence is not None: @@ -877,14 +899,14 @@ def pick_text_for_approaching_logout(self, p_rule_result_info): t = gettext.translation('messages', localedir=self._locale_dir, languages=[p_rule_result_info.locale], fallback=True) - if p_rule_result_info.approaching_logout_rules & rule_handler.RULE_TIME_PER_DAY: - return t.gettext(self.text_no_time_left_approaching).format(**p_rule_result_info.args) + if p_rule_result_info.approaching_logout_rules & rule_handler.RULE_ACTIVITY_DURATION: + return t.gettext(self.text_need_break_approaching).format(**p_rule_result_info.args) elif p_rule_result_info.approaching_logout_rules & rule_handler.RULE_TOO_LATE: return t.gettext(self.text_too_late_approaching).format(**p_rule_result_info.args) - elif p_rule_result_info.approaching_logout_rules & rule_handler.RULE_ACTIVITY_DURATION: - return t.gettext(self.text_need_break_approaching).format(**p_rule_result_info.args) + elif p_rule_result_info.approaching_logout_rules & rule_handler.RULE_TIME_PER_DAY: + return t.gettext(self.text_no_time_left_approaching).format(**p_rule_result_info.args) else: fmt = "pick_text_for_approaching_logout(): cannot derive text for rule result %d" % p_rule_result_info.approaching_logout_rules diff --git a/little_brother/persistence.py b/little_brother/persistence.py index 90e9f3e..c3b1e93 100644 --- a/little_brother/persistence.py +++ b/little_brother/persistence.py @@ -829,7 +829,7 @@ def update_rule_override(self, p_rule_override): def load_process_infos(self, p_lookback_in_days): with SessionContext(self) as session_context: - session_context = SessionContext(self) +# session_context = SessionContext(self) session = session_context.get_session() reference_time = datetime.datetime.now() + datetime.timedelta(days=-p_lookback_in_days) @@ -846,6 +846,44 @@ def load_process_infos(self, p_lookback_in_days): return result + def delete_historic_entries(self, p_history_length_in_days): + + msg = "Deleting historic entries older than {days} days..." + self._logger.info(msg.format(days=p_history_length_in_days)) + + with SessionContext(self) as session_context: + session = session_context.get_session() + reference_time = datetime.datetime.now() + datetime.timedelta(days=-p_history_length_in_days) + reference_date = reference_time.date() + + result = session.query(RuleOverride).filter(RuleOverride.reference_date < reference_date).all() + + msg = "Deleting {count} rule override entries..." + self._logger.info(msg.format(count=len(result))) + + for override in result: + session.delete(override) + + result = session.query(AdminEvent).filter(AdminEvent.event_time < reference_time).all() + + msg = "Deleting {count} admin events..." + self._logger.info(msg.format(count=len(result))) + + for event in result: + session.delete(event) + + result = session.query(ProcessInfo).filter(ProcessInfo.start_time < reference_time).all() + + msg = "Deleting {count} process infos..." + self._logger.info(msg.format(count=len(result))) + + for pinfo in result: + session.delete(pinfo) + + session.commit() + + + def load_rule_overrides(self, p_lookback_in_days): session = self.get_session() diff --git a/little_brother/settings.py b/little_brother/settings.py index 5b7ff8a..b130c1b 100644 --- a/little_brother/settings.py +++ b/little_brother/settings.py @@ -18,7 +18,7 @@ settings = { "name": "little-brother", "url": "https://github.com/marcus67/little_brother", - "version": "0.3.10", + "version": "0.3.11", "description": "Simple parental control application monitoring specific processes on Linux hosts " "to monitor and limit the play time of (young) children.", "author": "Marcus Rickert", diff --git a/little_brother/status_server.py b/little_brother/status_server.py index 321f399..1ce8d69 100644 --- a/little_brother/status_server.py +++ b/little_brother/status_server.py @@ -15,6 +15,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import datetime import gettext import os @@ -23,6 +24,7 @@ import flask_babel import flask_login import flask_wtf +import humanize import little_brother from some_flask_helpers import blueprint_adapter @@ -170,6 +172,7 @@ def __init__(self, self._app.jinja_env.filters['format_babel_date'] = self.format_babel_date self._app.jinja_env.filters['format_text_array'] = self.format_text_array self._app.jinja_env.filters['invert'] = self.invert + self._app.jinja_env.filters['seconds_as_humanized_duration'] = self.format_seconds_as_humanized_duration self._app.jinja_env.filters['_base'] = self._base_gettext self._babel = flask_babel.Babel(self._app) @@ -236,6 +239,21 @@ def format_time(self, value): else: return value.strftime(self._config.time_format) + def format_seconds_as_humanized_duration(self, seconds): + + if seconds is None: + return _("n/a") + + try: + # trying to activate the localization for a non-existing locale (including 'en'!) triggers an exception + if self._locale_helper.locale is not None: + humanize.i18n.activate(self._locale_helper.locale) + + except Exception: + humanize.i18n.deactivate() + + return humanize.naturaldelta(datetime.timedelta(seconds=seconds)) + @staticmethod def format_seconds(value): diff --git a/little_brother/templates/topology.template.html b/little_brother/templates/topology.template.html index 868faa8..4b010ee 100644 --- a/little_brother/templates/topology.template.html +++ b/little_brother/templates/topology.template.html @@ -66,7 +66,7 @@