#!/usr/bin/env python3 """BackOne system tray — opens /app UI and addon deep links.""" import os import signal import subprocess import sys import threading import time import urllib.request import gi gi.require_version('Gtk', '3.0') try: gi.require_version('AyatanaAppIndicator3', '0.1') from gi.repository import AyatanaAppIndicator3 as AppIndicator3 except (ImportError, ValueError): gi.require_version('AppIndicator3', '0.1') from gi.repository import AppIndicator3 from gi.repository import GLib, Gtk APP_URL = os.environ.get('BACKONE_APP_URL', 'http://127.0.0.1:9993/app/') HEALTH_URL = os.environ.get('BACKONE_HEALTH_URL', 'http://127.0.0.1:9993/health') ICON_NAME = 'network-workgroup' SERVICE_WAIT_SECONDS = int(os.environ.get('BACKONE_SERVICE_WAIT', '45')) class BackOneTray: def __init__(self): self._windows = [] self._service_ready = False self.indicator = AppIndicator3.Indicator.new( 'backone-tray', ICON_NAME, AppIndicator3.IndicatorCategory.APPLICATION_STATUS, ) self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) self.indicator.set_menu(self._build_menu()) try: self.indicator.connect('activate', lambda *_: self._open_url(APP_URL)) except TypeError: pass self._update_status() GLib.timeout_add_seconds(30, self._on_timer) GLib.idle_add(self._bootstrap_service) def _build_menu(self): menu = Gtk.Menu() def add_item(label, callback): item = Gtk.MenuItem(label=label) item.connect('activate', lambda *_: callback()) item.show() menu.append(item) return item add_item('Open BackOne App', lambda: self._open_url(APP_URL)) add_item('Voice', lambda: self._open_url(APP_URL + '#/voice')) add_item('Video', lambda: self._open_url(APP_URL + '#/video')) add_item('Chat', lambda: self._open_url(APP_URL + '#/chat')) menu.append(Gtk.SeparatorMenuItem()) add_item('Restart BackOne Service', self._restart_service) add_item('Quit Tray', Gtk.main_quit) menu.show() return menu def _service_ok(self): try: with urllib.request.urlopen(HEALTH_URL, timeout=2) as resp: return resp.status == 200 except Exception: return False def _systemd_active(self): try: result = subprocess.run( ['systemctl', 'is-active', 'backone'], capture_output=True, text=True, timeout=5, ) return result.returncode == 0 and result.stdout.strip() == 'active' except (OSError, subprocess.TimeoutExpired): return False def _start_service(self): for cmd in ( ['systemctl', 'start', 'backone'], ['pkexec', 'systemctl', 'start', 'backone'], ): try: result = subprocess.run( cmd, capture_output=True, timeout=30, ) if result.returncode == 0: return True except (OSError, subprocess.TimeoutExpired): continue return False def _wait_for_service(self, timeout=SERVICE_WAIT_SECONDS): deadline = time.time() + timeout while time.time() < deadline: if self._service_ok(): self._service_ready = True return True time.sleep(0.5) return False def _ensure_service(self): if self._service_ok(): self._service_ready = True return True if not self._systemd_active(): self._start_service() return self._wait_for_service() def _bootstrap_service(self): def worker(): ready = self._ensure_service() GLib.idle_add(self._on_bootstrap_done, ready) threading.Thread(target=worker, daemon=True).start() return False def _on_bootstrap_done(self, ready): self._service_ready = ready self._update_status() return False def _update_status(self): online = self._service_ok() self._service_ready = online title = 'BackOne (online)' if online else 'BackOne (offline)' self.indicator.set_title(title) def _on_timer(self): self._update_status() return True def _show_offline_dialog(self): dialog = Gtk.MessageDialog( transient_for=None, flags=0, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text='BackOne service is not running', ) dialog.format_secondary_text( 'Could not reach the BackOne web server on 127.0.0.1:9993.\n\n' 'Use "Restart BackOne Service" from the tray menu, or run:\n' ' sudo systemctl start backone' ) dialog.run() dialog.destroy() def _open_url(self, url): if not self._ensure_service(): self._show_offline_dialog() return if self._try_webkit(url): return for cmd in ( ['xdg-open', url], ['gio', 'open', url], ['sensible-browser', url], ): try: subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return except OSError: continue def _try_webkit(self, url): try: gi.require_version('WebKit2', '4.1') except ValueError: try: gi.require_version('WebKit2', '4.0') except ValueError: return False from gi.repository import WebKit2 win = Gtk.Window(title='BackOne') win.set_default_size(900, 640) view = WebKit2.WebView() view.load_uri(url) win.add(view) win.connect('destroy', lambda w: self._windows.remove(w) if w in self._windows else None) self._windows.append(win) win.show_all() return True def _restart_service(self): subprocess.Popen( ['pkexec', 'systemctl', 'restart', 'backone'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) def worker(): ready = self._wait_for_service(timeout=30) GLib.idle_add(self._on_bootstrap_done, ready) threading.Thread(target=worker, daemon=True).start() def main(): signal.signal(signal.SIGINT, signal.SIG_DFL) BackOneTray() Gtk.main() if __name__ == '__main__': sys.exit(main())