#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import datetime
import dbus
import dbus.mainloop.glib
import gettext
import gi
import json
import netifaces
import os
import psutil
import pwd
import random
import re
import signal
import socket
import string
import struct
import subprocess
import sys
import syslog
import time
import zeroconf

gi.require_version('AyatanaAppIndicator3', '0.1')
gi.require_version('Gtk', '3.0')
gi.require_version('Notify', '0.7')

from gi.repository import AyatanaAppIndicator3, Gdk, GLib, Gtk, Notify

gettext.bindtextdomain('puavo-veyon-applet', '/usr/share/locale')
gettext.textdomain('puavo-veyon-applet')
_tr = gettext.gettext

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

CLIENT_PROTOCOL_VERSION = 1

CSS = b"""
#cancel_button {
  background-color: #333333;
  border-color: grey;
  color: white;
  font-size: 70%;
  margin-top: 0.5em;
}

#cancel_button:hover {
  border-color: white;
  background-color: grey;
}

#connection_label {
  font-size: 70%;
}

#connection_spinner {
  margin-bottom: 5em;
}

#invite_button:hover, #invite_cancel:hover {
  border-color: white;
  background-color: grey;
  color: white;
}

#invite_cancel {
  font-size: 80%;
  margin: 1em;
  padding-left: 2em;
  padding-right: 2em;
}

#invite_label {
  padding: 0.5em;
}

#invite_to_session_window {
  font-size: 3em;
}

#pin_entry {
  background-color: white;
  color: black;
  margin-left: 0.5em;
}

#pin_request_message {
  color: lightgreen;
  font-size: 80%;
}

#pin_window {
  background-color: rgba(1.0, 1.0, 1.0, 0.85);
  color: orange;
  font-size: 3em;
}
"""

style_provider = Gtk.CssProvider()
style_provider.load_from_data(CSS)

Gtk.StyleContext.add_provider_for_screen(
    Gdk.Screen.get_default(),
    style_provider,
    Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)


def logmsg(priority, message):
  print(message, file=sys.stderr)
  syslog.syslog(priority, message)


def puavoconf_get(puavoconf_key):
  return subprocess.check_output([ 'puavo-conf', puavoconf_key ]).rstrip() \
                   .decode('utf-8')


def read_firstline(pathname):
  with open(pathname) as file:
    return (file.readlines())[0].rstrip("\n")


def desktop_notify(summary, body=None):
  # XXX some messsages should be flagged as warnings maybe?
  if body:
    Notify.Notification.new(summary, body).show()
  else:
    Notify.Notification.new(summary).show()


class VeyonRoleDenied(Exception):
  pass


class PuavoWidget:
  def connect_to_dbus(self, bus_name, service, interface):
    self.bus = dbus.SystemBus()
    dbusobj = self.bus.get_object(bus_name, service)
    return dbus.Interface(dbusobj, dbus_interface=interface)


  def lookup_username(self):
    userinfo = pwd.getpwuid( os.getuid() )
    if userinfo.pw_gecos != '':
      return userinfo.pw_gecos
    return userinfo.pw_name


class Veyon():
  SERVICE_TYPE = '_http._tcp.local.'
  SERVICE_BASE = 'VeyonMaster._http._tcp.local.'
  SERVICE_PORT = 2991

  def receive_msg(socket):
    size = socket.recv(4)

    if len(size) == 0:
      return None

    if len(size) != 4:
      raise Exception('no message size, connection closed')

    msgsize = struct.unpack('!i', size)[0]
    message_json = socket.recv(msgsize)
    if len(message_json) != msgsize:
      raise Exception('no message, connection closed')

    message = json.loads(message_json)

    if type(message) != dict:
      raise Exception('message is not a dict')
    if not 'type' in message:
      raise Exception('no type in message')
    if type(message['type']) != str:
      raise Exception('message type is not a string')

    return message


  def send_msg(socket, message):
    message = bytes(json.dumps(message), encoding='utf-8')
    msgsize = len(message)
    socket.send(struct.pack('!i', msgsize))
    socket.send(message)


