diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index d8fd15ec..e69de29b 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -1,6 +0,0 @@ -__all__ = ["Microscope", "config", "utilities"] -__version__ = "0.1.0" - -from .microscope import Microscope -from . import config -from . import utilities diff --git a/openflexure_microscope/api/microscope.py b/openflexure_microscope/api/microscope.py index 09711e50..d9496684 100644 --- a/openflexure_microscope/api/microscope.py +++ b/openflexure_microscope/api/microscope.py @@ -1,4 +1,4 @@ -from openflexure_microscope import Microscope +from openflexure_microscope.microscope import Microscope import logging diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 071a52c8..70bee3ec 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -98,7 +98,9 @@ def capture_from_exif(path, exif_dict): try: image_metadata = exif_dict.pop("image") except KeyError as e: - logging.error(f"Unable to obtain OpenFlexure metadata from file {path}") + logging.error( + f"Unable to obtain valid 2.0 OpenFlexure metadata from file {path}" + ) return None # Populate capture parameters diff --git a/openflexure_microscope/rescue/__init__.py b/openflexure_microscope/rescue/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openflexure_microscope/rescue/auto.py b/openflexure_microscope/rescue/auto.py new file mode 100644 index 00000000..9983c1a9 --- /dev/null +++ b/openflexure_microscope/rescue/auto.py @@ -0,0 +1,77 @@ +from openflexure_microscope.paths import ( + FALLBACK_OPENFLEXURE_VAR_PATH, + PREFERRED_OPENFLEXURE_VAR_PATH, +) +import logging +import os + +from . import check_settings, check_capture_reload + +# Paths for suggestions +LOGS_PATHS = [ + os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "logs"), + os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "logs"), +] +SETTINGS_PATHS = [ + os.path.join(var_path, "settings", "microscope_settings.json") + for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH) +] +CONFIG_PATHS = [ + os.path.join(var_path, "settings", "microscope_configuration.json") + for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH) +] +DATA_PATHS = [ + os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "data"), + os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "data"), +] + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +logging.debug("Testing debug logger. One two one two.") + + +class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +error_keys = { + "config_settings_import_error": "The configuration submodule could not be imported properly. This is usually because the default config or settings files are badly broken somehow. See further errors for details", + "default_config_empty": "The default configuration file is empty, and could not be automatically populated.", + "default_settings_empty": "The default settings file is empty, and could not be automatically populated.", + "default_config_error": f"The default configuration file could not be parsed. To fix, consider backing up and deleting your configuration files from {CONFIG_PATHS} to reset the configuration.", + "default_settings_error": f"The default settings file could not be parsed. To fix, consider backing up and deleting your configuration files from {SETTINGS_PATHS} to reset the configuration.", + "capture_rebuild_timeout": f"Capture database rebuilding took a long time. This may not cause catastrophic errors, but rather will cause the server to hang for a while. To fix, consider moving your captures from {DATA_PATHS} to another location.", +} + +if __name__ == "__main__": + spoof = False + + error_sources = [] + + if spoof: + error_sources = list(error_keys.keys()) + + error_sources.extend(check_settings.main()) + error_sources.extend(check_capture_reload.main()) + + if not error_sources: + + print() + print(bcolors.OKGREEN + "No errors found!" + bcolors.ENDC) + print( + "That's not to say everything is fine, only that our automatic diagnostics couldn't find much." + ) + print(f"You can check through the server logs at {LOGS_PATHS}") + + else: + for err_code in error_sources: + logging.error(err_code) + if error_keys.get(err_code): + print(bcolors.FAIL + error_keys.get(err_code) + bcolors.ENDC) diff --git a/openflexure_microscope/rescue/check_capture_reload.py b/openflexure_microscope/rescue/check_capture_reload.py new file mode 100644 index 00000000..3a61c692 --- /dev/null +++ b/openflexure_microscope/rescue/check_capture_reload.py @@ -0,0 +1,34 @@ +from openflexure_microscope.rescue.monitor_timeout import launch_timeout_test_process +from openflexure_microscope.camera.capture import build_captures_from_exif +from openflexure_microscope.camera.base import BASE_CAPTURE_PATH +from openflexure_microscope.config import user_settings + +import logging + + +def check_capture_rebuild(timeout=10): + logging.info("Loading user settings...") + settings = user_settings.load() + + cap_path = str(settings.get("camera", {}).get("paths", {}).get("default")) + logging.info(f"Capture path found: {cap_path}") + if not cap_path: + logging.error( + "No capture path defined in settings. This is unusual for anything other than a first-run. \nFalling back to default path." + ) + cap_path = BASE_CAPTURE_PATH + + logging.info("Starting capture reload with a 10 second timeout...") + passed_timeout_test = launch_timeout_test_process( + build_captures_from_exif, args=(cap_path,) + ) + + return passed_timeout_test + + +def main(): + error_sources = [] + passed_timeout = check_capture_rebuild() + if not passed_timeout: + error_sources.append("capture_rebuild_timeout") + return error_sources diff --git a/openflexure_microscope/rescue/check_settings.py b/openflexure_microscope/rescue/check_settings.py new file mode 100644 index 00000000..9ba33ba7 --- /dev/null +++ b/openflexure_microscope/rescue/check_settings.py @@ -0,0 +1,47 @@ +import logging +import json + +ERROR_SOURCES = [] + + +def trace_config_exceptions(): + error_sources = [] + + from openflexure_microscope.paths import ( + DEFAULT_CONFIGURATION_FILE_PATH, + SETTINGS_FILE_PATH, + ) + + try: + default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH) + if not default_config: + error_sources.append("default_config_empty") + except Exception as e: + logging.error("Error parsing config:") + logging.error(e) + error_sources.append("default_config_error") + + try: + default_settings = json.load(SETTINGS_FILE_PATH) + if not default_settings: + error_sources.append("default_settings_empty") + except Exception as e: + logging.error("Error parsing settings:") + logging.error(e) + error_sources.append("default_settings_error") + + return error_sources + + +def main(): + error_sources = [] + logging.info("Attempting default settings and config import...") + try: + from openflexure_microscope import config + except Exception as e: + error_sources.append("config_settings_import_error") + logging.error("Error importing config:") + logging.error(e) + error_sources.extend(trace_config_exceptions()) + + return error_sources diff --git a/openflexure_microscope/rescue/monitor_service.py b/openflexure_microscope/rescue/monitor_service.py new file mode 100644 index 00000000..f12ab1b9 --- /dev/null +++ b/openflexure_microscope/rescue/monitor_service.py @@ -0,0 +1,59 @@ +#!/bin/python +# +# Copyright 2016 Flavio Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Usage: monitor_process.py +# +# Example(crontab, every 5 minutes): +# */5 * * * * /root/bin/monitor_service.py prosody > /dev/null 2>&1 +# + +import sys +import subprocess + + +class ServiceMonitor(object): + def __init__(self, service): + self.service = service + + def is_active(self): + """Return True if service is running""" + for line in self.status(): + if "Active:" in line: + if "(running)" in line: + return True + return False + + def status(self): + cmd = f"/bin/systemctl status {self.service}.service" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + stdout_list = proc.communicate()[0].decode().split("\n") + return stdout_list + + def start(self): + cmd = f"/bin/systemctl start {self.service}.service" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + proc.communicate() + + def stop(self): + cmd = f"/bin/systemctl stop {self.service}.service" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + proc.communicate() + + def log(self, n_lines: int = 100): + cmd = f"/bin/journalctl -u {self.service}.service -n {n_lines} --no-pager" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + stdout_list = proc.communicate()[0].decode().split("\n") + return stdout_list diff --git a/openflexure_microscope/rescue/monitor_timeout.py b/openflexure_microscope/rescue/monitor_timeout.py new file mode 100644 index 00000000..b02b29b6 --- /dev/null +++ b/openflexure_microscope/rescue/monitor_timeout.py @@ -0,0 +1,39 @@ +import multiprocessing +import logging +import time + +# bar +def test_long_fn(): + for _ in range(100): + print("Tick") + time.sleep(1) + + +def launch_timeout_test_process(target, args=(), kwargs=None, timeout=10): + if not kwargs: + kwargs = {} + + # Start target as a process + p = multiprocessing.Process(target=target, args=args, kwargs=kwargs) + p.start() + + # Wait for 10 seconds or until process finishes + p.join(timeout) + + # If thread is still active + if p.is_alive(): + logging.error( + f"Function {target} reached timeout after {timeout} seconds. Terminating." + ) + + # Terminate + p.terminate() + p.join() + + return False + else: + return True + + +if __name__ == "__main__": + launch_timeout_test_process(test_long_fn, timeout=5)