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
-
+
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
+
+
+
+ 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):