# VeyonClientConnection keeps track of a connection from
# a master side to some client
class VeyonClientConnection(PuavoWidget):
  def __init__(self, master, client_socket, client_address):
    self.address         = client_address
    self.in_session      = False
    self.master          = master
    self.client_groups   = []
    self.client_hostname = '?'
    self.client_socket   = client_socket
    self.client_user     = None

    # XXX should this be asynchronous?
    self.client_socket_watch \
      = GLib.io_add_watch(GLib.IOChannel(self.client_socket.fileno()),
                          0,
                          GLib.IOCondition.IN,
                          self.incoming_client_data,
                          self.client_socket)


  def send_to_client(self, msg):
    try:
      Veyon.send_msg(self.client_socket, msg)
    except Exception as e:
      logmsg(syslog.LOG_ERR, 'error sending a message to client: %s' % e)
      self.master.remove_client(self.address)


  def incoming_client_data(self, io, cond, client_socket):
    try:
      message = Veyon.receive_msg(client_socket)
      if message == None:
        logmsg(syslog.LOG_INFO, 'client %s at %s has disconnected' \
                                  % (self.client_hostname, self.address))
        self.master.remove_client(self.address)
        return False

      if message['type'] == 'new_client':
        if not 'groups' in message:
          raise Exception('no groups in new_client message')
        if type(message['groups']) != list:
          raise Exception('groups is not a list')
        if not all([ type(g) == str for g in message['groups'] ]):
          raise Exception('some item of a group is not a string')

        if not 'hostname' in message:
          raise Exception('no hostname in new_client message')
        if type(message['hostname']) != str:
          raise Exception('hostname is not a string')

        if not 'user' in message:
          raise Exception('no user in new_client message')
        if type(message['user']) != str:
          raise Exception('user is not a string')

        if not 'version' in message:
          raise Exception('no protocol version in new_client message')
        if type(message['version']) != int:
          raise Exception('protocol version is not an integer')

        if message['version'] < CLIENT_PROTOCOL_VERSION:
          raise Exception(
            'unsupported client protocol version: %d (%d is required)' \
              % (message['version'], CLIENT_PROTOCOL_VERSION))

        self.client_groups   = message['groups']
        self.client_hostname = message['hostname']
        self.client_user     = message['user']

        self.master.manage_invite_to_session_button()

        logmsg(syslog.LOG_INFO,
               'new client %s at %s' % (self.client_hostname, self.address))

        msg = {
          'type': 'master_info',
          'hostname': self.master.hostname,
          'username': self.master.username,
        }
        self.send_to_client(msg)

      elif message['type'] == 'ping':
        self.send_to_client({ 'type': 'pong' })

      elif message['type'] == 'public_key_request':
        if not 'pin' in message:
          raise Exception('public key request without a PIN')
        if type(message['pin']) != str:
          raise Exception('PIN is not a string')

        if message['pin'] == self.master.pin:
          logmsg(syslog.LOG_INFO,
                 'client %s sent correct PIN' % self.client_hostname)
          self.send_public_key()
        else:
          logmsg(syslog.LOG_INFO,
                 'client %s sent wrong PIN' % self.client_hostname)
          self.send_to_client({ 'type': 'wrong_pin' })

      elif message['type'] == 'public_key_accepted':
        logmsg(syslog.LOG_INFO,
               'client %s accepted our public key, session open' \
                 % self.client_hostname)
        self.in_session = True
        self.master.update_config()

        if self.client_user:
          self.master.connection_log.new_session(
            _tr('%s on %s has joined session.') \
              % (self.client_user, self.client_hostname))

      elif message['type'] == 'close_session':
        if self.in_session:
          logmsg(syslog.LOG_INFO,
                 'client %s closed session' % self.client_hostname)
          self.in_session = False
          self.master.update_config()

          if self.client_user:
            self.master.connection_log.disconnected_session(
              _tr('%s on %s disconnected from session.') \
                % (self.client_user, self.client_hostname))

      else:
        logmsg(syslog.LOG_INFO,
               'client %s sent message %s that we do not understand' \
                 % (self.client_hostname, message['type']))

    except Exception as e:
      logmsg(syslog.LOG_ERR, 'error with client %s: %s' % (self.address, e))
      self.master.remove_client(self.address)
      return False

    return True


  def request_close_session(self):
    self.send_to_client({ 'type': 'request_close_session' })


  def send_pin_request(self, pin):
    if self.in_session:
      return

    logmsg(syslog.LOG_INFO, 'sending PIN request to %s' % self.client_hostname)
    self.send_to_client({ 'type': 'pin_request', 'pin': pin })


  def send_public_key(self):
    logmsg(syslog.LOG_INFO, 'sending public key to %s' % self.client_hostname)
    msg = { 'type': 'public_key', 'public_key': self.master.public_key }
    self.send_to_client(msg)


  def disconnect_client(self):
    logmsg(syslog.LOG_INFO,
           'disconnecting client %s at %s' \
             % (self.client_hostname, self.address))

    self.client_socket.close()
    self.client_socket = None
    GLib.source_remove(self.client_socket_watch)

    if self.in_session and self.client_user:
      self.master.connection_log.disconnected_session(
        _tr('%s on %s disconnected from session.') \
          % (self.client_user, self.client_hostname))


class IPAddressWatcher(PuavoWidget):
  def __init__(self, client, master):
    self.current_ip_address  = None
    self.previous_ip_address = None
    self.client = client
    self.master = master

    self.get_host_ip_address()
    GLib.timeout_add_seconds(10, self.get_host_ip_address)


  def get_host_ip_address(self):
    self.current_ip_address = None

    try:
      for intf in sorted(netifaces.interfaces()):
        if not re.match('^(eth|wlan)', intf):
          continue

        addresses = netifaces.ifaddresses(intf)
        if not netifaces.AF_INET in addresses:
          continue

        for ip_addr in addresses[netifaces.AF_INET]:
          if 'addr' in ip_addr:
            # pick the first IP address we got that is not localhost or
            # tun*/vpn* (if we would use several on the same host, that
            # might get tricky)
            ip = ip_addr['addr']
            self.current_ip_address = ip
            break

      if self.current_ip_address != self.previous_ip_address:
        if self.client:
          self.client.host_ip_address_change(self.current_ip_address)
        if self.master:
          self.master.host_ip_address_change(self.current_ip_address)

        if self.current_ip_address:
          logmsg(syslog.LOG_NOTICE,
                 'IP address used is now %s' % self.current_ip_address)
        else:
          logmsg(syslog.LOG_NOTICE,
                 'no IP address on any of the interfaces we are checking')
    except Exception as e:
      logmsg(syslog.LOG_ERR, 'error in determining IP address: %s' % e)

    self.previous_ip_address = self.current_ip_address

    return True


