6d0c2d93a8
The ReadersWritersSemaphore object protects concurrent access on EntropyRepository objects ensuring that they don't get closed by RigoServiceController while in use.
2532 lines
87 KiB
Python
2532 lines
87 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright (C) 2012 Fabio Erculiani
|
|
|
|
Authors:
|
|
Fabio Erculiani
|
|
|
|
This program is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free Software
|
|
Foundation; version 3.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import copy
|
|
import tempfile
|
|
import time
|
|
from threading import Lock, Semaphore
|
|
|
|
import dbus
|
|
import dbus.exceptions
|
|
|
|
sys.path.insert(0, "../lib")
|
|
sys.path.insert(1, "../client")
|
|
sys.path.insert(2, "./")
|
|
sys.path.insert(3, "/usr/lib/entropy/lib")
|
|
sys.path.insert(4, "/usr/lib/entropy/client")
|
|
sys.path.insert(5, "/usr/lib/entropy/rigo")
|
|
sys.path.insert(6, "/usr/lib/rigo")
|
|
|
|
from gi.repository import Gtk, Gdk, Gio, GLib, GObject, Vte, Pango, \
|
|
GdkPixbuf
|
|
|
|
from rigo.paths import DATA_DIR
|
|
from rigo.enums import Icons, AppActions, RigoViewStates, \
|
|
LocalActivityStates
|
|
from rigo.entropyapi import EntropyWebService, EntropyClient as Client
|
|
from rigo.models.application import Application
|
|
from rigo.ui.gtk3.widgets.apptreeview import AppTreeView
|
|
from rigo.ui.gtk3.widgets.notifications import NotificationBox, \
|
|
RepositoriesUpdateNotificationBox, UpdatesNotificationBox, \
|
|
LoginNotificationBox, ConnectivityNotificationBox, \
|
|
PleaseWaitNotificationBox
|
|
from rigo.ui.gtk3.controllers.applications import ApplicationsViewController
|
|
from rigo.ui.gtk3.controllers.application import ApplicationViewController
|
|
from rigo.ui.gtk3.controllers.authentication import AuthenticationController
|
|
from rigo.ui.gtk3.widgets.welcome import WelcomeBox
|
|
from rigo.ui.gtk3.widgets.stars import Star
|
|
from rigo.ui.gtk3.widgets.terminal import TerminalWidget
|
|
from rigo.ui.gtk3.models.appliststore import AppListStore
|
|
from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme
|
|
from rigo.utils import escape_markup, prepare_markup
|
|
|
|
from RigoDaemon.enums import ActivityStates as DaemonActivityStates, \
|
|
AppActions as DaemonAppActions
|
|
from RigoDaemon.config import DbusConfig as DaemonDbusConfig, \
|
|
PolicyActions
|
|
|
|
from entropy.const import etpConst, etpUi, const_debug_write, \
|
|
const_debug_enabled, const_convert_to_unicode
|
|
from entropy.client.interfaces.repository import Repository
|
|
from entropy.misc import TimeScheduled, ParallelTask, ReadersWritersSemaphore
|
|
from entropy.i18n import _, ngettext
|
|
from entropy.output import darkgreen, brown, darkred, red, blue
|
|
|
|
import entropy.tools
|
|
|
|
|
|
class RigoServiceController(GObject.Object):
|
|
|
|
"""
|
|
This is the Rigo Application frontend to Rigo Daemon.
|
|
Handles privileged requests on our behalf.
|
|
"""
|
|
|
|
NOTIFICATION_CONTEXT_ID = "RigoServiceControllerContextId"
|
|
|
|
class ServiceNotificationBox(NotificationBox):
|
|
|
|
def __init__(self, message, message_type):
|
|
NotificationBox.__init__(
|
|
self, message,
|
|
tooltip=_("Good luck!"),
|
|
message_type=message_type,
|
|
context_id=RigoServiceController.NOTIFICATION_CONTEXT_ID)
|
|
|
|
class SharedLocker(object):
|
|
|
|
"""
|
|
SharedLocker ensures that Entropy Resources
|
|
lock and unlock operations are called once,
|
|
avoiding reentrancy, which is a property of
|
|
lock_resources() and unlock_resources(), even
|
|
during concurrent access.
|
|
"""
|
|
|
|
def __init__(self, entropy_client, locked):
|
|
self._entropy = entropy_client
|
|
self._locking_mutex = Lock()
|
|
self._locked = locked
|
|
|
|
def lock(self):
|
|
with self._locking_mutex:
|
|
lock = False
|
|
if not self._locked:
|
|
lock = True
|
|
self._locked = True
|
|
if lock:
|
|
self._entropy.lock_resources(
|
|
blocking=True, shared=True)
|
|
|
|
def unlock(self):
|
|
with self._locking_mutex:
|
|
unlock = False
|
|
if self._locked:
|
|
unlock = True
|
|
self._locked = False
|
|
if unlock:
|
|
self._entropy.unlock_resources()
|
|
|
|
__gsignals__ = {
|
|
# we request to lock the whole UI wrt repo
|
|
# interaction
|
|
"start-working" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_PYOBJECT,
|
|
GObject.TYPE_PYOBJECT),
|
|
),
|
|
# Repositories have been updated
|
|
"repositories-updated" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_PYOBJECT,
|
|
GObject.TYPE_PYOBJECT,),
|
|
),
|
|
# Application actions have been completed
|
|
"applications-managed" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_PYOBJECT,),
|
|
),
|
|
# Application has been processed
|
|
"application-processed" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_PYOBJECT,
|
|
GObject.TYPE_PYOBJECT,),
|
|
),
|
|
# Application is being processed
|
|
"application-processing" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_PYOBJECT,
|
|
GObject.TYPE_PYOBJECT,),
|
|
),
|
|
"application-abort" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
(GObject.TYPE_PYOBJECT,
|
|
GObject.TYPE_PYOBJECT,),
|
|
),
|
|
}
|
|
|
|
DBUS_INTERFACE = DaemonDbusConfig.BUS_NAME
|
|
DBUS_PATH = DaemonDbusConfig.OBJECT_PATH
|
|
|
|
_OUTPUT_SIGNAL = "output"
|
|
_REPOSITORIES_UPDATED_SIGNAL = "repositories_updated"
|
|
_TRANSFER_OUTPUT_SIGNAL = "transfer_output"
|
|
_PING_SIGNAL = "ping"
|
|
_RESOURCES_UNLOCK_REQUEST_SIGNAL = "resources_unlock_request"
|
|
_RESOURCES_LOCK_REQUEST_SIGNAL = "resources_lock_request"
|
|
_ACTIVITY_STARTED_SIGNAL = "activity_started"
|
|
_ACTIVITY_COMPLETED_SIGNAL = "activity_completed"
|
|
_PROCESSING_APPLICATION_SIGNAL = "processing_application"
|
|
_APPLICATION_PROCESSED_SIGNAL = "application_processed"
|
|
_APPLICATIONS_MANAGED_SIGNAL = "applications_managed"
|
|
|
|
def __init__(self, rigo_app, activity_rwsem, auth,
|
|
entropy_client, entropy_ws):
|
|
GObject.Object.__init__(self)
|
|
self._rigo = rigo_app
|
|
self._activity_rwsem = activity_rwsem
|
|
self._auth = auth
|
|
self._nc = None
|
|
self._bottom_nc = None
|
|
self._wc = None
|
|
self._avc = None
|
|
self._apc = None
|
|
self._terminal = None
|
|
self._entropy = entropy_client
|
|
self._entropy_ws = entropy_ws
|
|
self.__dbus_main_loop = None
|
|
self.__system_bus = None
|
|
self.__entropy_bus = None
|
|
self.__entropy_bus_mutex = Lock()
|
|
self._registered_signals = {}
|
|
self._registered_signals_mutex = Lock()
|
|
|
|
self._local_activity = LocalActivityStates.READY
|
|
self._local_activity_mutex = Lock()
|
|
|
|
self._please_wait_box = None
|
|
self._please_wait_mutex = Lock()
|
|
|
|
self._application_request_serializer = Lock()
|
|
# this controls the the busy()/unbusy()
|
|
# atomicity.
|
|
self._application_request_mutex = Lock()
|
|
|
|
# threads doing repo activities must coordinate
|
|
# with this
|
|
self._update_repositories_mutex = Lock()
|
|
|
|
def set_applications_controller(self, avc):
|
|
"""
|
|
Bind ApplicationsViewController object to this class.
|
|
"""
|
|
self._avc = avc
|
|
|
|
def set_application_controller(self, apc):
|
|
"""
|
|
Bind ApplicationViewController object to this class.
|
|
"""
|
|
self._apc = apc
|
|
|
|
def set_terminal(self, terminal):
|
|
"""
|
|
Bind a TerminalWidget to this object, in order to be used with
|
|
events coming from dbus.
|
|
"""
|
|
self._terminal = terminal
|
|
|
|
def set_work_controller(self, wc):
|
|
"""
|
|
Bind a WorkViewController to this object in order to be used to
|
|
set progress status.
|
|
"""
|
|
self._wc = wc
|
|
|
|
def set_notification_controller(self, nc):
|
|
"""
|
|
Bind a NotificationViewController to this object.
|
|
"""
|
|
self._nc = nc
|
|
|
|
def set_bottom_notification_controller(self, bottom_nc):
|
|
"""
|
|
Bind a BottomNotificationViewController to this object.
|
|
"""
|
|
self._bottom_nc = bottom_nc
|
|
|
|
def setup(self, shared_locked):
|
|
"""
|
|
Execute object setup once initialization phase is complete.
|
|
This phase is comprehensive of all the set_* method calls.
|
|
"""
|
|
if self._apc is not None:
|
|
# connect application request events
|
|
self._apc.connect(
|
|
"application-request-action",
|
|
self._on_application_request_action)
|
|
|
|
# since we handle the lock/unlock of entropy
|
|
# resources here, we need to know what's the
|
|
# initial state
|
|
self._shared_locker = self.SharedLocker(
|
|
self._entropy, shared_locked)
|
|
|
|
def service_available(self):
|
|
"""
|
|
Return whether the RigoDaemon dbus service is
|
|
available.
|
|
"""
|
|
try:
|
|
self._entropy_bus
|
|
return True
|
|
except dbus.exceptions.DBusException:
|
|
return False
|
|
|
|
def busy(self, local_activity):
|
|
"""
|
|
Become busy, switch to some local activity.
|
|
If an activity is already taking place,
|
|
LocalActivityStates.BusyError is raised.
|
|
If the active activity equals the requested one,
|
|
LocalActivityStates.SameError is raised.
|
|
"""
|
|
with self._local_activity_mutex:
|
|
if self._local_activity == local_activity:
|
|
raise LocalActivityStates.SameError()
|
|
if self._local_activity != LocalActivityStates.READY:
|
|
raise LocalActivityStates.BusyError()
|
|
GLib.idle_add(self._bottom_nc.set_activity,
|
|
local_activity)
|
|
self._local_activity = local_activity
|
|
|
|
def unbusy(self, current_activity):
|
|
"""
|
|
Exit from busy state, switch to local activity called "READY".
|
|
If we're already out of any activity, raise
|
|
LocalActivityStates.AlreadyReadyError()
|
|
"""
|
|
with self._local_activity_mutex:
|
|
if self._local_activity == LocalActivityStates.READY:
|
|
raise LocalActivityStates.AlreadyReadyError()
|
|
if self._local_activity != current_activity:
|
|
raise LocalActivityStates.UnbusyFromDifferentActivity()
|
|
GLib.idle_add(self._bottom_nc.set_activity,
|
|
LocalActivityStates.READY)
|
|
self._local_activity = LocalActivityStates.READY
|
|
|
|
def local_activity(self):
|
|
"""
|
|
Return the current local activity (enum from LocalActivityStates)
|
|
"""
|
|
return self._local_activity
|
|
|
|
@property
|
|
def repositories_lock(self):
|
|
"""
|
|
Return the Repositories Update Mutex object.
|
|
This lock protects repositories access during their
|
|
physical update.
|
|
"""
|
|
return self._update_repositories_mutex
|
|
|
|
@property
|
|
def _dbus_main_loop(self):
|
|
if self.__dbus_main_loop is None:
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
self.__dbus_main_loop = DBusGMainLoop(set_as_default=True)
|
|
return self.__dbus_main_loop
|
|
|
|
@property
|
|
def _system_bus(self):
|
|
if self.__system_bus is None:
|
|
self.__system_bus = dbus.SystemBus(
|
|
mainloop=self._dbus_main_loop)
|
|
return self.__system_bus
|
|
|
|
@property
|
|
def _entropy_bus(self):
|
|
with self.__entropy_bus_mutex:
|
|
if self.__entropy_bus is None:
|
|
self.__entropy_bus = self._system_bus.get_object(
|
|
self.DBUS_INTERFACE, self.DBUS_PATH
|
|
)
|
|
|
|
# ping/pong signaling, used to let
|
|
# RigoDaemon release exclusive locks
|
|
# when no client is connected
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._PING_SIGNAL, self._ping_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# Entropy stdout/stderr messages
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._OUTPUT_SIGNAL, self._output_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# Entropy UrlFetchers messages
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._TRANSFER_OUTPUT_SIGNAL,
|
|
self._transfer_output_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# RigoDaemon Entropy Resources unlock requests
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._RESOURCES_UNLOCK_REQUEST_SIGNAL,
|
|
self._resources_unlock_request_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# RigoDaemon Entropy Resources lock requests
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._RESOURCES_LOCK_REQUEST_SIGNAL,
|
|
self._resources_lock_request_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# RigoDaemon is telling us that a new activity
|
|
# has just begun
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._ACTIVITY_STARTED_SIGNAL,
|
|
self._activity_started_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# RigoDaemon is telling us that an activity
|
|
# has been completed
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._ACTIVITY_COMPLETED_SIGNAL,
|
|
self._activity_completed_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# RigoDaemon tells us that a queue action
|
|
# is being processed as we cycle (lol)
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._PROCESSING_APPLICATION_SIGNAL,
|
|
self._processing_application_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# RigoDaemon tells us that a queued app action
|
|
# is now complete
|
|
self.__entropy_bus.connect_to_signal(
|
|
self._APPLICATION_PROCESSED_SIGNAL,
|
|
self._application_processed_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
return self.__entropy_bus
|
|
|
|
### GOBJECT EVENTS
|
|
|
|
def _on_application_request_action(self, apc, app, app_action):
|
|
"""
|
|
This event comes from ApplicationViewController notifying
|
|
that user would like to schedule the given action for App.
|
|
"app" is an Application object, "app_action" is an AppActions
|
|
enum value.
|
|
"""
|
|
const_debug_write(
|
|
__name__,
|
|
"_on_application_request_action: "
|
|
"%s -> %s" % (app, app_action))
|
|
self.application_request(app, app_action)
|
|
|
|
### DBUS SIGNALS
|
|
|
|
def _processing_application_signal(self, package_id, repository_id,
|
|
daemon_action):
|
|
const_debug_write(
|
|
__name__,
|
|
"_processing_application_signal: received for "
|
|
"%d, %s, action: %s" % (
|
|
package_id, repository_id, daemon_action))
|
|
|
|
def _redraw_callback(*args):
|
|
self._processing_application_signal(
|
|
package_id, repository_id,
|
|
daemon_action)
|
|
|
|
app = Application(
|
|
self._entropy, self._entropy_ws,
|
|
(package_id, repository_id),
|
|
redraw_callback=_redraw_callback)
|
|
self._wc.set_application(app, daemon_action)
|
|
|
|
self.emit("application-processing", app, daemon_action)
|
|
|
|
def _application_processed_signal(self, package_id, repository_id,
|
|
daemon_action, success):
|
|
const_debug_write(
|
|
__name__,
|
|
"_application_processed_signal: received for "
|
|
"%d, %s, action: %s, success: %s" % (
|
|
package_id, repository_id, daemon_action, success))
|
|
|
|
app = Application(
|
|
self._entropy, self._entropy_ws,
|
|
(package_id, repository_id),
|
|
redraw_callback=None)
|
|
self.emit("application-processed", app, daemon_action)
|
|
|
|
def _applications_managed_signal(self, success):
|
|
"""
|
|
Signal coming from RigoDaemon notifying us that the
|
|
MANAGING_APPLICATIONS is over.
|
|
"""
|
|
with self._registered_signals_mutex:
|
|
our_signals = self._registered_signals.get(
|
|
self._APPLICATIONS_MANAGED_SIGNAL)
|
|
if our_signals is None:
|
|
# not generated by us
|
|
return
|
|
if our_signals:
|
|
sig_match = our_signals.pop(0)
|
|
sig_match.remove()
|
|
else:
|
|
# somebody already consumed this signal
|
|
if const_debug_enabled():
|
|
const_debug_write(
|
|
__name__,
|
|
"_applications_managed_signal: "
|
|
"already consumed")
|
|
return
|
|
|
|
with self._application_request_mutex:
|
|
# should be safe to block in here, because
|
|
# the other thread can only block here when
|
|
# we're not in busy state
|
|
|
|
local_activity = LocalActivityStates.MANAGING_APPLICATIONS
|
|
# we don't expect to fail here, it would
|
|
# mean programming error.
|
|
self.unbusy(local_activity)
|
|
|
|
# 2 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
|
|
self.emit("applications-managed", success)
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_applications_managed_signal: applications-managed")
|
|
|
|
def _repositories_updated_signal(self, result, message):
|
|
"""
|
|
Signal coming from RigoDaemon notifying us that repositories have
|
|
been updated.
|
|
"""
|
|
with self._registered_signals_mutex:
|
|
our_signals = self._registered_signals.get(
|
|
self._REPOSITORIES_UPDATED_SIGNAL)
|
|
if our_signals is None:
|
|
# not generated by us
|
|
return
|
|
if our_signals:
|
|
sig_match = our_signals.pop(0)
|
|
sig_match.remove()
|
|
else:
|
|
# somebody already consumed this signal
|
|
if const_debug_enabled():
|
|
const_debug_write(
|
|
__name__,
|
|
"_repositories_updated_signal: "
|
|
"already consumed")
|
|
return
|
|
|
|
local_activity = LocalActivityStates.UPDATING_REPOSITORIES
|
|
# we don't expect to fail here, it would
|
|
# mean programming error.
|
|
self.unbusy(local_activity)
|
|
|
|
# 1 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
self.repositories_lock.release()
|
|
|
|
self.emit("repositories-updated",
|
|
result, message)
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_repositories_updated_signal: repositories-updated")
|
|
|
|
def _output_signal(self, text, header, footer, back, importance, level,
|
|
count_c, count_t, percent):
|
|
"""
|
|
Entropy Client output() method from RigoDaemon comes here.
|
|
Will be redirected to a virtual terminal here in Rigo.
|
|
This is called in the Gtk.MainLoop.
|
|
"""
|
|
if count_c == 0 and count_t == 0:
|
|
count = None
|
|
else:
|
|
count = (count_c, count_t)
|
|
|
|
if self._terminal is None:
|
|
self._entropy.output(text, header=header, footer=footer,
|
|
back=back, importance=importance,
|
|
level=level, count=count,
|
|
percent=percent)
|
|
return
|
|
|
|
color_func = darkgreen
|
|
if level == "warning":
|
|
color_func = brown
|
|
elif level == "error":
|
|
color_func = darkred
|
|
|
|
count_str = ""
|
|
if count:
|
|
if len(count) > 1:
|
|
if percent:
|
|
fraction = float(count[0])/count[1]
|
|
percent_str = str(round(fraction*100, 1))
|
|
count_str = " ("+percent_str+"%) "
|
|
else:
|
|
count_str = " (%s/%s) " % (red(str(count[0])),
|
|
blue(str(count[1])),)
|
|
|
|
# reset cursor
|
|
self._terminal.feed_child(chr(27) + '[2K')
|
|
if back:
|
|
msg = "\r" + color_func(">>") + " " + header + count_str + text \
|
|
+ footer
|
|
else:
|
|
msg = "\r" + color_func(">>") + " " + header + count_str + text \
|
|
+ footer + "\r\n"
|
|
|
|
self._terminal.feed_child(msg)
|
|
|
|
def _transfer_output_signal(self, average, downloaded_size, total_size,
|
|
data_transfer_bytes, time_remaining_secs):
|
|
"""
|
|
Entropy UrlFetchers update() method (via transfer_output()) from
|
|
RigoDaemon comes here. Will be redirected to WorkAreaController
|
|
Progress Bar if available.
|
|
"""
|
|
if self._wc is None:
|
|
return
|
|
|
|
fraction = float(average) / 100
|
|
|
|
human_dt = entropy.tools.bytes_into_human(data_transfer_bytes)
|
|
total = round(total_size, 1)
|
|
|
|
if total > 1:
|
|
text = "%s/%s kB @ %s/sec, %s" % (
|
|
round(float(downloaded_size)/1024, 1),
|
|
total,
|
|
human_dt, time_remaining_secs)
|
|
else:
|
|
text = None
|
|
|
|
self._wc.set_progress(fraction, text=text)
|
|
|
|
def _ping_signal(self):
|
|
"""
|
|
Need to call pong() as soon as possible to hold all Entropy
|
|
Resources allocated by RigoDaemon.
|
|
"""
|
|
dbus.Interface(
|
|
self._entropy_bus,
|
|
dbus_interface=self.DBUS_INTERFACE).pong()
|
|
|
|
def _resources_lock_request_signal(self, activity):
|
|
"""
|
|
RigoDaemon is asking us to acquire a shared Entropy Resources
|
|
lock. First we check if we have released it.
|
|
"""
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_lock_request_signal: "
|
|
"called, with remote activity: %s" % (activity,))
|
|
|
|
def _resources_lock():
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_lock_request_signal._resources_lock: "
|
|
"enter (sleep)")
|
|
|
|
self._shared_locker.lock()
|
|
clear_avc = True
|
|
if activity in (DaemonActivityStates.MANAGING_APPLICATIONS,):
|
|
clear_avc = False
|
|
self._release_local_resources(clear_avc=clear_avc)
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_lock_request_signal._resources_lock: "
|
|
"regained shared lock")
|
|
|
|
task = ParallelTask(_resources_lock)
|
|
task.name = "ResourceLockAfterRelease"
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
def _resources_unlock_request_signal(self, activity):
|
|
"""
|
|
RigoDaemon is asking us to release our Entropy Resources Lock.
|
|
An ActivityStates value is provided in order to let us decide
|
|
if we can acknowledge the request.
|
|
"""
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"called, with remote activity: %s" % (activity,))
|
|
|
|
if activity == DaemonActivityStates.UPDATING_REPOSITORIES:
|
|
|
|
# did we ask that or is it another client?
|
|
local_activity = self.local_activity()
|
|
if local_activity == LocalActivityStates.READY:
|
|
|
|
def _update_repositories():
|
|
accepted = self._update_repositories(
|
|
[], False, master=False)
|
|
if accepted:
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"_update_repositories accepted, unlocking")
|
|
self._shared_locker.unlock()
|
|
|
|
# another client, bend over XD
|
|
# LocalActivityStates value will be atomically
|
|
# switched in the above thread.
|
|
task = ParallelTask(_update_repositories)
|
|
task.daemon = True
|
|
task.name = "UpdateRepositoriesExternal"
|
|
task.start()
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"somebody called repo update, starting here too")
|
|
|
|
elif local_activity == \
|
|
LocalActivityStates.UPDATING_REPOSITORIES:
|
|
self._shared_locker.unlock()
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"it's been us calling repositories update")
|
|
# it's been us calling it, ignore request
|
|
return
|
|
|
|
else:
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"not accepting RigoDaemon resources unlock request, "
|
|
"local activity: %s" % (local_activity,))
|
|
|
|
elif activity == DaemonActivityStates.MANAGING_APPLICATIONS:
|
|
|
|
local_activity = self.local_activity()
|
|
if local_activity == LocalActivityStates.READY:
|
|
|
|
def _application_request():
|
|
accepted = self._application_request(
|
|
None, None, master=False)
|
|
if accepted:
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"_application_request accepted, unlocking")
|
|
self._shared_locker.unlock()
|
|
|
|
# another client, bend over XD
|
|
# LocalActivityStates value will be atomically
|
|
# switched in the above thread.
|
|
task = ParallelTask(_application_request)
|
|
task.daemon = True
|
|
task.name = "ApplicationRequestExternal"
|
|
task.start()
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"somebody called app request, starting here too")
|
|
|
|
elif local_activity == \
|
|
LocalActivityStates.MANAGING_APPLICATIONS:
|
|
self._shared_locker.unlock()
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal: "
|
|
"it's been us calling manage apps")
|
|
# it's been us calling it, ignore request
|
|
return
|
|
|
|
else:
|
|
const_debug_write(
|
|
__name__,
|
|
"_resources_unlock_request_signal 2: "
|
|
"not accepting RigoDaemon resources unlock request, "
|
|
"local activity: %s" % (local_activity,))
|
|
|
|
|
|
def _activity_started_signal(self, activity):
|
|
"""
|
|
RigoDaemon is telling us that the scheduled activity,
|
|
either by us or by another Rigo, has just begun and
|
|
that it, RigoDaemon, has now exclusive access to
|
|
Entropy Resources.
|
|
"""
|
|
const_debug_write(
|
|
__name__,
|
|
"_activity_started_signal: "
|
|
"called, with remote activity: %s" % (activity,))
|
|
# reset please wait notification then
|
|
self._please_wait(None)
|
|
|
|
def _activity_completed_signal(self, activity, success):
|
|
"""
|
|
RigoDaemon is telling us that the scheduled activity,
|
|
has been completed.
|
|
"""
|
|
const_debug_write(
|
|
__name__,
|
|
"_activity_completed_signal: "
|
|
"called, with remote activity: %s, success: %s" % (
|
|
activity, success,))
|
|
|
|
### GP PUBLIC METHODS
|
|
|
|
def application_request(self, app, app_action):
|
|
"""
|
|
Start Application Action (install/remove).
|
|
"""
|
|
task = ParallelTask(self._application_request,
|
|
app, app_action)
|
|
task.name = "ApplicationRequest{%s, %s}" % (
|
|
app, app_action,)
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
def update_repositories(self, repositories, force):
|
|
"""
|
|
Start Entropy Repositories Update
|
|
"""
|
|
task = ParallelTask(self._update_repositories,
|
|
repositories, force)
|
|
task.name = "UpdateRepositoriesThread"
|
|
task.daemon = True
|
|
task.start()
|
|
|
|
def activity(self):
|
|
"""
|
|
Return RigoDaemon activity states (any of RigoDaemon.ActivityStates
|
|
values).
|
|
"""
|
|
return dbus.Interface(
|
|
self._entropy_bus,
|
|
dbus_interface=self.DBUS_INTERFACE).activity()
|
|
|
|
def action_queue_length(self):
|
|
"""
|
|
Return the current size of the RigoDaemon Application Action Queue.
|
|
"""
|
|
return dbus.Interface(
|
|
self._entropy_bus,
|
|
dbus_interface=self.DBUS_INTERFACE).action_queue_length()
|
|
|
|
def exclusive(self):
|
|
"""
|
|
Return whether RigoDaemon is running in with
|
|
Entropy Resources acquired in exclusive mode.
|
|
"""
|
|
return dbus.Interface(
|
|
self._entropy_bus,
|
|
dbus_interface=self.DBUS_INTERFACE).exclusive()
|
|
|
|
def _release_local_resources(self, clear_avc=True):
|
|
"""
|
|
Release all the local resources (like repositories)
|
|
that shall be used by RigoDaemon.
|
|
For example, leaving EntropyRepository objects open
|
|
would cause sqlite3 to deadlock.
|
|
"""
|
|
self._entropy.rwsem().writer_acquire()
|
|
try:
|
|
if clear_avc:
|
|
self._avc.clear_safe()
|
|
self._entropy.close_repositories()
|
|
finally:
|
|
self._entropy.rwsem().writer_release()
|
|
|
|
def _please_wait(self, show):
|
|
"""
|
|
Show a Please Wait NotificationBox if show is not None,
|
|
otherwise hide, if there.
|
|
"show" contains the NotificationBox message.
|
|
"""
|
|
with self._please_wait_mutex:
|
|
|
|
if show and self._please_wait_box:
|
|
return
|
|
|
|
if not show and not self._please_wait_box:
|
|
return
|
|
|
|
if not show and self._please_wait_box:
|
|
# remove from NotificationController
|
|
# if there
|
|
box = self._please_wait_box
|
|
self._please_wait_box = None
|
|
if self._nc is not None:
|
|
GLib.idle_add(self._nc.remove, box)
|
|
return
|
|
|
|
if show and not self._please_wait_box:
|
|
# create a new Please Wait Notification Box
|
|
sem = Semaphore(0)
|
|
|
|
def _make():
|
|
box = PleaseWaitNotificationBox(
|
|
show,
|
|
RigoServiceController.NOTIFICATION_CONTEXT_ID)
|
|
self._please_wait_box = box
|
|
sem.release()
|
|
if self._nc is not None:
|
|
self._nc.append(box)
|
|
|
|
GLib.idle_add(_make)
|
|
sem.acquire()
|
|
|
|
def _authorize(self, daemon_activity):
|
|
"""
|
|
Authorize privileged Activity.
|
|
Return True for success, False for failure.
|
|
"""
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_authorize: enter")
|
|
auth_res = {
|
|
'sem': Semaphore(0),
|
|
'result': None,
|
|
}
|
|
|
|
def _authorized_callback(result):
|
|
auth_res['result'] = result
|
|
auth_res['sem'].release()
|
|
|
|
action_id = None
|
|
if daemon_activity == DaemonActivityStates.UPDATING_REPOSITORIES:
|
|
action_id = PolicyActions.UPDATE_REPOSITORIES
|
|
elif daemon_activity == DaemonActivityStates.MANAGING_APPLICATIONS:
|
|
action_id = PolicyActions.MANAGE_APPLICATIONS
|
|
elif daemon_activity == DaemonActivityStates.UPGRADING_SYSTEM:
|
|
action_id = PolicyActions.UPGRADE_SYSTEM
|
|
|
|
if action_id is None:
|
|
raise AttributeError("unsupported daemon activity")
|
|
|
|
self._auth.authenticate(action_id, _authorized_callback)
|
|
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_authorize: sleeping on sem")
|
|
auth_res['sem'].acquire()
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_authorize: got result: %s" % (
|
|
auth_res['result'],))
|
|
|
|
return auth_res['result']
|
|
|
|
def _scale_up(self, activity):
|
|
"""
|
|
Make sure User is authorized to perform a privileged
|
|
operation.
|
|
"""
|
|
granted = self._authorize(activity)
|
|
if not granted:
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_scale_up: abort")
|
|
return False
|
|
|
|
ms = _("Waiting for <b>RigoDaemon</b>, please wait...")
|
|
# this will be reset when activity_started() arrives
|
|
self._please_wait(ms)
|
|
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_scale_up: leave")
|
|
return True
|
|
|
|
def _update_repositories(self, repositories, force,
|
|
master=True):
|
|
"""
|
|
Ask RigoDaemon to update repositories once we're
|
|
100% sure that the UI is locked down.
|
|
"""
|
|
# 1 -- ACTIVITY CRIT :: ON
|
|
self._activity_rwsem.writer_acquire() # CANBLOCK
|
|
|
|
local_activity = LocalActivityStates.UPDATING_REPOSITORIES
|
|
try:
|
|
self.busy(local_activity)
|
|
# will be unlocked when we get the signal back
|
|
except LocalActivityStates.BusyError:
|
|
const_debug_write(__name__, "_update_repositories: "
|
|
"LocalActivityStates.BusyError!")
|
|
# 1 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
return False
|
|
except LocalActivityStates.SameError:
|
|
const_debug_write(__name__, "_update_repositories: "
|
|
"LocalActivityStates.SameError!")
|
|
# 1 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
return False
|
|
|
|
if master:
|
|
scaled = self._scale_up(
|
|
DaemonActivityStates.UPDATING_REPOSITORIES)
|
|
if not scaled:
|
|
self.unbusy(local_activity)
|
|
# 1 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
return False
|
|
|
|
accepted = self._update_repositories_unlocked(
|
|
repositories, force, master)
|
|
|
|
if not accepted:
|
|
self.unbusy(local_activity)
|
|
# 1 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
|
|
def _notify():
|
|
box = self.ServiceNotificationBox(
|
|
prepare_markup(
|
|
_("Another activity is currently in progress")),
|
|
Gtk.MessageType.ERROR)
|
|
box.add_destroy_button(_("K thanks"))
|
|
self._nc.append(box)
|
|
GLib.idle_add(_notify)
|
|
return False
|
|
|
|
return True
|
|
|
|
def _update_repositories_unlocked(self, repositories, force,
|
|
master):
|
|
"""
|
|
Internal method handling the actual Repositories Update
|
|
execution.
|
|
"""
|
|
if self._wc is not None:
|
|
GLib.idle_add(self._wc.activate_progress_bar)
|
|
GLib.idle_add(self._wc.deactivate_app_box)
|
|
|
|
GLib.idle_add(self.emit, "start-working",
|
|
RigoViewStates.WORK_VIEW_STATE, True)
|
|
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_update_repositories_unlocked: "
|
|
"start-working")
|
|
|
|
while not self._rigo.is_ui_locked():
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_update_repositories_unlocked: "
|
|
"waiting Rigo UI lock")
|
|
time.sleep(0.5)
|
|
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_update_repositories_unlocked: "
|
|
"rigo UI now locked!")
|
|
|
|
signal_sem = Semaphore(1)
|
|
|
|
def _repositories_updated_signal(result, message):
|
|
if not signal_sem.acquire(False):
|
|
# already called, no need to call again
|
|
return
|
|
# this is done in order to have it called
|
|
# only once by two different code paths
|
|
self._repositories_updated_signal(
|
|
result, message)
|
|
|
|
with self._registered_signals_mutex:
|
|
# connect our signal
|
|
sig_match = self._entropy_bus.connect_to_signal(
|
|
self._REPOSITORIES_UPDATED_SIGNAL,
|
|
_repositories_updated_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# and register it as a signal generated by us
|
|
obj = self._registered_signals.setdefault(
|
|
self._REPOSITORIES_UPDATED_SIGNAL, [])
|
|
obj.append(sig_match)
|
|
|
|
# Clear all the NotificationBoxes from upper area
|
|
# we don't want people to click on them during the
|
|
# the repo update. Kill the completely.
|
|
if self._nc is not None:
|
|
self._nc.clear_safe(managed=False)
|
|
|
|
if self._terminal is not None:
|
|
self._terminal.reset()
|
|
|
|
self.repositories_lock.acquire()
|
|
# not allowing other threads to mess with repos
|
|
# will be released on repo updated signal
|
|
|
|
self._release_local_resources()
|
|
|
|
accepted = True
|
|
if master:
|
|
accepted = dbus.Interface(
|
|
self._entropy_bus,
|
|
dbus_interface=self.DBUS_INTERFACE
|
|
).update_repositories(repositories, force)
|
|
else:
|
|
# check if we need to cope with races
|
|
self._update_repositories_signal_check(
|
|
sig_match, signal_sem)
|
|
|
|
return accepted
|
|
|
|
def _update_repositories_signal_check(self, sig_match, signal_sem):
|
|
"""
|
|
Called via _update_repositories_unlocked() in order to handle
|
|
the possible race between RigoDaemon signal and the fact that
|
|
we just lost it.
|
|
This is only called in slave mode. When we didn't spawn the
|
|
repositories update directly.
|
|
"""
|
|
activity = self.activity()
|
|
if activity == DaemonActivityStates.UPDATING_REPOSITORIES:
|
|
return
|
|
|
|
# lost the signal or not, we're going to force
|
|
# the callback.
|
|
if not signal_sem.acquire(False):
|
|
# already called, no need to call again
|
|
const_debug_write(
|
|
__name__,
|
|
"_update_repositories_signal_check: abort")
|
|
return
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_update_repositories_signal_check: accepting")
|
|
# Run in the main loop, to avoid calling a signal
|
|
# callback in random threads.
|
|
GLib.idle_add(self._repositories_updated_signal,
|
|
0, "", activity)
|
|
|
|
def _ask_blocking_question(self, ask_meta, message, message_type):
|
|
"""
|
|
Ask a task blocking question to User and waits for the
|
|
answer.
|
|
"""
|
|
box = self.ServiceNotificationBox(
|
|
prepare_markup(message), message_type)
|
|
|
|
def _say_yes(widget):
|
|
ask_meta['res'] = True
|
|
self._nc.remove(box)
|
|
ask_meta['sem'].release()
|
|
|
|
def _say_no(widget):
|
|
ask_meta['res'] = False
|
|
self._nc.remove(box)
|
|
ask_meta['sem'].release()
|
|
|
|
box.add_button(_("Yes, thanks"), _say_yes)
|
|
box.add_button(_("No, sorry"), _say_no)
|
|
self._nc.append(box)
|
|
|
|
def _notify_blocking_message(self, sem, message, message_type):
|
|
"""
|
|
Notify a task blocking information to User and wait for the
|
|
acknowledgement.
|
|
"""
|
|
box = self.ServiceNotificationBox(
|
|
prepare_markup(message), message_type)
|
|
|
|
def _say_kay(widget):
|
|
self._nc.remove(box)
|
|
if sem is not None:
|
|
sem.release()
|
|
|
|
box.add_button(_("Ok then"), _say_kay)
|
|
self._nc.append(box)
|
|
|
|
def _application_request_removal_checks(self, app):
|
|
"""
|
|
Examine Application Removal Request on behalf of
|
|
_application_request_checks().
|
|
"""
|
|
removable = app.is_removable()
|
|
if not removable:
|
|
msg = _("<b>%s</b>\nis part of the Base"
|
|
" System and <b>cannot</b> be removed")
|
|
msg = msg % (app.get_markup(),)
|
|
message_type = Gtk.MessageType.ERROR
|
|
|
|
GLib.idle_add(
|
|
self._notify_blocking_message,
|
|
None, msg, message_type)
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
def _application_request_install_checks(self, app):
|
|
"""
|
|
Examine Application Install Request on behalf of
|
|
_application_request_checks().
|
|
"""
|
|
installable = app.is_installable()
|
|
if not installable:
|
|
msg = _("<b>%s</b>\ncannot be installed at this time"
|
|
" due to <b>missing/masked</b> dependencies or"
|
|
" dependency <b>conflict</b>")
|
|
msg = msg % (app.get_markup(),)
|
|
message_type = Gtk.MessageType.ERROR
|
|
|
|
GLib.idle_add(
|
|
self._notify_blocking_message,
|
|
None, msg, message_type)
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
def _application_request_checks(self, app, daemon_action):
|
|
"""
|
|
Examine Application Request before sending it to RigoDaemon.
|
|
Specifically, check for things like system apps removal asking
|
|
User confirmation.
|
|
"""
|
|
if daemon_action == DaemonAppActions.REMOVE:
|
|
accepted = self._application_request_removal_checks(app)
|
|
else:
|
|
accepted = self._application_request_install_checks(app)
|
|
if not accepted:
|
|
def _emit():
|
|
self.emit("application-abort", app, daemon_action)
|
|
GLib.idle_add(_emit)
|
|
return accepted
|
|
|
|
def _application_request_unlocked(self, app, daemon_action,
|
|
master, busied):
|
|
"""
|
|
Internal method handling the actual Application Request
|
|
execution.
|
|
"""
|
|
if app is not None:
|
|
package_id, repository_id = app.get_details().pkg
|
|
else:
|
|
package_id, repository_id = None, None
|
|
|
|
if busied:
|
|
if self._wc is not None:
|
|
GLib.idle_add(self._wc.activate_progress_bar)
|
|
# this will be back active once we have something
|
|
# to show
|
|
GLib.idle_add(self._wc.deactivate_app_box)
|
|
|
|
# emit, but we don't really need to switch to
|
|
# the work view nor locking down the UI
|
|
GLib.idle_add(self.emit, "start-working", None, False)
|
|
|
|
const_debug_write(__name__, "RigoServiceController: "
|
|
"_application_request_unlocked: "
|
|
"start-working")
|
|
# don't check if UI is locked though
|
|
|
|
signal_sem = Semaphore(1)
|
|
|
|
def _applications_managed_signal(success):
|
|
if not signal_sem.acquire(False):
|
|
# already called, no need to call again
|
|
return
|
|
# this is done in order to have it called
|
|
# only once by two different code paths
|
|
self._applications_managed_signal(
|
|
success)
|
|
|
|
with self._registered_signals_mutex:
|
|
# connect our signal
|
|
sig_match = self._entropy_bus.connect_to_signal(
|
|
self._APPLICATIONS_MANAGED_SIGNAL,
|
|
_applications_managed_signal,
|
|
dbus_interface=self.DBUS_INTERFACE)
|
|
|
|
# and register it as a signal generated by us
|
|
obj = self._registered_signals.setdefault(
|
|
self._APPLICATIONS_MANAGED_SIGNAL, [])
|
|
obj.append(sig_match)
|
|
|
|
self._release_local_resources(clear_avc=False)
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_application_request_unlocked, about to 'schedule'")
|
|
|
|
accepted = True
|
|
if master:
|
|
accepted = dbus.Interface(
|
|
self._entropy_bus,
|
|
dbus_interface=self.DBUS_INTERFACE
|
|
).enqueue_application_action(
|
|
package_id, repository_id, daemon_action)
|
|
const_debug_write(
|
|
__name__,
|
|
"service enqueue_application_action, got: %s, type: %s" % (
|
|
accepted, type(accepted),))
|
|
|
|
def _notify():
|
|
queue_len = self.action_queue_length()
|
|
msg = prepare_markup(_("<b>%s</b> action enqueued") % (
|
|
app.name,))
|
|
if queue_len > 0:
|
|
msg += prepare_markup(ngettext(
|
|
", <b>%i</b> Application enqueued so far...",
|
|
", <b>%i</b> Applications enqueued so far...",
|
|
queue_len)) % (queue_len,)
|
|
box = self.ServiceNotificationBox(
|
|
msg,
|
|
Gtk.MessageType.INFO)
|
|
self._nc.append(box, timeout=10)
|
|
GLib.idle_add(_notify)
|
|
|
|
# FIXME: notify UI to change app state, and make
|
|
# it back available (or recalculate its state
|
|
# afterwards)
|
|
|
|
else:
|
|
self._applications_managed_signal_check(
|
|
sig_match, signal_sem)
|
|
|
|
return accepted
|
|
|
|
def _applications_managed_signal_check(self, sig_match, signal_sem):
|
|
"""
|
|
Called via _application_request_unlocked() in order to handle
|
|
the possible race between RigoDaemon signal and the fact that
|
|
we just lost it.
|
|
This is only called in slave mode. When we didn't spawn the
|
|
repositories update directly.
|
|
"""
|
|
activity = self.activity()
|
|
if activity == DaemonActivityStates.MANAGING_APPLICATIONS:
|
|
return
|
|
|
|
# lost the signal or not, we're going to force
|
|
# the callback.
|
|
if not signal_sem.acquire(False):
|
|
# already called, no need to call again
|
|
const_debug_write(
|
|
__name__,
|
|
"_applications_managed_signal_check: abort")
|
|
return
|
|
|
|
const_debug_write(
|
|
__name__,
|
|
"_applications_managed_signal_check: accepting")
|
|
# Run in the main loop, to avoid calling a signal
|
|
# callback in random threads.
|
|
GLib.idle_add(self._applications_managed_signal,
|
|
True)
|
|
|
|
def _application_request(self, app, app_action, master=True):
|
|
"""
|
|
Forward Application Request (install or remove) to RigoDaemon.
|
|
Make sure there isn't any other ongoing activity.
|
|
"""
|
|
# Need to serialize access to this method because
|
|
# we're going to acquire several resources in a non-atomic
|
|
# way wrt access to this method.
|
|
with self._application_request_serializer:
|
|
|
|
with self._application_request_mutex:
|
|
busied = True
|
|
# since we need to writer_acquire(), which is blocking
|
|
# better try to allocate the local activity first
|
|
local_activity = LocalActivityStates.MANAGING_APPLICATIONS
|
|
try:
|
|
self.busy(local_activity)
|
|
except LocalActivityStates.BusyError:
|
|
const_debug_write(__name__, "_application_request: "
|
|
"LocalActivityStates.BusyError!")
|
|
# doing other stuff, cannot go ahead
|
|
return False
|
|
except LocalActivityStates.SameError:
|
|
const_debug_write(__name__, "_application_request: "
|
|
"LocalActivityStates.SameError, "
|
|
"no need to acquire writer")
|
|
# we're already doing this activity, do not acquire
|
|
# activity_rwsem
|
|
busied = False
|
|
|
|
if busied:
|
|
# 2 -- ACTIVITY CRIT :: ON
|
|
const_debug_write(__name__, "_application_request: "
|
|
"about to acquire writer end of "
|
|
"activity rwsem")
|
|
self._activity_rwsem.writer_acquire() # CANBLOCK
|
|
|
|
def _unbusy():
|
|
if busied:
|
|
self.unbusy(local_activity)
|
|
# 2 -- ACTIVITY CRIT :: OFF
|
|
self._activity_rwsem.writer_release()
|
|
|
|
if master and busied:
|
|
scaled = self._scale_up(
|
|
DaemonActivityStates.MANAGING_APPLICATIONS)
|
|
if not scaled:
|
|
_unbusy()
|
|
return False
|
|
|
|
# clean terminal, make sure no crap is left there
|
|
if self._terminal is not None:
|
|
self._terminal.reset()
|
|
|
|
daemon_action = None
|
|
if app_action == AppActions.INSTALL:
|
|
daemon_action = DaemonAppActions.INSTALL
|
|
elif app_action == AppActions.REMOVE:
|
|
daemon_action = DaemonAppActions.REMOVE
|
|
|
|
accepted = True
|
|
do_notify = True
|
|
if master:
|
|
accepted = self._application_request_checks(
|
|
app, daemon_action)
|
|
if not accepted:
|
|
do_notify = False
|
|
const_debug_write(
|
|
__name__,
|
|
"_application_request, checks result: %s" % (
|
|
accepted,))
|
|
|
|
if accepted:
|
|
accepted = self._application_request_unlocked(
|
|
app, daemon_action, master,
|
|
busied)
|
|
|
|
if not accepted:
|
|
with self._application_request_mutex:
|
|
_unbusy()
|
|
|
|
def _notify():
|
|
box = self.ServiceNotificationBox(
|
|
prepare_markup(
|
|
_("Another activity is currently in progress")
|
|
),
|
|
Gtk.MessageType.ERROR)
|
|
box.add_destroy_button(_("K thanks"))
|
|
self._nc.append(box)
|
|
if do_notify:
|
|
GLib.idle_add(_notify)
|
|
|
|
return accepted
|
|
|
|
|
|
class WorkViewController(GObject.Object):
|
|
|
|
APP_IMAGE_SIZE = 48
|
|
|
|
def __init__(self, icons, rigo_service, work_box):
|
|
self._icons = icons
|
|
self._service = rigo_service
|
|
self._box = work_box
|
|
|
|
def _setup_terminal_menu(self):
|
|
"""
|
|
Setup TerminalWidget Right Click popup-menu.
|
|
"""
|
|
self._terminal_menu = Gtk.Menu()
|
|
|
|
sall_menu_item = Gtk.ImageMenuItem.new_from_stock(
|
|
"gtk-select-all", None)
|
|
sall_menu_item.connect("activate", self._on_terminal_select_all)
|
|
self._terminal_menu.append(sall_menu_item)
|
|
|
|
copy_menu_item = Gtk.ImageMenuItem.new_from_stock(
|
|
"gtk-copy", None)
|
|
copy_menu_item.connect("activate", self._on_terminal_copy)
|
|
self._terminal_menu.append(copy_menu_item)
|
|
|
|
reset_menu_item = Gtk.ImageMenuItem.new_from_stock(
|
|
"gtk-clear", None)
|
|
reset_menu_item.connect("activate", self._on_terminal_reset)
|
|
self._terminal_menu.append(reset_menu_item)
|
|
|
|
self._terminal_menu.show_all()
|
|
|
|
def _setup_terminal_area(self):
|
|
"""
|
|
Setup TerminalWidget area (including ScrollBar).
|
|
"""
|
|
hbox = Gtk.HBox()
|
|
|
|
self._terminal = TerminalWidget()
|
|
self._terminal.connect(
|
|
"button-press-event",
|
|
self._on_terminal_click)
|
|
self._terminal.reset()
|
|
hbox.pack_start(self._terminal, True, True, 0)
|
|
|
|
scrollbar = Gtk.VScrollbar.new(self._terminal.adjustment)
|
|
hbox.pack_start(scrollbar, False, False, 0)
|
|
hbox.show_all()
|
|
|
|
return hbox
|
|
|
|
def _setup_progress_area(self):
|
|
"""
|
|
Setup Progress Bar area.
|
|
"""
|
|
self._progress_box = Gtk.VBox()
|
|
|
|
progress_align = Gtk.Alignment()
|
|
progress_align.set_padding(10, 10, 0, 0)
|
|
self._progress_bar = Gtk.ProgressBar()
|
|
progress_align.add(self._progress_bar)
|
|
self._progress_box.pack_start(progress_align, False, False, 0)
|
|
self._progress_box.show_all()
|
|
|
|
return self._progress_box
|
|
|
|
def _setup_app_area(self):
|
|
"""
|
|
Setup Application Information Area.
|
|
"""
|
|
self._app_box = Gtk.VBox()
|
|
|
|
hbox = Gtk.HBox()
|
|
|
|
self._missing_icon = self._icons.load_icon(
|
|
Icons.MISSING_APP,
|
|
self.APP_IMAGE_SIZE, 0)
|
|
|
|
# Image
|
|
image_box = Gtk.VBox()
|
|
self._app_image = Gtk.Image.new_from_pixbuf(
|
|
self._missing_icon)
|
|
|
|
stars_align = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0)
|
|
stars_align.set_padding(5, 0, 0, 0)
|
|
self._stars = Star()
|
|
stars_align.add(self._stars)
|
|
self._stars.set_size_as_pixel_value(16)
|
|
|
|
image_box.pack_start(self._app_image, False, False, 0)
|
|
image_box.pack_start(stars_align, False, False, 0)
|
|
|
|
hbox.pack_start(image_box, False, False, 0)
|
|
|
|
# Action, App Name & Description
|
|
name_align = Gtk.Alignment()
|
|
name_align.set_padding(0, 0, 5, 0)
|
|
name_box = Gtk.VBox()
|
|
|
|
action_align = Gtk.Alignment()
|
|
self._action_label = Gtk.Label("Action")
|
|
self._action_label.set_alignment(0.0, 0.0)
|
|
action_align.add(self._action_label)
|
|
action_align.set_padding(0, 4, 0, 0)
|
|
|
|
self._appname_label = Gtk.Label("App Name")
|
|
self._appname_label.set_line_wrap(True)
|
|
self._appname_label.set_line_wrap_mode(Pango.WrapMode.WORD)
|
|
self._appname_label.set_alignment(0.0, 1.0)
|
|
|
|
name_box.pack_start(action_align, False, False, 0)
|
|
name_box.pack_start(self._appname_label, True, True, 0)
|
|
name_align.add(name_box)
|
|
|
|
hbox.pack_start(name_align, True, True, 5)
|
|
|
|
self._app_box.pack_start(hbox, False, False, 5)
|
|
|
|
return self._app_box
|
|
|
|
def setup(self):
|
|
"""
|
|
Initialize WorkViewController controlled resources.
|
|
"""
|
|
self._setup_terminal_menu()
|
|
|
|
box = self._setup_app_area()
|
|
self._box.pack_start(box, False, False, 0)
|
|
|
|
box = self._setup_progress_area()
|
|
self._box.pack_start(box, False, False, 0)
|
|
|
|
box = self._setup_terminal_area()
|
|
self._box.pack_start(box, True, True, 0)
|
|
|
|
self._service.set_terminal(self._terminal)
|
|
|
|
self.deactivate_progress_bar()
|
|
self.deactivate_app_box()
|
|
|
|
def activate_app_box(self):
|
|
"""
|
|
Activate the Application Box showing information
|
|
about the Application being currently handled.
|
|
"""
|
|
self._app_box.show_all()
|
|
|
|
def deactivate_app_box(self):
|
|
"""
|
|
Deactivate the Application Box showing information
|
|
about the Application being currently handled.
|
|
"""
|
|
self._app_box.hide()
|
|
|
|
def activate_progress_bar(self):
|
|
"""
|
|
Activate the Progress Bar showing progress information.
|
|
"""
|
|
self._progress_box.show_all()
|
|
|
|
def deactivate_progress_bar(self):
|
|
"""
|
|
Deactivate the Progress Bar showing progress information.
|
|
"""
|
|
self._progress_box.hide()
|
|
|
|
def _set_application_icon(self, app):
|
|
"""
|
|
Set Application Icon image.
|
|
"""
|
|
icon, cache_hit = app.get_icon()
|
|
if icon is None:
|
|
self._app_image.set_from_pixbuf(
|
|
self._missing_icon)
|
|
return
|
|
|
|
icon_path = icon.local_document()
|
|
if not os.path.isfile(icon_path):
|
|
self._app_image.set_from_pixbuf(
|
|
self._missing_icon)
|
|
return
|
|
|
|
try:
|
|
img = Gtk.Image.new_from_file(icon_path)
|
|
except GObject.GError:
|
|
img = None
|
|
|
|
img_buf = None
|
|
if img is not None:
|
|
img_buf = img.get_pixbuf()
|
|
if img_buf is not None:
|
|
w, h = img_buf.get_width(), \
|
|
img_buf.get_height()
|
|
width = self.APP_IMAGE_SIZE
|
|
if w < 1:
|
|
# not legit
|
|
height = self.APP_IMAGE_SIZE
|
|
else:
|
|
height = width * h / w
|
|
|
|
del img_buf
|
|
del img
|
|
|
|
try:
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
|
|
icon_path, width, height)
|
|
if pixbuf is not None:
|
|
self._app_image.set_from_pixbuf(pixbuf)
|
|
else:
|
|
self._app_image.set_from_pixbuf(
|
|
self._missing_icon)
|
|
except GObject.GError:
|
|
self._app_image.set_from_pixbuf(
|
|
self._missing_icon)
|
|
|
|
def set_application(self, app, daemon_action):
|
|
"""
|
|
Set Application information by providing its Application
|
|
object.
|
|
"""
|
|
msg = None
|
|
if daemon_action == DaemonAppActions.INSTALL:
|
|
msg = _("Installing")
|
|
elif daemon_action == DaemonAppActions.REMOVE:
|
|
msg = _("Removing")
|
|
|
|
if msg is not None:
|
|
self._action_label.set_markup(
|
|
"<big><b>%s</b></big>" % (escape_markup(msg),))
|
|
|
|
self._appname_label.set_markup(
|
|
app.get_extended_markup())
|
|
|
|
self._set_application_icon(app)
|
|
|
|
# rating
|
|
stats = app.get_review_stats()
|
|
self._stars.set_rating(stats.ratings_average)
|
|
|
|
self.activate_app_box()
|
|
self._app_box.queue_draw()
|
|
|
|
def set_progress(self, fraction, text=None):
|
|
"""
|
|
Set Progress Bar progress, progress must be a value between
|
|
0.0 and 1.0. You can also provide a new text for progress at
|
|
the same time, the same will be escaped and cleaned out
|
|
by the callee.
|
|
"""
|
|
self._progress_bar.set_fraction(fraction)
|
|
if text is not None:
|
|
self._progress_bar.set_text(escape_markup(text))
|
|
self._progress_bar.set_show_text(text is not None)
|
|
|
|
def set_progress_text(self, text):
|
|
"""
|
|
Set Progress Bar text. The same will be escaped and cleaned out by
|
|
the callee.
|
|
"""
|
|
self._progress_bar.set_text(escape_markup(text))
|
|
|
|
def _on_terminal_click(self, widget, event):
|
|
"""
|
|
Right Click on the TerminalWidget area.
|
|
"""
|
|
if event.button == 3:
|
|
self._terminal_menu.popup(
|
|
None, None, None, None, event.button, event.time)
|
|
|
|
def _on_terminal_copy(self, widget):
|
|
"""
|
|
Copy to clipboard Terminal GtkMenuItem clicked.
|
|
"""
|
|
self._terminal.copy_clipboard()
|
|
|
|
def _on_terminal_reset(self, widget):
|
|
"""
|
|
Reset Terminal GtkMenuItem clicked.
|
|
"""
|
|
self._terminal.reset()
|
|
|
|
def _on_terminal_select_all(self, widget):
|
|
"""
|
|
Select All Terminal GtkMenuItem clicked.
|
|
"""
|
|
self._terminal.select_all()
|
|
|
|
|
|
class NotificationViewController(GObject.Object):
|
|
|
|
"""
|
|
Base Class for NotificationBox Controller.
|
|
"""
|
|
|
|
def __init__(self, notification_box):
|
|
GObject.Object.__init__(self)
|
|
self._box = notification_box
|
|
self._context_id_map = {}
|
|
|
|
def setup(self):
|
|
"""
|
|
Controller Setup code
|
|
"""
|
|
|
|
def append(self, box, timeout=None, context_id=None):
|
|
"""
|
|
Append a notification to the Notification area.
|
|
context_id is used to automatically drop any other
|
|
notification exposing the same context identifier.
|
|
"""
|
|
context_id = box.get_context_id()
|
|
if context_id is not None:
|
|
old_box = self._context_id_map.get(context_id)
|
|
if old_box is not None:
|
|
old_box.destroy()
|
|
self._context_id_map[context_id] = box
|
|
box.render()
|
|
self._box.pack_start(box, False, False, 0)
|
|
box.show()
|
|
self._box.show()
|
|
if timeout is not None:
|
|
GLib.timeout_add_seconds(timeout, self.remove, box)
|
|
|
|
def append_safe(self, box, timeout=None):
|
|
"""
|
|
Thread-safe version of append().
|
|
"""
|
|
def _append():
|
|
self.append(box, timeout=timeout)
|
|
GLib.idle_add(_append)
|
|
|
|
def remove(self, box):
|
|
"""
|
|
Remove a NotificationBox from this notification
|
|
area, if there.
|
|
"""
|
|
if box in self._box.get_children():
|
|
context_id = box.get_context_id()
|
|
if context_id is not None:
|
|
self._context_id_map.pop(context_id, None)
|
|
box.destroy()
|
|
|
|
def remove_safe(self, box):
|
|
"""
|
|
Thread-safe version of remove().
|
|
"""
|
|
GLib.idle_add(self.remove, box)
|
|
|
|
def clear(self, managed=True):
|
|
"""
|
|
Clear all the notifications.
|
|
"""
|
|
for child in self._box.get_children():
|
|
if child.is_managed() and not managed:
|
|
continue
|
|
context_id = child.get_context_id()
|
|
if context_id is not None:
|
|
self._context_id_map.pop(context_id, None)
|
|
child.destroy()
|
|
|
|
def clear_safe(self, managed=True):
|
|
"""
|
|
Thread-safe version of clear().
|
|
"""
|
|
GLib.idle_add(self.clear, managed)
|
|
|
|
|
|
class UpperNotificationViewController(NotificationViewController):
|
|
|
|
"""
|
|
Notification area widget controller.
|
|
This class features the handling of some built-in
|
|
Notification objects (like updates and outdated repositories)
|
|
but also accepts external NotificationBox instances as well.
|
|
"""
|
|
|
|
def __init__(self, activity_rwsem, entropy_client, entropy_ws,
|
|
rigo_service, avc, notification_box):
|
|
|
|
NotificationViewController.__init__(
|
|
self, notification_box)
|
|
|
|
self._avc = avc
|
|
self._service = rigo_service
|
|
self._activity_rwsem = activity_rwsem
|
|
self._entropy = entropy_client
|
|
self._entropy_ws = entropy_ws
|
|
self._updates = None
|
|
self._security_updates = None
|
|
|
|
def setup(self):
|
|
"""
|
|
Reimplemented from NotificationViewController.
|
|
"""
|
|
GLib.timeout_add_seconds(1, self._calculate_updates)
|
|
GLib.idle_add(self._check_connectivity)
|
|
|
|
def _check_connectivity(self):
|
|
th = ParallelTask(self.__check_connectivity)
|
|
th.daemon = True
|
|
th.name = "CheckConnectivity"
|
|
th.start()
|
|
|
|
def _calculate_updates(self):
|
|
th = ParallelTask(self.__calculate_updates)
|
|
th.daemon = True
|
|
th.name = "CalculateUpdates"
|
|
th.start()
|
|
|
|
def __check_connectivity(self):
|
|
"""
|
|
Execute connectivity check basing on Entropy
|
|
Web Services availability.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
repositories = self._entropy.repositories()
|
|
available = False
|
|
for repository_id in repositories:
|
|
if self._entropy_ws.get(repository_id) is not None:
|
|
available = True
|
|
break
|
|
if not repositories:
|
|
# no repos to check against
|
|
available = True
|
|
|
|
if not available:
|
|
GLib.idle_add(self._notify_connectivity_issues)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def __order_updates(self, updates):
|
|
"""
|
|
Order updates using PN.
|
|
"""
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
def _key_func(x):
|
|
return self._entropy.open_repository(
|
|
x[1]).retrieveName(x[0]).lower()
|
|
return sorted(updates, key=_key_func)
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
|
|
def __calculate_updates(self):
|
|
self._activity_rwsem.reader_acquire()
|
|
self._entropy.rwsem().reader_acquire()
|
|
try:
|
|
unavailable_repositories = \
|
|
self._entropy.unavailable_repositories()
|
|
if unavailable_repositories:
|
|
GLib.idle_add(self._notify_unavailable_repositories_safe,
|
|
unavailable_repositories)
|
|
return
|
|
if Repository.are_repositories_old():
|
|
GLib.idle_add(self._notify_old_repositories_safe)
|
|
return
|
|
|
|
updates, removal, fine, spm_fine = \
|
|
self._entropy.calculate_updates()
|
|
self._updates = self.__order_updates(updates)
|
|
self._security_updates = self._entropy.calculate_security_updates()
|
|
finally:
|
|
self._entropy.rwsem().reader_release()
|
|
self._activity_rwsem.reader_release()
|
|
|
|
GLib.idle_add(self._notify_updates_safe)
|
|
|
|
def _notify_connectivity_issues(self):
|
|
"""
|
|
Cannot connect to Entropy Web Services.
|
|
"""
|
|
box = ConnectivityNotificationBox()
|
|
self.append(box)
|
|
|
|
def _notify_updates_safe(self):
|
|
"""
|
|
Add NotificationBox signaling the user that updates
|
|
are available.
|
|
"""
|
|
updates_len = len(self._updates)
|
|
if updates_len == 0:
|
|
# no updates, do not show anything
|
|
return
|
|
|
|
box = UpdatesNotificationBox(
|
|
self._entropy, self._avc,
|
|
updates_len, len(self._security_updates))
|
|
box.connect("upgrade-request", self._on_upgrade)
|
|
box.connect("show-request", self._on_update_show)
|
|
self.append(box)
|
|
|
|
def _notify_old_repositories_safe(self):
|
|
"""
|
|
Add a NotificationBox signaling the User that repositories
|
|
are old..
|
|
"""
|
|
box = RepositoriesUpdateNotificationBox(
|
|
self._entropy, self._avc)
|
|
box.connect("update-request", self._on_update)
|
|
self.append(box)
|
|
|
|
def _notify_unavailable_repositories_safe(self, unavailable):
|
|
"""
|
|
Add a NotificationBox signaling the User that some repositories
|
|
are unavailable..
|
|
"""
|
|
box = RepositoriesUpdateNotificationBox(
|
|
self._entropy, self._avc, unavailable=unavailable)
|
|
box.connect("update-request", self._on_update)
|
|
self.append(box)
|
|
|
|
def _on_upgrade(self, *args):
|
|
"""
|
|
Callback requesting Packages Update.
|
|
"""
|
|
# FIXME, lxnay complete
|
|
print("On Upgrade Request Received", args)
|
|
|
|
def _on_update(self, box):
|
|
"""
|
|
Callback requesting Repositories Update.
|
|
"""
|
|
self.remove(box)
|
|
self._service.update_repositories([], True)
|
|
|
|
def _on_update_show(self, *args):
|
|
"""
|
|
Callback from UpdatesNotification "Show" button.
|
|
Showing updates.
|
|
"""
|
|
self._avc.set_many_safe(self._updates)
|
|
|
|
class BottomNotificationViewController(NotificationViewController):
|
|
|
|
"""
|
|
Bottom Notification Area.
|
|
This area is only used to show Activity controls to User.
|
|
For example, during repositories update, this area just
|
|
shows one notification box stating that the above activity is in
|
|
progress, making possible to switch to the Work View anytime.
|
|
"""
|
|
|
|
UNIQUE_CONTEXT_ID = "BottomNotificationBoxContextId"
|
|
|
|
__gsignals__ = {
|
|
"show-work-view" : (GObject.SignalFlags.RUN_LAST,
|
|
None,
|
|
tuple(),
|
|
),
|
|
}
|
|
|
|
def __init__(self, notification_box):
|
|
|
|
NotificationViewController.__init__(
|
|
self, notification_box)
|
|
|
|
def _on_work_view_show(self, widget):
|
|
"""
|
|
User is asking to show the Work View.
|
|
"""
|
|
self.emit("show-work-view")
|
|
|
|
def _append_repositories_update(self):
|
|
"""
|
|
Add a NotificationBox related to Repositories Update
|
|
Activity in progress.
|
|
"""
|
|
msg = _("Repositories Update in <b>progress</b>...")
|
|
box = NotificationBox(
|
|
msg, message_type=Gtk.MessageType.INFO,
|
|
context_id=self.UNIQUE_CONTEXT_ID)
|
|
box.add_button(_("Show me"), self._on_work_view_show)
|
|
self.append(box)
|
|
|
|
def _append_installing_apps(self):
|
|
"""
|
|
Add a NotificationBox related to Applications Install
|
|
Activity in progress.
|
|
"""
|
|
msg = _("<b>Application Management</b> in progress...")
|
|
box = NotificationBox(
|
|
msg, message_type=Gtk.MessageType.INFO,
|
|
context_id=self.UNIQUE_CONTEXT_ID)
|
|
box.add_button(_("Show me"), self._on_work_view_show)
|
|
self.append(box)
|
|
|
|
def set_activity(self, local_activity):
|
|
"""
|
|
Set a current local Activity, showing the
|
|
most appropriate NotificationBox.
|
|
This method must be called from the MainLoop.
|
|
"""
|
|
if local_activity == LocalActivityStates.READY:
|
|
self.clear()
|
|
return
|
|
|
|
if local_activity == LocalActivityStates.UPDATING_REPOSITORIES:
|
|
self._append_repositories_update()
|
|
return
|
|
elif local_activity == LocalActivityStates.MANAGING_APPLICATIONS:
|
|
self._append_installing_apps()
|
|
return
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
class Rigo(Gtk.Application):
|
|
|
|
class RigoHandler(object):
|
|
|
|
def __init__(self, rigo_app):
|
|
self._app = rigo_app
|
|
|
|
def onDeleteWindow(self, window, event):
|
|
# if UI is locked, do not allow to close Rigo
|
|
if self._app.is_ui_locked():
|
|
rc = self._app._show_yesno_dialog(
|
|
None,
|
|
escape_markup(_("Hey hey hey!")),
|
|
escape_markup(_("Rigo is working, are you sure?")))
|
|
if rc == Gtk.ResponseType.NO:
|
|
return True
|
|
|
|
while True:
|
|
try:
|
|
entropy.tools.kill_threads()
|
|
Gtk.main_quit((window, event))
|
|
except KeyboardInterrupt:
|
|
continue
|
|
break
|
|
|
|
def __init__(self):
|
|
self._current_state_lock = False
|
|
self._current_state = RigoViewStates.STATIC_VIEW_STATE
|
|
self._state_transactions = {
|
|
RigoViewStates.BROWSER_VIEW_STATE: (
|
|
self._enter_browser_state,
|
|
self._exit_browser_state),
|
|
RigoViewStates.STATIC_VIEW_STATE: (
|
|
self._enter_static_state,
|
|
self._exit_static_state),
|
|
RigoViewStates.APPLICATION_VIEW_STATE: (
|
|
self._enter_application_state,
|
|
self._exit_application_state),
|
|
RigoViewStates.WORK_VIEW_STATE: (
|
|
self._enter_work_state,
|
|
self._exit_work_state),
|
|
}
|
|
self._state_mutex = Lock()
|
|
|
|
icons = get_sc_icon_theme(DATA_DIR)
|
|
app_handler = Rigo.RigoHandler(self)
|
|
|
|
self._activity_rwsem = ReadersWritersSemaphore()
|
|
self._entropy = Client()
|
|
self._entropy_ws = EntropyWebService(self._entropy)
|
|
self._auth = AuthenticationController()
|
|
self._service = RigoServiceController(
|
|
self, self._activity_rwsem,
|
|
self._auth, self._entropy, self._entropy_ws)
|
|
|
|
self._builder = Gtk.Builder()
|
|
self._builder.add_from_file(os.path.join(DATA_DIR, "ui/gtk3/rigo.ui"))
|
|
self._builder.connect_signals(app_handler)
|
|
self._window = self._builder.get_object("rigoWindow")
|
|
self._window.set_name("rigo-view")
|
|
self._apps_view = self._builder.get_object("appsViewVbox")
|
|
self._scrolled_view = self._builder.get_object("appsViewScrolledWindow")
|
|
self._app_view = self._builder.get_object("appViewScrollWin")
|
|
self._app_view.set_name("rigo-view")
|
|
self._app_view_port = self._builder.get_object("appViewVport")
|
|
self._app_view_port.set_name("rigo-view")
|
|
self._not_found_box = self._builder.get_object("appsViewNotFoundVbox")
|
|
self._search_entry = self._builder.get_object("searchEntry")
|
|
self._search_entry_completion = self._builder.get_object(
|
|
"searchEntryCompletion")
|
|
self._search_entry_store = self._builder.get_object(
|
|
"searchEntryStore")
|
|
self._static_view = self._builder.get_object("staticViewVbox")
|
|
self._notification = self._builder.get_object("notificationBox")
|
|
self._bottom_notification = \
|
|
self._builder.get_object("bottomNotificationBox")
|
|
self._work_view = self._builder.get_object("workViewVbox")
|
|
self._work_view.set_name("rigo-view")
|
|
|
|
self._app_view_c = ApplicationViewController(
|
|
self._entropy, self._entropy_ws, self._builder)
|
|
|
|
self._view = AppTreeView(
|
|
self._entropy, self._service, self._app_view_c, icons,
|
|
True, AppListStore.ICON_SIZE, store=None)
|
|
self._scrolled_view.add(self._view)
|
|
|
|
self._app_store = AppListStore(
|
|
self._entropy, self._entropy_ws,
|
|
self._view, icons)
|
|
def _queue_draw(*args):
|
|
self._view.queue_draw()
|
|
self._app_store.connect("redraw-request", _queue_draw)
|
|
|
|
self._app_view_c.set_store(self._app_store)
|
|
self._app_view_c.connect("application-show",
|
|
self._on_application_show)
|
|
|
|
self._welcome_box = WelcomeBox()
|
|
|
|
settings = Gtk.Settings.get_default()
|
|
settings.set_property("gtk-error-bell", False)
|
|
# wire up the css provider to reconfigure on theme-changes
|
|
self._window.connect("style-updated",
|
|
self._on_style_updated,
|
|
init_sc_css_provider,
|
|
settings,
|
|
Gdk.Screen.get_default(),
|
|
DATA_DIR)
|
|
|
|
self._avc = ApplicationsViewController(
|
|
self._activity_rwsem,
|
|
self._entropy, self._entropy_ws, self._service,
|
|
icons, self._not_found_box,
|
|
self._search_entry, self._search_entry_completion,
|
|
self._search_entry_store, self._app_store, self._view)
|
|
|
|
self._avc.connect("view-cleared", self._on_view_cleared)
|
|
self._avc.connect("view-filled", self._on_view_filled)
|
|
self._avc.connect("view-want-change", self._on_view_change)
|
|
|
|
self._nc = UpperNotificationViewController(
|
|
self._activity_rwsem, self._entropy,
|
|
self._entropy_ws, self._service,
|
|
self._avc, self._notification)
|
|
|
|
# Bottom NotificationBox controller.
|
|
# Bottom notifications are only used for
|
|
# providing Activity control to User during
|
|
# the Activity itself.
|
|
self._bottom_nc = BottomNotificationViewController(
|
|
self._bottom_notification)
|
|
self._service.set_bottom_notification_controller(
|
|
self._bottom_nc)
|
|
|
|
self._app_view_c.set_notification_controller(self._nc)
|
|
self._app_view_c.set_applications_controller(self._avc)
|
|
|
|
self._service.set_applications_controller(self._avc)
|
|
self._service.set_application_controller(self._app_view_c)
|
|
self._service.set_notification_controller(self._nc)
|
|
|
|
self._service.connect("start-working", self._on_start_working)
|
|
self._service.connect("repositories-updated",
|
|
self._on_repo_updated)
|
|
self._service.connect("applications-managed",
|
|
self._on_applications_managed)
|
|
|
|
self._work_view_c = WorkViewController(
|
|
icons, self._service, self._work_view)
|
|
self._service.set_work_controller(self._work_view_c)
|
|
|
|
self._bottom_nc.connect("show-work-view", self._on_show_work_view)
|
|
|
|
def is_ui_locked(self):
|
|
"""
|
|
Return whether the UI is currently locked.
|
|
"""
|
|
return self._current_state_lock
|
|
|
|
def _on_start_working(self, widget, state, lock):
|
|
"""
|
|
Emitted by RigoServiceController when we're asked to
|
|
switch to the Work View and, if lock = True, lock UI.
|
|
"""
|
|
if lock:
|
|
self._search_entry.set_sensitive(False)
|
|
if state is not None:
|
|
self._change_view_state(state, lock=lock)
|
|
|
|
def _on_show_work_view(self, widget):
|
|
"""
|
|
We've been explicitly asked to switch to WORK_VIEW_STATE
|
|
"""
|
|
self._change_view_state(RigoViewStates.WORK_VIEW_STATE,
|
|
_ignore_lock=True)
|
|
|
|
def _on_repo_updated(self, widget, result, message):
|
|
"""
|
|
Emitted by RigoServiceController telling us that
|
|
repositories have been updated.
|
|
"""
|
|
with self._state_mutex:
|
|
self._current_state_lock = False
|
|
self._search_entry.set_sensitive(True)
|
|
if result != 0:
|
|
msg = "<b>%s</b>: %s" % (
|
|
_("Repositories update error"),
|
|
message,)
|
|
message_type = Gtk.MessageType.ERROR
|
|
else:
|
|
msg = _("Repositories updated <b>successfully</b>!")
|
|
message_type = Gtk.MessageType.INFO
|
|
|
|
box = NotificationBox(
|
|
msg, message_type=message_type,
|
|
context_id=RigoServiceController.NOTIFICATION_CONTEXT_ID)
|
|
box.add_destroy_button(_("Ok, thanks"))
|
|
self._nc.append(box)
|
|
|
|
def _on_applications_managed(self, widget, success):
|
|
"""
|
|
Emitted by RigoServiceController telling us that
|
|
enqueue application actions have been completed.
|
|
"""
|
|
if not success:
|
|
msg = "<b>%s</b>: %s" % (
|
|
_("Application Management Error"),
|
|
_("please check the install log"),)
|
|
message_type = Gtk.MessageType.ERROR
|
|
else:
|
|
msg = _("Applications managed <b>successfully</b>!")
|
|
message_type = Gtk.MessageType.INFO
|
|
|
|
box = NotificationBox(
|
|
msg, message_type=message_type,
|
|
context_id=RigoServiceController.NOTIFICATION_CONTEXT_ID)
|
|
box.add_destroy_button(_("Ok, thanks"))
|
|
box.add_button(_("Show me"), self._on_show_work_view)
|
|
self._nc.append(box)
|
|
self._work_view_c.deactivate_app_box()
|
|
|
|
def _on_view_cleared(self, *args):
|
|
self._change_view_state(RigoViewStates.STATIC_VIEW_STATE)
|
|
|
|
def _on_view_filled(self, *args):
|
|
self._change_view_state(RigoViewStates.BROWSER_VIEW_STATE)
|
|
|
|
def _on_view_change(self, widget, state):
|
|
self._change_view_state(state)
|
|
|
|
def _on_application_show(self, *args):
|
|
self._change_view_state(RigoViewStates.APPLICATION_VIEW_STATE)
|
|
|
|
def _exit_browser_state(self):
|
|
"""
|
|
Action triggered when UI exits the Application Browser
|
|
state (or mode).
|
|
"""
|
|
self._apps_view.hide()
|
|
|
|
def _enter_browser_state(self):
|
|
"""
|
|
Action triggered when UI exits the Application Browser
|
|
state (or mode).
|
|
"""
|
|
self._apps_view.show()
|
|
|
|
def _exit_static_state(self):
|
|
"""
|
|
Action triggered when UI exits the Static Browser
|
|
state (or mode). AKA the Welcome Box.
|
|
"""
|
|
self._static_view.hide()
|
|
# release all the childrens of static_view
|
|
for child in self._static_view.get_children():
|
|
self._static_view.remove(child)
|
|
|
|
def _enter_static_state(self):
|
|
"""
|
|
Action triggered when UI exits the Static Browser
|
|
state (or mode). AKA the Welcome Box.
|
|
"""
|
|
# keep the current widget if any, or add the
|
|
# welcome widget
|
|
if not self._static_view.get_children():
|
|
self._welcome_box.show()
|
|
self._static_view.pack_start(self._welcome_box,
|
|
True, True, 10)
|
|
self._static_view.show()
|
|
|
|
def _enter_application_state(self):
|
|
"""
|
|
Action triggered when UI enters the Package Information
|
|
state (or mode). Showing application information.
|
|
"""
|
|
# change search_entry first icon to emphasize the
|
|
# back action
|
|
self._search_entry.set_icon_from_stock(
|
|
Gtk.EntryIconPosition.PRIMARY,
|
|
"gtk-go-back")
|
|
self._app_view.show()
|
|
|
|
def _exit_application_state(self):
|
|
"""
|
|
Action triggered when UI exits the Package Information
|
|
state (or mode). Hiding back application information.
|
|
"""
|
|
self._search_entry.set_icon_from_stock(
|
|
Gtk.EntryIconPosition.PRIMARY, "gtk-find")
|
|
self._app_view.hide()
|
|
self._app_view_c.hide()
|
|
|
|
def _enter_work_state(self):
|
|
"""
|
|
Action triggered when UI enters the Work View state (or mode).
|
|
Either for Updating Repositories or Installing new Apps.
|
|
"""
|
|
self._work_view.show()
|
|
|
|
def _exit_work_state(self):
|
|
"""
|
|
Action triggered when UI exits the Work View state (or mode).
|
|
"""
|
|
self._work_view.hide()
|
|
|
|
def _change_view_state(self, state, lock=False, _ignore_lock=False):
|
|
"""
|
|
Change Rigo Application UI state.
|
|
You can pass a custom widget that will be shown in case
|
|
of static view state.
|
|
"""
|
|
with self._state_mutex:
|
|
if self._current_state_lock and not _ignore_lock:
|
|
const_debug_write(
|
|
__name__,
|
|
"cannot change view state, UI locked")
|
|
return False
|
|
txc = self._state_transactions.get(state)
|
|
if txc is None:
|
|
raise AttributeError("wrong view state")
|
|
enter_st, exit_st = txc
|
|
|
|
current_enter_st, current_exit_st = self._state_transactions.get(
|
|
self._current_state)
|
|
# exit from current state
|
|
current_exit_st()
|
|
# enter the new state
|
|
enter_st()
|
|
self._current_state = state
|
|
if lock:
|
|
self._current_state_lock = True
|
|
|
|
return True
|
|
|
|
def _change_view_state_safe(self, state):
|
|
"""
|
|
Thread-safe version of change_view_state().
|
|
"""
|
|
def _do_change():
|
|
return self._change_view_state(state)
|
|
GLib.idle_add(_do_change)
|
|
|
|
def _on_style_updated(self, widget, init_css_callback, *args):
|
|
"""
|
|
Gtk Style callback, nothing to see here.
|
|
"""
|
|
init_css_callback(widget, *args)
|
|
|
|
def _show_ok_dialog(self, parent, title, message):
|
|
"""
|
|
Show ugly OK dialog window.
|
|
"""
|
|
dlg = Gtk.MessageDialog(parent=parent,
|
|
type=Gtk.MessageType.INFO,
|
|
buttons=Gtk.ButtonsType.OK)
|
|
dlg.set_markup(message)
|
|
dlg.set_title(title)
|
|
dlg.run()
|
|
dlg.destroy()
|
|
|
|
def _show_yesno_dialog(self, parent, title, message):
|
|
"""
|
|
Show ugly Yes/No dialog window.
|
|
"""
|
|
dlg = Gtk.MessageDialog(parent=parent,
|
|
type=Gtk.MessageType.INFO,
|
|
buttons=Gtk.ButtonsType.YES_NO)
|
|
dlg.set_markup(message)
|
|
dlg.set_title(title)
|
|
rc = dlg.run()
|
|
dlg.destroy()
|
|
return rc
|
|
|
|
def _permissions_setup(self):
|
|
"""
|
|
Check execution privileges and spawn the Rigo UI.
|
|
"""
|
|
if not entropy.tools.is_user_in_entropy_group():
|
|
# otherwise the lock handling would potentially
|
|
# fail.
|
|
self._show_ok_dialog(
|
|
None,
|
|
escape_markup(_("Not authorized")),
|
|
escape_markup(_("You are not authorized to run Rigo")))
|
|
entropy.tools.kill_threads()
|
|
Gtk.main_quit()
|
|
return
|
|
|
|
if not self._service.service_available():
|
|
self._show_ok_dialog(
|
|
None,
|
|
escape_markup(_("Rigo")),
|
|
escape_markup(_("RigoDaemon service is not available")))
|
|
entropy.tools.kill_threads()
|
|
Gtk.main_quit()
|
|
return
|
|
|
|
acquired = not self._entropy.wait_resources(
|
|
max_lock_count=1,
|
|
shared=True)
|
|
is_exclusive = False
|
|
if not acquired:
|
|
# check whether RigoDaemon is running in excluive mode
|
|
# and ignore non-atomicity here (failing with error
|
|
# is acceptable)
|
|
if not self._service.exclusive():
|
|
self._show_ok_dialog(
|
|
None,
|
|
escape_markup(_("Rigo")),
|
|
escape_markup(_("Another Application Manager is active")))
|
|
entropy.tools.kill_threads()
|
|
Gtk.main_quit()
|
|
return
|
|
is_exclusive = True
|
|
# otherwise we can go ahead and handle our state later
|
|
|
|
# check RigoDaemon, don't worry about races between Rigo Clients
|
|
# it is fine to have multiple Rigo Clients connected. Mutual
|
|
# exclusion is handled via Entropy Resources Lock (which is a file
|
|
# based rwsem).
|
|
activity = self._service.activity()
|
|
if activity != DaemonActivityStates.AVAILABLE:
|
|
msg = ""
|
|
show_dialog = True
|
|
|
|
if activity == DaemonActivityStates.NOT_AVAILABLE:
|
|
msg = _("Background Service is currently not available")
|
|
|
|
elif activity == DaemonActivityStates.UPDATING_REPOSITORIES:
|
|
show_dialog = False
|
|
task = ParallelTask(
|
|
self._service._update_repositories,
|
|
[], False, master=False)
|
|
task.daemon = True
|
|
task.name = "UpdateRepositoriesUnlocked"
|
|
task.start()
|
|
|
|
elif activity == DaemonActivityStates.MANAGING_APPLICATIONS:
|
|
show_dialog = False
|
|
task = ParallelTask(
|
|
self._service._application_request,
|
|
None, None, master=False)
|
|
task.daemon = True
|
|
task.name = "ApplicationRequestUnlocked"
|
|
task.start()
|
|
|
|
elif activity == DaemonActivityStates.UPGRADING_SYSTEM:
|
|
# FIXME, jump to WORK_VIEW and show the progress.
|
|
msg = _("Background Service is updating your system")
|
|
elif activity == DaemonActivityStates.INTERNAL_ROUTINES:
|
|
msg = _("Background Service is currently busy")
|
|
else:
|
|
msg = _("Background Service is incompatible with Rigo")
|
|
|
|
if show_dialog:
|
|
self._show_ok_dialog(
|
|
None,
|
|
escape_markup(_("Rigo")),
|
|
escape_markup(msg))
|
|
entropy.tools.kill_threads()
|
|
Gtk.main_quit()
|
|
return
|
|
|
|
elif is_exclusive:
|
|
msg = _("Background Service is currently unavailable")
|
|
# no lock acquired, cannot continue the initialization
|
|
self._show_ok_dialog(
|
|
None,
|
|
escape_markup(_("Rigo")),
|
|
escape_markup(msg))
|
|
entropy.tools.kill_threads()
|
|
Gtk.main_quit()
|
|
return
|
|
|
|
self._app_view_c.setup()
|
|
self._avc.setup()
|
|
self._nc.setup()
|
|
self._work_view_c.setup()
|
|
self._service.setup(acquired)
|
|
self._window.show()
|
|
|
|
def run(self):
|
|
"""
|
|
Run Rigo ;-)
|
|
"""
|
|
self._welcome_box.render()
|
|
self._change_view_state(self._current_state)
|
|
GLib.idle_add(self._permissions_setup)
|
|
|
|
GLib.threads_init()
|
|
Gdk.threads_enter()
|
|
Gtk.main()
|
|
Gdk.threads_leave()
|
|
entropy.tools.kill_threads()
|
|
|
|
if __name__ == "__main__":
|
|
import signal
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
app = Rigo()
|
|
app.run()
|