import abc
import uuid
from fortrace.core.qemu_monitor import QEMUMonitorSession
from fortrace.utility.applications.application import (
ApplicationEvent,
ApplicationType,
GenericApplication,
GenericPopup,
ParentNotifier,
)
from fortrace.utility.distribution_constants import DesktopEnvironmentType, OSType
from fortrace.utility.image_processing.image_similarity import (
detect_newly_opened_window,
)
[docs]
class DesktopEnvironment(abc.ABC):
"""Desktop environment of a domain.
Should be implemented by all desktop environments in ForTrace. This class is
responsible for managing application windows and keeping track of the one that is
currently in focus. Furthermore, it is also responsible for the login on a
graphical domain.
"""
_applications: list[GenericApplication]
_active_application: GenericApplication | None
_on_change: ParentNotifier
def __init__(
self,
os_type: OSType,
desktop_env: DesktopEnvironmentType,
qemu_monitor_session: QEMUMonitorSession,
):
"""Create a Desktop environment attribute of a domain.
Args:
os_type: type of the operating system
desktop_env: type of the desktop environment
qemu_monitor_session: active QEMU monitor session to the domain
"""
self._os_type = os_type
self._desktop_env = desktop_env
self._applications = []
self._active_application = None
self._user_selected = False
self._session_unlocked = False
self._qs = qemu_monitor_session
[docs]
def is_application_open(self, application_name: str) -> bool:
"""Determine if an application is opened, based on its name.
Args:
application_name: the name of the application to test
Returns:
Status of the application
"""
for application in self._applications:
if application_name == application.name:
return True
return False
[docs]
def get_application(
self, application_identifier: str | uuid.UUID
) -> GenericApplication:
"""Get an application based on its name or uuid
Args:
application_identifier: provide name if only one instance is open, else uuid
Returns:
handle to application
"""
if isinstance(application_identifier, uuid.UUID):
return [
application
for application in self._applications
if application_identifier == application.uuid
][0]
else:
return [
application
for application in self._applications
if application_identifier == application.name
][0]
[docs]
@abc.abstractmethod
def open_application(
self, application_type: ApplicationType, application_name: str, **kwargs
) -> GenericApplication:
"""Open the specified application and move focus to it.
Implementation has to add application to list of applications.
Args:
application_type: Type of application to filter for correct sub-factory
application_name: Name of the application to open
**kwargs: Desktop environment specific arguments, e.g.
`fortrace.utility.desktop_environments.Windows.Windows`
Returns:
handle to newly opened application
"""
pass
[docs]
def close_application(self, application: GenericApplication):
"""Close the given application via key-combination alt-f4.
Do not use the application object afterward.
Args:
application: application object which refers to the application that is to
be closed
"""
self._applications.remove(application)
if self._active_application != application:
prev_focus = self._active_application
self.focus_application(application)
self._qs.send_key_combination("alt-f4")
self.focus_application(prev_focus)
else:
self._qs.send_key_combination("alt-f4")
self._active_application = None
del application
[docs]
@abc.abstractmethod
def login(self, username: str, password: str | None):
"""Perform a login for the given user.
Args:
username: name of the user to login
password: password of the given user or None if there is none
Note:
This call blocks until the desktop environment is ready to receive input.
"""
pass
[docs]
def lock_screen(self):
self._qs.send_key_combination("meta_l-l")
self._session_unlocked = False
[docs]
def show_desktop(self):
"""Minimized all windows and shows desktop.
Can be useful when simulating multiple applications.
Note:
If executed twice, the previous window state is restored, however the focus
of the active application is lost
"""
self.active_application = None
self._qs.send_key_combination("meta_l-d")
[docs]
def maximize_application(self):
"""Maximize the current focused application.
Note:
Useful before conducting text recognition.
"""
screenshot_before = self._qs.take_screenshot()
self._qs.send_key_combination("meta_l-pgup")
self._on_change(
ApplicationEvent.APPLICATION_RESIZED, screenshot_before=screenshot_before
)
[docs]
def system_power_down(self):
"""Powers down the system using the QEMUMonitor.
FIXME: On Windows guests this method may require the installation of QEMU Agent
FIXME: This method should use the desktop env to power down the system
"""
self._qs.direct_command("system_powerdown") # if supported by system
[docs]
@abc.abstractmethod
def focus_application(self, application: GenericApplication):
"""Bring the given application into focus so that it can receive input commands.
Args:
application: application object to focus
Returns:
"""
pass
@property
def active_application(self) -> GenericApplication:
return self._active_application
@active_application.setter
def active_application(self, application: GenericApplication | None):
"""Setter for active application which handles focus attribute.
Args:
application: handle to new active application
"""
if self._active_application is not None:
self._active_application.focused = False
self._active_application = application
if application is not None:
self._active_application.focused = True
@property
def qemu_monitor_session(self) -> QEMUMonitorSession:
return self._qs
@property
def session_unlocked(self) -> bool:
return self._session_unlocked
def _on_change(self, event: ApplicationEvent, **kwargs):
"""Call from application to notify desktop environment about change of state.
Graphical applications can use this method to notify the desktop environment
about changes.
Args:
event: at least needs type of event to process
**kwargs: event specific arguments must be passed as keyword arguments
Examples:
- Application A opens another application (e.g. open a file from the file
browser)
- Application closes itself by NOT using close_application
"""
match event:
case ApplicationEvent.CLOSED:
if self._active_application == kwargs["application_reference"]:
self.active_application = None
self._applications.remove(kwargs["application_reference"])
# TODO: in windows the focus is laid on the application to the right in self._applications
case ApplicationEvent.NEW_APPLICATION_OPENED:
if self._active_application is not None:
self.active_application.focused = False
self.active_application = kwargs["application_reference"]
self._applications.append(kwargs["application_reference"])
case ApplicationEvent.FOCUS_SHIFTED:
self.active_application = kwargs["application_reference"]
case ApplicationEvent.FOCUS_APPLICATION:
self.focus_application(kwargs["application_reference"])
self.active_application = kwargs["application_reference"]
case ApplicationEvent.APPLICATION_RESIZED:
self._active_application.coordinates = detect_newly_opened_window(
kwargs["screenshot_before"], self._qs.take_screenshot()
)
case ApplicationEvent.APPLICATION_POPUP_OPENED:
popup = kwargs["popup_reference"] # type: GenericPopup
application = kwargs[
"application_reference"
] # type: GenericApplication
application.focused = False
popup.focused = True
case ApplicationEvent.APPLICATION_POPUP_CLOSED:
popup = kwargs["popup_reference"] # type: GenericPopup
application = kwargs[
"application_reference"
] # type: GenericApplication
self._applications.remove(popup)
self.active_application = application
case _:
raise ValueError(f"Unknown application event '{event}'")