class VeyonMaster(PuavoWidget):
  CHANGE_PIN_ICON    = Gtk.Image.new_from_icon_name('object-rotate-left', 32)
  CLIENT_COUNT_ICON  = Gtk.Image.new_from_icon_name('format-justify-left', 32)
  INVITE_ICON        = Gtk.Image.new_from_icon_name('call-start', 32)
  OPEN_MASTER_ICON   = Gtk.Image.new_from_icon_name('view-grid', 32)
  OPEN_SESSION_ICON  = Gtk.Image.new_from_icon_name('send-to', 32)
  CLOSE_SESSION_ICON = Gtk.Image.new_from_icon_name('window-close', 32)

  def __init__(self, applet, samehost_client):
    self.dbus_iface = self.connect_to_dbus('org.puavo.Veyon',
                                           '/server',
                                           'org.puavo.Veyon.master')

    try:
      self.dbus_iface.CheckAccess()
    except dbus.exceptions.DBusException as e:
      if e.get_dbus_name() == 'org.freedesktop.DBus.Error.AccessDenied':
        raise VeyonRoleDenied('no permission to master role')
      raise e

    self.applet = applet
    self.connection_log = VeyonMasterLog()
    self.samehost_client = samehost_client

    # this will be filled in by IPAddressWatcher
    self.host_ip_address = None

    self.pin = None
    self.public_key = None
    self.serviceinfo = None
    self.listener_socket = None
    self.update_config_call_pending = False
    self.username = self.lookup_username()
    self.veyon_clients = {}
    self.zeroconf = None

    self.hostname = read_firstline('/etc/puavo/hostname')

    if self.samehost_client:
      separator = Gtk.SeparatorMenuItem()
      separator.show()
      self.applet.menu.append(separator)

    label_text =_tr('Participants: %d (see log)') % 0
    self.client_count_label = Gtk.ImageMenuItem(label=label_text)
    self.client_count_label.set_image(self.CLIENT_COUNT_ICON)
    self.client_count_label.set_always_show_image(True)
    self.client_count_label.set_sensitive(False)
    self.client_count_label.connect('activate', self.connection_log.view)
    self.applet.menu.append(self.client_count_label)

    # The ImageMenuItem objects use methods deprecated since 3.10.
    # See https://docs.gtk.org/gtk3/class.ImageMenuItem.html
    # BUT "should use GtkMenuItem and pack a GtkBox with a GtkImage
    # and a GtkLabel instead" advice does not work in this context.

    self.change_pin_button = Gtk.ImageMenuItem(label=_tr('Change PIN'))
    self.change_pin_button.set_image(self.CHANGE_PIN_ICON)
    self.change_pin_button.set_always_show_image(True)
    self.change_pin_button.set_sensitive(False)
    self.change_pin_button.connect('activate', self.create_new_pin)
    self.applet.menu.append(self.change_pin_button)

    self.open_master_button \
      = Gtk.ImageMenuItem(label=_tr('Open control panel'))
    self.open_master_button.set_image(self.OPEN_MASTER_ICON)
    self.open_master_button.set_always_show_image(True)
    self.open_master_button.set_sensitive(False)
    self.open_master_button.connect('activate', self.open_veyon_master)
    self.applet.menu.append(self.open_master_button)

    self.invite_to_session_button \
      = Gtk.ImageMenuItem(label=_tr('Invite to session by group'))
    self.invite_to_session_button.set_image(self.INVITE_ICON)
    self.invite_to_session_button.set_always_show_image(True)
    self.invite_to_session_button.set_sensitive(False)
    self.invite_to_session_button.connect('activate',
                                          self.show_invite_to_session_window)
    self.applet.menu.append(self.invite_to_session_button)

    self.session_button = Gtk.ImageMenuItem(label=_tr('Setup a Veyon session'))
    self.session_button.set_image(self.OPEN_SESSION_ICON)
    self.session_button.set_always_show_image(True)
    self.session_button.set_sensitive(False)
    self.session_button_handler \
      = self.session_button.connect('activate', self.setup_session)
    self.applet.menu.append(self.session_button)


  def host_ip_address_change(self, ip_address):
    self.host_ip_address = ip_address
    self.close_session()

    if self.host_ip_address:
      self.enable()
    else:
      self.disable()


  def disable(self):
    self.session_button.set_sensitive(False)


  def enable(self):
    self.session_button.set_sensitive(True)


  def cleanup_config(self, reply_handler):
    self.dbus_iface.CleanupConfig(reply_handler=reply_handler,
                                  error_handler=self.handle_cleanup_error)


  def handle_cleanup_error(self, dbusexception):
    self.applet.fatal_dbus_error('master', dbusexception)


  def setup_session(self, widget=None):
    if self.samehost_client:
      self.samehost_client.disable()

    self.applet.in_session = True

    self.dbus_iface \
        .NewSession(reply_handler=self.handle_new_session,
                    error_handler=self.handle_new_session_error)


  def close_session(self, widget=None):
    self.close_veyon_master()

    self.unsetup_master_service()

    self.applet.indicator.set_label('', '')
    self.pin = None

    self.change_pin_button.set_sensitive(False)
    self.open_master_button.set_sensitive(False)

    for client_conn in list(self.veyon_clients.values()):
      if client_conn.in_session:
        client_conn.request_close_session()

    self.applet.in_session = False

    label = _tr('Setup a Veyon session')
    self.session_button.get_child().set_text(label)
    self.session_button.set_image(self.OPEN_SESSION_ICON)
    self.session_button.disconnect(self.session_button_handler)
    self.session_button.set_sensitive(True)
    self.session_button_handler \
      = self.session_button.connect('activate', self.setup_session)
    self.invite_to_session_button.set_sensitive(False)

    if self.samehost_client:
      self.samehost_client.enable()


  def create_new_pin(self, widget=None):
    # We try to make a random PIN that is unique on a network with
    # maximum of 4096 hosts (netmask 255.255.240.0), on bigger networks
    # this might sometimes fail.
    split_ip = self.host_ip_address.split('.')
    ip_based_number = 256 * (int(split_ip[2]) % 16) + int(split_ip[3])

    create_new = True
    while create_new:
      random_number = random.randint(0, 23)
      new_pin = '%05d' % (4096 * random_number + ip_based_number)
      if new_pin != self.pin:
        create_new = False

    self.pin = new_pin
    self.applet.indicator.set_label(_tr('PIN: %s') % new_pin, '')


  def handle_new_session(self, public_key):
    if not self.host_ip_address:
      logmsg(syslog.LOG_INFO, 'lost IP when setting up a new session')
      return

    logmsg(syslog.LOG_INFO, 'got new session and public key through dbus')

    self.setup_master_service()

    self.public_key = public_key
    self.create_new_pin()
    self.update_config()
    self.change_pin_button.set_sensitive(True)
    self.session_button.get_child().set_text(_tr('Close a Veyon session'))
    self.session_button.set_image(self.CLOSE_SESSION_ICON)
    self.session_button.disconnect(self.session_button_handler)
    self.session_button_handler \
      = self.session_button.connect('activate', self.close_session)

    self.client_count_label.set_sensitive(True)
    self.open_master_button.set_sensitive(True)

    self.zeroconf.register_service(self.serviceinfo)

    self.open_veyon_master()


  def handle_new_session_error(self, dbusexception):
    self.applet.fatal_dbus_error('master', dbusexception)


  def open_veyon_master(self, widget=None):
    uid = os.getuid()
    procs = [
      p for p in psutil.process_iter() \
        if 'veyon-master' in p.name().lower() and p.uids().real == uid
    ]

    if len(procs) == 0:
      try:
        pid = os.fork()
        if (pid != 0):
          return
        pid = os.fork()
        if (pid != 0):
          os._exit(0)
      except OSError as e:
        logmsg(syslog.LOG_ERR,
               'could not fork to execute veyon-master: %s' % e.message)
        return

      os.execv('/usr/bin/veyon-master', ['/usr/bin/veyon-master'])

    subprocess.Popen([ 'wmctrl', '-a', 'Veyon Master' ], close_fds=True)


  def close_veyon_master(self):
    subprocess.Popen([ 'pkill', '-x', 'veyon-master' ], close_fds=True)


  def manage_invite_to_session_button(self):
    for client_conn in self.veyon_clients.values():
      if client_conn.client_groups:
        self.invite_to_session_button.set_sensitive(True)
        return

    self.invite_to_session_button.set_sensitive(False)


  def show_invite_to_session_window(self, widget=None):
    all_groups = []

    for client_conn in self.veyon_clients.values():
      for group in client_conn.client_groups:
        all_groups.append(group)

    unique_groups = sorted(set(all_groups), key=str.casefold)

    if not unique_groups:
      return

    choicebox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

    invite_text = Gtk.Label(label=_tr('Choose a group to invite to a session:'),
                            name='invite_label')
    choicebox.pack_start(invite_text, False, False, 0)

    for group in unique_groups:
      button = Gtk.Button(label=group, name='invite_button')
      button.connect('clicked', self.invite_to_session)
      choicebox.pack_start(button, False, False, 0)

    cancel_button = Gtk.Button(label=_tr('Cancel'), name='invite_cancel')
    cancel_button.connect('clicked', self.close_invite_to_session)
    cancel_button_align = Gtk.Alignment(xalign=1.0, yalign=0, xscale=0,
                                        yscale=0)
    cancel_button_align.add(cancel_button)

    choicebox_align = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.9,
                                    yscale=0)
    choicebox_align.add(choicebox)

    invite_dialog_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    invite_dialog_box.pack_start(choicebox_align, True, True, 0)
    invite_dialog_box.pack_start(cancel_button_align, False, False, 0)

    self.invite_to_session_window = Gtk.Window(name='invite_to_session_window')
    self.invite_to_session_window.connect('destroy',
                                          self.close_invite_to_session)
    self.invite_to_session_window.connect('focus-out-event',
                                          self.close_invite_to_session)

    self.invite_to_session_window.fullscreen()
    self.invite_to_session_window.present_with_time( int(time.time()) )
    self.invite_to_session_window.set_keep_above(True)
    self.invite_to_session_window.add(invite_dialog_box)
    self.invite_to_session_window.show_all()


  def close_invite_to_session(self, widget=None, event=None):
    if self.invite_to_session_window:
      self.invite_to_session_window.destroy()
      self.invite_to_session_window = None


  def invite_to_session(self, widget):
    group_name = widget.get_label()

    logmsg(syslog.LOG_INFO, 'inviting "%s" to join a session' % group_name)

    desktop_notify(_tr('Inviting "%s" to join a session.') % group_name)

    for client_conn in list(self.veyon_clients.values()):
      if group_name in client_conn.client_groups:
        client_conn.send_pin_request(self.pin)

    self.invite_to_session_window.destroy()


  def setup_master_service(self):
    logmsg(syslog.LOG_NOTICE, 'setting up master service')

    self.listener_socket = socket.socket()
    self.listener_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self.listener_socket.bind((self.host_ip_address, Veyon.SERVICE_PORT))
    self.listener_socket.listen()

    self.zeroconf = zeroconf.Zeroconf()

    host_ip_addresses = [
      socket.inet_pton(socket.AF_INET, self.host_ip_address)
    ]
    my_service_name = self.make_service_name()
    self.serviceinfo = zeroconf.ServiceInfo(Veyon.SERVICE_TYPE,
                                            my_service_name,
                                            addresses=host_ip_addresses,
                                            port=Veyon.SERVICE_PORT)

    # XXX should this be asynchronous?
    self.listener_socket_watch \
      = GLib.io_add_watch(GLib.IOChannel(self.listener_socket.fileno()),
                          0,
                          GLib.IOCondition.IN,
                          self.handle_new_client,
                          self.listener_socket)

    self.session_button.set_sensitive(True)


  def unsetup_master_service(self):
    service_closed = False

    if self.listener_socket:
      self.listener_socket.close()
      self.listener_socket = None
      GLib.source_remove(self.listener_socket_watch)
      service_closed = True

    if self.zeroconf:
      self.zeroconf.unregister_service(self.serviceinfo)
      self.zeroconf = None
      service_closed = True

    self.serviceinfo = None

    if service_closed:
      logmsg(syslog.LOG_NOTICE, 'closed the master service')


  def make_service_name(self):
    time_seconds = str(int(time.time()))
    return (self.hostname + '_' + time_seconds + '.' + Veyon.SERVICE_BASE)


  def handle_new_client(self, io, cond, sock):
    try:
      (conn, sock_client_address) = sock.accept()
      client_address = sock_client_address[0]
      if client_address in self.veyon_clients:
        self.remove_client(client_address)
      client_conn = VeyonClientConnection(self, conn, client_address)
      self.veyon_clients[client_conn.address] = client_conn
    except Exception as e:
      logmsg(syslog.LOG_ERR, 'could not accept a client connection: %s' % e)

    return True


  def update_config(self):
    self.update_config_hosts = []
    for client_conn in self.veyon_clients.values():
      if not client_conn.in_session:
        continue

      host = {
        'address':  client_conn.address,
        'hostname': client_conn.client_hostname,
        'location': 'Classroom',
      }
      self.update_config_hosts.append(host)

    client_count = len(self.update_config_hosts)
    count_text = _tr('Participants: %d (see log)') % client_count
    self.client_count_label.get_child().set_text(count_text)

    if self.update_config_call_pending:
      return

    # Wait a bit in case we have more update_config requests coming up.
    # The server restricts configuration updates to once-a-second anyway.
    GLib.timeout_add_seconds(1, self.dbus_update_config)
    self.update_config_call_pending = True


  def dbus_update_config(self):
    self.update_config_call_pending = False
    logmsg(syslog.LOG_INFO, 'updating veyon configuration')
    self.dbus_iface \
        .UpdateConfig(json.dumps(self.update_config_hosts),
                      reply_handler=self.handle_update_config,
                      error_handler=self.handle_update_config_error)

    return False


  def handle_update_config(self):
    logmsg(syslog.LOG_INFO, 'veyon configuration update ok')


  def handle_update_config_error(self, dbusexception):
    self.applet.fatal_dbus_error('master', dbusexception)


  def remove_client(self, address):
    client_conn = self.veyon_clients[address]
    in_session = client_conn.in_session
    client_conn.disconnect_client()
    del(self.veyon_clients[address])
    if in_session:
       self.update_config()
    self.manage_invite_to_session_button()


