Files
BackOne/app-tray/backone-tray.py
T
2026-06-30 13:44:11 +07:00

215 lines
6.6 KiB
Python
Executable File

#!/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())