215 lines
6.6 KiB
Python
Executable File
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())
|