class VeyonMasterLog():
  def __init__(self):
    self.dialog = Gtk.Dialog(title=_tr('Puavo Veyon connection log'),
                             parent=None,
                             modal=True,
                             destroy_with_parent=True)
    self.dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)
    self.dialog.set_default_size(600, 300)

    view = Gtk.TextView()
    view.set_editable(False)
    view.set_cursor_visible(False)

    self.buffer = view.get_buffer()
    view.show()

    self.log_ok_tag      = self.buffer.create_tag(foreground='green')
    self.log_warning_tag = self.buffer.create_tag(foreground='red')

    scroll = Gtk.ScrolledWindow()
    scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    scroll.add(view)
    scroll.show()

    self.dialog.vbox.pack_start(scroll, True, True, 0)


  def append(self, text, tag=None):
    timestamped_text = re.sub(r'^',
                              '%s: ' % datetime.datetime.now(),
                              text.rstrip(),
                              flags=re.MULTILINE) + "\n"
    end_iter = self.buffer.get_end_iter()
    self.buffer.insert_with_tags(end_iter, timestamped_text, tag)


  def new_session(self, text):
    self.append(text, self.log_ok_tag)


  def disconnected_session(self, text):
    self.append(text, self.log_warning_tag)
    desktop_notify(_tr('Disconnected participant'), text)


  def view(self, widget):
    self.dialog.present_with_time( int(time.time()) )
    self.dialog.run()
    self.dialog.hide()


