diff --git a/rigo/RigoDaemon/app.py b/rigo/RigoDaemon/app.py
index b108be1ec..67acc8a21 100755
--- a/rigo/RigoDaemon/app.py
+++ b/rigo/RigoDaemon/app.py
@@ -59,7 +59,7 @@ from entropy.core.settings.base import SystemSettings
import entropy.tools
-from RigoDaemon.enums import ActivityStates
+from RigoDaemon.enums import ActivityStates, AppActions
from RigoDaemon.config import DbusConfig
TEXT = TextInterface()
@@ -168,6 +168,51 @@ class DaemonUrlFetcher(UrlFetcher):
int(self.__remotesize), int(self.__datatransfer),
self.__time_remaining)
+class ApplicationsTransaction(object):
+
+ """
+ RigoDaemon Application Transaction Controller.
+ """
+
+ def __init__(self):
+ self._transactions = {}
+ self._transactions_mutex = Lock()
+
+ def set(self, package_id, repository_id, app_action):
+ """
+ Set transaction state for Application.
+ """
+ with self._transactions_mutex:
+ match = (package_id, repository_id)
+ self._transactions[match] = app_action
+
+ def unset(self, package_id, repository_id):
+ """
+ Unset transaction state for Application.
+ """
+ with self._transactions_mutex:
+ match = (package_id, repository_id)
+ self._transactions.pop(match, None)
+
+ def reset(self):
+ """
+ Reset all the transaction states.
+ """
+ with self._transactions_mutex:
+ self._transactions.clear()
+
+ def get(self, package_id, repository_id):
+ """
+ Get transaction state (in for of AppActions enum)
+ for given Application.
+ """
+ with self._transactions_mutex:
+ match = (package_id, repository_id)
+ tx = self._transactions.get(match)
+ if tx is None:
+ return AppActions.IDLE
+ return tx
+
class RigoDaemonService(dbus.service.Object):
@@ -231,6 +276,7 @@ class RigoDaemonService(dbus.service.Object):
bus = system_bus)
dbus.service.Object.__init__(self, name, object_path)
+ self._txs = ApplicationsTransaction()
# used by non-daemon thread to exit
self._stop_signal = False
@@ -331,6 +377,7 @@ class RigoDaemonService(dbus.service.Object):
self._close_local_resources()
self._entropy_setup()
self.activity_started(activity)
+ self.activity_progress(activity, 0)
if not repositories:
repositories = list(
@@ -361,6 +408,7 @@ class RigoDaemonService(dbus.service.Object):
"available, wtf !?!?")
# wtf??
self._release_exclusive(activity)
+ self.activity_progress(activity, 100)
self.activity_completed(activity, result == 0)
self.repositories_updated(result, msg)
@@ -422,33 +470,36 @@ class RigoDaemonService(dbus.service.Object):
write_output("_action_queue_worker_thread: "
"doing %s" % (
item,), debug=True)
- self._process_action(item)
+ self._process_action(item, activity)
finally:
_action_queue_finally()
- def _process_action(self, item):
+ def _process_action(self, item, activity):
"""
This is the real Application Action processing function.
"""
package_id, repository_id = item.pkg
action = item.action()
+ self._txs.reset()
+ self._txs.set(package_id, repository_id, action)
+
self.processing_application(package_id, repository_id, action)
success = False
try:
+ self.activity_progress(activity, 0)
# FIXME, complete
- # 1. calculate dependencies
- # 2. notify (and block) if conflicts arise
- # 3. ?? TBD show install/removal queue?
- # 4. [GUI?] if removal, ask if action is really wanted?
- # 5. [GUI?] if system package, avoid removal?
# simulate
self._entropy.output("Application Management Message")
- time.sleep(5)
+ time.sleep(2.5)
+ self.activity_progress(activity, 50)
+ time.sleep(2.5)
self._entropy.output("Application Management Complete")
success = True
finally:
+ self.activity_progress(activity, 100)
+ self._txs.reset()
self.application_processed(
package_id, repository_id, action, success)
@@ -603,6 +654,17 @@ class RigoDaemonService(dbus.service.Object):
write_output("action_queue_length called", debug=True)
return len(self._action_queue)
+ @dbus.service.method(BUS_NAME, in_signature='is',
+ out_signature='s')
+ def action(self, package_id, repository_id):
+ """
+ Return Application transaction state (AppAction enum
+ value)
+ """
+ write_output("action called: %s, %s" % (
+ package_id, repository_id,), debug=True)
+ return self._txs.get(package_id, repository_id)
+
@dbus.service.method(BUS_NAME, in_signature='',
out_signature='b')
def exclusive(self):
@@ -610,7 +672,7 @@ class RigoDaemonService(dbus.service.Object):
Return whether RigoDaemon is running in with
Entropy Resources acquired in exclusive mode.
"""
- write_output("is_exclusive called: %s, %s", debug=True)
+ write_output("exclusive called", debug=True)
return self._acquired_exclusive
@@ -705,6 +767,16 @@ class RigoDaemonService(dbus.service.Object):
write_output("activity_started() issued for %d" % (
activity,), debug=True)
+ @dbus.service.signal(dbus_interface=BUS_NAME,
+ signature='ii')
+ def activity_progress(self, activity, progress):
+ """
+ Signal all the connected Clients the ongoing activity
+ progress state (from 0 to 100).
+ """
+ write_output("activity_progress() issued for %d, state: %i" % (
+ activity, progress,), debug=True)
+
@dbus.service.signal(dbus_interface=BUS_NAME,
signature='ib')
def activity_completed(self, activity, success):
diff --git a/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml b/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml
index 602c0ccd2..cd1b9596c 100644
--- a/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml
+++ b/rigo/RigoDaemon/dbus/org.sabayon.Rigo.xml
@@ -23,6 +23,12 @@
+
+
+
+
+
+
diff --git a/rigo/RigoDaemon/enums.py b/rigo/RigoDaemon/enums.py
index cd0198686..fb7624f96 100644
--- a/rigo/RigoDaemon/enums.py
+++ b/rigo/RigoDaemon/enums.py
@@ -45,3 +45,4 @@ class ActivityStates:
class AppActions:
INSTALL = "install"
REMOVE = "remove"
+ IDLE = "idle"
diff --git a/rigo/rigo/ui/gtk3/controllers/application.py b/rigo/rigo/ui/gtk3/controllers/application.py
index 8903682a2..7395268d0 100644
--- a/rigo/rigo/ui/gtk3/controllers/application.py
+++ b/rigo/rigo/ui/gtk3/controllers/application.py
@@ -31,6 +31,8 @@ from rigo.ui.gtk3.widgets.images import ImageBox
from rigo.utils import build_application_store_url, \
escape_markup, prepare_markup
+from RigoDaemon.enums import AppActions as DaemonAppActions
+
from entropy.const import etpConst, const_debug_write, \
const_debug_enabled, const_convert_to_unicode, const_isunicode
from entropy.misc import ParallelTask
@@ -90,15 +92,17 @@ class ApplicationViewController(GObject.Object):
VOTE_NOTIFICATION_CONTEXT_ID = "VoteNotificationContext"
COMMENT_NOTIFICATION_CONTEXT_ID = "CommentNotificationContext"
- def __init__(self, entropy_client, entropy_ws, builder):
+ def __init__(self, entropy_client, entropy_ws, rigo_service, builder):
GObject.Object.__init__(self)
self._builder = builder
self._entropy = entropy_client
self._entropy_ws = entropy_ws
+ self._service = rigo_service
self._app_store = None
self._last_app = None
self._nc = None
self._avc = None
+ self._visible = False
self._window = self._builder.get_object("rigoWindow")
self._image = self._builder.get_object("appViewImage")
@@ -166,6 +170,40 @@ class ApplicationViewController(GObject.Object):
"changed", self._on_comment_buffer_changed)
self._stars.connect("changed", self._on_stars_clicked)
+ self._service.connect(
+ "application-processed", self._on_reload_state)
+ self._service.connect(
+ "application-processing", self._on_reload_state)
+ self._service.connect(
+ "application-abort", self._on_reload_state)
+
+ def _get_app_transaction(self, app):
+ """
+ Get Application transaction state (AppAction enum).
+ """
+ pkg_match = app.get_details().pkg
+ local_txs = self._service.local_transactions()
+ tx = local_txs.get(pkg_match)
+ if tx is None:
+ tx = self._service.action(app)
+ if tx == DaemonAppActions.IDLE:
+ tx = None
+ return tx
+
+ def _on_reload_state(self, srv, app, daemon_action):
+ """
+ Reload Application state due to a transaction event.
+ """
+ if not self._visible:
+ return
+ last_app = self._last_app
+ if last_app is not None:
+ app = last_app
+ task = ParallelTask(self._reload_application_state, app)
+ task.daemon = True
+ task.name = "OnReloadAppState"
+ task.start()
+
def _on_comment_buffer_changed(self, widget):
"""
Our comment text is changed, decide if to activate the Send button.
@@ -180,6 +218,7 @@ class ApplicationViewController(GObject.Object):
information. Once we're done loading the shit, we just emit
'application-show' and let others do the UI switch.
"""
+ self._visible = True
self._last_app = app
task = ParallelTask(self.__application_activate, app)
task.name = "ApplicationActivate"
@@ -363,6 +402,7 @@ class ApplicationViewController(GObject.Object):
This method shall be called when the Controller widgets are
going to hide.
"""
+ self._visible = False
self._last_app = None
for child in self._app_my_comments_box.get_children():
child.destroy()
@@ -741,40 +781,57 @@ class ApplicationViewController(GObject.Object):
for child in button_area.get_children():
child.destroy()
- if is_installed:
- if is_updatable:
- update_button = Gtk.Button()
- update_button.set_label(
- escape_markup(_("Update")))
- def _on_app_update(widget):
- return self._on_app_install(widget, app)
- update_button.connect("clicked", _on_app_update)
- button_area.pack_start(update_button, False, False, 0)
- else:
- reinstall_button = Gtk.Button()
- reinstall_button.set_label(
- escape_markup(_("Reinstall")))
- def _on_app_reinstall(widget):
- return self._on_app_install(widget, app)
- reinstall_button.connect("clicked", _on_app_reinstall)
- button_area.pack_start(reinstall_button, False, False, 0)
-
- remove_button = Gtk.Button()
- remove_button.set_label(
- escape_markup(_("Remove")))
- def _on_app_remove(widget):
- return self._on_app_remove(widget, app)
- remove_button.connect("clicked", _on_app_remove)
- button_area.pack_start(remove_button, False, False, 0)
-
+ daemon_action = self._get_app_transaction(app)
+ if daemon_action is not None:
+ button = Gtk.Button()
+ if daemon_action == DaemonAppActions.INSTALL:
+ button.set_label(escape_markup("Installing"))
+ elif daemon_action == DaemonAppActions.REMOVE:
+ button.set_label(escape_markup("Removing"))
+ button.set_sensitive(False)
+ button_area.pack_start(
+ button, False, False, 0)
else:
- install_button = Gtk.Button()
- install_button.set_label(
- escape_markup(_("Install")))
- def _on_app_install(widget):
- return self._on_app_install(widget, app)
- install_button.connect("clicked", _on_app_install)
- button_area.pack_start(install_button, False, False, 0)
+ if is_installed:
+ if is_updatable:
+ update_button = Gtk.Button()
+ update_button.set_label(
+ escape_markup(_("Update")))
+ def _on_app_update(widget):
+ return self._on_app_install(widget, app)
+ update_button.connect("clicked",
+ _on_app_update)
+ button_area.pack_start(update_button,
+ False, False, 0)
+ else:
+ reinstall_button = Gtk.Button()
+ reinstall_button.set_label(
+ escape_markup(_("Reinstall")))
+ def _on_app_reinstall(widget):
+ return self._on_app_install(widget, app)
+ reinstall_button.connect("clicked",
+ _on_app_reinstall)
+ button_area.pack_start(reinstall_button,
+ False, False, 0)
+
+ remove_button = Gtk.Button()
+ remove_button.set_label(
+ escape_markup(_("Remove")))
+ def _on_app_remove(widget):
+ return self._on_app_remove(widget, app)
+ remove_button.connect("clicked", _on_app_remove)
+ button_area.pack_start(remove_button,
+ False, False, 0)
+
+ else:
+ install_button = Gtk.Button()
+ install_button.set_label(
+ escape_markup(_("Install")))
+ def _on_app_install(widget):
+ return self._on_app_install(widget, app)
+ install_button.connect("clicked", _on_app_install)
+ button_area.pack_start(install_button,
+ False, False, 0)
button_area.show_all()
@@ -798,6 +855,18 @@ class ApplicationViewController(GObject.Object):
self._stars.set_rating(stats.ratings_average)
self._stars_alignment.show_all()
+ def _reload_application_state(self, app):
+ """
+ Reload Application state after transaction.
+ """
+ is_installed = app.is_installed()
+ is_updatable = app.is_updatable()
+
+ def _setup():
+ self._setup_buttons(
+ app, is_installed, is_updatable)
+ GLib.idle_add(_setup)
+
def _setup_application_info(self, app, metadata):
"""
Setup the actual UI widgets content and emit 'application-show'
diff --git a/rigo/rigo/ui/gtk3/widgets/apptreeview.py b/rigo/rigo/ui/gtk3/widgets/apptreeview.py
index 18cd4be47..e5bb1e40e 100644
--- a/rigo/rigo/ui/gtk3/widgets/apptreeview.py
+++ b/rigo/rigo/ui/gtk3/widgets/apptreeview.py
@@ -58,8 +58,6 @@ class AppTreeView(Gtk.TreeView):
self.pressed = False
self.focal_btn = None
- # pkg match is key, AppAction is val
- self._action_block_list = {}
self.expanded_path = None
#~ # if this hacked mode is available everything will be fast
@@ -286,23 +284,24 @@ class AppTreeView(Gtk.TreeView):
# update active app, use row-ref as argument
self.expand_path(row)
- app = model[row][COL_ROW_DATA]
+ pkg_match = model[row][COL_ROW_DATA]
# make sure this is not a category (LP: #848085)
- if self.rowref_is_category(app):
+ if self.rowref_is_category(pkg_match):
return False
action_btn = tr.get_button_by_name(
CellButtonIDs.ACTION)
#if not action_btn: return False
- app_action = self._action_block_list.get(app)
+ app = self.appmodel.get_application(pkg_match)
+ app_action = self._get_app_transaction(app)
if app_action is None:
- if self.appmodel.is_installed(app):
+ if self.appmodel.is_installed(pkg_match):
action_btn.set_variant(self.VARIANT_REMOVE)
action_btn.set_sensitive(True)
action_btn.show()
- elif self.appmodel.is_available(app):
+ elif self.appmodel.is_available(pkg_match):
action_btn.set_variant(self.VARIANT_INSTALL)
action_btn.set_sensitive(True)
action_btn.show()
@@ -310,7 +309,7 @@ class AppTreeView(Gtk.TreeView):
action_btn.set_sensitive(False)
action_btn.hide()
self._apc.emit("application-selected",
- self.appmodel.get_application(app))
+ app)
return
if self.pressed and self.focal_btn == action_btn:
@@ -329,15 +328,14 @@ class AppTreeView(Gtk.TreeView):
action_btn.show()
action_btn.set_state(Gtk.StateFlags.INSENSITIVE)
- #~ self.emit("application-selected", self.appmodel.get_application(app))
- self._apc.emit("application-selected",
- self.appmodel.get_application(app))
+ self._apc.emit("application-selected", app)
return False
def _on_row_activated(self, view, path, column, tr):
rowref = self.get_rowref(view.get_model(), path)
- if not rowref: return
+ if not rowref:
+ return
if self.rowref_is_category(rowref): return
@@ -483,8 +481,8 @@ class AppTreeView(Gtk.TreeView):
cell.set_property('isactive', is_active)
return
- def _app_activated_cb(self, btn, btn_id, app, store, path):
- if self.rowref_is_category(app):
+ def _app_activated_cb(self, btn, btn_id, pkg_match, store, path):
+ if self.rowref_is_category(pkg_match):
return
# FIXME: would be nice if that would be more elegant
@@ -493,28 +491,28 @@ class AppTreeView(Gtk.TreeView):
if type(store) is Gtk.TreeModelFilter:
store = store.get_model()
+ app = self.appmodel.get_application(pkg_match)
if btn_id == CellButtonIDs.INFO:
- self._apc.emit("application-activated",
- self.appmodel.get_application(app))
+ self._apc.emit("application-activated", app)
elif btn_id == CellButtonIDs.ACTION:
btn.set_sensitive(False)
store.row_changed(path, store.get_iter(path))
# be sure we dont request an action for a
# pkg with pre-existing actions
- if app in self._action_block_list:
+ daemon_action = self._get_app_transaction(app)
+ if daemon_action is not None:
logging.debug(
"Action already in progress for match: %s" % (
- (app,)))
+ (pkg_match,)))
return False
- if self.appmodel.is_installed(app):
+ if self.appmodel.is_installed(pkg_match):
perform_action = AppActions.REMOVE
else:
perform_action = AppActions.INSTALL
- self._action_block_list[app] = perform_action
+ self._set_app_transaction(app, perform_action)
self._apc.emit("application-request-action",
- self.appmodel.get_application(app),
- perform_action)
+ app, perform_action)
self.queue_draw()
return False
@@ -548,8 +546,7 @@ class AppTreeView(Gtk.TreeView):
"""
self.emit("cursor-changed")
# remove pkg from the block list
- pkg = app.get_details().pkg
- self._check_remove_pkg_from_blocklist(pkg)
+ self._pop_app_transaction(app)
action_btn = tr.get_button_by_name(CellButtonIDs.ACTION)
if action_btn:
@@ -588,8 +585,7 @@ class AppTreeView(Gtk.TreeView):
callback when an application install/remove
transaction has stopped
"""
- pkg = app.get_details().pkg
- self._check_remove_pkg_from_blocklist(pkg)
+ self._pop_app_transaction(app)
action_btn = tr.get_button_by_name(CellButtonIDs.ACTION)
if action_btn:
@@ -601,8 +597,34 @@ class AppTreeView(Gtk.TreeView):
self._set_cursor(action_btn, self._cursor_hand)
self.queue_draw()
- def _check_remove_pkg_from_blocklist(self, app):
- action = self._action_block_list.pop(app, None)
+ def _get_app_transaction(self, app):
+ """
+ Get Application transaction state (AppAction enum).
+ """
+ pkg_match = app.get_details().pkg
+ local_txs = self._backend.local_transactions()
+ tx = local_txs.get(pkg_match)
+ if tx is None:
+ tx = self._backend.action(app)
+ if tx == DaemonAppActions.IDLE:
+ tx = None
+ return tx
+
+ def _set_app_transaction(self, app, daemon_action):
+ """
+ Set Application local transaction state (AppAction enum).
+ """
+ pkg_match = app.get_details().pkg
+ local_txs = self._backend.local_transactions()
+ local_txs[pkg_match] = daemon_action
+
+ def _pop_app_transaction(self, app):
+ """
+ Drop Application local transaction state.
+ """
+ pkg_match = app.get_details().pkg
+ local_txs = self._backend.local_transactions()
+ action = local_txs.pop(pkg_match, None)
return action
def _xy_is_over_focal_row(self, x, y):
@@ -635,4 +657,3 @@ def on_entry_changed(widget, data):
if widget.stamp:
GObject.source_remove(widget.stamp)
widget.stamp = GObject.timeout_add(250, _work)
-
diff --git a/rigo/rigo_app.py b/rigo/rigo_app.py
index a9ef9c903..4f2514520 100644
--- a/rigo/rigo_app.py
+++ b/rigo/rigo_app.py
@@ -74,11 +74,10 @@ 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.
+ This is the Rigo Application frontend to RigoDaemon.
Handles privileged requests on our behalf.
"""
@@ -175,6 +174,7 @@ class RigoServiceController(GObject.Object):
_RESOURCES_UNLOCK_REQUEST_SIGNAL = "resources_unlock_request"
_RESOURCES_LOCK_REQUEST_SIGNAL = "resources_lock_request"
_ACTIVITY_STARTED_SIGNAL = "activity_started"
+ _ACTIVITY_PROGRESS_SIGNAL = "activity_progress"
_ACTIVITY_COMPLETED_SIGNAL = "activity_completed"
_PROCESSING_APPLICATION_SIGNAL = "processing_application"
_APPLICATION_PROCESSED_SIGNAL = "application_processed"
@@ -201,8 +201,10 @@ class RigoServiceController(GObject.Object):
self._registered_signals = {}
self._registered_signals_mutex = Lock()
+ self._local_transactions = {}
self._local_activity = LocalActivityStates.READY
self._local_activity_mutex = Lock()
+ self._daemon_activity_progress = 0
self._please_wait_box = None
self._please_wait_mutex = Lock()
@@ -320,6 +322,12 @@ class RigoServiceController(GObject.Object):
"""
return self._local_activity
+ def local_transactions(self):
+ """
+ Return the current local transaction state mapping.
+ """
+ return self._local_transactions
+
@property
def repositories_lock(self):
"""
@@ -388,6 +396,13 @@ class RigoServiceController(GObject.Object):
self._activity_started_signal,
dbus_interface=self.DBUS_INTERFACE)
+ # RigoDaemon is telling us the activity
+ # progress
+ self.__entropy_bus.connect_to_signal(
+ self._ACTIVITY_PROGRESS_SIGNAL,
+ self._activity_progress_signal,
+ dbus_interface=self.DBUS_INTERFACE)
+
# RigoDaemon is telling us that an activity
# has been completed
self.__entropy_bus.connect_to_signal(
@@ -776,6 +791,18 @@ class RigoServiceController(GObject.Object):
# reset please wait notification then
self._please_wait(None)
+ def _activity_progress_signal(self, activity, progress):
+ """
+ RigoDaemon is telling us the currently running activity
+ progress.
+ """
+ const_debug_write(
+ __name__,
+ "_activity_progress_signal: "
+ "called, with remote activity: %s, val: %i" % (
+ activity, progress,))
+ self._daemon_activity_progress = progress
+
def _activity_completed_signal(self, activity, success):
"""
RigoDaemon is telling us that the scheduled activity,
@@ -827,6 +854,17 @@ class RigoServiceController(GObject.Object):
self._entropy_bus,
dbus_interface=self.DBUS_INTERFACE).action_queue_length()
+ def action(self, app):
+ """
+ Return Application transaction state (RigoDaemon.AppAction enum
+ value).
+ """
+ package_id, repository_id = app.get_details().pkg
+ return dbus.Interface(
+ self._entropy_bus,
+ dbus_interface=self.DBUS_INTERFACE).action(
+ package_id, repository_id)
+
def exclusive(self):
"""
Return whether RigoDaemon is running in with
@@ -2108,7 +2146,8 @@ class Rigo(Gtk.Application):
self._work_view.set_name("rigo-view")
self._app_view_c = ApplicationViewController(
- self._entropy, self._entropy_ws, self._builder)
+ self._entropy, self._entropy_ws, self._service,
+ self._builder)
self._view = AppTreeView(
self._entropy, self._service, self._app_view_c, icons,