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,