# VeyonMasterConnection keeps track of a connection from
# a client side to some master
class VeyonMasterConnection():
  def __init__(self, address, port, client):
    self.address       = address
    self.client        = client
    self.in_session    = False
    self.pong_received = True
    self.master_info   = { 'hostname': '?', 'username': '?' }
    self.pin_sent      = False

    self.master_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.master_socket.connect((self.address, port))

    # XXX should this be asynchronous?
    self.master_socket_watch \
      = GLib.io_add_watch(GLib.IOChannel(self.master_socket.fileno()),
                          0,
                          GLib.IOCondition.IN,
                          self.incoming_master_data,
                          self.master_socket)

    message = {
      'groups':   client.user_groups,
      'hostname': self.client.hostname,
      'user':     self.client.username,
      'type':     'new_client',
      'version':  CLIENT_PROTOCOL_VERSION,
    }

    logmsg(syslog.LOG_INFO, 'sending message %s to master' % message)

    Veyon.send_msg(self.master_socket, message)

    self.ping_sender = GLib.timeout_add_seconds(20, self.send_ping)


  def send_ping(self):
    if not self.pong_received:
      logmsg(syslog.LOG_WARNING,
             ('master %s did not reply with a pong in 20 seconds,' \
                + ' closing connection') % self.address)
      self.client.remove_master(self.address)
      # VeyonListenForMasters() may not realize a master is gone,
      # so we must help it by restarting the service browser.
      self.client.setup_master_browser()
      return False

    self.pong_received = False
    self.send_to_master({ 'type': 'ping' })

    return True


  def send_to_master(self, msg):
    try:
      Veyon.send_msg(self.master_socket, msg)
    except Exception as e:
      logmsg(syslog.LOG_ERR, 'error sending a message to master: %s' % e)
      self.client.remove_master(self.address)


  def incoming_master_data(self, io, cond, master_socket):
    try:
      message = Veyon.receive_msg(master_socket)
      if message == None:
        logmsg(syslog.LOG_INFO, 'master %s has disconnected' % self.address)
        self.client.remove_master(self.address)
        return False

      if message['type'] == 'request_close_session':
        if self.in_session:
          self.client.close_session()

      elif message['type'] == 'master_info':
        if not 'hostname' in message:
          raise Exception('no hostname in master info')
        if type(message['hostname']) != str:
          raise Exception('master hostname is not a string')
        if not 'username' in message:
          raise Exception('no username in master info')
        if type(message['username']) != str:
          raise Exception('master username is not a string')

        self.master_info = {
          'hostname': message['hostname'],
          'username': message['username'],
        }

      elif message['type'] == 'pin_request':
        # ignore join requests if we already have a session to some master
        # or we have setup a master session
        if self.client.applet.in_session:
          return

        # we expect PIN in PIN request so that it is easy for user
        # to provide the PIN in the client
        if not 'pin' in message :
          raise Exception('no pin in pin request')
        if type(message['pin']) != str:
          raise Exception('pin in pin request is not a string')
        self.client.show_pin_entry(self.master_info['hostname'],
                                   self.master_info['username'],
                                   message['pin'])

      elif message['type'] == 'pong':
        self.pong_received = True

      elif message['type'] == 'public_key':
        # Reset everything if we already have a session with some master
        # and we are receiving a new public key.  It is wrong to get another
        # public key from the same or a different host when on a session,
        # and it is quite possible that someone on the network is an impostor.
        # We might get away with less but it is important to also close the
        # negotation state with other masters and resetting state
        # does achieve that.
        if self.client.applet.in_session:
          logmsg(syslog.LOG_WARNING,
                 'got another public key when already in a session,' \
                   + ' maybe two masters share the provided PIN?' \
                   + ' (resetting state)')
          self.client.reset_everything(True)
          self.client.close_pin_entry()
          desktop_notify(
            _tr('Duplicate session error'),
            _tr('Closing connections because of a duplicate' \
                  ' session, are there many sessions with the same PIN?'))
          return

        if not 'public_key' in message:
          raise Exception('no public key in public_key message')
        if type(message['public_key']) != str:
          raise Exception('public_key is not a string')

        if self.pin_sent:
          self.in_session = True
          self.client.open_session(self, message['public_key'])
        else:
          logmsg(syslog.LOG_INFO,
                 'master %s sent public key even if we have not sent a PIN' \
                   ' (maybe it came back too late?)' % self.address)

      elif message['type'] == 'wrong_pin':
        if self.pin_sent:
          self.pin_sent = False
          self.client.handle_wrong_pin()
      else:
        # XXX should we define a protocol version and pass it on at some point?
        logmsg(syslog.LOG_INFO,
               'master %s sent message %s that we do not understand' \
                 % (self.address, message['type']))


    except Exception as e:
      logmsg(syslog.LOG_ERR, 'error with master %s: %s' % (self.address, e))
      self.client.remove_master(self.address)
      return False
    finally:
      self.client.check_and_maybe_enable_samehost_master()

    return True


  def close_session(self):
    self.in_session = False
    if self.master_socket:
      self.send_to_master({ 'type': 'close_session' })


  def send_public_key_request(self, pin):
    self.send_to_master({ 'type': 'public_key_request', 'pin': pin })
    self.pin_sent = True


  def disconnect_master(self):
    logmsg(syslog.LOG_INFO, 'disconnecting master %s' % self.address)
    self.master_socket.close()
    self.master_socket = None
    GLib.source_remove(self.master_socket_watch)
    GLib.source_remove(self.ping_sender)


