diff --git a/qubes_config/global_config.glade b/qubes_config/global_config.glade index 36db4729..10c5d18f 100644 --- a/qubes_config/global_config.glade +++ b/qubes_config/global_config.glade @@ -976,7 +976,7 @@ False natural - + True False @@ -1069,7 +1069,7 @@ 0 - 10 + 11 2 @@ -1085,7 +1085,7 @@ 0 - 13 + 14 @@ -1101,7 +1101,7 @@ 1 - 13 + 14 @@ -1114,7 +1114,7 @@ 1 - 14 + 15 @@ -1127,7 +1127,7 @@ 1 - 15 + 16 @@ -1238,7 +1238,7 @@ 0 - 17 + 18 2 @@ -1255,7 +1255,7 @@ 0 - 18 + 19 2 @@ -1271,7 +1271,7 @@ 0 - 22 + 23 2 @@ -1283,7 +1283,7 @@ 1 - 24 + 25 @@ -1295,7 +1295,7 @@ 0 - 25 + 26 2 @@ -1312,7 +1312,7 @@ 0 - 23 + 24 2 @@ -1462,7 +1462,7 @@ 0 - 11 + 12 2 @@ -1509,7 +1509,7 @@ 0 - 12 + 13 2 @@ -1554,7 +1554,7 @@ 0 - 14 + 15 @@ -1578,7 +1578,7 @@ 0 - 9 + 10 @@ -1590,7 +1590,7 @@ 0 - 16 + 17 @@ -1602,7 +1602,7 @@ 0 - 21 + 22 @@ -1633,7 +1633,7 @@ 0 - 15 + 16 @@ -1648,7 +1648,7 @@ 0 - 24 + 25 @@ -1663,7 +1663,7 @@ 0 - 19 + 20 @@ -1678,7 +1678,7 @@ 0 - 20 + 21 @@ -1718,7 +1718,7 @@ 1 - 19 + 20 @@ -1758,7 +1758,7 @@ 1 - 20 + 21 @@ -1858,7 +1858,7 @@ False True - 2 + 0 @@ -1876,7 +1876,7 @@ False True - 3 + 1 @@ -1885,6 +1885,77 @@ 8 + + + True + False + + + True + True + center + 5 + 6 + + + False + True + 0 + + + + + True + False + start + 5 + 5 + True + MiB + + + + False + True + 1 + + + + + 1 + 9 + + + + + True + False + vertical + + + True + False + The amount of free system memory that will never be used for preloading disposables. Ensures preloaded disposables do not consume all available memory. + True + 0 + + + + False + True + 0 + + + + + 0 + 9 + + diff --git a/qubes_config/global_config/basics_handler.py b/qubes_config/global_config/basics_handler.py index bc2cfe92..e1f1974e 100644 --- a/qubes_config/global_config/basics_handler.py +++ b/qubes_config/global_config/basics_handler.py @@ -223,22 +223,37 @@ def __init__( self.preload_dispvm_spin: Gtk.SpinButton = gtk_builder.get_object( "basics_preload_dispvm" ) + self.preload_dispvm_threshold_spin: Gtk.SpinButton = gtk_builder.get_object( + "basics_preload_dispvm_threshold" + ) self.preload_dispvm_check: Gtk.CheckButton = gtk_builder.get_object( "basics_preload_dispvm_check" ) self.defdispvm_model.connect_change_callback(self.on_defdispvm_changed) self.preload_dispvm_check.connect("toggled", self.on_check_changed) + + self.default_max = 1 self.preload_dispvm_spin.props.numeric = True self.preload_dispvm_spin_adjustment = Gtk.Adjustment() self.preload_dispvm_spin_adjustment.configure(0, 0, 9999, 1, 5, 0) self.preload_dispvm_spin.configure(self.preload_dispvm_spin_adjustment, 0.1, 0) + self.preload_dispvm_threshold_spin.props.numeric = True + self.preload_dispvm_threshold_spin_adjustment = Gtk.Adjustment() + self.preload_dispvm_threshold_spin_adjustment.configure( + 0, 0, 999999, 100, 1000, 0 + ) + self.preload_dispvm_threshold_spin.configure( + self.preload_dispvm_threshold_spin_adjustment, 0.1, 0 + ) + self.on_defdispvm_changed() self.on_check_changed() self.initial_preload_dispvm_spin_sensitive = ( self.preload_dispvm_spin.is_sensitive() ) + self.preload_dispvm_threshold_spin.set_value(self.get_current_threshold_value()) def on_defdispvm_changed(self): defdispvm = self.defdispvm_model.get_selected() @@ -254,7 +269,7 @@ def on_check_changed(self, *args): # pylint: disable=unused-argument preloadcheck = self.preload_dispvm_check.get_active() if defdispvm and preloadcheck: if self.get_feat_value() is None: - value = 1 + value = self.default_max else: value = self.get_current_value() self.preload_dispvm_spin.set_value(value) @@ -274,11 +289,19 @@ def get_feat_value(self): """Get current system value as is""" return get_feature(self.qapp.domains["dom0"], "preload-dispvm-max") + def get_feat_threshold_value(self): + """Get current system threshold value as is""" + return get_feature(self.qapp.domains["dom0"], "preload-dispvm-threshold") + def get_current_value(self): """Get current system value of the handled feature""" return int(self.get_feat_value() or 0) - def is_changed(self) -> bool: + def get_current_threshold_value(self): + """Get current system value of the handled feature""" + return int(self.get_feat_threshold_value() or 0) + + def is_max_changed(self) -> bool: """Has the user selected something different from the initial value?""" if ( self.initial_preload_dispvm_spin_sensitive @@ -289,6 +312,19 @@ def is_changed(self) -> bool: return True return False + def is_threshold_changed(self) -> bool: + """Has the user selected something different from the initial value?""" + return ( + self.preload_dispvm_threshold_spin.get_value_as_int() + != self.get_current_threshold_value() + ) + + def is_changed(self) -> bool: + """Has the user selected something different from the initial value?""" + if self.is_max_changed() or self.is_threshold_changed(): + return True + return False + def get_unsaved(self): """Get human-readable description of unsaved changes, or empty string if none were found.""" @@ -300,25 +336,39 @@ def save(self): """Save changes: update system value and mark it as new initial value""" if not self.is_changed(): return - if self.preload_dispvm_spin.is_sensitive(): - value = str(self.preload_dispvm_spin.get_value_as_int()) - else: - value = None - apply_feature_change( - self.qapp.domains["dom0"], - "preload-dispvm-max", - value, - ) - if value is None: - self.initial_preload_dispvm_spin_sensitive = False - else: - self.initial_preload_dispvm_spin_sensitive = True + + if self.is_threshold_changed(): + threshold_value = str(self.preload_dispvm_threshold_spin.get_value_as_int()) + apply_feature_change( + self.qapp.domains["dom0"], + "preload-dispvm-threshold", + threshold_value, + ) + + # Every other feature must be set before "max" (prior to preload routine). + if self.is_max_changed(): + if self.preload_dispvm_spin.is_sensitive(): + value = str(self.preload_dispvm_spin.get_value_as_int()) + else: + value = None + apply_feature_change( + self.qapp.domains["dom0"], + "preload-dispvm-max", + value, + ) + if value is None: + self.initial_preload_dispvm_spin_sensitive = False + else: + self.initial_preload_dispvm_spin_sensitive = True def reset(self): """Reset selection to the initial value.""" - if not self.preload_dispvm_spin.is_sensitive(): - return - self.preload_dispvm_spin.set_value(self.get_current_value()) + if self.preload_dispvm_spin.is_sensitive(): + self.preload_dispvm_spin.set_value(self.get_current_value()) + if self.preload_dispvm_threshold_spin.is_sensitive(): + self.preload_dispvm_threshold_spin.set_value( + self.get_current_threshold_value() + ) def update_current_value(self): """This should never be called.""" diff --git a/qubes_config/tests/test_basics_handler.py b/qubes_config/tests/test_basics_handler.py index 1e2f45e8..7ea4368f 100644 --- a/qubes_config/tests/test_basics_handler.py +++ b/qubes_config/tests/test_basics_handler.py @@ -23,7 +23,7 @@ # pylint: disable=protected-access import os -from unittest.mock import Mock, patch +from unittest.mock import Mock, patch, call as mock_call from ..global_config.basics_handler import ( KernelVersion, PropertyHandler, @@ -344,6 +344,8 @@ def test_kernels(test_qapp): def test_preload_handler( mock_apply, mock_get, real_builder, test_qapp ): # pylint: disable=unused-argument + default_max = 1 + default_threshold = 0 description = "Number of preloaded disposables from default dispvm" mock_get.return_value = None basics_handler = BasicSettingsHandler(real_builder, test_qapp) @@ -356,6 +358,9 @@ def test_preload_handler( preload_dispvm_spin: Gtk.SpinButton = real_builder.get_object( "basics_preload_dispvm" ) + preload_dispvm_threshold_spin: Gtk.SpinButton = real_builder.get_object( + "basics_preload_dispvm_threshold" + ) preload_dispvm_spin_check: Gtk.CheckButton = real_builder.get_object( "basics_preload_dispvm_check" ) @@ -364,11 +369,15 @@ def test_preload_handler( # Start available but unchecked. assert preload_dispvm_spin_check.is_sensitive() + assert preload_dispvm_spin.get_value_as_int() == 0 assert not preload_dispvm_spin.is_sensitive() + assert preload_dispvm_threshold_spin.is_sensitive() + assert preload_dispvm_threshold_spin.get_value_as_int() == default_threshold # Check to allow spin. preload_dispvm_spin_check.set_active(True) assert preload_dispvm_spin.is_sensitive() + assert preload_dispvm_spin.get_value_as_int() == default_max # Assert that with no default_dispvm, even if changing preload spin button, # doesn't save the preload feature. @@ -391,7 +400,7 @@ def test_preload_handler( # Assert that reset ignores insensitive. initial_preload_dispvm = preload_dispvm_spin.get_value_as_int() assert not preload_dispvm_spin.is_sensitive() - mock_get_count = mock_get.call_count + mock_get_count = mock_get.call_count + 1 basics_handler.reset() assert mock_get_count == mock_get.call_count @@ -416,48 +425,68 @@ def test_preload_handler( assert call not in test_qapp.actual_calls basics_handler.save() mock_apply.assert_called_once_with( - test_qapp.domains["dom0"], "preload-dispvm-max", str(new_preload_value) + test_qapp.domains["dom0"], + "preload-dispvm-max", + str(new_preload_value), ) for call in expected_calls: assert call in test_qapp.actual_calls test_qapp.actual_calls = [] - # Assert that saving '0' set the value '0' instead of feature deletion. + # Assert that saving '0' sets the value '0' instead of feature deletion. mock_get.return_value = new_preload_value preload_dispvm_spin.set_value(0) basics_handler.save() - assert mock_apply.call_args[0] == ( + assert mock_apply.call_args == mock_call( test_qapp.domains["dom0"], "preload-dispvm-max", str(0), ) + test_qapp.actual_calls = [] # Assert that deselecting the check box deletes the feature. mock_get.return_value = 0 + threshold_value = preload_dispvm_threshold_spin.get_value_as_int() preload_dispvm_spin.set_value(1) preload_dispvm_spin_check.set_active(False) + assert not preload_dispvm_spin.is_sensitive() + # Assert that threshold is not impacted. + assert preload_dispvm_threshold_spin.is_sensitive() + assert preload_dispvm_threshold_spin.get_value_as_int() == threshold_value + mock_apply_count = mock_apply.call_count basics_handler.save() - assert mock_apply.call_args[0] == ( + assert mock_apply.call_count == mock_apply_count + 1 + assert mock_apply.call_args == mock_call( test_qapp.domains["dom0"], "preload-dispvm-max", None, ) assert not preload_dispvm_spin.is_sensitive() - # Assert that reset sets the current value. + # Assert that reset sets the current value of 'max' and 'threshold' preload_dispvm_spin_check.set_active(True) assert preload_dispvm_spin.is_sensitive() mock_get_count = mock_get.call_count - mock_get.return_value = 1 + + def feature_side_effect(_vm, feature, default=None): + if feature == "preload-dispvm-max": + return default_max + if feature == "preload-dispvm-threshold": + return default_threshold + return default + + mock_get.side_effect = feature_side_effect basics_handler.reset() - assert mock_get.call_count == mock_get_count + 1 - assert preload_dispvm_spin.get_value_as_int() == 1 + assert mock_get.call_count == mock_get_count + 2 + assert preload_dispvm_spin.get_value_as_int() == default_max + assert preload_dispvm_threshold_spin.get_value_as_int() == default_threshold + mock_get.side_effect = None # Assert that value is 1 when feature is None, get current value otherwise. mock_get.return_value = None preload_dispvm_spin_check.set_active(False) preload_dispvm_spin_check.set_active(True) - assert preload_dispvm_spin.get_value_as_int() == 1 + assert preload_dispvm_spin.get_value_as_int() == default_max mock_get.return_value = 0 preload_dispvm_spin_check.set_active(False) @@ -472,7 +501,20 @@ def test_preload_handler( mock_get.return_value = 1 preload_dispvm_spin_check.set_active(False) preload_dispvm_spin_check.set_active(True) - assert preload_dispvm_spin.get_value_as_int() == 1 + assert preload_dispvm_spin.get_value_as_int() == default_max + + mock_get.return_value = None + preload_dispvm_spin_check.set_active(False) + new_threshold = preload_dispvm_threshold_spin.get_value_as_int() + 1 + preload_dispvm_threshold_spin.set_value(new_threshold) + mock_apply_count = mock_apply.call_count + basics_handler.save() + assert mock_apply.call_count == mock_apply_count + 1 + assert mock_apply.call_args == mock_call( + test_qapp.domains["dom0"], + "preload-dispvm-threshold", + str(new_threshold), + ) def test_basics_handler(real_builder, test_qapp):