diff --git a/launcher/actions.py b/launcher/actions.py index 1f52ef4..f18b694 100644 --- a/launcher/actions.py +++ b/launcher/actions.py @@ -2,7 +2,8 @@ import subprocess import shutil class ActionDispatcher: - def dispatch(self, service_type, config): + def dispatch(self, config): + service_type = config.get("type", "command") print(f"Executing action -> Type: {service_type}") if service_type == "browser": diff --git a/launcher/checks.py b/launcher/checks.py index b53e772..7c86441 100644 --- a/launcher/checks.py +++ b/launcher/checks.py @@ -1,24 +1,74 @@ # checks.py import socket +import threading import urllib.request import urllib.error class SystemChecks: - def __init__(self, network_targets, ziti_target): self.network_targets = network_targets self.ziti_target = ziti_target + self.network_online = None + self.ziti_online = None + + self._stop_event = threading.Event() + self._thread = None + + @property + def ready(self): + """Return True when network & ziti are both available""" + return ( + self.network_online is True + and self.ziti_online is True + ) + + def start(self): + """Start background status polling""" + if self._thread is not None: + return + + self._thread = threading.Thread( + target=self._poll, + daemon=True + ) + + self._thread.start() + + def stop(self): + """Stop background status polling""" + self._stop_event.set() + + def _poll(self): + """Continuously check system status""" + while not self._stop_event.is_set(): + self.network_online = self.check_network_status() + self.ziti_online = self.check_ziti_status() + + print( + f"System status => " + f"Network: {self.network_online}, " + f"Ziti: {self.ziti_online}" + ) + + if self.ready: + interval = 30 + else: + interval = 3 + + self._stop_event.wait(interval) + def check_network_status(self): - """Loops through configured network targets and returns True if any succeed.""" + """Loops through configured network targets and return True if any respond""" for host, port in self.network_targets: if self._tcp_ping(host, int(port), timeout=1.5): return True + return False def check_ziti_status(self): - """Makes an HTTP request to the configured address.""" + """Makes HTTP request to Ziti endpoint""" if not self.ziti_target: return False diff --git a/launcher/config.py b/launcher/config.py index a54d093..3e422d4 100644 --- a/launcher/config.py +++ b/launcher/config.py @@ -6,7 +6,7 @@ DEFAULT_CONFIG = { "app_title": "FJLA Gateway", "checks": { "network_targets": [["1.1.1.1", 53], ["8.8.8.8", 53]], - "ziti_target": ["127.0.0.1", 3020], + "ziti_target": "http://test.ziti", }, "services": [ { diff --git a/launcher/main.py b/launcher/main.py index 332583d..7be0479 100644 --- a/launcher/main.py +++ b/launcher/main.py @@ -15,7 +15,9 @@ class LauncherApp(Gtk.Application): super().__init__(application_id="com.local.Launcher") self.config = load_launcher_config() + check_cfg = self.config.get("checks", {}) + self.checks = SystemChecks( network_targets=check_cfg.get("network_targets"), ziti_target=check_cfg.get("ziti_target"), @@ -23,35 +25,24 @@ class LauncherApp(Gtk.Application): self.dispatcher = ActionDispatcher() self.ui = UIController( - status_provider_callback=lambda: { - "net": self.checks.check_network_status(), - "ziti": self.checks.check_ziti_status(), - }, + system_checks=self.checks, power_action_callback=self.dispatcher.dispatch, + service_action_callback=self.dispatcher.dispatch, + services=self.config.get("services", []), ) def do_activate(self): + self.checks.start() + self.ui.set_app_title(self.config.get("app_title", "Launcher")) - for service in self.config.get("services", []): - title = service.get("title", "Service") - desc = service.get("desc", "") - bg_img = service.get("bg_image", "") - - self.ui.add_card( - title, - desc, - bg_img, - lambda svc=service: self.dispatcher.dispatch( - svc.get("type", "command"), - svc, - ), - ) - self.ui.setup_exit_shortcut(self.quit) self.ui.present(self) + def do_shutdown(self): + self.checks.stop() + Gtk.Application.do_shutdown(self) if __name__ == "__main__": app = LauncherApp() diff --git a/launcher/style.css b/launcher/style.css index 340b8e1..90190a8 100644 --- a/launcher/style.css +++ b/launcher/style.css @@ -5,6 +5,21 @@ window { font-family: sans-serif; } +/* Startup Messages */ +.startup-message { + padding: 32px; +} + +.startup-message-title { + font-size: 55px; + font-weight: 700; +} + +.startup-message-description { + font-size: 28px; + opacity: 0.7; +} + /* Large, prominent clock */ label.date-display { font-size: 28px; diff --git a/launcher/ui.py b/launcher/ui.py index cd3a06e..dd7e313 100644 --- a/launcher/ui.py +++ b/launcher/ui.py @@ -12,47 +12,78 @@ class UIController: self, ui_file="window.ui", css_file="style.css", - status_provider_callback=None, - power_action_callback=None + system_checks=None, + power_action_callback=None, + service_action_callback=None, + services = None ): self.builder = Gtk.Builder.new_from_file(ui_file) self.window = self.builder.get_object("main_window") self._load_styles(css_file) self._setup_clock() - self._initialist_power_options() + self._initialise_power_options() self.power_action_callback = power_action_callback + self.system_checks = system_checks + self.service_action_callback = service_action_callback + self.services = services or [] - if status_provider_callback: - self.setup_status_poller(status_provider_callback) + self._cards_created = False + self._services_visible = False - def setup_status_poller(self, status_provider_callback, interval_seconds=20): - """ Accepts any function that returns a dictionary of statuses, - currently: {'net':True, 'ziti':True} - """ - def poll(): - try: - statuses = status_provider_callback() - print(f"Status result {statuses}") + if self.system_checks: + self._setup_system_status() - # UPDATE UI - self._update_label( - "net_status", - "Net: Connected" if statuses.get("net") else "Net: Disconnected", - statuses.get("net"), - ) - self._update_label( - "ziti_status", - "Ziti: Connected" if statuses.get("ziti") else "Ziti: Disconnected", - statuses.get("ziti") - ) - except Exception as e: - print(f"Error polling status: {e}") - import traceback - traceback.print_exc() - return True + if not self._cards_created: + self._create_cards() + self._cards_created = True + + def _setup_system_status(self): + """Monitor SystemChecks state from GTK Main thread""" + self._update_system_status() + GLib.timeout_add_seconds( + 1, + self._update_system_status + ) + + def _update_system_status(self): + """Update Status display and create cards when system is ready""" + network = self.system_checks.network_online + ziti = self.system_checks.ziti_online + + if network is None: + self._update_label( + "net_status", + "Net: Checking", + None, + ) + else: + self._update_label( + "net_status", + "Net: Connected" if network else "Net: Disconnected", + network, + ) + + if ziti is None: + self._update_label( + "ziti_status", + "Ziti: Checking", + None, + ) + else : + self._update_label( + "ziti_status", + "Ziti: Connected" if ziti else "Ziti: Disconnected", + ziti, + ) + + if self.system_checks.ready and not self._services_visible: + self.builder.get_object("startup_revealer").set_reveal_child(False) + self.builder.get_object("cards_revealer").set_reveal_child(True) + + self._services_visible = True + + return True - poll() - GLib.timeout_add_seconds(interval_seconds, poll) def _update_label(self, widget_id, text, is_healthy): label = self.builder.get_object(widget_id) @@ -65,6 +96,21 @@ class UIController: label.remove_css_class("ok-label") label.add_css_class("error-label") + def _create_cards(self): + """Create Service Cards""" + + for service in self.services: + title = service.get("title", "Service") + desc = service.get("desc", "") + bg_img = service.get("bg_image", "") + + self.add_card( + title, + desc, + bg_img, + lambda svc=service: self.service_action_callback(svc), + ) + def _load_styles(self, css_file): css_provider = Gtk.CssProvider() css_provider.load_from_path(css_file) @@ -87,7 +133,7 @@ class UIController: update() GLib.timeout_add_seconds(10, update) - def _initialist_power_options(self): + def _initialise_power_options(self): btn_reboot = self.builder.get_object("btn_reboot") btn_shutdown = self.builder.get_object("btn_shutdown") diff --git a/launcher/window.ui b/launcher/window.ui index 37480d2..f44e87d 100644 --- a/launcher/window.ui +++ b/launcher/window.ui @@ -49,11 +49,24 @@ - + + + + true + true + + + + + crossfade + 500 + false + - + true true + 3 @@ -68,6 +81,55 @@ + + + + + + + crossfade + 500 + true + 3 + 3 + + + + + 1 + 8 + 3 + 3 + + + + Connecting... + + + + + + + Waiting for network connectivity before enabling services. + 2 + + + + + + + + + + + +