class VeyonClient(PuavoWidget):
  CLOSE_ICON = Gtk.Image.new_from_icon_name('go-down', 32)
  JOIN_ICON  = Gtk.Image.new_from_icon_name('go-up',   32)


  def __init__(self, applet):
    self.dbus_iface = self.connect_to_dbus('org.puavo.Veyon',
                                           '/server',
                                           'org.puavo.Veyon.client')
    try:
      self.dbus_iface.CheckAccess()
    except dbus.exceptions.DBusException as e:
      if e.get_dbus_name() == 'org.freedesktop.DBus.Error.AccessDenied':
        raise VeyonRoleDenied('no permission to client role')
      raise e

    try:
      self.user_groups = self.get_user_groups()
    except Exception as e:
      raise Exception('could not determine user groups: %s' % e)

    self.applet = applet
    self.enabled = True
    self.in_session = False
    self.samehost_master = None

    self.browser     = None
    self.hostname    = read_firstline('/etc/puavo/hostname')
    self.master_conn = None
    self.username    = self.lookup_username()
    self.zeroconf    = None

    self.public_key_request_timer_id = None

    self.veyon_masters = {}

    self.action_button = Gtk.ImageMenuItem(label=_tr('Join a Veyon session'))
    self.action_button.set_image(self.JOIN_ICON)
    self.action_button.set_always_show_image(True)
    self.action_button.set_sensitive(True)
    self.action_button_handler \
      = self.action_button.connect('activate', self.activate_pin_entry)
    self.update_action_button()
    self.applet.menu.append(self.action_button)

    self.pin_window = None


  def get_user_groups(self):
    puavo_desktop_path = os.getenv('PUAVO_SESSION_PATH')
    if not puavo_desktop_path:
      raise Exception('PUAVO_SESSION_PATH environment variable is not set')

    with open(puavo_desktop_path) as file:
      puavo_session_info = json.load(file)

    if type(puavo_session_info) != dict:
      raise Exception('puavo session is not of correct type')
    if not 'user' in puavo_session_info:
      raise Exception('no user information in puavo session')

    user = puavo_session_info['user']
    if type(user) != dict:
      raise Exception('session user information is not of correct type')
    if not 'groups' in user:
      raise Exception('no user group information in puavo session')

    groups = user['groups']
    if type(groups) != list:
      raise Exception('user group information is not of correct type')
    if not all([ type(g) == dict for g in groups ]):
      raise Exception('some user group is not of correct type')
    if not all([ 'name' in g for g in groups ]):
      raise Exception('some user group does not have a name')

    group_names = [ g['name'] for g in groups ]
    if not all([ type(gn) == str for gn in group_names ]):
      raise Exception('some user group name is not a string')

    return group_names


  def host_ip_address_change(self, ip_address):
    logmsg(syslog.LOG_INFO, 'client got a host ip address change request')

    if ip_address:
      self.reset_everything(True)
    else:
      self.reset_everything(False)


  def reset_everything(self, is_enabled):
    for address in list(self.veyon_masters):
      self.remove_master(address)

    if not is_enabled:
      self.unsetup_master_browser()
      return

    self.setup_master_browser()


  def cleanup_config(self, reply_handler):
    self.dbus_iface.CleanupConfig(reply_handler=reply_handler,
                                  error_handler=self.handle_cleanup_error)


  def connect_to_samehost_master(self, master):
    self.samehost_master = master


  def disable(self):
    self.enabled = False
    self.unsetup_master_browser()
    self.update_action_button()


  def enable(self):
    if not self.enabled:
      self.setup_master_browser()

    self.enabled = True
    self.update_action_button()


  def handle_session_cleanup(self):
    logmsg(syslog.LOG_INFO, 'veyon client config cleanup done')
    self.update_action_button()
    desktop_notify(_tr('Veyon session closed'))


  def handle_cleanup_error(self, dbusexception):
    self.applet.fatal_dbus_error('client', dbusexception)


  def setup_master_browser(self):
    have_browser = False
    if self.browser and self.zeroconf:
      have_browser = True

    self.unsetup_master_browser()

    if have_browser:
      info_msg = 'restarting the master service browser'
    else:
      info_msg = 'setting up master service browser'

    logmsg(syslog.LOG_INFO, info_msg)

    self.zeroconf = zeroconf.Zeroconf()
    self.browser = zeroconf.ServiceBrowser(self.zeroconf,
                                           Veyon.SERVICE_TYPE,
                                           VeyonListenForMasters(self))


  def unsetup_master_browser(self):
    if self.browser:
      self.browser.cancel()
      self.browser = None

    self.zeroconf = None


  def add_masters(self, address_list, port):
    for address in address_list:
      if not address in self.veyon_masters:
        logmsg(syslog.LOG_INFO,
               'connecting to a veyon master %s' % address)
        try:
          master_conn = VeyonMasterConnection(address, port, self)
          self.veyon_masters[address] = master_conn
        except Exception as e:
          logmsg(syslog.LOG_ERR,
                 'could not connect to master %s: %s' % (address, e))

    self.update_action_button()


  def remove_master(self, address):
    if not address in self.veyon_masters:
      return

    veyon_master = self.veyon_masters[address]
    in_session = veyon_master.in_session
    if in_session:
      self.close_session()

    veyon_master.disconnect_master()
    del(self.veyon_masters[address])
    if not self.veyon_masters and self.pin_window:
      self.pin_window.destroy()

    self.update_action_button()


  def activate_pin_entry(self, widget=None):
    self.show_pin_entry(None, None, None)


  def show_pin_entry(self, requesting_host, requesting_user, suggested_pin):
    if self.pin_window:
      self.pin_window.present_with_time( int(time.time()) )
      return

    pin_label = Gtk.Label(label=_tr('PIN for Veyon session:'))

    self.pin_entry = Gtk.Entry(name='pin_entry')
    self.pin_entry.set_input_purpose(Gtk.InputPurpose.DIGITS)
    self.pin_entry.set_max_length(5)
    self.pin_entry.set_width_chars(5)

    if suggested_pin:
      self.pin_entry.set_text(suggested_pin)

    self.pin_entry.connect('activate', self.pin_entry_changed)
    self.pin_entry.connect('changed',  self.pin_entry_changed)

    pin_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
    pin_box.pack_start(pin_label, False, False, 0)
    pin_box.pack_start(self.pin_entry, False, False, 0)

    request_msg_box = None
    self.enter_label = None
    if requesting_host and requesting_user:
      request_msg_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
                                name='pin_request_message')
      msg = _tr('%s on host %s requests you to join to a Veyon session.') \
               % (requesting_user, requesting_host)
      request_msg = Gtk.Label(label=msg)
      request_msg_box.pack_start(request_msg, False, False, 0)
      self.enter_label = Gtk.Label(label=_tr('Check PIN.'))
      request_msg_box.pack_start(self.enter_label, False, False, 0)
      self.pin_entry.set_sensitive(False)

    entry_and_cancel_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

    cancel_button = Gtk.Button(label=_tr('Cancel'), name='cancel_button')
    cancel_button.set_can_focus(False)
    cancel_button.connect('clicked', self.close_pin_entry)
    button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
    button_box.pack_start(cancel_button, False, False, 0)
    button_align = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0, yscale=0)
    button_align.add(button_box)

    entry_and_cancel_box.pack_start(pin_box, False, False, 0)
    entry_and_cancel_box.pack_start(button_align, False, False, 0)

    box_align = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0, yscale=0)
    box_align.add(entry_and_cancel_box)

    self.connection_label = Gtk.Label(label='', name='connection_label')
    self.connection_spinner = Gtk.Spinner(name='connection_spinner')
    window_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    if request_msg_box:
      window_box.pack_start(request_msg_box, True, False, 0)
    window_box.pack_start(box_align, True, False, 0)
    window_box.pack_start(self.connection_label, False, False, 0)
    window_box.pack_start(self.connection_spinner, False, False, 0)

    self.pin_window = Gtk.Window(name='pin_window')

    screen = self.pin_window.get_screen()
    visual = screen.get_rgba_visual()
    self.pin_window.set_visual(visual)

    self.pin_window.connect('destroy', self.pin_window_closed)
    self.pin_window.connect('focus-out-event', self.close_pin_entry)

    self.pin_window.fullscreen()
    self.pin_window.present_with_time( int(time.time()) )
    self.pin_window.set_keep_above(True)
    self.pin_window.add(window_box)
    self.pin_window.show_all()

    if self.enter_label:
      GLib.timeout_add_seconds(3, self.enable_pin_box)
    else:
      self.pin_entry.grab_focus()


  def enable_pin_box(self):
    if self.enter_label and self.pin_entry:
      self.enter_label.set_label(
        _tr('Check PIN and press ENTER to accept external control.'))
      self.enter_label.show()
      self.pin_entry.set_sensitive(True)
      self.pin_entry.grab_focus()

    return False


  def close_pin_entry(self, widget=None, event=None):
    if self.pin_window:
      self.pin_window.destroy()
      self.pin_window = None

    return False


  def update_action_button(self):
    # XXX GLib.timeout is used here due to some odd bug in underlying
    # XXX libraries (otherwise only the first action happens)

    for master_conn in self.veyon_masters.values():
      if master_conn.in_session:
        self.action_button.disconnect(self.action_button_handler)
        self.action_button_handler \
          = self.action_button.connect('activate', self.close_session)
        GLib.timeout_add(0, lambda: self.action_button.set_sensitive(True))
        label = _tr('Close a Veyon session that is controlled by %s on %s') \
                  % (master_conn.master_info['username'],
                     master_conn.master_info['hostname'])
        GLib.timeout_add(0,
                         lambda: self.action_button.get_child().set_text(label))
        GLib.timeout_add(0,
                         lambda: self.action_button.set_image(self.CLOSE_ICON))
        self.applet.set_icon('OPEN')
        return

    self.check_and_maybe_enable_samehost_master()

    self.applet.set_icon('CLOSED')

    self.action_button.disconnect(self.action_button_handler)
    self.action_button_handler \
      = self.action_button.connect('activate', self.activate_pin_entry)

    label = _tr('Join a Veyon session')
    GLib.timeout_add(0, lambda: self.action_button.get_child().set_text(label))
    GLib.timeout_add(0,
                     lambda: self.action_button.set_image(self.JOIN_ICON))

    if self.enabled and self.veyon_masters:
      GLib.timeout_add(0, lambda: self.action_button.set_sensitive(True))
    else:
      GLib.timeout_add(0, lambda: self.action_button.set_sensitive(False))


  def reenable_pin_entry(self):
    if not self.pin_window:
      return False

    self.pin_entry.set_sensitive(True)
    self.pin_entry.grab_focus()
    self.connection_label.set_markup('')
    self.connection_spinner.stop()

    return False


  def handle_wrong_pin(self):
    for master_conn in self.veyon_masters.values():
      if master_conn.pin_sent:
        # still waiting for some answer
        return

    if self.pin_window:
      logmsg(syslog.LOG_INFO, 'no matching session found for PIN')
      markup = '<span color="%s">%s</span>' \
                 % ('red', _tr('no matching session found for PIN'))
      self.connection_label.set_markup(markup)
      self.connection_spinner.stop()
      GLib.timeout_add_seconds(2, self.reenable_pin_entry)


  def pin_entry_changed(self, entry):
    entry.handler_block_by_func(self.pin_entry_changed)
    new_text = ''.join([c for c in entry.get_text() if c in string.digits])
    entry.set_text(new_text)
    entry.handler_unblock_by_func(self.pin_entry_changed)
    if len(new_text) == 5:
      self.pin_entry.set_sensitive(False)
      markup = '<span color="%s">%s</span>' % ('orange', _tr('connecting...'))
      self.connection_label.set_markup(markup)
      self.connection_spinner.start()

      # Sending public key request, disable master functionality on this host.
      # Enabling this will be checked by calls to
      # check_and_maybe_enable_samehost_master().
      if self.samehost_master:
        self.samehost_master.disable()

      for master_conn in list(self.veyon_masters.values()):
        master_conn.send_public_key_request(new_text)
      if self.public_key_request_timer_id:
        GLib.source_remove(self.public_key_request_timer_id)
      self.public_key_request_timer_id \
        = GLib.timeout_add_seconds(10, self.public_key_request_timeout)


  def check_and_maybe_enable_samehost_master(self):
    if not self.samehost_master:
      return

    for master_conn in self.veyon_masters.values():
      if master_conn.in_session or master_conn.pin_sent:
        return

    # no open session or pending requests, enable master functionality
    # on this host

    self.samehost_master.enable()


  def public_key_request_timeout(self):
    there_was_a_timeout = False
    for master_conn in self.veyon_masters.values():
      if master_conn.pin_sent:
        there_was_a_timeout = True
        logmsg(syslog.LOG_INFO,
               'master %s did not answer in time' % master_conn.address)
      master_conn.pin_sent = False

    self.check_and_maybe_enable_samehost_master()

    self.public_key_request_timer_id = None
    if there_was_a_timeout and self.pin_window:
      markup = '<span color="%s">%s</span>' % ('red', _tr('timeout'))
      self.connection_label.set_markup(markup)
      self.connection_spinner.stop()
      self.pin_entry.set_sensitive(False)
      GLib.timeout_add_seconds(2, self.reenable_pin_entry)

    return False


  def pin_window_closed(self, widget):
    self.pin_window = None


  def open_session(self, master_conn, public_key):
    if self.pin_window:
      hostname = master_conn.master_info['hostname']
      username = master_conn.master_info['username']
      msg = _tr('connecting to %s (%s)') % (hostname, username)
      markup = '<span color="%s">%s</span>' % ('lightgreen', msg)
      self.connection_label.set_markup(markup)

    for m_conn in self.veyon_masters.values():
      m_conn.pin_sent = False

    self.applet.in_session = True

    self.master_conn = master_conn

    # It could be nice if we could somehow verify that hostname is what it
    # claims to be, but we can not use host certificates or DNS in each
    # setting where this application might be used.  But at least log
    # the name, hostname and IP address, where the IP address is the only
    # information we can actually trust.
    msg = ('setting a new public key received from master {host}' \
             ' sent by "{name}" ({address})') \
            .format(host=self.master_conn.master_info['hostname'],
                    name=self.master_conn.master_info['username'],
                    address=self.master_conn.address)
    logmsg(syslog.LOG_INFO, msg)

    self.dbus_iface.AddPublicKey(public_key,
                                 reply_handler=self.handle_add_public_key,
                                 error_handler=self.handle_add_public_key_error)


  def close_session(self, widget=None):
    self.applet.in_session = False

    if self.master_conn:
      self.master_conn.close_session()
      self.master_conn = None
      self.cleanup_config(self.handle_session_cleanup)


  def handle_add_public_key(self):
    # this might have disappeared during dbus call
    if not self.master_conn:
      return

    logmsg(syslog.LOG_INFO, 'a veyon session is now open')

    self.master_conn.send_to_master({ 'type': 'public_key_accepted' })

    self.update_action_button()

    self.applet.set_icon('OPEN')
    desktop_notify(
      _tr('Veyon session opened'),
      _tr('This computer may now be viewed and controlled by %s from %s.') \
        % (self.master_conn.master_info['username'],
           self.master_conn.master_info['hostname']))

    GLib.timeout_add_seconds(2, self.close_pin_entry)


  def handle_add_public_key_error(self, dbusexception):
    self.applet.fatal_dbus_error('client', dbusexception)


