class InputField(object): """An input field containing data to be checked. The input field can have an initial value that can be monitored for change via signals. """ def __init__(self, initial_content): self._initial_content = initial_content self._content = initial_content self.changed = Signal() self._initial_change_signal_fired = False self.changed_from_initial_state = Signal() @property def content(self): return self._content @content.setter def content(self, new_content): old_content = self._content self._content = new_content # check if the input changed from the initial state if old_content != new_content: self.changed.emit() # also fire the changed-from-initial-state signal if required if not self._initial_change_signal_fired and new_content != self._initial_content: self.changed_from_initial_state.emit() self._initial_change_signal_fired = True
class CheckResult(object): """Result of an input check.""" def __init__(self): self._success = False self._error_message = "" self.error_message_changed = Signal() @property def success(self): return self._success @success.setter def success(self, value): self._success = value @property def error_message(self): """Optional error message describing why the input is not valid. :returns: why the input is bad (provided it is bad) or None :rtype: str or None """ return self._error_message @error_message.setter def error_message(self, new_error_message): self._error_message = new_error_message self.error_message_changed.emit(new_error_message)
def clear_test(self): """Test if the clear() method correctly clears any connected callbacks.""" def set_var(value): self.var = value signal = Signal() foo = FooClass() lambda_foo = FooClass() self.assertIsNone(foo.var) self.assertIsNone(lambda_foo.var) self.assertIsNone(self.var) # connect the callbacks signal.connect(set_var) signal.connect(foo.set_var) # pylint: disable=unnecessary-lambda signal.connect(lambda x: lambda_foo.set_var(x)) # trigger the signal signal.emit("bar") # check that the callbacks were triggered self.assertEqual(self.var, "bar") self.assertEqual(foo.var, "bar") self.assertEqual(lambda_foo.var, "bar") # clear the callbacks signal.clear() # trigger the signal again signal.emit("anaconda") # check that the callbacks were not triggered self.assertEqual(self.var, "bar") self.assertEqual(foo.var, "bar") self.assertEqual(lambda_foo.var, "bar")
def signal_chain_test(self): """Check if signals can be chained together.""" foo = FooClass() self.assertIsNone(foo.var) signal1 = Signal() signal1.connect(foo.set_var) signal2 = Signal() signal2.connect(signal1.emit) signal3 = Signal() signal3.connect(signal2.emit) # trigger the chain signal3.emit("bar") # check if the initial callback was triggered self.assertEqual(foo.var, "bar")
def method_test(self): """Test if a method can be correctly connected to a signal.""" signal = Signal() foo = FooClass() self.assertIsNone(foo.var) # connect the signal signal.connect(foo.set_var) # trigger the signal signal.emit("bar") # check if the callback triggered correctly self.assertEqual(foo.var, "bar") # try to trigger the signal again signal.emit("baz") self.assertEqual(foo.var, "baz") # now try to disconnect the signal signal.disconnect(foo.set_var) # check that calling the signal again # no longer triggers the callback signal.emit("anaconda") self.assertEqual(foo.var, "baz")
def lambda_test(self): """Test if a lambda can be correctly connected to a signal.""" foo = FooClass() signal = Signal() self.assertIsNone(foo.var) # connect the signal # pylint: disable=unnecessary-lambda lambda_instance = lambda x: foo.set_var(x) signal.connect(lambda_instance) # trigger the signal signal.emit("bar") # check if the callback triggered correctly self.assertEqual(foo.var, "bar") # try to trigger the signal again signal.emit("baz") self.assertEqual(foo.var, "baz") # now try to disconnect the signal signal.disconnect(lambda_instance) # check that calling the signal again # no longer triggers the callback signal.emit("anaconda") self.assertEqual(foo.var, "baz")
def function_test(self): """Test if a local function can be correctly connected to a signal.""" # create a local function def set_var(value): self.var = value signal = Signal() self.assertIsNone(self.var) # connect the signal signal.connect(set_var) # trigger the signal signal.emit("bar") # check if the callback triggered correctly self.assertEqual(self.var, "bar") # try to trigger the signal again signal.emit("baz") self.assertEqual(self.var, "baz") # now try to disconnect the signal signal.disconnect(set_var) # check that calling the signal again # no longer triggers the callback signal.emit("anaconda") self.assertEqual(self.var, "baz")
class PasswordChecker(object): """Run multiple password and input checks in a given order and report the results. All added checks (in insertion order) will be run and results returned as error message and success value (True/False). If any check fails success will be False and the error message of the first check to fail will be returned. It's also possible to mark individual checks to be skipped by setting their skip property to True. Such check will be skipped during the checking run. """ def __init__(self, initial_password_content, initial_password_confirmation_content, policy): self._password = InputField(initial_password_content) self._password_confirmation = InputField( initial_password_confirmation_content) self._checks = [] self._success = False self._error_message = "" self._failed_checks = [] self._successful_checks = [] self._policy = policy self._username = None self._fullname = "" # connect to the password field signals self.password.changed.connect(self.run_checks) self.password_confirmation.changed.connect(self.run_checks) # password naming (for use in status/error messages) self._name_of_password = _(constants.NAME_OF_PASSWORD) self._name_of_password_plural = _(constants.NAME_OF_PASSWORD_PLURAL) # signals self.checks_done = Signal() @property def password(self): """Main password field.""" return self._password @property def password_confirmation(self): """Password confirmation field.""" return self._password_confirmation @property def checks(self): return self._checks @property def success(self): return self._success @property def error_message(self): return self._error_message @property def successful_checks(self): """List of successful checks during the last checking run. If no checks have succeeded the list will be empty. :returns: list of successful checks (if any) :rtype: list """ return self._successful_checks @property def failed_checks(self): """List of checks failed during the last checking run. If no checks have failed the list will be empty. :returns: list of failed checks (if any) :rtype: list """ return self._failed_checks @property def policy(self): return self._policy @property def username(self): return self._username @username.setter def username(self, new_username): self._username = new_username @property def fullname(self): """The full name of the user for which the password is being set. If no full name is provided, "root" will be used. :returns: full user name corresponding to the password :rtype: str or None """ return self._fullname @fullname.setter def fullname(self, new_fullname): self._fullname = new_fullname # password naming @property def name_of_password(self): """Name of the password to be used called in warnings and error messages. For example: "%s contains non-ASCII characters" can be customized to: "Password contains non-ASCII characters" or "Passphrase contains non-ASCII characters" :returns: name of the password being checked :rtype: str """ return self._name_of_password @name_of_password.setter def name_of_password(self, name): self._name_of_password = name @property def name_of_password_plural(self): """Plural name of the password to be used called in warnings and error messages. :returns: plural name of the password being checked :rtype: str """ return self._name_of_password_plural @name_of_password_plural.setter def name_of_password_plural(self, name_plural): self._name_of_password_plural = name_plural def add_check(self, check_instance): """Add check instance to list of checks.""" self._checks.append(check_instance) def run_checks(self): # first we need to prepare a check request instance check_request = PasswordCheckRequest() check_request.password = self.password.content check_request.password_confirmation = self.password_confirmation.content check_request.policy = self.policy check_request.username = self.username check_request.fullname = self.fullname check_request.name_of_password = self.name_of_password check_request.name_of_password_plural = self.name_of_password_plural # reset the list of failed checks self._failed_checks = [] error_message = "" for check in self.checks: if not check.skip: check.run(check_request) if check.result.success: self._successful_checks.append(check) else: self._failed_checks.append(check) if not check.result.success and not self.failed_checks: # a check failed: # - remember that & it's error message # - run other checks as well and ignore their error messages (if any) # - fail the overall check run (success = False) error_message = check.result.error_message if self.failed_checks: self._error_message = error_message self._success = False else: self._success = True self._error_message = "" # trigger the success changed signal self.checks_done.emit(self._error_message)
class PasswordValidityCheckResult(CheckResult): """A wrapper for results for a password check.""" def __init__(self): super().__init__() self._check_request = None self._password_score = 0 self.password_score_changed = Signal() self._status_text = "" self.status_text_changed = Signal() self._password_quality = 0 self.password_quality_changed = Signal() self._length_ok = False self.length_ok_changed = Signal() @property def check_request(self): """The check request used to generate this check result object. Can be used to get the password text and checking parameters for this password check result. :returns: the password check request that triggered this password check result :rtype: a PasswordCheckRequest instance """ return self._check_request @check_request.setter def check_request(self, new_request): self._check_request = new_request @property def password_score(self): """A high-level integer score indicating password quality. Goes from 0 (invalid password) to 4 (valid & very strong password). Mainly used to drive the password quality indicator in the GUI. """ return self._password_score @password_score.setter def password_score(self, new_score): self._password_score = new_score self.password_score_changed.emit(new_score) @property def status_text(self): """A short overall status message describing the password. Generally something like "Good.", "Too short.", "Empty.", etc. :rtype: short status message :rtype: str """ return self._status_text @status_text.setter def status_text(self, new_status_text): self._status_text = new_status_text self.status_text_changed.emit(new_status_text) @property def password_quality(self): """More fine grained integer indicator describing password strength. This basically exports the quality score assigned by libpwquality to the password, which goes from 0 (unacceptable password) to 100 (strong password). Note of caution though about using the password quality value - it is intended mainly for on-line password strength hints, not for long-term stability, even just because password dictionary updates and other peculiarities of password strength judging. :returns: password quality value as reported by libpwquality :rtype: int """ return self._password_quality @password_quality.setter def password_quality(self, value): self._password_quality = value self.password_quality_changed.emit(value) @property def length_ok(self): """Reports if the password is long enough. :returns: if the password is long enough :rtype: bool """ return self._length_ok @length_ok.setter def length_ok(self, value): self._length_ok = value self.length_ok_changed.emit(value)
class Controller(object): """A singleton that track initialization of Anaconda modules.""" def __init__(self): self._lock = RLock() self._modules = set() self._all_modules_added = False self.init_done = Signal() self._init_done_triggered = False self._added_module_count = 0 @synchronized def module_init_start(self, module): """Tell the controller that a module has started initialization. :param module: a module which has started initialization """ if self._all_modules_added: log.warning("Late module_init_start() from: %s", self) elif module in self._modules: log.warning("Module already marked as initializing: %s", module) else: self._added_module_count += 1 self._modules.add(module) def all_modules_added(self): """Tell the controller that all expected modules have started initialization. Tell the controller that all expected modules have been registered for initialization tracking (or have already been initialized) and no more are expected to be added. This is needed so that we don't prematurely trigger the init_done signal when all known modules finish initialization while other modules have not yet been added. """ init_done = False with self._lock: log.info("Initialization of all modules (%d) has been started.", self._added_module_count) self._all_modules_added = True # if all modules finished initialization before this was added then # trigger the init_done signal at once if not self._modules and not self._init_done_triggered: self._init_done_triggered = True init_done = True # we should emit the signal out of the main lock as it doesn't make sense # to hold the controller-state lock once we decide to the trigger init_done signal # (and any callbacks registered on it) if init_done: self._trigger_init_done() def module_init_done(self, module): """Tell the controller that a module has finished initialization. And if no more modules are being initialized trigger the init_done signal. :param module: a module that has finished initialization """ init_done = False with self._lock: # prevent the init_done signal from # being triggered more than once if self._init_done_triggered: log.warning("Late module_init_done from module %s.", module) else: if module in self._modules: log.info("Module initialized: %s", module) self._modules.discard(module) else: log.warning("Unknown module reported as initialized: %s", module) # don't trigger the signal if all modules have not yet been added if self._all_modules_added and not self._modules: init_done = True self._init_done_triggered = True # we should emit the signal out of the main lock as it doesn't make sense # to hold the controller-state lock once we decide to the trigger init_done signal # (and any callbacks registered on it) if init_done: self._trigger_init_done() def _trigger_init_done(self): log.info("All modules have been initialized.") self.init_done.emit()
class InstallManager(object): """Manager to control module installation. Installation tasks will be collected from modules and run one by one. Provides summarized API (InstallationInterface class) for UI. """ def __init__(self): """ Create installation manager.""" self._tasks = set() self._actual_task = None self._step_sum = 0 self._tasks_done_step = 0 self._installation_terminated = False self._modules = [] self._install_started_signal = Signal() self._install_stopped_signal = Signal() self._task_changed_signal = Signal() self._progress_changed_signal = Signal() self._progress_changed_float_signal = Signal() self._error_raised_signal = Signal() self._subscriptions = [] @property def installation_started(self): return self._install_started_signal @property def installation_stopped(self): return self._install_stopped_signal @property def task_changed_signal(self): """Signal when installation task changed.""" return self._task_changed_signal @property def progress_changed_signal(self): """Signal when progress changed.""" return self._progress_changed_signal @property def progress_changed_float_signal(self): """Signal when progress in float changed.""" return self._progress_changed_float_signal @property def error_raised_signal(self): """Signal which will be emitted when error raised during installation.""" return self._error_raised_signal @property def available_modules(self): """Get available modules which will be used for installation.""" return self._modules @available_modules.setter def available_modules(self, modules): """Set available modules which will be used for installation. :param modules: Modules list. :type modules: list """ self._modules = modules def start_installation(self): """Start the installation.""" self._collect_tasks() self._sum_steps_count() self._disconnect_task() self._tasks_done_step = 0 self._installation_terminated = False if self._tasks: self._actual_task = self._tasks.pop() self._install_started_signal.emit() self._run_task() def _collect_tasks(self): self._tasks.clear() if not self._modules: log.error("Starting installation without available modules.") for module_service in self._modules: # FIXME: This is just a temporary solution. module_object = DBus.get_proxy(module_service, auto_object_path(module_service)) tasks = module_object.AvailableTasks() for task in tasks: log.debug("Getting task %s from module %s", task[TASK_NAME], module_service) task_proxy = DBus.get_proxy(module_service, task[TASK_PATH]) self._tasks.add(task_proxy) def _sum_steps_count(self): self._step_sum = 0 for task in self._tasks: self._step_sum += task.ProgressStepsCount def _run_task(self): if self._installation_terminated: log.debug("Don't run another task. The installation was terminated.") return task_name = self._actual_task.Name log.debug("Running installation task %s", task_name) self._disconnect_task() self._connect_task() self._task_changed_signal.emit(task_name) self._actual_task.Start() def _connect_task(self): s = self._actual_task.ProgressChanged.connect(self._progress_changed) self._subscriptions.append(s) s = self._actual_task.Started.connect(self._task_started()) self._subscriptions.append(s) s = self._actual_task.Stopped.connect(self._task_stopped) self._subscriptions.append(s) s = self._actual_task.ErrorRaised.connect(self._task_error_raised) self._subscriptions.append(s) def _disconnect_task(self): for subscription in self._subscriptions: subscription.disconnect() def _test_if_running(self, log_msg=None): if self._actual_task is not None: return True else: log.warning(log_msg) return False def _task_stopped(self): self._tasks_done_step += self._actual_task.ProgressStepsCount if self._tasks: self._actual_task = self._tasks.pop() self._run_task() else: log.info("Installation finished.") self._actual_task = None self._install_stopped_signal.emit() def _task_started(self): log.info("Installation task %s has started.", self._actual_task) def _task_error_raised(self, error_description): self._error_raised_signal.emit(error_description) @property def installation_running(self): """Installation is running right now. :returns: True if installation is running. False otherwise. """ return self._actual_task is not None @property def task_name(self): """Get name of the running task.""" if self._test_if_running("Can't get task name when installation is not running."): return self._actual_task.Name else: return "" @property def task_description(self): """Get description of the running task.""" if self._test_if_running("Can't get task description when installation is not running."): return self._actual_task.Description else: return "" @property def progress_steps_count(self): """Sum of steps in all tasks used for installation.""" if self._test_if_running("Can't get sum of all tasks when installation is not running."): return self._step_sum else: return 0 def _progress_changed(self, step, msg): actual_progress = step + self._tasks_done_step self._progress_changed_signal.emit(actual_progress, msg) self._progress_changed_float_signal.emit(actual_progress / self._step_sum, msg) @property def progress(self): """Get progress of the installation. :returns: (step: int, msg: str) tuple. step - step in the installation process. msg - short description of the step """ if self._test_if_running("Can't get task progress when installation is not running."): (step, msg) = self._actual_task.Progress actual_progress = step + self._tasks_done_step return actual_progress, msg else: return 0, "" @property def progress_float(self): """Get progress of the installation as float number from 0.0 to 1.0. :returns: (step: float, msg: str) tuple. step - step in the installation process. msg - short description of the step """ if self._test_if_running("Can't get task progress in float " "when installation is not running."): (step, msg) = self._actual_task.Progress actual_progress = step + self._tasks_done_step actual_progress = actual_progress / self._step_sum return actual_progress, msg else: return 0, "" def cancel(self): """Cancel installation. Installation will be canceled as soon as possible. When exactly depends on the actual task running. """ if self._test_if_running(): self._installation_terminated = True if self._actual_task.IsCancelable: self._actual_task.Cancel() else: raise InstallationNotRunning("Can't cancel task when installation is not running.")
class Task(ABC): """Base class implementing DBus Task interface.""" def __init__(self, dbus_modul_path): super().__init__() self._progress = (0, "") self.__cancel_lock = Lock() self.__cancel = False self._started_signal = Signal() self._stopped_signal = Signal() self._progress_changed_signal = Signal() self._error_raised_signal = Signal() self._thread_name = "{}-{}".format(THREAD_DBUS_TASK, self.name) @property def started_signal(self): """Signal emitted when this tasks starts.""" return self._started_signal @property def stopped_signal(self): """Signal emitted when this task stops.""" return self._stopped_signal @property def progress_changed_signal(self): """Signal emits when the progress of this task will change.""" return self._progress_changed_signal @property def error_raised_signal(self): """Signal emits if error is raised during installation.""" return self._error_raised_signal @property @abstractmethod def name(self): """Name of this task.""" pass @property @abstractmethod def description(self): """Short description of this task.""" pass @property @abstractmethod def progress_steps_count(self): """Number of the steps in this task.""" pass @property def progress(self): """Actual progress of this task. :returns: tuple (step, description). """ return self._progress @property def is_running(self): """Is this task running.""" return threadMgr.exists(self._thread_name) @property def is_cancelable(self): """Can this task be cancelled? :returns: bool. """ return False @async_action_nowait def progress_changed(self, step, message): """Update actual progress. Thread safe method. Can be used from the self.run_task() method. Signal change of the progress and update Progress DBus property. :param step: Number of the actual step. :type step: int :param message: Short description of the actual step. :type message: str """ self._progress = (step, message) self._progress_changed_signal.emit(step, message) @async_action_nowait def error_raised(self, error_message): self._error_raised_signal.emit(error_message) @async_action_nowait def running_changed(self): """Notify about change when this task stops/starts.""" if self.is_running: self._started_signal.emit() else: self._stopped_signal.emit() def cancel(self): """Cancel this task. This will do something only if the IsCancelable property will return `True`. """ with self.__cancel_lock: self.__cancel = True def check_cancel(self, clear=True): """Check if Task should be canceled and clear the cancel flag. :param clear: Clear the flag. :returns: bool """ with self.__cancel_lock: if self.__cancel: if clear: self.__cancel = False return True return False def run(self): """Run Task job. Overriding of the self.run_task() method instead is recommended. This method will create thread which will run the self.run_task() method. """ thread = AnacondaThread(name=self._thread_name, target=self.runnable) if not threadMgr.exists(self._thread_name): threadMgr.add(thread) self.running_changed() threadMgr.call_when_thread_terminates(self._thread_name, self.running_changed) else: raise TaskAlreadyRunningException( "Task {} is already running".format(self.name)) @abstractmethod def runnable(self): """Tasks job implementation. This will run in separate thread by calling the self.run() method. To report progress change use the self.progress_changed() method. To report fatal error use the self.error_raised() method. If this Task can be cancelled check the self.check_cancel() method. """ pass