class VeyonListenForMasters():
  def __init__(self, client):
    self.master_services = {}
    self.client = client


  def check_if_ours(self, service_type, name):
    name_base = '.'.join(name.split('.')[1:])
    is_ours = (service_type == Veyon.SERVICE_TYPE
                and name_base == Veyon.SERVICE_BASE)
    return is_ours


  def add_service(self, zeroconf, service_type, name):
    if self.check_if_ours(service_type, name):
      service = zeroconf.get_service_info(service_type, name)
      address_list = [
        str(socket.inet_ntop(socket.AF_INET, a)) for a in service.addresses
      ]
      self.master_services[name] = address_list
      self.client.add_masters(address_list, service.port)


  def remove_service(self, zeroconf, service_type, name):
    if self.check_if_ours(service_type, name):
      if name in self.master_services:
        for address in self.master_services[name]:
          self.client.remove_master(address)


  def update_service(self, zeroconf, service_type, name):
    pass


class PuavoVeyonApplet:
  ICON_DIR = '/usr/share/icons/Faenza/status/scalable'
  ICON_CLOSED = os.path.join(ICON_DIR,
                             'software-update-available-symbolic.svg')
  ICON_OPEN   = os.path.join(ICON_DIR,
                             'software-update-urgent-symbolic.svg')

  def __init__(self):
    self.client     = None
    self.exit_done  = False
    self.in_session = False

    # XXX the icon should be something else and maybe puavo-conf configurable?
    self.indicator \
      = AyatanaAppIndicator3.Indicator.new('puavo-veyon-applet',
          self.ICON_CLOSED,
          AyatanaAppIndicator3.IndicatorCategory.SYSTEM_SERVICES)

    self.indicator.set_attention_icon_full(self.ICON_OPEN, 'open')
    self.indicator.set_icon_full(self.ICON_CLOSED, 'closed')

    self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)

    Notify.init('puavo-veyon-applet')

    self.menu = Gtk.Menu()

    # Try to activate both roles and see if we can do that
    # (puavo-veyon might respond we do or do not have access to either).
    try:
      self.client = VeyonClient(self)
      logmsg(syslog.LOG_NOTICE, 'client role activated')
    except VeyonRoleDenied as e:
      self.client = None

    try:
      self.master = VeyonMaster(self, self.client)
      logmsg(syslog.LOG_NOTICE, 'master role activated')
      if self.client:
        self.client.connect_to_samehost_master(self.master)
    except VeyonRoleDenied as e:
      self.master = None

    if not self.client and not self.master:
      logmsg(syslog.LOG_NOTICE, 'no master or client role active, closing')
      sys.exit(0)

    self.ip_address_watcher = IPAddressWatcher(self.client, self.master)

    # Call cleanup_config() either through client or master and show menu
    # only after we are ready.
    self.cleanup_config(self.start)


  def fatal_dbus_error(self, mode, dbusexception):
    desktop_notify(_tr('Fatal error on Puavo Veyon Applet'),
                   _tr('Unexpected dbus error on Puavo Veyon Applet.'))
    logmsg(syslog.LOG_ERR, 'dbus exception in %s mode: %s' \
                             % (mode, str(dbusexception)))
    sys.exit(1)


  def cleanup_config(self, cb):
    if self.client:
      self.client.cleanup_config(cb)
    else:
      self.master.cleanup_config(cb)


  def start(self):
    self.menu.show_all()
    self.indicator.set_menu(self.menu)


  def set_icon(self, icon):
    if icon == 'OPEN':
      self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ATTENTION)
    else:
      self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)


  def main(self):
    Gtk.main()


  def quit(self):
    logmsg(syslog.LOG_INFO,
           'veyon config cleanup done at exit, quitting')
    Gtk.main_quit()


  def signal_exit(self, signum, frame):
    self.exit()


  def exit(self):
    if not self.exit_done:
      self.exit_done = True
      self.cleanup_config(self.quit)


exitstatus = 0

syslog.openlog('puavo-veyon-applet')
logmsg(syslog.LOG_NOTICE, 'puavo-veyon-applet starting up')

applet = None
try:
  applet = PuavoVeyonApplet()
  signal.signal(signal.SIGINT,  applet.signal_exit)
  signal.signal(signal.SIGTERM, applet.signal_exit)
  applet.main()
except Exception as e:
  logmsg(syslog.LOG_ERR, 'unexpected error: %s' % e)
  exitstatus = 1

if applet:
  applet.exit()

syslog.closelog()

sys.exit(exitstatus)
