<!-- Yandex.Metrika counter -->
<script async="" src="https://mc.yandex.ru/metrika/tag.js"></script>
<script type="text/javascript">
	(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
	m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
	(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
 
	ym(88079149, "init", {
		 clickmap:true,
		 trackLinks:true,
		 accurateTrackBounce:true,
		 webvisor:true
	});
 </script>
 <noscript><div><img src="https://mc.yandex.ru/watch/88079149" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
 <!-- /Yandex.Metrika counter -->

Действие: Навести курсор мыши на UI объект, который интересует и зажать клавишу «Ctrl» на 3 секунды

Действие: Навести курсор мыши на UI объект, который интересует и зажать клавишу "Ctrl" на 3 секунды

OSFileBinaryDataBase64StrAppend() (в модуле pyOpenRPA.Agent.__Agent__)

OSFileBinaryDataBase64StrCreate() (в модуле pyOpenRPA.Agent.__Agent__)

OSFileBinaryDataBase64StrReceive() (в модуле pyOpenRPA.Agent.__Agent__)

ProcessWOExeUpperUserListGet() (в модуле pyOpenRPA.Agent.__Agent__)

ActivityItemCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ActivityItemDefAliasCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ActivityItemDefAliasModulesLoad() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ActivityItemDefAliasUpdate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ActivityItemHelperDefAutofill() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ActivityItemHelperDefList() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentActivityItemAdd() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentActivityItemExists() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentActivityItemReturnExists() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentActivityItemReturnGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileBinaryDataBase64StrAppend() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileBinaryDataBase64StrCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileBinaryDataBase64StrReceive() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileBinaryDataBytesCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileBinaryDataReceive() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileSend() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileTextDataStrCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSFileTextDataStrReceive() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentOSLogoff() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

AgentProcessWOExeUpperUserListGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

GSettingsGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

GSettingsKeyListValueAppend() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

GSettingsKeyListValueGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

GSettingsKeyListValueOperatorPlus() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

GSettingsKeyListValueSet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OSCredentialsVerify() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OSRemotePCRestart() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

Orchestrator() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorInitWait() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorIsAdmin() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorIsInited() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorLoggerGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorPySearchInit() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorRerunAsAdmin() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorRestart() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorScheduleGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorSessionRestore() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorSessionSave() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

OrchestratorThreadStart() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessDefIntervalCall() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessIsStarted() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessListGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessStart() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessStop() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessorActivityItemAppend() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessorActivityItemCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessorAliasDefCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

ProcessorAliasDefUpdate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

PythonStart() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionCMDRun() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionConnect() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionDisconnect() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionFileStoredRecieve() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionFileStoredSend() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionLogoff() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionMonitorStop() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionProcessStartIfNotRunning() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionProcessStop() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPSessionReconnect() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

RDPTemplateCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

SchedulerActivityTimeAddWeekly() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

StorageRobotExists() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

StorageRobotGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

UACKeyListCheck() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

UACSuperTokenUpdate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

UACUserDictGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebAuditMessageCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebCPUpdate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebListenCreate() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestParseBodyBytes() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestParseBodyJSON() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestParseBodyStr() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestParseFile() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestParsePath() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebRequestResponseSend() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebURLConnectDef() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebURLConnectFile() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebURLConnectFolder() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebURLIndexChange() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebUserInfoGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebUserIsSuperToken() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

WebUserUACHierarchyGet() (в модуле pyOpenRPA.Orchestrator.__Orchestrator__)

BackendStr_GetTopLevelList_UIOInfo() (в модуле pyOpenRPA.Robot.UIDesktop)

PWASpecification_Get_PWAApplication() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelectorSecs_WaitAppear_Bool() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelectorSecs_WaitDisappear_Bool() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelectorUIOActivity_Run_Dict() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_FocusHighlight() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_GetChildList_UIOList() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_Get_BitnessInt() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_Get_BitnessStr() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_Get_UIOActivityList() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_Get_UIOInfoList() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_SafeOtherGet_Process() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_SearchChildByMouse_UIO() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_SearchChildByMouse_UIOTree() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_SearchProcessNormalize_UIOSelector() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_SearchUIONormalize_UIOSelector() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelector_TryRestore_Dict() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelectorsSecs_WaitAppear_List() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOSelectorsSecs_WaitDisappear_List() (в модуле pyOpenRPA.Robot.UIDesktop)

UIOXY_SearchChild_ListDict() (в модуле pyOpenRPA.Robot.UIDesktop)

import threading, socket, getpass, sys, uuid, subprocess, base64, psutil, getpass, time
from . import O2A, A2O # Data flow Orchestrator To Agent
from . import Processor # Processor Queue
from ..Tools import Usage
from ..Tools import License
from pyOpenRPA.Tools import CrossOS
if CrossOS.IS_WINDOWS_BOOL: from subprocess import CREATE_NEW_CONSOLE # Flag to create new process in another CMD
if CrossOS.IS_LINUX_BOOL: pass

import os

gSettings = None

# Create binary file by the base64 string (safe for JSON transmition)
def OSFileBinaryDataBase64StrCreate(inFilePathStr, inFileDataBase64Str,inGSettings = None):
    """
    Create binary file by the base64 string (safe for JSON transmition)

    """
    lFile = open(inFilePathStr, "wb")
    lFile.write(base64.b64decode(inFileDataBase64Str))
    lFile.close()
    lL = inGSettings.get("Logger", None) if type(inGSettings) is dict else None
    lMessageStr = f"AGENT Binary file {inFilePathStr} has been created."
    if lL: lL.info(lMessageStr)
    A2O.LogListSend(inGSettings=inGSettings, inLogList=[lMessageStr])

# Append binary file by the base64 string (safe for JSON transmition)
def OSFileBinaryDataBase64StrAppend(inFilePathStr, inFileDataBase64Str,inGSettings = None):
    """

    """
    lFile = open(inFilePathStr, "ab")
    lMessageStr = f"AGENT Binary file {inFilePathStr} has been appended."

# Create text file by the string
def OSFileTextDataStrCreate(inFilePathStr, inFileDataStr, inEncodingStr = "utf-8",inGSettings = None):
    """
    Create text file in the agent GUI session

    :param inFilePathStr: File abs path
    :param inFileDataStr: File data text content
    :param inEncodingStr:  Write file encoding
    :param inGSettings: global settings of the Agent (singleton)
    :return:
    """
    lFile = open(inFilePathStr, "w", encoding=inEncodingStr)
    lFile.write(inFileDataStr)
    lMessageStr = f"AGENT Text file {inFilePathStr} has been created."

def OSFileBinaryDataBase64StrReceive(inFilePathStr, inGSettings=None):
    """
    Read binary file and encode in base64 to transmit (safe for JSON transmition)

    :param inFilePathStr: File path to read
    :param inGSettings:  global settings of the Agent (singleton)
    :return: File content in string base64 format (use base64.b64decode to decode data). Return None if file is not exist
    """
    lFileDataBase64Str = None
    if os.path.exists(inFilePathStr):
        lFile = open(inFilePathStr, "rb")
        lFileDataBytes = lFile.read()
        lFile.close()
        lFileDataBase64Str = base64.b64encode(lFileDataBytes).decode("utf-8")
        lMessageStr = f"OSFileBinaryDataBase64StrReceive: file {inFilePathStr} has been read"
        if lL: lL.debug(lMessageStr)
        #A2O.LogListSend(inGSettings=inGSettings, inLogList=[lMessageStr])
    else: 
        if lL: lL.debug(f"OSFileBinaryDataBase64StrReceive: file {inFilePathStr} is not exists - return None")
    return lFileDataBase64Str

def OSFileMTimeGet(inFilePathStr: str) -> float or None:
    """
    Read file modification time timestamp format (float)

    :return: timestamp (float) Return None if file is not exist
    """
    global gSettings
    lL = gSettings.get("Logger", None) if type(gSettings) is dict else None
    lFileMTimeFloat = None
        lFileMTimeFloat = os.path.getmtime(inFilePathStr)
        if lL: lL.debug(f"OSFileMTimeGet: file {inFilePathStr} has been read")
    else: 
        if lL: lL.debug(f"OSFileMTimeGet: file {inFilePathStr} is not exists - return None")
    return lFileMTimeFloat

def OSFileTextDataStrReceive(inFilePathStr, inEncodingStr="utf-8", inGSettings=None):
    """
    Read text file in the agent GUI session

    :param inEncodingStr:  Read file encoding. Default utf-8
    :return: File text content in string format (use base64.b64decode to decode data). Return None if file is not exist
    """
    lFileDataStr = None
        lFile = open(inFilePathStr, "r", encoding=inEncodingStr)
        lFileDataStr = lFile.read()
        lMessageStr = f"OSFileTextDataStrReceive: file {inFilePathStr} has been read"
        if lL: lL.info(lMessageStr)
    else:
        if lL: lL.info(f"OSFileTextDataStrReceive: file {inFilePathStr} is not exists - return None")
    return lFileDataStr

# Send CMD to OS. Result return to log + Orchestrator by the A2O connection
def OSCMD(inCMDStr, inRunAsyncBool=True, inGSettings = None, inSendOutputToOrchestratorLogsBool = True, inCMDEncodingStr = "cp1251", inCaptureBool = True):
    """
    Execute CMD on the Agent daemonic process

    :param inCMDStr: command to execute on the Agent session
    :param inRunAsyncBool: True - Agent processor don't wait execution; False - Agent processor wait cmd execution
    :param inGSettings: Agent global settings dict
    :param inSendOutputToOrchestratorLogsBool: True - catch cmd execution output and send it to the Orchestrator logs; Flase - else case; Default True
    :param inCMDEncodingStr: Set the encoding of the DOS window on the Agent server session. Windows is beautiful :) . Default is "cp1251" early was "cp866" - need test
    :param inCaptureBool: !ATTENTION! If you need to start absolutely encapsulated app - set this flag as False. If you set True - the app output will come to Agent
    :return:
    """
    lResultStr = ""
    # New feature
    if inSendOutputToOrchestratorLogsBool == False and inCaptureBool == False:
        inCMDStr = f"start {inCMDStr}"
    # Subdef to listen OS result
    def _CMDRunAndListenLogs(inCMDStr, inSendOutputToOrchestratorLogsBool, inCMDEncodingStr,  inGSettings = None, inCaptureBool = True):
        lL = inGSettings.get("Logger",None) if type(inGSettings) is dict else None
        lResultStr = ""
        lOSCMDKeyStr = str(uuid.uuid4())[0:4].upper()
        lCMDProcess = None
        if inCaptureBool == True:
            lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        else:
            if CrossOS.IS_WINDOWS_BOOL:
                lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=None, stderr=None,
                                            creationflags=CREATE_NEW_CONSOLE)
            if CrossOS.IS_LINUX_BOOL:
                lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=None, stderr=None)
        lListenBool = True
        lMessageStr = f"{lOSCMDKeyStr}: # # # # AGENT CMD Process has been STARTED # # # # "
        if inSendOutputToOrchestratorLogsBool == True:  # Capturing can be turned on!
            A2O.LogListSend(inGSettings=inGSettings,inLogList=[lMessageStr])
        lMessageStr = f"{lOSCMDKeyStr}: {inCMDStr}"
            A2O.LogListSend(inGSettings=inGSettings, inLogList=[lMessageStr])
        while lListenBool:
            #if inSendOutputToOrchestratorLogsBool == True: # Capturing can be turned on!
            if inCaptureBool == True: # Capturing can be turned on!
                lOutputLineBytes = lCMDProcess.stdout.readline()
                if lOutputLineBytes == b"":
                    lListenBool = False
                lStr = lOutputLineBytes.decode(inCMDEncodingStr) # was cp866, on win server don't work properly - set cp1251
                if lStr.endswith("\n"): lStr = lStr[:-1]
                lMessageStr = f"{lOSCMDKeyStr}: {lStr}"
                if lL: lL.info(lMessageStr)
                if inSendOutputToOrchestratorLogsBool == True:  # Capturing can be turned on!
                    A2O.LogListSend(inGSettings=inGSettings, inLogList=[lMessageStr])
                lResultStr+=lStr
            else: #Capturing is not turned on - wait until process will be closed
                lCMDProcessPoll = lCMDProcess.poll()
                if lCMDProcessPoll is None: # Process is alive - wait
                    time.sleep(2)
                else:
        lMessageStr = f"{lOSCMDKeyStr}: # # # # AGENT CMD Process has been FINISHED # # # # "
        return lResultStr
    # New call
    if inRunAsyncBool:
        lThread = threading.Thread(target=_CMDRunAndListenLogs, kwargs={"inCMDStr":inCMDStr, "inGSettings":inGSettings, "inSendOutputToOrchestratorLogsBool":inSendOutputToOrchestratorLogsBool, "inCMDEncodingStr":inCMDEncodingStr, "inCaptureBool": inCaptureBool })
        lThread.start()
        lResultStr="ActivityList has been started in async mode - no output is available here."
    else:
        lResultStr = _CMDRunAndListenLogs(inCMDStr=inCMDStr, inGSettings=inGSettings, inSendOutputToOrchestratorLogsBool = inSendOutputToOrchestratorLogsBool, inCMDEncodingStr = inCMDEncodingStr, inCaptureBool=inCaptureBool)
    #lCMDCode = "cmd /c " + inCMDStr
    #subprocess.Popen(lCMDCode)
    #lResultCMDRun = 1  # os.system(lCMDCode)
    return lResultStr

def ProcessWOExeUpperUserListGet():
    """
    Return the process list only for the current user (where Agent is running) without .EXE in upper case. Can use in ActivityItem from Orchestrator to Agent

    :param inProcessNameWOExeList:
    :return: list of the agent user process in upper case without .EXE. Example ["NOTEPAD","..."],

    """
    lUserNameStr = getpass.getuser()
    lResult = []
    # Create updated list for quick check
    lProcessNameWOExeList = []
    # Iterate over the list
    for proc in psutil.process_iter():
        try:
            # Fetch process details as dict
            pinfo = proc.as_dict(attrs=['pid', 'name', 'username'])
            # Add if empty inProcessNameWOExeList or if process in inProcessNameWOExeList
            lUserNameWODomainStr = proc.username().split('\\')[-1]
            if lUserNameWODomainStr == lUserNameStr:
                lResult.append(pinfo['name'][:-4].upper())
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
           pass
    return lResult

# Main def
def Agent(inGSettings):
    License.ConsoleVerify()
    lL = inGSettings["Logger"]
    gSettings = inGSettings
    # Append Orchestrator def to ProcessorDictAlias
    lModule = sys.modules[__name__]
    lModuleDefList = dir(lModule)
    for lItemDefNameStr in lModuleDefList:
        # Dont append alias for defs Agent
        if lItemDefNameStr not in ["Agent"]:
            lItemDef = getattr(lModule,lItemDefNameStr)
            if callable(lItemDef): inGSettings["ProcessorDict"]["AliasDefDict"][lItemDefNameStr]=lItemDef

    # Detect Machine host name and username
    inGSettings["AgentDict"]["HostNameUpperStr"] = socket.gethostname().upper()
    inGSettings["AgentDict"]["UserUpperStr"] = getpass.getuser().upper()

    # Processor thread
    lProcessorThread = threading.Thread(target= Processor.ProcessorRunSync, kwargs={"inGSettings":inGSettings})
    lProcessorThread.daemon = True # Run the thread in daemon mode.
    lProcessorThread.start() # Start the thread execution.
    if lL: lL.info("Processor has been started (ProcessorDict)")  #Logging

    # Start thread to wait data from Orchestrator (O2A)
    lO2AThread = threading.Thread(target=O2A.O2A_Loop, kwargs={"inGSettings":inGSettings})
    lO2AThread.start()
    Usage.Process(inComponentStr="Agent")
    
    # Send log that Agent has been started
    A2O.LogListSend(inGSettings=inGSettings, inLogList=[f'Host: {inGSettings["AgentDict"]["HostNameUpperStr"]}, User: {inGSettings["AgentDict"]["UserUpperStr"]}, Agent has been started.'])

import subprocess, json, psutil, time, os
if CrossOS.IS_WINDOWS_BOOL: import win32security #CrossOS
if CrossOS.IS_LINUX_BOOL: from simplepam import authenticate #CrossOS
import sys, base64, logging, ctypes, copy #Get input argument
import pickle
import inspect
import schedule
#from partd import Server

from . import Server
from . import Timer
from . import Processor
from . import BackwardCompatibility # Backward compatibility from v1.1.13
from . import Core
from . import Managers

if CrossOS.IS_WINDOWS_BOOL: from subprocess import CREATE_NEW_CONSOLE

from .Utils import LoggerHandlerDumpLogList
from ..Tools import Debugger

# ATTENTION! HERE IS NO Relative import because it will be imported dynamically
# All function check the flag SessionIsWindowResponsibleBool == True else no cammand is processed
# All functions can return None, Bool or Dict { "IsSuccessful": True }
from .RobotRDPActive import CMDStr # Create CMD Strings
from .RobotRDPActive import Connector # RDP API

#from .Settings import Settings
import importlib
from importlib import util
import threading # Multi-threading for RobotRDPActive
from .RobotRDPActive import RobotRDPActive #Start robot rdp active
from .RobotScreenActive import Monitor #Start robot screen active
from . import SettingsTemplate # Settings template
import uuid # Generate uuid
import datetime # datetime
import math
import glob # search the files
import urllib

#Единый глобальный словарь (За основу взять из Settings.py)
gSettingsDict = None

# AGENT DEFS

def AgentActivityItemAdd(inHostNameStr, inUserStr, inActivityItemDict, inGSettings=None):
    """
    Добавить активность в словарь активностей выбранного Агента

    :param inGSettings: Глобальный словарь настроек Оркестратора (синглтон)
    :param inHostNameStr: Наименование хоста, на котором запущен Агент. Наименования подключенных агентов доступно для просмотра в панели управления
    :param inUserStr: Наименование пользователя, на графической сессии которого запущен Агент. Наименования подключенных агентов доступно для просмотра в панели управления
    :param inActivityItemDict: Активность (ActivityItem). См. функцию ProcessorActivityitemCreate
    :return: ГУИД (GUID) строка Активности (ActivityItem). Далее можно ожидать результат этой функции по ГУИД с помощью функции AgentActivityItemReturnGet
    """

    inGSettings = GSettingsGet(inGSettings=inGSettings)  # Set the global settings
    lActivityItemDict = copy.deepcopy(inActivityItemDict)
    # Add GUIDStr if not exist
    lGUIDStr = None
    if "GUIDStr" not in lActivityItemDict:
        lGUIDStr = str(uuid.uuid4()) # generate new GUID
        lActivityItemDict["GUIDStr"] = lGUIDStr
    else: lGUIDStr = lActivityItemDict["GUIDStr"]
    # Add CreatedByDatetime
    lActivityItemDict["CreatedByDatetime"] = datetime.datetime.now()
    # Main alg
    lAgentDictItemKeyTurple = (inHostNameStr.upper(),inUserStr.upper())
    if lAgentDictItemKeyTurple not in inGSettings["AgentDict"]:
        inGSettings["AgentDict"][lAgentDictItemKeyTurple] = SettingsTemplate.__AgentDictItemCreate__()
    lThisAgentDict = inGSettings["AgentDict"][lAgentDictItemKeyTurple]
    lThisAgentDict["ActivityList"].append(lActivityItemDict)
    # Return the result
    return lGUIDStr

def AgentActivityItemExists(inHostNameStr, inUserStr, inGUIDStr, inGSettings = None):
    """
    Выполнить проверку, что активность (ActivityItem) была отправлена на сторону Агента.

    :param inGUIDStr: ГУИД (GUID) активности (ActivityItem)
    :return: True - Активность присутствует ; False - Активность еще не была отправлена на сторону Агента
    """
    # Check if GUID is exists in dict - has been recieved
    # Main alg
    lResultBool = False
    if lAgentDictItemKeyTurple in inGSettings["AgentDict"]:
        for lActivityItem in inGSettings["AgentDict"][lAgentDictItemKeyTurple]["ActivityList"]:
            if inGUIDStr == lActivityItem.get("GUIDStr",None):
                lResultBool = True
                break
    return lResultBool

def AgentActivityItemReturnExists(inGUIDStr, inGSettings = None):
    """
    Выполнить проверку, что активность (ActivityItem) была выполнена на стороне Агента и результат был получен на стороне Оркестратора.

    :return: True - Активность присутствует; False - Активность еще не была выполнена на стороне Агента
    """

    return inGUIDStr in inGSettings["AgentActivityReturnDict"]

def AgentActivityItemReturnGet(inGUIDStr, inCheckIntervalSecFloat = 0.5, inGSettings=None):
    """
    Ожидает появления результата по активности (ActivityItem). Возвращает результат выполнения активности.
    
    !ВНИМАНИЕ! Замораживает поток, пока не будет получен результат. 
    !ВНИМАНИЕ! Запускать следует после того как будет инициализировано ядро Оркестратора (см. функцию OrchestratorInitWait), иначе будет инициирована ошибка. 

    :param inCheckIntervalSecFloat: Интервал в секундах, с какой частотой выполнять проверку результата. По умолчанию 0.5
    :return: Результат выполнения активности. !ВНИМАНИЕ! Возвращаются только то результаты, которые могут быть интерпретированы в JSON формате.
    """
    #Check if Orchestrator has been initialized - else raise exception
    if Core.IsOrchestratorInitialized(inGSettings=inGSettings) == True:
        # Wait while result will not come here
        while not AgentActivityItemReturnExists(inGSettings=inGSettings, inGUIDStr=inGUIDStr):
            time.sleep(inCheckIntervalSecFloat)
        # Return the result
        return inGSettings["AgentActivityReturnDict"][inGUIDStr]["Return"]
    else:
        raise Exception(f"__Orchestrator__.AgentActivityItemReturnGet !ATTENTION! Use this function only after Orchestrator initialization! Before orchestrator init exception will be raised.")

def AgentOSCMD(inHostNameStr, inUserStr, inCMDStr, inRunAsyncBool=True, inSendOutputToOrchestratorLogsBool=True, inCMDEncodingStr="cp1251", inGSettings=None, inCaptureBool=True):
    """
    Отправка команды командной строки на сессию, где работает pyOpenRPA.Agent. Результат выполнения команды можно выводить в лог оркестратора.

    :param inCMDStr: Команда для исполнения на стороне сессии Агента
    :param inRunAsyncBool: True - Агент не ожидает окончания выполнения команды. !ВНИМАНИЕ! Логирование в такой ситуации будет невозможно; False - Агент ожидает окончания выполнения операции.
    :param inSendOutputToOrchestratorLogsBool: True - отправлять весь вывод от команды в логи Оркестратора; Flase - Не отправлять; Default True
    :param inCMDEncodingStr: Кодировка DOS среды, в которой исполняется команда. Если некорректно установить кодировку - русские символы будут испорчены. По умолчанию установлена "cp1251"
    :param inCaptureBool: True - не запускать приложение как отдельное. Результат выполнения команды будет выводиться в окне Агента (если окно Агента присутствует на экране). False - команда будет запущена в отдельном DOS окне.
    """
    lActivityItemDict = {
        "Def":"OSCMD", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgList":[], # Args list
        "ArgDict":{"inCMDStr":inCMDStr,"inRunAsyncBool":inRunAsyncBool, "inSendOutputToOrchestratorLogsBool": inSendOutputToOrchestratorLogsBool, "inCMDEncodingStr": inCMDEncodingStr, "inCaptureBool":inCaptureBool}, # Args dictionary
        "ArgGSettings": "inGSettings", # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        "ArgLogger": None # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
    }
    #Send item in AgentDict for the futher data transmition
    return AgentActivityItemAdd(inGSettings=inGSettings, inHostNameStr=inHostNameStr, inUserStr=inUserStr, inActivityItemDict=lActivityItemDict)

def AgentOSLogoff(inHostNameStr, inUserStr):
    """
    Выполнить операцию logoff на стороне пользователя.

    """
    inGSettings = GSettingsGet()  # Set the global settings
    lCMDStr = "shutdown /l"
        "ArgDict":{"inCMDStr":lCMDStr,"inRunAsyncBool":False, "inSendOutputToOrchestratorLogsBool": True, "inCMDEncodingStr": "cp1251"}, # Args dictionary
    }

def AgentOSFileSend(inHostNameStr, inUserStr, inOrchestratorFilePathStr, inAgentFilePathStr, inGSettings = None):
    """
    Отправить файл по адресу inOrchestratorFilePathStr со стороны Оркестратора и сохранить по адресу inAgentFilePathStr на стороне Агента.
    Поддерживает передачу крупных файлов (более 2-х Гб.). Функция является синхронной - не закончит свое выполнение, пока файл не будет передан полностью.

    !ВНИМАНИЕ - ПОТОКОБЕЗОПАСНАЯ! Вы можете вызвать эту функцию до инициализации ядра Оркестратора. Оркестратор добавит эту функцию в процессорную очередь на исполение. Если вам нужен результат функции, то необходимо сначала убедиться в том, что ядро Оркестратора было инициализированно (см. функцию OrchestratorInitWait). 
    
    :param inOrchestratorFilePathStr: Полный путь к передаваемому файлу на стороне Оркестратора.
    :param inAgentFilePathStr: Полный путь к локации, в которую требуется сохранить передаваемый файл.
    """

    # Check thread
    if  inGSettings["ServerDict"]["ServerThread"] is None:
        if inGSettings["Logger"]: inGSettings["Logger"].warning(f"AgentOSFileSend run before server init - activity will be append in the processor queue.")
        lResult = {
            "Def": AgentOSFileSend, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inHostNameStr":inHostNameStr, "inUserStr":inUserStr, "inOrchestratorFilePathStr":inOrchestratorFilePathStr, "inAgentFilePathStr": inAgentFilePathStr},  # Args dictionary
            "ArgGSettings": "inGSettings",  # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
            "ArgLogger": None  # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        }
        inGSettings["ProcessorDict"]["ActivityList"].append(lResult)
    else: # In processor - do execution
        lActivityItemCheckIntervalSecFloat = inGSettings["ServerDict"]["AgentFileChunkCheckIntervalSecFloat"]

        # Get the chunk limit
        lChunkByteSizeInt = inGSettings["ServerDict"]["AgentFileChunkBytesSizeInt"]

        lL = inGSettings.get("Logger",None)

        # Open the file and get the size (in bytes)
        lFile = open(inOrchestratorFilePathStr,"rb")
        lFileSizeBytesInt = lFile.seek(0,2)
        lFile.seek(0)
        #import pdb
        #pdb.set_trace()
        lChunkCountInt = math.ceil(lFileSizeBytesInt/lChunkByteSizeInt)
        if lL: lL.info(f"O2A: Start to send binary file via chunks. Chunk count: {lChunkCountInt}, From (Orch side): {inOrchestratorFilePathStr}, To (Agent side): {inAgentFilePathStr}")
        for lChunkNumberInt in range(lChunkCountInt):
            # Read chunk
            lFileChunkBytes = lFile.read(lChunkByteSizeInt)
            # Convert to base64
            lFileChunkBase64Str = base64.b64encode(lFileChunkBytes).decode("utf-8")
            # Send chunk
            if lChunkNumberInt == 0:
                lActivityItemGUIDStr = AgentOSFileBinaryDataBase64StrCreate(inGSettings=inGSettings,inHostNameStr=inHostNameStr,
                                                     inUserStr=inUserStr,inFilePathStr=inAgentFilePathStr,
                                                     inFileDataBase64Str=lFileChunkBase64Str)
            else:
                lActivityItemGUIDStr = AgentOSFileBinaryDataBase64StrAppend(inGSettings=inGSettings, inHostNameStr=inHostNameStr,
                                                     inUserStr=inUserStr, inFilePathStr=inAgentFilePathStr,
            # Wait for the activity will be deleted
            while AgentActivityItemExists(inGSettings=inGSettings,inHostNameStr=inHostNameStr,inUserStr=inUserStr,inGUIDStr=lActivityItemGUIDStr):
                time.sleep(lActivityItemCheckIntervalSecFloat)
            if lL: lL.debug(
                    f"O2A: BINARY SEND: Current chunk index: {lChunkNumberInt}")
        if lL: lL.info(
            f"O2A: BINARY SEND: Transmition has been complete")
        # Close the file

def AgentOSFileBinaryDataBytesCreate(inHostNameStr, inUserStr, inFilePathStr, inFileDataBytes, inGSettings=None):
    """
    Создать бинарный файл, который будет расположен по адресу inFilePathStr на стороне Агента с содержимым inFileDataBytes

    :param inFilePathStr: Полный путь к сохраняемому файлу на стороне Агента.
    :param inFileDataBytes: Строка байт (b'') для отправки в создаваемый файл на стороне Агента.
    """
    lFileDataBase64Str = base64.b64encode(inFileDataBytes).decode("utf-8")
        "Def":"OSFileBinaryDataBase64StrCreate", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataBase64Str":lFileDataBase64Str}, # Args dictionary
    }

def AgentOSFileBinaryDataBase64StrCreate(inHostNameStr, inUserStr, inFilePathStr, inFileDataBase64Str, inGSettings=None):
    """
    Создать бинарный файл, который будет расположен по адресу inFilePathStr на стороне Агента с содержимым, декодированным с формата base64: inFileDataBase64Str
    
    :param inFileDataBase64Str: Строка в формате base64 для отправки в создаваемый файл на стороне Агента.
    """
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataBase64Str":inFileDataBase64Str}, # Args dictionary
    }

def AgentOSFileBinaryDataBase64StrAppend(inHostNameStr, inUserStr, inFilePathStr, inFileDataBase64Str, inGSettings = None):
    """
    Добавить бинарную информацию в существующий бинарный файл, который будет расположен по адресу inFilePathStr на стороне Агента с содержимым, декодированным с формата base64: inFileDataBase64Str
    
    """
        "Def":"OSFileBinaryDataBase64StrAppend", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
    }

# Send text file to Agent (string)
def AgentOSFileTextDataStrCreate(inHostNameStr, inUserStr, inFilePathStr, inFileDataStr, inEncodingStr = "utf-8",inGSettings=None):
    """
    Создать текстовый файл, который будет расположен по адресу inFilePathStr на стороне Агента с содержимым inFileDataStr в кодировке inEncodingStr
    
    :param inFileDataStr: Строка для отправки в создаваемый файл на стороне Агента.
    :param inEncodingStr: Кодировка текстового файла. По умолчанию utf-8
    """
        "Def":"OSFileTextDataStrCreate", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataStr":inFileDataStr, "inEncodingStr": inEncodingStr}, # Args dictionary
    }

def AgentOSFileBinaryDataBase64StrReceive(inHostNameStr, inUserStr, inFilePathStr, inGSettings = None):
    """
    Выполнить чтение бинарного файла и получить содержимое в формате base64 (строка)

    :param inFilePathStr: Путь к бинарному файлу на чтение на стороне Агента
    """
        "Def":"OSFileBinaryDataBase64StrReceive", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgDict":{"inFilePathStr":inFilePathStr}, # Args dictionary
    }

def AgentOSFileBinaryDataReceive(inHostNameStr, inUserStr, inFilePathStr):
    """
    Чтение бинарного файла на стороне Агента по адресу inFilePathStr.
    
    !ВНИМАНИЕ - СИНХРОННАЯ! Функция не завершится, пока не будет получен результат чтения на стороне Агента.
    
    :param inFilePathStr: Путь к бинарному файлу, который требуется прочитать на стороне Агента
    :return: Строка байт (b'') - содержимое бинарного файла
    """
    lFileDataBytes = None
    if  OrchestratorIsInited() == False:
        if inGSettings["Logger"]: inGSettings["Logger"].warning(f"AgentOSFileBinaryDataReceive run before orc init - activity will be append in the processor queue.")
        lResult = {
            "Def": AgentOSFileBinaryDataReceive, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inHostNameStr":inHostNameStr, "inUserStr":inUserStr, "inFilePathStr":inFilePathStr},  # Args dictionary
            "ArgGSettings": None,  # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        }
        lActivityItemDict = {
            "Def":"OSFileBinaryDataBase64StrReceive", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
            "ArgList":[], # Args list
            "ArgDict":{"inFilePathStr":inFilePathStr}, # Args dictionary
            "ArgGSettings": "inGSettings", # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
            "ArgLogger": None # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        }
        
        #Send item in AgentDict for the futher data transmition
        lGUIDStr = AgentActivityItemAdd(inGSettings=inGSettings, inHostNameStr=inHostNameStr, inUserStr=inUserStr, inActivityItemDict=lActivityItemDict)
        lFileBase64Str = AgentActivityItemReturnGet(inGUIDStr=lGUIDStr)
        if lFileBase64Str is not None: lFileDataBytes = base64.b64decode(lFileBase64Str)
        return lFileDataBytes

def AgentOSFileTextDataStrReceive(inHostNameStr, inUserStr, inFilePathStr, inEncodingStr="utf-8", inGSettings = None):
    """
    Чтение текстового файла на стороне Агента по адресу inFilePathStr. По ГИУД с помощью функции AgentActivityItemReturnGet можно будет получить текстовую строку данных, которые были расположены в файле.
    
    !ВНИМАНИЕ - АСИНХРОННАЯ! Функция завершится сразу, не дожидаясь окончания выполнения операции на стороне Агента.
    
    """
        "Def":"OSFileTextDataStrReceive", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgDict":{"inFilePathStr":inFilePathStr, "inEncodingStr": inEncodingStr}, # Args dictionary
    }

def AgentProcessWOExeUpperUserListGet(inHostNameStr, inUserStr, inGSettings = None):
    """
    Получить список процессов, которые выполняется на сессии Агента. Все процессы фиксируются без постфикса .exe, а также в верхнем регистре.

    ПРИМЕР РЕЗУЛЬТАТА, КОТОРЫЙ МОЖНО ПОЛУЧИТЬ ПО ГУИД ЧЕРЕЗ ФУНКЦИЮ AgentActivityItemReturnGet: ["ORCHESTRATOR", "AGENT", "CHROME", "EXPLORER", ...]

    """
        "Def":"ProcessWOExeUpperUserListGet", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgDict":{}, # Args dictionary
    }

# OS DEFS
def OSLogoff():
    """
    Выполнить отключение сессии, на которой выполняется Оркестратор.
    
    :return:
    """
    os.system("shutdown /l")

def OSCredentialsVerify(inUserStr, inPasswordStr, inDomainStr=""): ##
    """
    Выполнить верификацию доменного (локального) пользователя по паре логин/пароль

    :param inUserStr: Наименование пользователя
    :param inPasswordStr: Пароль
    :param inDomainStr: Домен. Если домена нет - не указывать или ""
    :return: True - Учетные данные верны; False - Учетные данные представлены некорректно    
    """
    if CrossOS.IS_WINDOWS_BOOL:
        try:
            hUser = win32security.LogonUser(
                inUserStr,inDomainStr, inPasswordStr,
                win32security.LOGON32_LOGON_NETWORK, win32security.LOGON32_PROVIDER_DEFAULT
            )
        except win32security.error:
            return False
        else:
            return True
    if CrossOS.IS_LINUX_BOOL:
        return authenticate(inUserStr, inPasswordStr, service='login', encoding='utf-8', resetcred=True)

def OSRemotePCRestart(inHostStr, inForceBool=True, inLogger = None):
    """
    Отправить сигнал на удаленную перезагрузку операционной системы. 
    
    !ВНИМАНИЕ! Перезапуск будет принят, если учетная запись имеет полномочия на перезапуск на соответсвующей машине.

    :param inHostStr: Имя хоста, который требуется перезагрузить
    :param inForceBool: True - принудительная перезагрузка; False - мягкая перезагрузка (дождаться окончания выполнения всех операций). По умолчанию True
    :param inLogger: Логгер, в который отправлять информацию о результате выполнения команды
    :return:
    """
    if inLogger is None: inLogger = OrchestratorLoggerGet()
    lCMDStr = f"powershell -Command Restart-Computer -ComputerName {inHostStr}"
    if inForceBool == True: lCMDStr = lCMDStr + " -Force"
    OSCMD(inCMDStr=lCMDStr,inLogger=inLogger)

def OSRestart(inForceBool=True, inLogger = None):
    """L+,W+: Отправить сигнал на перезагрузку операционной системы. 
    
    :return:
    """
    if CrossOS.IS_WINDOWS_BOOL: 
        lCMDStr = f"shutdown -r"
        if inForceBool == True: lCMDStr = lCMDStr + " -f"
    elif CrossOS.IS_LINUX_BOOL:
        lCMDStr = f"reboot"
        if inForceBool == True: lCMDStr = lCMDStr + " -f" 

def OSCMD(inCMDStr, inRunAsyncBool=True, inLogger = None):
    """
    Отправить команду на выполнение на сессию, где выполняется Оркестратор.

    Поддерживается: Windows: +, Linux: +

    :param inCMDStr: Команда на отправку
    :param inRunAsyncBool: True - выполнить команду в асинхронном режиме (не дожидаться окончания выполнения программы и не захватывать результат выполнения); False - Ждать окончания выполнения и захватывать результат
    :return: Строка результата выполнения команды. Если inRunAsyncBool = False
    """
    if inRunAsyncBool == True:
    def _CMDRunAndListenLogs(inCMDStr, inLogger):
        # CROSS OS
        if CrossOS.IS_WINDOWS_BOOL: lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=CREATE_NEW_CONSOLE)
        if CrossOS.IS_LINUX_BOOL: lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        if inLogger:
            lListenBool = True
            inLogger.info(f"{lOSCMDKeyStr}: # # # # CMD Process has been STARTED # # # # ")
            inLogger.info(f"{lOSCMDKeyStr}: {inCMDStr}")
            while lListenBool:
                lStr = lOutputLineBytes.decode('cp866')
                inLogger.info(f"{lOSCMDKeyStr}: {lStr}")
            inLogger.info(f"{lOSCMDKeyStr}: # # # # CMD Process has been FINISHED # # # # ")
    # New call
        lThread = threading.Thread(target=_CMDRunAndListenLogs, kwargs={"inCMDStr":inCMDStr, "inLogger":inLogger})
    else:
        lResultStr = _CMDRunAndListenLogs(inCMDStr=inCMDStr, inLogger=inLogger)

def OrchestratorRestart(inGSettings=None):
    """
    Перезапуск Оркестратора с сохранением информации о запущенных RDP сессиях.

    """
    OrchestratorSessionSave(inGSettings=inGSettings) # Dump RDP List in file json
    if inGSettings is not None:
        lL = inGSettings["Logger"]
        if lL: lL.info(f"Do restart")
    # Restart session
    if CrossOS.IS_WINDOWS_BOOL: os.execl(sys.executable, os.path.abspath(__file__), *sys.argv)
    if CrossOS.IS_LINUX_BOOL: os.execl(sys.executable, sys.executable, *sys.argv)
    sys.exit(0)

def OrchestratorLoggerGet() -> logging.Logger:
    """
    Получить логгер Оркестратора

    :return: Логгер
    """
    return GSettingsGet().get("Logger",None)

def OrchestratorScheduleGet() -> schedule:
    """
    Базовый объект расписания, который можно использовать для запуска / остановки роботов.
    Подробнее про объект schedule и его примеры использования см. по адресу: schedule.readthedocs.io

    .. code-block:: python
        
        # Однопоточный schedule
        Orchestrator.OrchestratorScheduleGet().every(5).seconds.do(lProcess.StatusCheckStart)

        #Многопоточный schedule. cм. описание Orchestrator.OrchestratorThreadStart
        Orchestrator.OrchestratorScheduleGet().every(5).seconds.do(Orchestrator.OrchestratorThreadStart,lProcess.StatusCheckStart)

    :return: schedule объект
    """
    if GSettingsGet().get("SchedulerDict",{}).get("Schedule",None) is None:
        GSettingsGet()["SchedulerDict"]["Schedule"]=schedule
    return GSettingsGet().get("SchedulerDict",{}).get("Schedule",None)

def OrchestratorThreadStart(inDef, *inArgList, **inArgDict):
    """
    Запустить функцию в отдельном потоке. В таком случае получить результат выполнения функции можно только через общие переменные. (Например через GSettings) 

    :param inDef: Python функция
    :param inArgList: Список неименованных аргументов функции inDef
    :param inArgDict: Словарь именованных аргументов функции inDef
    :return: threading.Thread экземпляр
    """
    lDefThread = threading.Thread(target=inDef,args=inArgList,kwargs=inArgDict)
    lDefThread.start()
    return lDefThread

def OrchestratorIsAdmin():
    """
    Проверить, запущен ли Оркестратор с правами администратора. Права администратора нужны Оркестратору только для контроля графической сессии, на которой он запущен. Если эти права выделить индивидуально, то права администратора будут необязательны.

    :return: True - Запущен с правами администратора; False - Не запущен с правами администратора
    """
        try:
            return ctypes.windll.shell32.IsUserAnAdmin()
        except:
    else: return True

def OrchestratorIsInited() -> bool:
    """
    Проверить, было ли проинициализировано ядро Оркестратора

    :return: True - Ядро Оркестратора было проинициализировано; False - Требуется время на инициализацию
    :rtype: bool
    """    

    return Core.IsOrchestratorInitialized(inGSettings=GSettingsGet())

def OrchestratorInitWait() -> None:
    """
    Ожидать инициализацию ядра Оркестратора

    !ВНИМАНИЕ!: НИ В КОЕМ СЛУЧАЕ НЕ ЗАПУСКАТЬ ЭТУ ФУНКЦИЮ В ОСНОВНОМ ПОТОКЕ, ГДЕ ПРОИСХОДИТ ИНИЦИАЛИЗАЦИЯ ЯДРА ОРКЕСТРАТОРА - ВОЗНИКНЕТ ВЕЧНЫЙ ЦИКЛ
    """
    lIntervalSecFloat = 0.5
    while not OrchestratorIsInited():
        time.sleep(lIntervalSecFloat)
    
def OrchestratorRerunAsAdmin():
    """
    Перезапустить Оркестратор с правами локального администратора. Права администратора нужны Оркестратору только для контроля графической сессии, на которой он запущен. Если эти права выделить индивидуально, то права администратора будут необязательны.

    """
    if not OrchestratorIsAdmin():
        # Re-run the program with admin rights
        ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
    else:
        print(f"!SKIPPED! Already run as administrator!")

def OrchestratorPySearchInit(inGlobPatternStr, inDefStr = None, inDefArgNameGSettingsStr = None, inAsyncInitBool = False):
    """
    Выполнить поиск и инициализацию пользовательских .py файлов в Оркестраторе (например панелей управления роботов)

    Добавляет инициализированный модуль в пространство sys.modules как imported (имя модуля = имя файла без расширения).
    
    ВНИМАНИЕ! ПРЕОБРАЗУЕТ ПУТИ МЕЖДУ WINDOWS И LINUX НОТАЦИЯМИ

        # ВАРИАНТ ИСПОЛЬЗОВАНИЯ 1 (инициализация модуля py без вызова каких-либо функций внутри)
        # автоинициализация всех .py файлов, с префиксом CP_, которые расположены в папке ControlPanel
        Orchestrator.OrchestratorPySearchInit(inGlobPatternStr="ControlPanel\\CP_*.py")

        # ВАРИАНТ ИСПОЛЬЗОВАНИЯ 2 (инициализация модуля py с вызовом функции внутри) - преимущественно для обратной совместимости старых версий панелей управления < 1.2.7
        Orchestrator.OrchestratorPySearchInit(inGlobPatternStr="ControlPanel\\CP_*.py", inDefStr="SettingsUpdate", inDefArgNameGSettingsStr="inGSettings")

        # ДЛЯ СПРАВКИ & ИСТОРИИ: Код выше позволил отказаться от блока кода ниже для каждого .py файла
        ## !!! For Relative import !!! CP Version Check
        try:
            sys.path.insert(0,os.path.abspath(os.path.join(r"")))
            from ControlPanel import CP_VersionCheck
            CP_VersionCheck.SettingsUpdate(inGSettings=gSettings)
        except Exception as e:
            gSettings["Logger"].exception(f"Exception when init CP. See below.")

    :param inGlobPatternStr: Пример "..\\*\\*\\*_CP*.py"
    :param inDefStr: ОПЦИОНАЛЬНО Строковое наименование функции. Преимущественно для обратной совместимости
    :param inDefArgNameGSettingsStr: ОПЦИОНАЛЬНО Наименование аргумента, в который требуется передать GSettings (если необходимо)
    :param inAsyncInitBool: ОПЦИОНАЛЬНО True - Инициализация py модулей в отдельных параллельных потоках - псевдопараллельное выполнение. False - последовательная инициализация
    :return: Сведения об инициализированных модулях в структуре: { "ModuleNameStr":{"PyPathStr": "", "Module": ...},  ...}
    """
    inGlobPatternStr = CrossOS.PathStr(inGlobPatternStr)
    # # # # # # # #
    def __execute__(inResultDict, inPyPathItemStr, inDefStr = None, inDefArgNameGSettingsStr = None):
        try:
            lPyPathItemStr = inPyPathItemStr
            lModuleNameStr = os.path.basename(lPyPathItemStr)[0:-3]
            lTechSpecification = importlib.util.spec_from_file_location(lModuleNameStr, lPyPathItemStr)
            lTechModuleFromSpec = importlib.util.module_from_spec(lTechSpecification)
            sys.modules[lModuleNameStr] = lTechModuleFromSpec  # Add initialized module in sys - python will not init this module enought
            lTechSpecificationModuleLoader = lTechSpecification.loader.exec_module(lTechModuleFromSpec)
            lItemDict = {"ModuleNameStr": lModuleNameStr, "PyPathStr": lPyPathItemStr, "Module": lTechModuleFromSpec}
            if lL: lL.info(f"Py module {lModuleNameStr} has been successfully initialized.")
            inResultDict[lModuleNameStr]=lItemDict
            # Backward compatibility to call def with gsettings when init
            if inDefStr is not None and inDefStr != "":
                lDef = getattr(lTechModuleFromSpec, inDefStr)
                lArgDict = {}
                if inDefArgNameGSettingsStr is not None and inDefArgNameGSettingsStr != "":
                    lArgDict = {inDefArgNameGSettingsStr:GSettingsGet()}
                lDef(**lArgDict)
            if lL: lL.exception(f"Exception when init the .py file {os.path.abspath(lPyPathItemStr)}")
    # # # # # # # #

    lResultDict = {}

    lPyPathStrList = glob.glob(inGlobPatternStr) # get the file list
    lL = OrchestratorLoggerGet() # get the logger
    for lPyPathItemStr in lPyPathStrList:
        if inAsyncInitBool == True:
            # ASYNC EXECUTION
            lThreadInit = threading.Thread(target=__execute__,kwargs={
                "inResultDict":lResultDict, "inPyPathItemStr": lPyPathItemStr, 
                "inDefStr": inDefStr, "inDefArgNameGSettingsStr": inDefArgNameGSettingsStr}, daemon=True)
            lThreadInit.start()
        else:
            # SYNC EXECUTION
            __execute__(inResultDict=lResultDict, inPyPathItemStr=lPyPathItemStr, inDefStr = inDefStr, inDefArgNameGSettingsStr = inDefArgNameGSettingsStr)
    return lResultDict

def OrchestratorSessionSave(inGSettings=None):
    """
    Сохранить состояние Оркестратора (для дальнейшего восстановления в случае перезапуска): 
    
    - RDP сессий, которые контролирует Оркестратор
    - Хранилища DataStorage в глобальном словаре настроек GSettings. DataStorage поддерживает хранение объектов Python

    (до версии 1.2.7)
        _SessionLast_GSettings.pickle (binary)

    (начиная с версии 1.2.7)
        _SessionLast_RDPList.json (encoding = "utf-8")
        _SessionLast_StorageDict.pickle (binary)

    """
    try:
        # Dump RDP List in file json
        #lFile = open("_SessionLast_RDPList.json", "w", encoding="utf-8")
        #lFile.write(json.dumps(inGSettings["RobotRDPActive"]["RDPList"]))  # dump json to file
        #lFile.close()  # Close the file
        #if inGSettings is not None:
        #    if lL: lL.info(
        #        f"Orchestrator has dump the RDP list before the restart.")
        ## _SessionLast_StorageDict.pickle (binary)
        #if "StorageDict" in inGSettings:
        #    with open('_SessionLast_StorageDict.pickle', 'wb') as lFile:
        #        pickle.dump(inGSettings["StorageDict"], lFile)
        #        if lL: lL.info(
        #            f"Orchestrator has dump the StorageDict before the restart.")

        #SessionLast
        lDumpDict = {"StorageDict":inGSettings["StorageDict"], "ManagersProcessDict":inGSettings["ManagersProcessDict"],
                     "RobotRDPActive":{"RDPList": inGSettings["RobotRDPActive"]["RDPList"]}}
        with open('_SessionLast_GSettings.pickle', 'wb') as lFile:
            pickle.dump(lDumpDict, lFile)
            if lL: lL.info(
                f"Orchestrator has dump the GSettings (new dump mode from v1.2.7) before the restart.")

    except Exception as e:
        if lL: lL.exception(f"Exception when dump data before restart the Orchestrator")
    return True

def OrchestratorSessionRestore(inGSettings=None):
    """
    Восстановить состояние Оркестратора, если ранее состояние Оркестратора было сохранено с помощью функции OrchestratorSessionSave: 
    
    """
    lL = inGSettings.get("Logger",None)
    # _SessionLast_RDPList.json (encoding = "utf-8")
    if os.path.exists("_SessionLast_RDPList.json"):
        lFile = open("_SessionLast_RDPList.json", "r", encoding="utf-8")
        lSessionLastRDPList = json.loads(lFile.read())
        lFile.close()  # Close the file
        os.remove("_SessionLast_RDPList.json")  # remove the temp file
        inGSettings["RobotRDPActive"]["RDPList"] = lSessionLastRDPList  # Set the last session dict
        if lL: lL.warning(f"RDP Session List was restored from previous Orchestrator session")
    # _SessionLast_StorageDict.pickle (binary)
    if os.path.exists("_SessionLast_StorageDict.pickle"):
        if "StorageDict" not in inGSettings:
            inGSettings["StorageDict"] = {}
        with open('_SessionLast_StorageDict.pickle', 'rb') as lFile:
            lStorageDictDumpDict = pickle.load(lFile)
            Server.__ComplexDictMerge2to1Overwrite__(in1Dict=inGSettings["StorageDict"],
                                                     in2Dict=lStorageDictDumpDict)  # Merge dict 2 into dict 1
            if lL: lL.warning(f"StorageDict was restored from previous Orchestrator session")
        os.remove("_SessionLast_StorageDict.pickle")  # remove the temp file
    # _SessionLast_Gsettings.pickle (binary)
    if os.path.exists("_SessionLast_GSettings.pickle"):
        if "ManagersProcessDict" not in inGSettings:
            inGSettings["ManagersProcessDict"] = {}
        with open('_SessionLast_GSettings.pickle', 'rb') as lFile:
            Server.__ComplexDictMerge2to1Overwrite__(in1Dict=inGSettings,
            if lL: lL.warning(f"GSettings was restored from previous Orchestrator session")
        os.remove("_SessionLast_GSettings.pickle")  # remove the temp file

def UACKeyListCheck(inRequest, inRoleKeyList) -> bool:
    """
    Проверить права доступа для пользователя запроса по списку ключей до права.

        Orchestrator.UACKeyListCheck(inRequest=lRequest, inRoleKeyList=["ROBOT1","DISPLAY_DASHBOARD"])

    :param inRequest: Экземпляр request (from http.server import BaseHTTPRequestHandler)
    :param inRoleKeyList: Список ключей, права доступа к которому требуется проверить
    :return: True - Пользователь обладает соответствующим правом; False - Пользователь не обладает соответствующим правом
    """
    return inRequest.UACClientCheck(inRoleKeyList=inRoleKeyList)

def UACUserDictGet(inRequest) -> dict:
    """
    Вернуть UAC (User Access Control) словарб доступов для пользователя, который отправил запрос. Пустой словарь - супердоступ (доступ ко всему)

    :return: Словарь доступов пользователя. Пустой словарь - супердоступ (доступ ко всему)
    """
    return inRequest.UserRoleHierarchyGet() # get the Hierarchy

def UACUpdate(inADLoginStr, inADStr="", inADIsDefaultBool=True, inURLList=None, inRoleHierarchyAllowedDict=None, inGSettings = None):
    """
    Дообогащение словаря доступа UAC пользователя inADStr\\inADLoginStr. Если ранее словарь не устанавливался, то по умолчанию он {}. Далее идет дообогащение теми ключами, которые присутствуют в inRoleHierarchyAllowedDict

    :param inADLoginStr: Логин пользователя
    :param inADStr: Домен пользователя. Если пусто - локальный компьютер или домен по-умолчанию, который настроен в ОС
    :param inADIsDefaultBool: True - домен настроен по умолчанию; False - домен требуется обязательно указывать
    :param inURLList: Список разрешенных URL для пользователя. Для добавления URL рекомендуем использовать функции WebURLConnectDef, WebURLConnectFile, WebURLConnectFolder
        Формат: {
            "Method": "GET" || "POST", 
            "URL": "/GetScreenshot", 
            "MatchType": "BeginWith" || "Equal" || "EqualCase" || "Contains" || "EqualNoParam", 
            "ResponseDefRequestGlobal": Функция python || "ResponseFilePath": Путь к файлу ||  "ResponseFolderPath": Путь к папке, в которой искать файлы,
            "ResponseContentType": пример MIME "image/png",
            "UACBool":False - не выполнять проверку прав доступа по запросу, 
            "UseCacheBool": True - кэшировать ответ},
    :param inRoleHierarchyAllowedDict: Словарь доступа пользователя UAC. Пустой словарь - полный доступ
    """
    lUserTurple = (inADStr.upper(),inADLoginStr.upper()) # Create turple key for inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"]
    if inURLList is None: inURLList = [] # Check if None
    if inRoleHierarchyAllowedDict is None: inRoleHierarchyAllowedDict = {} # Check if None
    # Get the old URLList
    try:
        inURLList += inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"][lUserTurple]["MethodMatchURLBeforeList"]
    except:
        pass
    # Check RoleHierarchyAllowedDict in gSettings for the old role hierarchy - include in result.
    if lUserTurple in inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"] and "RoleHierarchyAllowedDict" in inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"][lUserTurple]:
        lRoleHierarchyAllowedOLDDict = inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"][lUserTurple]["RoleHierarchyAllowedDict"]
        Server.__ComplexDictMerge2to1__(in1Dict=inRoleHierarchyAllowedDict, in2Dict=lRoleHierarchyAllowedOLDDict) # Merge dict 2 into dict 1

    # Create Access item
    lRuleDomainUserDict = {
        "MethodMatchURLBeforeList": inURLList,
        "RoleHierarchyAllowedDict": inRoleHierarchyAllowedDict
    }
    # Case add domain + user
    inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"].update({(inADStr.upper(),inADLoginStr.upper()):lRuleDomainUserDict})
    if inADIsDefaultBool:
        # Case add default domain + user
        inGSettings["ServerDict"]["AccessUsers"]["RuleDomainUserDict"].update({("",inADLoginStr.upper()):lRuleDomainUserDict})

def UACSuperTokenUpdate(inSuperTokenStr, inGSettings=None):
    """
    Добавить супертокен (полный доступ). Супертокены используются для подключения к Оркестратору по API

    :param inSuperTokenStr: Кодовая строковая комбинация, которая будет предоставлять доступ роботу / агенту для взаимодействия с Оркестратором по API
    """
    lLoginStr = "SUPERTOKEN"
    UACUpdate(inGSettings=inGSettings, inADLoginStr=lLoginStr)
    inGSettings["ServerDict"]["AccessUsers"]["AuthTokensDict"].update(
        {inSuperTokenStr:{"User":lLoginStr, "Domain":"", "TokenDatetime":  datetime.datetime.now(), "FlagDoNotExpire":True}}
    )

# # # # # # # # # # # # # # # # # # # # # # #
# OrchestratorWeb defs
def WebURLIndexChange(inURLIndexStr:str ="/"):
    """
    Изменить адрес главной страницы Оркестратора. По умолчанию '/'

    :param inURLIndexStr: Новый адрес главной страницы Оркестратора.
    :type inURLIndexStr: str, опционально
    """
    GSettingsGet()["ServerDict"]["URLIndexStr"] = inURLIndexStr

def WebURLConnectDef(inMethodStr, inURLStr, inMatchTypeStr, inDef, inContentTypeStr="application/octet-stream", inGSettings = None, inUACBool = None):
    """
     Подключить функцию Python к URL.
    
    :param inMethodStr: Метод доступа по URL "GET" || "POST"
    :param inURLStr: URL адрес. Пример  "/index"
    :param inMatchTypeStr: Тип соответсвия строки URL с inURLStr: "BeginWith" || "Contains" || "Equal" || "EqualCase" || "EqualNoParam"
    :param inDef: Функция Python. Допускаются функции, которые принимают следующие наборы параметров: 2:[inRequest, inGSettings], 1: [inRequest], 0: []
    :param inContentTypeStr: МИМЕ тип. По умолчанию: "application/octet-stream"
    :param inUACBool: True - Выполнять проверку прав доступа пользователя перед отправкой ответа; False - не выполнять проверку прав доступа пользователя
    """
    lURLItemDict = {
        "Method": inMethodStr.upper(),
        "URL": inURLStr,  # URL of the request
        "MatchType": inMatchTypeStr,  # "BeginWith|Contains|Equal|EqualCase",
        # "ResponseFilePath": "", #Absolute or relative path
        #"ResponseFolderPath": "C:\Abs\Archive\scopeSrcUL\OpenRPA\Orchestrator\Settings",
        # Absolute or relative path
        "ResponseContentType": inContentTypeStr, #HTTP Content-type
        "ResponseDefRequestGlobal": inDef, #Function with str result
        "UACBool": inUACBool
    }
    inGSettings["ServerDict"]["URLList"].append(lURLItemDict)

def WebURLConnectFolder(inMethodStr, inURLStr, inMatchTypeStr, inFolderPathStr, inGSettings = None, inUACBool = None, inUseCacheBool= False):
    """
    Подключить папку к URL.

    :param inFolderPathStr: Путь к папке на диске, в которой искать файл и возвращать пользователю по HTTP
    :param inUseCacheBool: True - выполнить кэширование страницы, чтобы в следющих запросах открыть быстрее; False - не кэшировать
    """
    # Check if last symbol is "/" - append if not exist
    lFolderPathStr = os.path.abspath(inFolderPathStr)
    if lFolderPathStr[-1]!="/":lFolderPathStr+="/"
    # Prepare URLItem
        "ResponseFolderPath": lFolderPathStr, # Absolute or relative path
        "ResponseContentType": None, #HTTP Content-type
        #"ResponseDefRequestGlobal": inDef #Function with str result
        "UACBool": inUACBool,
        "UseCacheBool": inUseCacheBool
    }

def WebURLConnectFile(inMethodStr, inURLStr, inMatchTypeStr, inFilePathStr, inContentTypeStr=None, inGSettings = None, inUACBool = None, inUseCacheBool = False):
    """

    Подключить файл к URL.

    :param inFilePathStr: Путь к файлу на диске, который возвращать пользователю по HTTP
    :param inContentTypeStr: МИМЕ тип. Если None - выполнить автоматическое определение

    """
        "ResponseFilePath": os.path.abspath(inFilePathStr), #Absolute or relative path
        #"ResponseFolderPath": os.path.abspath(inFilePathStr), # Absolute or relative path
        "UACBool":inUACBool,
    }

def WebListenCreate(inServerKeyStr="Default", inAddressStr="", inPortInt=80, inCertFilePEMPathStr=None, inKeyFilePathStr=None, inGSettings = None):
    """
    Настроить веб-сервер Оркестратора.
    
    :param inAddressStr: IP адрес для прослушивания. Если "", то прослушивать запросы со всех сетевых карт. Если "127.0.0.1", то слушать запросы только с той ОС, на которой работает Оркестратор
    :param inPortInt: Номер порта для прослушивания. Если HTTP - 80; Если HTTPS - 443. По умолчанию 80. Допускается установка других портов
    :param inCertFilePEMPathStr: Путь файлу сертификата, сгенерированного в .pem (base64) формате. Обязателен при использовании защищенного HTTPS/SSL соединения.
    :param inKeyFilePathStr: Путь к файлу закрытого ключа в base64 формате
    :param inGSettings:  Глобальный словарь настроек Оркестратора (синглтон)
    :return: 
    """
    inGSettings["ServerDict"]["ListenDict"][inServerKeyStr]={
        "AddressStr":inAddressStr,
        "PortInt":inPortInt,
        "CertFilePEMPathStr":inCertFilePEMPathStr,
        "KeyFilePathStr":inKeyFilePathStr,
        "ServerInstance": None
    }

def WebCPUpdate(inCPKeyStr, inHTMLRenderDef=None, inJSONGeneratorDef=None, inJSInitGeneratorDef=None, inGSettings = None):
    """
    Добавить панель управления робота в Оркестратор. Для панели управления открыт доступ для использования функции-генератора HTML, генератора JSON данных, генератора JS кода.

    :param inCPKeyStr: Текстовый ключ панели управления. Ключ для каждой панели управления должен быть уникальным!
    :param inHTMLRenderDef: Функция Python для генерации HTML кода. Для использования Jinja2 шаблонов HTML см. pyOpenRPA.Orchestrator.Managers.ControlPanel
    :param inJSONGeneratorDef: Функция Python для генерации JSON контейнера для отправки на клиентскую часть Оркестратора
    :param inJSInitGeneratorDef: Функция Python для генерации JS кода для отправки на клиентскую часть Оркестратора
    """
    lCPManager = Managers.ControlPanel(inControlPanelNameStr=inCPKeyStr, inRefreshHTMLJinja2TemplatePathStr=None)
    # CASE HTMLRender
    if inHTMLRenderDef is not None: lCPManager.mBackwardCompatibilityHTMLDef = inHTMLRenderDef
    # CASE JSONGenerator
    if inJSONGeneratorDef is not None: lCPManager.mBackwardCompatibilityJSONDef = inJSONGeneratorDef
    # CASE JSInitGeneratorDef
    if inJSInitGeneratorDef is not None: lCPManager.mBackwardCompatibilityJSDef = inJSInitGeneratorDef

def WebAuditMessageCreate(inRequest=None, inOperationCodeStr="-", inMessageStr="-"):
    """
    Создание сообщения ИТ аудита с такими сведениями как (Домен, IP, логин и тд.). Данная функция особенно актуальна в том случае, если требуется реализовать дополнительные меры контроля ИТ системы. 
    
        # ПРИМЕР
        from pyOpenRPA import Orchestrator

        lWebAuditMessageStr = Orchestrator.WebAuditMessageCreate(
            inRequest = lRequest,
            inOperationCodeStr = "OP_CODE_1",
            inMessageStr="Success"):

        # Логгирование сообщения
        lLogger.info(lWebAuditMessageStr)

    :param inRequest: Экземпляр HTTP request. Опционален, если сообщение фиксируется из под потока, который был инициирован запросом пользователя
    :param inOperationCodeStr: Код операции, который принят в компании в соответствии с регламентными процедурами
    :param inMessageStr: Дополнительное сообщение, которое необходимо отправить в сообщение об ИТ аудите
    :return: Формат сообщения: "WebAudit :: DOMAIN\\USER@101.121.123.12 :: operation code :: message"
    """
    try:
        if inRequest is None: inRequest = WebRequestGet()
        lClientIPStr = inRequest.client_address[0]
        lUserDict = WebUserInfoGet(inRequest=inRequest)
        lDomainUpperStr = lUserDict["DomainUpperStr"]
        lUserLoginStr = lUserDict["UserNameUpperStr"]
        lResultStr = f"WebAudit :: {lDomainUpperStr}\\\\{lUserLoginStr}@{lClientIPStr} :: {inOperationCodeStr} :: {inMessageStr}"
        print(str(e)) # Has no logger - must be dead alg branch
        lResultStr = inMessageStr

def WebRequestParseBodyBytes(inRequest=None):
    """
    Извлечь данные в байт виде из тела (body) HTTP запроса.

    :return: Строка байт b'' или None (если тело запроса было пустым)
    """
    if inRequest is None: inRequest = WebRequestGet()
    lBodyBytes=None
    if inRequest.headers.get('Content-Length') is not None:
        lInputByteArrayLength = int(inRequest.headers.get('Content-Length'))
        lBodyBytes = inRequest.rfile.read(lInputByteArrayLength)
    return lBodyBytes

def WebRequestParseBodyStr(inRequest=None):
    """
    Извлечь данные в виде строки из тела (body) HTTP запроса.

    :return: Текстовая строка '' или None (если тело запроса было пустым)
    """
    return WebRequestParseBodyBytes(inRequest=inRequest).decode('utf-8')

def WebRequestParseBodyJSON(inRequest=None):
    """
    Извлечь из тела (body) запроса HTTP JSON данные и преобразовать в Dict / List структуры языка Python.

    :return: Словарь (dict), список (list) или None (если тело запроса было пустым)
    """
    return json.loads(WebRequestParseBodyStr(inRequest=inRequest))

def WebRequestParsePath(inRequest=None):
    """
    Извлечь декодированный URL путь из HTTP запроса пользователя в формате строки.   

    :return: str, пример: /pyOpenRPA/Debugging/DefHelper
    """
    return urllib.parse.unquote(inRequest.path)

def WebRequestParseFile(inRequest=None):
    """
    Извлечь файл (наименование + содержимое в виде строки байт b'') из HTTP запроса пользователя.

    :return: Кортеж формата (FileNameStr, FileBodyBytes) or (None, None), если файл не был обнаружен
    """
    lResultTurple=(None,None)
        lInputByteArray = WebRequestParseBodyBytes(inRequest=inRequest)
        #print(f"BODY:ftInputByteArrayl")
        # Extract bytes data
        lBoundaryStr = str(inRequest.headers.get('Content-Type'))
        lBoundaryStr = lBoundaryStr[lBoundaryStr.index("boundary=")+9:] # get the boundary key #print(LBoundoryStr)
        lSplit = lInputByteArray.split('\r\n\r\n')
        lDelimiterRNRNIndex = lInputByteArray.index(b'\r\n\r\n') #print(LSplit) # Get file name
        lSplit0 = lInputByteArray[:lDelimiterRNRNIndex].split(b'\r\n')[1]
        lFileNameBytes = lSplit0[lSplit0.index(b'filename="')+10:-1]
        lFileNameStr = lFileNameBytes.decode("utf-8")
        # File data bytes
        lFileDataBytes = lInputByteArray[lDelimiterRNRNIndex+4:]
        lFileDataBytes = lFileDataBytes[:lFileDataBytes.index(b"\r\n--"+lBoundaryStr.encode("utf-8"))]
        lResultTurple = (lFileNameStr, lFileDataBytes)

    return lResultTurple

def WebRequestResponseSend(inResponeStr, inRequest=None, inContentTypeStr: str = None, inHeadersDict: dict = None):
    """
    Установить ответ на HTTP запрос пользователя.

    :param inResponeStr: Тело ответа (строка)
    :param inContentTypeStr: МИМЕ тип. Пример: "html/text"
    :param inHeadersDict: Словарь (dict) ключей, которые добавить в headers HTTP ответа на запрос пользователя
    """
    inRequest.OpenRPAResponseDict["Body"] = bytes(inResponeStr, "utf8")
    if inHeadersDict is not None:
        inRequest.OpenRPAResponseDict["Headers"].update(inHeadersDict)
    if inContentTypeStr is not None:
        inRequest.OpenRPAResponseDict["Headers"]["Content-type"] = inContentTypeStr

def WebRequestGet():
    """
    Вернуть экземпляр HTTP запроса, если функция вызвана в потоке, который был порожден для отработки HTTP запроса пользователя.
    """
    lCurrentThread = threading.current_thread()
    if hasattr(lCurrentThread, "request"):
        return lCurrentThread.request

def WebUserInfoGet(inRequest=None):
    """
    Информация о пользователе, который отправил HTTP запрос.

    :return: Сведения в формате {"DomainUpperStr": "PYOPENRPA", "UserNameUpperStr": "IVAN.MASLOV"}
    """
    lDomainUpperStr = inRequest.OpenRPA["Domain"].upper()
    lUserUpperStr = inRequest.OpenRPA["User"].upper()
    return {"DomainUpperStr": lDomainUpperStr, "UserNameUpperStr": lUserUpperStr}

def WebUserIsSuperToken(inRequest = None, inGSettings = None):
    """
    Проверить, авторизован ли HTTP запрос с помощью супер токена (токен, который не истекает).

    :return: True - является супертокеном; False - не является супертокеном
    """
    lIsSuperTokenBool = False
    # Get Flag is supertoken (True|False)
    lIsSuperTokenBool = inGSettings.get("ServerDict", {}).get("AccessUsers", {}).get("AuthTokensDict", {}).get(inRequest.OpenRPA["AuthToken"], {}).get("FlagDoNotExpire", False)
    return lIsSuperTokenBool

def WebUserUACHierarchyGet(inRequest = None):
    """
    Вернуть словарь доступа UAC в отношении пользователя, который выполнил HTTP запрос inRequest

    :return: UAC словарь доступа или {}, что означает полный доступ
    """
    return inRequest.UserRoleHierarchyGet()

## GSettings defs

from . import SettingsTemplate

GSettings = SettingsTemplate.Create(inModeStr = "BASIC")
# Modules alias for pyOpenRPA.Orchestrator and pyOpenRPA.Orchestrator.__Orchestrator__
lCurrentModule = sys.modules[__name__]
if __name__ == "pyOpenRPA.Orchestrator" and "pyOpenRPA.Orchestrator.__Orchestrator__" not in sys.modules:
    sys.modules["pyOpenRPA.Orchestrator.__Orchestrator__"] = lCurrentModule
if __name__ == "pyOpenRPA.Orchestrator.__Orchestrator__" and "pyOpenRPA.Orchestrator" not in sys.modules:
    sys.modules["pyOpenRPA.Orchestrator"] = lCurrentModule

def GSettingsGet(inGSettings=None):
    """
    Вернуть глобальный словарь настроек Оркестратора. Во время выполнения программы глобальный словарь настроек существует в единственном экземпляре (синглтон)

    :param inGSettings: Дополнительный словарь настроек, который необходимо добавить в основной глобальный словарь настроек Оркестратора (синглтон)
    :return: Глобальный словарь настроек GSettings
    """
    global GSettings # identify the global variable
    # Merge dictionaries if some new dictionary has come
    if inGSettings is not None and GSettings is not inGSettings:
        GSettings = Server.__ComplexDictMerge2to1Overwrite__(in1Dict = inGSettings, in2Dict = GSettings)
    return GSettings # Return the result

def GSettingsKeyListValueSet(inValue, inKeyList=None, inGSettings = None):
    """
    Установить значение из глобального словаря настроек Оркестратора GSettings по списку ключей.

    Пример: Для того, чтобы установить значение для ключа car в словаре {"complex":{"car":"green"}, "simple":"HELLO"}, необходимо передать список ключей: ["complex", "car"]

    :param inValue: Значение для установки в глобальный словарь настроек Оркестратора GSettings
    :param inKeyList: Список ключей, по адресу которого установить значение в GSettings
    """
    if inKeyList is None: inKeyList = []
    lDict = inGSettings
    for lItem2 in inKeyList[:-1]:
        #Check if key - value exists
        if lItem2 in lDict:
            pass
        else:
            lDict[lItem2]={}
        lDict=lDict[lItem2]
    lDict[inKeyList[-1]] = inValue #Set value

def GSettingsKeyListValueGet(inKeyList=None, inGSettings = None):
    """
    Получить значение из глобального словаря настроек Оркестратора GSettings по списку ключей.

    :param inKeyList: Список ключей, по адресу которого получить значение из GSettings
    :return: Значение (тип данных определяется форматом хранения в GSettings)
    """
            pass
        else:
    return lDict.get(inKeyList[-1],None)

def GSettingsKeyListValueAppend(inValue, inKeyList=None, inGSettings = None):
    """
    Применить операцию .append к обьекту, расположенному по адресу списка ключей inKeyList в глобальном словаре настроек Оркестратора GSettings. Пример: Добавить значение в конец списка (list).

        # ПРИМЕР

        Orchestrator.GSettingsKeyListValueAppend(
            inGSettings = gSettings,
            inValue = "NewValue",
            inKeyList=["NewKeyDict","NewKeyList"]):
        # result inGSettings: {
        #    ... another keys in gSettings ...,
        #    "NewKeyDict":{
        #        "NewKeyList":[
        #            "NewValue"
        #        ]
        #    }
        #}

    :param inKeyList: Список ключей, по адресу которого выполнить добавление в конец списка (list)
    """
            pass
        else:
    lDict[inKeyList[-1]].append(inValue) #Set value

def GSettingsKeyListValueOperatorPlus(inValue, inKeyList=None, inGSettings = None):
    """
    Применить оператор сложения (+) к обьекту, расположенному по адресу списка ключей inKeyList в глобальном словаре настроек Оркестратора GSettings. Пример: соединить 2 списка (list).

        # ПРИМЕР

        Orchestrator.GSettingsKeyListValueOperatorPlus(
            inValue = [1,2,3],
        #            "NewValue",
        #            1,
        #            2,
        #            3
        #        ]
        #    }
        #}

    """
            pass
        else:
    lDict[inKeyList[-1]] += inValue #Set value

def StorageRobotExists(inRobotNameStr):
    """
    Проверить, существует ли ключ inRobotNameStr в хранилище пользовательской информации StorageDict (GSettings > StarageDict)

    :param inRobotNameStr: Наименование (ключ) робота. !ВНИМАНИЕ! Наименование чувствительно к регистру
    :return: True - ключ робота присутствует в хранилище; False - отсутствует
    """
    return inRobotNameStr in GSettingsGet()["StorageDict"]

def StorageRobotGet(inRobotNameStr):
    """
    Получить содержимое по ключу робота inRobotNameStr в хранилище пользовательской информации StorageDict (GSettings > StarageDict)

    :return: Значение или структура, которая расположена по адресу (GSettings > StarageDict > inRobotNameStr)
    """
    if inRobotNameStr not in GSettingsGet()["StorageDict"]:
        GSettingsGet()["StorageDict"][inRobotNameStr]={}
    return GSettingsGet()["StorageDict"][inRobotNameStr]

def ProcessorAliasDefCreate(inDef, inAliasStr=None, inGSettings = None):
    """
    Создать синоним (текстовый ключ) для инициации выполнения функции в том случае, если запрос на выполнения пришел из вне (передача функций невозможна). 
    
    Старая версия. Новую версию см. ActivityItemDefAliasCreate

        # USAGE

        def TestDef():
            pass
        lAliasStr = Orchestrator.ProcessorAliasDefCreate(
            inDef = TestDef,
            inAliasStr="TestDefAlias")
        # Now you can call TestDef by the alias from var lAliasStr with help of ActivityItem (key Def = lAliasStr)

    :param inDef: функция Python
    :param inAliasStr: Строковый ключ (синоним), который можно будет использовать в Активности (ActivityItem)
    :return: Строковый ключ, который был назначен. Ключ может быть изменен, если входящий текстовый ключ был уже занят.
    """
    return ActivityItemDefAliasCreate(inDef=inDef, inAliasStr=inAliasStr, inGSettings = inGSettings)

def ProcessorAliasDefUpdate(inDef, inAliasStr, inGSettings = None):
    """
    Обновить синоним (текстовый ключ) для инициации выполнения функции в том случае, если запрос на выполнения пришел из вне (передача функций невозможна). 
    
    Старая версия. Новую версию см. ActivityItemDefAliasUpdate

        # USAGE

            pass
        Orchestrator.ProcessorAliasDefUpdate(
        # Now you can call TestDef by the alias "TestDefAlias" with help of ActivityItem (key Def = "TestDefAlias")

    :return: Строковый ключ, который был назначен. Ключ будет тем же, если входящий текстовый ключ был уже занят.
    """
    return ActivityItemDefAliasUpdate(inDef=inDef, inAliasStr=inAliasStr, inGSettings = inGSettings)

# ActivityItem defs
def ActivityItemHelperDefList(inDefQueryStr=None):
    """
    Получить список синонимов (текстовых ключей), доступных для использования в Активностях (ActivityItem).

    :param inDefStr: Часть текстового ключ (начало / середина / конец)
    :return: Список доступных ключей в формате: ["ActivityItemDefAliasUpdate", "ActivityItemDefAliasCreate", etc...]
    """
    lResultList = []
    if inDefQueryStr is not None: # do search alg
        for lKeyStr in GSettingsGet()["ProcessorDict"]["AliasDefDict"]:
            if inDefQueryStr.upper() in lKeyStr.upper():
                lResultList.append(lKeyStr)
    else:
            lResultList.append(lKeyStr)
    return lResultList

def ActivityItemHelperDefAutofill(inDef):
    """
    Анализ аргументов функции по синониму (текстовому ключу).

    :param inDef: Часть текстового ключ (начало / середина / конец)
    :return: Преднастроенная структура активности (ActivityItem)         
        {
            "Def": None,
            "ArgList": [],
            "ArgDict": {},
            "ArgGSettingsStr": None,
            "ArgLoggerStr": None
        }
    """
    lResultDict = {
        "Def": None,
        "ArgList": [],
        "ArgDict": {},
        "ArgGSettingsStr": None,
        "ArgLoggerStr": None
    }
    lResultDict["Def"] = inDef
    lGS = GSettingsGet()
    if inDef in lGS["ProcessorDict"]["AliasDefDict"]:
        lDefSignature = inspect.signature(lGS["ProcessorDict"]["AliasDefDict"][inDef])
        for lItemKeyStr in lDefSignature.parameters:
            lItemValue = lDefSignature.parameters[lItemKeyStr]
            # Check if arg name contains "GSetting" or "Logger"
            if "GSETTING" in lItemKeyStr.upper():
                lResultDict["ArgGSettingsStr"] = lItemKeyStr
            elif "LOGGER" in lItemKeyStr.upper():
                lResultDict["ArgLoggerStr"] = lItemKeyStr
            else:
                if lItemValue.default is inspect._empty:
                    lResultDict["ArgDict"][lItemKeyStr] = None
                else:
                    lResultDict["ArgDict"][lItemKeyStr] = lItemValue.default

def ActivityItemCreate(inDef, inArgList=None, inArgDict=None, inArgGSettingsStr=None, inArgLoggerStr=None, inGUIDStr = None, inThreadBool = False):
    """
    Создать Активность (ActivityItem). Активность можно использовать в ProcessorActivityItemAppend или в Processor.ActivityListExecute или в функциях работы с Агентами.

        # ПРИМЕР

        # ВАРИАНТ 1
        def TestDef(inArg1Str, inGSettings, inLogger):
            pass
        lActivityItem = Orchestrator.ActivityItemCreate(
            inArgList=[],
            inArgDict={"inArg1Str": "ArgValueStr"},
            inArgGSettingsStr = "inGSettings",
            inArgLoggerStr = "inLogger")
        # lActivityItem:
        #   {
        #       "Def":TestDef,
        #       "ArgList":inArgList,
        #       "ArgDict":inArgDict,
        #       "ArgGSettings": "inArgGSettings",
        #       "ArgLogger": "inLogger"
        #   }

        # ВАРИАНТ 2
        def TestDef(inArg1Str):
            pass
        Orchestrator.ActivityItemDefAliasUpdate(
            inDef = "TestDefAlias",
            inArgGSettingsStr = None,
            inArgLoggerStr = None)
        #   {
        #       "Def":"TestDefAlias",
        #       "ArgGSettings": None,
        #       "ArgLogger": None
        #   }

    :param inDef: Функция Python или синоним (текстовый ключ)
    :param inArgList: Список (list) неименованных аргументов к функции
    :param inArgDict: Словарь (dict) именованных аргументов к функции
    :param inArgGSettingsStr: Текстовое наименование аргумента GSettings (если требуется передавать)
    :param inArgLoggerStr: Текстовое наименование аргумента logger (если требуется передавать)
    :param inGUIDStr: ГУИД идентификатор активности (ActivityItem). Если ГУИД не указан, то он будет сгенерирован автоматически
    :param inThreadBool: True - выполнить ActivityItem в новом потоке; False - выполнить последовательно в общем потоке процессорной очереди
    :return: 
        lActivityItemDict= {
            "Def":inDef, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList":inArgList, # Args list
            "ArgDict":inArgDict, # Args dictionary
            "ArgGSettings": inArgGSettingsStr, # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
            "ArgLogger": inArgLoggerStr, # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
            "GUIDStr": inGUIDStr,
            "ThreadBool": inThreadBool
        }
    """
    # Work about GUID in Activity items
    if inGUIDStr is None:
        inGUIDStr = str(uuid.uuid4())  # generate new GUID
    if inArgList is None: inArgList=[]
    if inArgDict is None: inArgDict={}
    lActivityItemDict= {
        "Def":inDef, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
        "ArgList":inArgList, # Args list
        "ArgDict":inArgDict, # Args dictionary
        "ArgGSettings": inArgGSettingsStr, # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        "ArgLogger": inArgLoggerStr, # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        "GUIDStr": inGUIDStr,
        "ThreadBool": inThreadBool
    }
    return lActivityItemDict

def ActivityItemDefAliasCreate(inDef, inAliasStr=None, inGSettings = None):
    """
    
        # ПРИМЕР

            pass
        lAliasStr = Orchestrator.ActivityItemDefAliasCreate(

    """
    #TODO Pay attention - New alias can be used too - need to create more complex algorythm to create new alias!
    if inAliasStr is None: inAliasStr = str(inDef)
    # Check if key is not exists
    if inAliasStr in inGSettings["ProcessorDict"]["AliasDefDict"]:
        inAliasStr = str(inDef)
        if lL: lL.warning(f"Orchestrator.ProcessorAliasDefCreate: Alias {inAliasStr} already exists in alias dictionary. Another alias will be generated and returned")
    inGSettings["ProcessorDict"]["AliasDefDict"][inAliasStr] = inDef
    return inAliasStr

def ActivityItemDefAliasModulesLoad():
    """
    Загрузить все функции из импортированных модулей sys.modules в ActivityItem синонимы - полезно для отладки со стороны панели управления.
    """
    lL = OrchestratorLoggerGet()
    lL.info(f"ActivityItem aliases: start to load sys.modules")
    lSysModulesSnapshot = copy.copy(sys.modules) # Actual when start from jupyter
    for lModuleItemStr in lSysModulesSnapshot:
        lModuleItem = lSysModulesSnapshot[lModuleItemStr]
        for lDefItemStr in dir(lModuleItem):
            try:
                lDefItem = getattr(lModuleItem,lDefItemStr)
                if callable(lDefItem) and not lDefItemStr.startswith("_"):
                    ActivityItemDefAliasCreate(inDef=lDefItem, inAliasStr=f"{lModuleItemStr}.{lDefItemStr}")
            except ModuleNotFoundError:
                pass
    lL.info(f"ActivityItem aliases: finish to load sys.modules")

def ActivityItemDefAliasUpdate(inDef, inAliasStr, inGSettings = None):
    """
    
        # USAGE

            pass

    """
    if callable(inDef): inGSettings["ProcessorDict"]["AliasDefDict"][inAliasStr] = inDef
    else: raise Exception(f"pyOpenRPA Exception: You can't use Orchestrator.ActivityItemDefAliasUpdate with arg 'inDef' string value. inDef is '{inDef}', inAliasStr is '{inAliasStr}'")

def ProcessorActivityItemCreate(inDef, inArgList=None, inArgDict=None, inArgGSettingsStr=None, inArgLoggerStr=None, inGUIDStr = None, inThreadBool = False):
    """

    Старая версия. Новую версию см. в ActivityItemCreate
    
        # ПРИМЕР

        # ВАРИАНТ 1
            pass
        lActivityItem = Orchestrator.ProcessorActivityItemCreate(
        #   {
        #   }

        # ВАРИАНТ 2
            pass
        #   {
        #   }

    :return: 
        }
    """
    return ActivityItemCreate(inDef=inDef, inArgList=inArgList, inArgDict=inArgDict, inArgGSettingsStr=inArgGSettingsStr, inArgLoggerStr=inArgLoggerStr,
                           inGUIDStr=inGUIDStr, inThreadBool=inThreadBool)

def ProcessorActivityItemAppend(inGSettings = None, inDef=None, inArgList=None, inArgDict=None, inArgGSettingsStr=None, inArgLoggerStr=None, inActivityItemDict=None):
    """
    Добавить активность (ActivityItem) в процессорную очередь.
    
        # USAGE

        # EXAMPLE 1
            pass
        lActivityItem = Orchestrator.ProcessorActivityItemAppend(
            inGSettings = gSettingsDict,
        # Activity have been already append in the processor queue

        # EXAMPLE 2
            pass
        Orchestrator.ProcessorActivityItemAppend(
            inActivityItemDict = lActivityItem)

    :param inActivityItemDict: Альтернативный вариант заполнения, если уже имеется Активность (ActivityItem). В таком случае не требуется заполнять аргументы inDef, inArgList, inArgDict, inArgGSettingsStr, inArgLoggerStr
    :return ГУИД активности (ActivityItem)
    """
    if inActivityItemDict is None:
        if inArgList is None: inArgList=[]
        if inArgDict is None: inArgDict={}
        if inDef is None: raise Exception(f"pyOpenRPA Exception: ProcessorActivityItemAppend need inDef arg if you dont use inActivityItemDict")
        lActivityList=[
            {
                "Def":inDef, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
                "ArgList":inArgList, # Args list
                "ArgDict":inArgDict, # Args dictionary
                "ArgGSettings": inArgGSettingsStr, # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
                "ArgLogger": inArgLoggerStr # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
            }
        ]
    else:
        lActivityList = [inActivityItemDict]
    for lItemDict in lActivityList:
        # Add GUIDStr if not exist
        if "GUIDStr" not in lItemDict:
            lGUIDStr = str(uuid.uuid4())  # generate new GUID
            lItemDict["GUIDStr"] = lGUIDStr
    # Add activity list in ProcessorDict
    inGSettings["ProcessorDict"]["ActivityList"]+=lActivityList

## Process defs
def ProcessIsStarted(inProcessNameWOExeStr): # Check if process is started
    """
    Проверить, запущен ли процесс, который в наименовании содержит inProcessNameWOExeStr.

    !ВНИМАНИЕ! ПРОВЕРЯЕТ ВСЕ ПРОЦЕССЫ ОПЕРАЦИОННОЙ СИСТЕМЫ. И ДРУГИХ ПОЛЬЗОВАТЕЛЬСКИХ СЕССИЙ, ЕСЛИ ОНИ ЗАПУЩЕНЫ НА ЭТОЙ МАШИНЕ. 
 
        # ПРИМЕР

        lProcessIsStartedBool = Orchestrator.ProcessIsStarted(inProcessNameWOExeStr = "notepad")
        # lProcessIsStartedBool is True - notepad.exe is running on the Orchestrator machine

    :param inProcessNameWOExeStr: Наименование процесса без расширения .exe (WO = WithOut = Без) Пример: Для проверки процесса блокнота нужно указывать "notepad", не "notepad.exe"
    :return: True - Процесс запущен на ОС ; False - Процесс не запущен на ОС, где работает Оркестратор
    """
    #Iterate over the all the running process
        try:
            # Check if process name contains the given name string.
            if inProcessNameWOExeStr.lower() in proc.name().lower():
                return True
            pass
    return False

def ProcessStart(inPathStr, inArgList, inStopProcessNameWOExeStr=None):
    """
    Запуск процесса на сессии Оркестратора, если на ОС не запущен процесс inStopProcessNameWOExeStr.

    !ВНИМАНИЕ! ПРИ ПРОВЕРКЕ РАНЕЕ ЗАПУЩЕННЫХ ПРОЦЕССОВ ПРОВЕРЯЕТ ВСЕ ПРОЦЕССЫ ОПЕРАЦИОННОЙ СИСТЕМЫ. И ДРУГИХ ПОЛЬЗОВАТЕЛЬСКИХ СЕССИЙ, ЕСЛИ ОНИ ЗАПУЩЕНЫ НА ЭТОЙ МАШИНЕ. 

        # ПРИМЕР

        Orchestrator.ProcessStart(
            inPathStr = "notepad"
            inArgList = []
            inStopProcessNameWOExeStr = "notepad")
        # notepad.exe will be started if no notepad.exe is active on the machine

    :param inPathStr: Путь к файлу запускаемого процесса
    :param inArgList: Список аргументов, передаваемых в запускаемый процесс. Пример: ["test.txt"]
    :param inStopProcessNameWOExeStr: Доп. контроль: Не запускать процесс, если обнаружен запущенный процесс под наименованием inStopProcessNameWOExeStr без расширения .exe (WO = WithOut = Без)
    """
    lStartProcessBool = True
    if inStopProcessNameWOExeStr is not None: #Check if process running
        lCheckTaskName = inStopProcessNameWOExeStr
        if len(lCheckTaskName)>4:
            if lCheckTaskName[-4:].upper() != ".EXE":
                lCheckTaskName = lCheckTaskName+".exe"
        else:
            lCheckTaskName = lCheckTaskName+".exe"
        #Check if process exist
        if not ProcessIsStarted(inProcessNameWOExeStr=lCheckTaskName): lStartProcessBool=True

    if lStartProcessBool == True: # Start if flag is true
        lItemArgs=[inPathStr]
        if inArgList is None: inArgList = [] # 2021 02 22 Minor fix default value
        lItemArgs.extend(inArgList)
        subprocess.Popen(lItemArgs,shell=True)

def ProcessStop(inProcessNameWOExeStr, inCloseForceBool, inUserNameStr = "%username%"):
    """
    Остановить процесс на ОС, где работает Оркестратор, под учетной записью inUserNameStr.

        # ПРИМЕР

        Orchestrator.ProcessStop(
            inProcessNameWOExeStr = "notepad"
            inCloseForceBool = True
            inUserNameStr = "USER_99")
        # Will close process "notepad.exe" on the user session "USER_99" (!ATTENTION! if process not exists no exceptions will be raised)

    :param inProcessNameWOExeStr: Наименование процесса без расширения .exe (WO = WithOut = Без). Пример: Для проверки процесса блокнота нужно указывать "notepad", не "notepad.exe"
    :param inCloseForceBool: True - принудительно завершить процесс. False - отправить сигнал на безопасное отключение (!ВНИМАНИЕ! - ОС не позволяет отправлять сигнал на безопасное отключение на другую сесиию - только на той, где работает Оркестратор)
    :param inUserNameStr: Наименование пользователя, под именем которого искать процесс для остановки. По умолчанию "%username%" - искать процесс на текущей сессии
    """
    # Support input arg if with .exe
    lProcessNameWExeStr = inProcessNameWOExeStr
    if len(lProcessNameWExeStr) > 4:
        if lProcessNameWExeStr[-4:].upper() != ".EXE":
            lProcessNameWExeStr = lProcessNameWExeStr + ".exe"
    else:
        lProcessNameWExeStr = lProcessNameWExeStr + ".exe"
    # Flag Force
    lActivityCloseCommand = 'taskkill /im ' + lProcessNameWExeStr
    if inCloseForceBool == True:
        lActivityCloseCommand += " /F"
    # None - all users, %username% - current user, another str - another user
    if inUserNameStr is not None:
        lActivityCloseCommand += f' /fi "username eq {inUserNameStr}"'
    # Kill process
    os.system(lActivityCloseCommand)

def ProcessListGet(inProcessNameWOExeList=None):
    """
    Вернуть список процессов, запущенных на ОС, где работает Оркестратор. В списке отсортировать процессы по количеству используемой оперативной памяти. Также можно указать перечень процессов, которые интересуют - функция будет показывать активные из них.

    !ВНИМАНИЕ! ДЛЯ ПОЛУЧЕНИЯ СПИСКА ВСЕХ ПРОЦЕССОВ ОС НЕОБХОДИМО ЗАПУСКАТЬ ОРКЕСТРАТОР С ПРАВАМИ АДМИНИСТРАТОРА.

        # ПРИМЕР

        lProcessList = Orchestrator.ProcessListGet()
        # Return the list of the process on the machine.
        # !ATTENTION! RUn orchestrator as administrator to get all process list on the machine.

    :param inProcessNameWOExeList: Список процессов, среди которых искать активные. Если параметр не указывать - вернет перечень всех доступных процессов 
    :return: Сведения о запущенных процессах в следующем формате {
        "ProcessWOExeList": ["notepad","..."],
        "ProcessWOExeUpperList": ["NOTEPAD","..."],
        "ProcessDetailList": [
            {
                'pid': 412, # Идентификатор процесса
                'username': "DESKTOP\\USER",
                'name': 'notepad.exe',
                'vms': 13.77767775, # В Мб
                'NameWOExeUpperStr': 'NOTEPAD',
                'NameWOExeStr': "'notepad'"},
            {...}]

    """
    if inProcessNameWOExeList is None: inProcessNameWOExeList = []
    lMapUPPERInput = {} # Mapping for processes WO exe
    lResult = {"ProcessWOExeList":[], "ProcessWOExeUpperList":[],"ProcessDetailList":[]}
    for lItem in inProcessNameWOExeList:
        if lItem is not None:
            lProcessNameWOExeList.append(f"{lItem.upper()}.EXE")
            lMapUPPERInput[f"{lItem.upper()}.EXE"]= lItem
        try:
            pinfo['vms'] = proc.memory_info().vms / (1024 * 1024)
            pinfo['NameWOExeUpperStr'] = pinfo['name'][:-4].upper()
            if len(lProcessNameWOExeList)==0 or pinfo['name'].upper() in lProcessNameWOExeList:
                try: # 2021 02 22 Minor fix if not admin rights
                    pinfo['NameWOExeStr'] = lMapUPPERInput[pinfo['name'].upper()]
                except Exception as e:
                    pinfo['NameWOExeStr'] = pinfo['name'][:-4]
                lResult["ProcessDetailList"].append(pinfo) # Append dict to list
                lResult["ProcessWOExeList"].append(pinfo['NameWOExeStr'])
                lResult["ProcessWOExeUpperList"].append(pinfo['NameWOExeUpperStr'])
           pass

def ProcessDefIntervalCall(inDef, inIntervalSecFloat, inIntervalAsyncBool=False, inDefArgList=None, inDefArgDict=None, inDefArgGSettingsNameStr=None, inDefArgLoggerNameStr=None, inExecuteInNewThreadBool=True, inLogger=None, inGSettings = None):
    """
    Периодический вызов функции Python.

    :param inDef: Функция Python, которую потребуется периодически вызывать
    :param inIntervalSecFloat: Интервал между вызовами функции в сек.
    :param inIntervalAsyncBool: False - ожидать интервал inIntervalSecFloat только после окончания выполнения предыдущей итерации; True - Ожидать интервал сразу после запуска итерации
    :param inDefArgList: Список (list) неименованных аргументов для передачи в функцию. По умолчанию None
    :param inDefArgDict: Словарь (dict) именованных аргументов для передачи в функцию. По умолчанию None
    :param inDefArgGSettingsNameStr: Наименование аргумента глобального словаря настроек Оркестратора GSettings (опционально)
    :param inDefArgLoggerNameStr: Наименование аргумента логгера Оркестратора (опционально)
    :param inExecuteInNewThreadBool: True - периодический вызов выполнять в отдельном потоке (не останавливать выполнение текущего потока); False - Выполнение периодического вызова в текущем потоке, в котором была вызвана функция ProcessDefIntervalCall. По умолчанию: True
    :param inLogger: Логгер для фиксации сообщений выполнения функции (опционально)
    """
    #Some edits on start
    if inDefArgDict is None: inDefArgDict = {}
    if inDefArgList is None: inDefArgList = []
    # Check if inDefArgLogger is set and inLogger is exist
    if inDefArgLoggerNameStr=="": inDefArgLoggerNameStr=None
    if inDefArgGSettingsNameStr=="": inDefArgGSettingsNameStr=None
    if inDefArgLoggerNameStr is not None and not inLogger:
        raise Exception(f"!ERROR! ProcessDefIntervalCall - You need to send logger in def because your def is require logger. Raise error!")

    if not Core.IsProcessorThread(inGSettings=inGSettings):
        if inGSettings["Logger"]: inGSettings["Logger"].warning(f"__Orchestrator__.ProcessDefIntervalCall def was called not from processor queue - activity will be append in the processor queue.")
        lProcessorActivityDict = {
            "Def": ProcessDefIntervalCall, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inDef": inDef, "inIntervalSecFloat": inIntervalSecFloat,
                        "inIntervalAsyncBool":inIntervalAsyncBool, "inDefArgList": inDefArgList,
                        "inDefArgDict": inDefArgDict, "inDefArgGSettingsNameStr":inDefArgGSettingsNameStr,
                        "inDefArgLoggerNameStr": inDefArgLoggerNameStr, "inExecuteInNewThreadBool": inExecuteInNewThreadBool},  # Args dictionary
            "ArgLogger": "inLogger"  # Name of GSettings attribute: str (ArgDict) or index (for ArgList)
        }
        inGSettings["ProcessorDict"]["ActivityList"].append(lProcessorActivityDict)
    else:
        # Internal def to execute periodically
        def __Execute__(inGSettings, inDef, inIntervalSecFloat, inIntervalAsyncBool, inDefArgList, inDefArgDict, inLogger,  inDefArgGSettingsNameStr, inDefArgLoggerNameStr):
            if inLogger: inLogger.info(f"__Orchestrator__.ProcessDefIntervalCall: Interval execution has been started. Def: {str(inDef)}")
            # Prepare gSettings and logger args
            if inDefArgGSettingsNameStr is not None:
                inDefArgDict[inDefArgGSettingsNameStr] = inGSettings
            if inDefArgLoggerNameStr is not None:
                inDefArgDict[inDefArgLoggerNameStr] = inLogger
            while True:
                try:
                    # Call async if needed
                    if inIntervalAsyncBool == False:  # Case wait result then wait
                        inDef(*inDefArgList, **inDefArgDict)
                    else:  # Case dont wait result - run sleep then new iteration (use many threads)
                        lThread2 = threading.Thread(target=inDef,
                                                    args=inDefArgList,
                                                    kwargs=inDefArgDict)
                        lThread2.start()
                    if inLogger: inLogger.exception(
                        f"ProcessDefIntervalCall: Interval call has been failed. Traceback is below. Code will sleep for the next call")
                # Sleep interval
                time.sleep(inIntervalSecFloat)

        # Check to call in new thread
        if inExecuteInNewThreadBool:
            lThread = threading.Thread(target=__Execute__,
                                       kwargs={"inGSettings":inGSettings, "inDef": inDef, "inIntervalSecFloat": inIntervalSecFloat,
                                               "inIntervalAsyncBool": inIntervalAsyncBool, "inDefArgList": inDefArgList,
                                               "inDefArgDict": inDefArgDict, "inLogger": inLogger,
                                               "inDefArgGSettingsNameStr":inDefArgGSettingsNameStr , "inDefArgLoggerNameStr":inDefArgLoggerNameStr})
            lThread.start()
        else:
            __Execute__(inGSettings=inGSettings, inDef=inDef, inIntervalSecFloat=inIntervalSecFloat, inIntervalAsyncBool=inIntervalAsyncBool,
                        inDefArgList=inDefArgList, inDefArgDict=inDefArgDict, inLogger=inLogger,
                        inDefArgGSettingsNameStr=inDefArgGSettingsNameStr , inDefArgLoggerNameStr=inDefArgLoggerNameStr)

# Python def - start module function
def PythonStart(inModulePathStr, inDefNameStr, inArgList=None, inArgDict=None, inLogger = None):
    """
    Импорт модуля и выполнение функции в процессе Оркестратора.

    .. note::

        Импорт модуля inModulePathStr будет происходить каждый раз в вызовом функции PythonStart.

        # ПРИМЕР

        Orchestrator.PythonStart(
            inModulePathStr="ModuleToCall.py", # inModulePathStr: Working Directory\\ModuleToCall.py
            inDefNameStr="TestDef")
        # Import module in Orchestrator process and call def "TestDef" from module "ModuleToCall.py"

    :param inModulePathStr: Абсолютный или относительный путь (относительно рабочей директории процесса Оркестратора), по которому расположен импортируемый .py файл
    :param inDefNameStr: Наименование функции в модуле .py
    :param inArgList: Список (list) неименованных аргументов для передачи в функцию. По умолчанию None
    :param inArgDict: Словарь (dict) именованных аргументов для передачи в функцию. По умолчанию None
    """
    try:
        lModule=importlib.import_module(inModulePathStr) #Подключить модуль для вызова
        lFunction=getattr(lModule,inDefNameStr) #Найти функцию
        return lFunction(*inArgList,**inArgDict)
        if inLogger: inLogger.exception("Loop activity error: module/function not founded")

# Scheduler
def SchedulerActivityTimeAddWeekly(inTimeHHMMStr="23:55:", inWeekdayList=None, inActivityList=None, inGSettings = None):
    """
    Добавить активность по расписанию. Допускается указание времени и дней недели, в которые производить запуск.

        # ПРИМЕР

        # ВАРИАНТ 1
            pass
        Orchestrator.SchedulerActivityTimeAddWeekly(
            inTimeHHMMStr = "04:34",
            inWeekdayList=[2,3,4],
            inActivityList = [lActivityItem])
        # Activity will be executed at 04:34 Wednesday (2), thursday (3), friday (4)

    :param inTimeHHMMStr: Время запуска активности. Допускаются значения от "00:00" до "23:59". Example: "05:29"
    :param inWeekdayList: Список (list) дней недели, в которые выполнять запуск список активностей (ActivityItem list). 0 (понедельник) - 6 (воскресенье). Пример: [0,1,2,3,4]. По умолчанию устанавливается каждый день [0,1,2,3,4,5,6]
    :param inActivityList: Список активностей (ActivityItem list) на исполнение
    """
    if inWeekdayList is None: inWeekdayList=[0,1,2,3,4,5,6]
    if inActivityList is None: inActivityList=[]
    Processor.__ActivityListVerify__(inActivityList=inActivityList) # DO VERIFICATION FOR THE inActivityList
    lActivityTimeItemDict = {
        "TimeHH:MMStr": inTimeHHMMStr,  # Time [HH:MM] to trigger activity
        "WeekdayList": inWeekdayList, # List of the weekday index when activity is applicable, Default [1,2,3,4,5,6,7]
        "ActivityList": inActivityList,
        "GUID": None  #    # Will be filled in Orchestrator automatically - is needed for detect activity completion
    }
    inGSettings["SchedulerDict"]["ActivityTimeList"].append(lActivityTimeItemDict)

# RDPSession

def RDPTemplateCreate(inLoginStr, inPasswordStr, inHostStr="127.0.0.1", inPortInt = 3389, inWidthPXInt = 1680,  inHeightPXInt = 1050,
                      inUseBothMonitorBool = False, inDepthBitInt = 32, inSharedDriveList=None, inRedirectClipboardBool=True):
    """
    Создать шаблон подключения RDP (dict). Данный шаблон далее можно использовать в Orchestrator.RDPSessionConnect

        # ПРИМЕР

        lRDPItemDict = Orchestrator.RDPTemplateCreate(
            inLoginStr = "USER_99",
            inPasswordStr = "USER_PASS_HERE",
            inHostStr="127.0.0.1",
            inPortInt = 3389,
            inWidthPXInt = 1680,
            inHeightPXInt = 1050,
            inUseBothMonitorBool = False,
            inDepthBitInt = 32,
            inSharedDriveList=None,
            inRedirectClipboardBool=False)

    :param inLoginStr: Логин учетной записи, на которую будет происходить вход по RDP
    :param inPasswordStr: Пароль учетной записи, на которую будет происходить вход по RDP. !ВНИМАНИЕ! Пароль нигде не сохраняется - конфиденциальность обеспечена
    :param inHostStr: Имя хоста, к которому планируется подключение по RDP. Пример "77.77.22.22"
    :param inPortInt: RDP порт, по которому будет происходить подключение. По умолчанию 3389 (стандартный порт RDP)
    :param inWidthPXInt: Разрешение ширины RDP графической сессии в пикселях. По умолчанию 1680
    :param inHeightPXInt: Разрешение высоты RDP графической сессии в пикселях. По умолчанию 1050
    :param inUseBothMonitorBool: True - Использовать все подключенные мониторы на RDP сессии; False - Использовать только один монитор на подключенной RDP сессии
    :param inDepthBitInt: Глубина цвета на удаленной RDP графической сессии. Допустимые варианты: 32 || 24 || 16 || 15. По умолчанию 32
    :param inSharedDriveList: Перечень общих дисков, доступ к которым предоставить на сторону удаленной RDP сессии. Пример: ["c", "d"]
    :param inRedirectClipboardBool: True - Синхронизировать буфер обмена между сессией Оркестратора и удаленной RDP сессией; False - Не синхронизировать буфер обмена. По умолчанию True (в целях обратной совместимости). !ВНИМАНИЕ! Для учетных записей роботов мы рекомендуем не синхронизировать буфер обмена, так как это может привести к ошибкам роботов, которые работают с клавиатурой и буфером обмена
    :return:
        {
            "Host": inHostStr,  # Адрес хоста, пример "77.77.22.22"
            "Port": str(inPortInt),  # RDP порт, пример "3389"
            "Login": inLoginStr,  # Логин УЗ, пример "test"
            "Password": inPasswordStr,  # Пароль УЗ, пример "test"
            "Screen": {
                "Width": inWidthPXInt,  # Разрешение ширины RDP графической сессии в пикселях. По умолчанию 1680
                "Height": inHeightPXInt,  Разрешение высоты RDP графической сессии в пикселях. По умолчанию 1050
                "FlagUseAllMonitors": inUseBothMonitorBool,  
                "DepthBit": str(inDepthBitInt) 
            },
            "SharedDriveList": inSharedDriveList,  
            "RedirectClipboardBool": True, 
            ###### PROGRAM VARIABLE ############
            "SessionHex": "77777sdfsdf77777dsfdfsf77777777",  
            "SessionIsWindowExistBool": False,
            "SessionIsWindowResponsibleBool": False,
            "SessionIsIgnoredBool": False
        }

    """
    if inSharedDriveList is None: inSharedDriveList = ["c"]
    if inPortInt is None: inPortInt = 3389
    if inRedirectClipboardBool is None: inRedirectClipboardBool = True
    lRDPTemplateDict= {  # Init the configuration item
        "Host": inHostStr,  # Host address, example "77.77.22.22"
        "Port": str(inPortInt),  # RDP Port, example "3389"
        "Login": inLoginStr,  # Login, example "test"
        "Password": inPasswordStr,  # Password, example "test"
        "Screen": {
            "Width": inWidthPXInt,  # Width of the remote desktop in pixels, example 1680
            "Height": inHeightPXInt,  # Height of the remote desktop in pixels, example 1050
            # "640x480" or "1680x1050" or "FullScreen". If Resolution not exists set full screen, example
            "FlagUseAllMonitors": inUseBothMonitorBool,  # True or False, example False
            "DepthBit": str(inDepthBitInt)  # "32" or "24" or "16" or "15", example "32"
        },
        "SharedDriveList": inSharedDriveList,  # List of the Root sesion hard drives, example ["c"],
        "RedirectClipboardBool": inRedirectClipboardBool, # True - share clipboard to RDP; False - else
        ###### Will updated in program ############
        "SessionHex": "77777sdfsdf77777dsfdfsf77777777",  # Hex is created when robot runs, example ""
        "SessionIsWindowExistBool": False,
        # Flag if the RDP window is exist, old name "FlagSessionIsActive". Check every n seconds , example False
        "SessionIsWindowResponsibleBool": False,
        # Flag if RDP window is responsible (recieve commands). Check every nn seconds. If window is Responsible - window is Exist too , example False
        "SessionIsIgnoredBool": False  # Flag to ignore RDP window False - dont ignore, True - ignore, example False
    }
    return lRDPTemplateDict

# TODO Search dublicates in GSettings RDPlist !
# Return list if dublicates
def RDPSessionDublicatesResolve(inGSettings):
    pass
    #for lItemKeyStr in inGSettings["RobotRDPActive"]["RDPList"]:
    #   lItemDict = inGSettings["RobotRDPActive"]["RDPList"][lItemKeyStr]

def RDPSessionConnect(inRDPSessionKeyStr, inRDPTemplateDict=None, inHostStr=None, inPortStr=None, inLoginStr=None, inPasswordStr=None, inGSettings = None, inRedirectClipboardBool=True):
    """
    Выполнить подключение к RDP и следить за стабильностью соединения со стороны Оркестратора.
    !ВНИМАНИЕ! - Подключение будет проигнорировано, если соединение по таком RDP ключу уже было инициализировано ранее.

    2 способа использования функции:
    ВАРИАНТ 1 (ОСНОВНОЙ): inGSettings, inRDPSessionKeyStr, inRDPTemplateDict. Для получения inRDPTemplateDict см. функцию Orchestrator.RDPTemplateCreate
    ВАРИАНТ 2 (ОБРАТНАЯ СОВМЕСТИМОСТЬ ДО ВЕРСИИ 1.1.20): inGSettings, inRDPSessionKeyStr, inHostStr, inPortStr, inLoginStr, inPasswordStr

        # ПРИМЕР (ВАРИАНТ 1)

            inPasswordStr = "USER_PASS_HERE", inHostStr="127.0.0.1", inPortInt = 3389, inWidthPXInt = 1680,
            inHeightPXInt = 1050, inUseBothMonitorBool = False, inDepthBitInt = 32, inSharedDriveList=None)
        Orchestrator.RDPSessionConnect(
            inRDPSessionKeyStr = "RDPKey",
            inRDPTemplateDict = lRDPItemDict)
        # Orchestrator will create RDP session by the lRDPItemDict configuration
    
    :param inRDPSessionKeyStr: Ключ RDP сессии - необходим для дальнейшей идентификации
    :param inRDPTemplateDict: Конфигурационный словарь (dict) RDP подключения (см. функцию Orchestrator.RDPTemplateCreate)
    :param inLoginStr: Логин учетной записи, на которую будет происходить вход по RDP. Обратная совместимость до версии v 1.1.20. Мы рекомендуем использовать inRDPTemplateDict (см. функцию Orchestrator.RDPTemplateCreate)
    :param inPasswordStr: Пароль учетной записи, на которую будет происходить вход по RDP. !ВНИМАНИЕ! Пароль нигде не сохраняется - конфиденциальность обеспечена. Обратная совместимость до версии v 1.1.20. Мы рекомендуем использовать inRDPTemplateDict (см. функцию Orchestrator.RDPTemplateCreate)
    :param inHostStr: Имя хоста, к которому планируется подключение по RDP. Пример "77.77.22.22". Обратная совместимость до версии v 1.1.20. Мы рекомендуем использовать inRDPTemplateDict (см. функцию Orchestrator.RDPTemplateCreate)
    :param inPortInt: RDP порт, по которому будет происходить подключение. По умолчанию 3389 (стандартный порт RDP). Обратная совместимость до версии v 1.1.20. Мы рекомендуем использовать inRDPTemplateDict (см. функцию Orchestrator.RDPTemplateCreate)
    """
        if inGSettings["Logger"]: inGSettings["Logger"].warning(f"RDP def was called not from processor queue - activity will be append in the processor queue.")
        lResult = {
            "Def": RDPSessionConnect, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inRDPTemplateDict":inRDPTemplateDict, "inHostStr": inHostStr, "inPortStr": inPortStr,
                    "inLoginStr": inLoginStr, "inPasswordStr": inPasswordStr, "inRedirectClipboardBool": inRedirectClipboardBool},  # Args dictionary
        }
        # Var 1 - if RDPTemplateDict is input
        lRDPConfigurationItem=inRDPTemplateDict
        # Var 2 - backward compatibility
        if lRDPConfigurationItem is None:
            lRDPConfigurationItem = RDPTemplateCreate(inLoginStr=inLoginStr, inPasswordStr=inPasswordStr,
                  inHostStr=inHostStr, inPortInt = int(inPortStr), inRedirectClipboardBool=inRedirectClipboardBool)            # ATTENTION - dont connect if RDP session is exist
        # Start the connect
        if inRDPSessionKeyStr not in inGSettings["RobotRDPActive"]["RDPList"]:
            inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr] = lRDPConfigurationItem # Add item in RDPList
            Connector.Session(lRDPConfigurationItem) # Create the RDP session
            Connector.SystemRDPWarningClickOk()  # Click all warning messages
        else:
            if inGSettings["Logger"]: inGSettings["Logger"].warning(f"RDP session was not created because it is alredy exists in the RDPList. Use RDPSessionReconnect if you want to update RDP configuration.")

def RDPSessionDisconnect(inRDPSessionKeyStr, inBreakTriggerProcessWOExeList = None, inGSettings = None):
    """
    Выполнить отключение RDP сессии и прекратить мониторить его активность.

        # ПРИМЕР

        Orchestrator.RDPSessionDisconnect(
            inRDPSessionKeyStr = "RDPKey")
        # Orchestrator will disconnect RDP session and will stop to monitoring current RDP

    :param inBreakTriggerProcessWOExeList: Список процессов, наличие которых в диспетчере задач приведет к прерыванию задачи по остановке RDP соединения. Пример ["notepad"]
    """
    if inBreakTriggerProcessWOExeList is None: inBreakTriggerProcessWOExeList = []
        lResult = {
            "Def": RDPSessionDisconnect, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inBreakTriggerProcessWOExeList": inBreakTriggerProcessWOExeList },  # Args dictionary
        }
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr,{}).get("SessionHex", None)
        if lSessionHex:
            lProcessListResult = {"ProcessWOExeList":[],"ProcessDetailList":[]}
            if len(inBreakTriggerProcessWOExeList) > 0:
                lProcessListResult = ProcessListGet(inProcessNameWOExeList=inBreakTriggerProcessWOExeList)  # Run the task manager monitor
            if len(lProcessListResult["ProcessWOExeList"]) == 0: # Start disconnect if no process exist
                inGSettings["RobotRDPActive"]["RDPList"].pop(inRDPSessionKeyStr,None)
                Connector.SessionClose(inSessionHexStr=lSessionHex)
                Connector.SystemRDPWarningClickOk()  # Click all warning messages

def RDPSessionReconnect(inRDPSessionKeyStr, inRDPTemplateDict=None, inGSettings = None):
    """
    Выполнить переподключение RDP сессии и продолжить мониторить его активность.

        # ПРИМЕР

        Orchestrator.RDPSessionReconnect(
            inRDPTemplateDict = inRDPTemplateDict)
        # Orchestrator will reconnect RDP session and will continue to monitoring current RDP

    """
        lResult = {
            "Def": RDPSessionReconnect, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inRDPTemplateDict":inRDPTemplateDict },  # Args dictionary
        }
    else:
        lRDPConfigurationItem = inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr]
        RDPSessionDisconnect(inGSettings = inGSettings, inRDPSessionKeyStr=inRDPSessionKeyStr) # Disconnect the RDP 2021 02 22 minor fix by Ivan Maslov
        # Replace Configuration item if inRDPTemplateDict exists
        if inRDPTemplateDict is not None: lRDPConfigurationItem=inRDPTemplateDict
        # Add item in RDPList
        inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr] = lRDPConfigurationItem
        # Create the RDP session
        Connector.Session(lRDPConfigurationItem)

def RDPSessionMonitorStop(inRDPSessionKeyStr, inGSettings = None):
    """
    Прекратить мониторить активность RDP соединения со стороны Оркестратора. Данная функция не уничтожает активное RDP соединение.
    
        # ПРИМЕР

        Orchestrator.RDPSessionMonitorStop(
        # Orchestrator will stop the RDP monitoring

    """
    lResult = True
    inGSettings["RobotRDPActive"]["RDPList"].pop(inRDPSessionKeyStr,None) # Remove item from RDPList

def RDPSessionLogoff(inRDPSessionKeyStr, inBreakTriggerProcessWOExeList = None, inGSettings = None):
    """
    Выполнить отключение (logoff) на RDP сессии и прекратить мониторить активность со стороны Оркестратора.

        # ПРИМЕР

        Orchestrator.RDPSessionLogoff(
            inBreakTriggerProcessWOExeList = ['Notepad'])
        # Orchestrator will logoff the RDP session

    :return: True - Отключение прошло успешно; False - были зафиксированы ошибки при отключении.
    """
        lResult = {
            "Def": RDPSessionLogoff, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
        }
    else:
        lCMDStr = "shutdown -L" # CMD logoff command
        # Calculate the session Hex
            if len(lProcessListResult["ProcessWOExeList"]) == 0: # Start logoff if no process exist
                # Run CMD - dont crosscheck because CMD dont return value to the clipboard when logoff
                Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="RUN", inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
                inGSettings["RobotRDPActive"]["RDPList"].pop(inRDPSessionKeyStr,None) # Remove item from RDPList

def RDPSessionResponsibilityCheck(inRDPSessionKeyStr, inGSettings = None):
        lResult = {
            "Def": RDPSessionResponsibilityCheck, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr },  # Args dictionary
        }
    else:
        lRDPConfigurationItem = inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr] # Get the alias
        # set the fullscreen
        # ATTENTION!!! Session hex can be updated!!!
        Connector.SessionScreenFull(inSessionHex=lRDPConfigurationItem["SessionHex"], inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
        time.sleep(1)
        # Check RDP responsibility
        lDoCheckResponsibilityBool = True
        lDoCheckResponsibilityCountMax = 20
        lDoCheckResponsibilityCountCurrent = 0
        while lDoCheckResponsibilityBool:
            # Check if counter is exceed - raise exception
            if lDoCheckResponsibilityCountCurrent >= lDoCheckResponsibilityCountMax:
                pass
                #raise ConnectorExceptions.SessionWindowNotResponsibleError("Error when initialize the RDP session - RDP window is not responding!")
            # Check responding
            lDoCheckResponsibilityBool = not Connector.SystemRDPIsResponsible(inSessionHexStr = lRDPConfigurationItem["SessionHex"])
            # Wait if is not responding
            if lDoCheckResponsibilityBool:
                time.sleep(3)
            # increase the couter
            lDoCheckResponsibilityCountCurrent+=1

def RDPSessionProcessStartIfNotRunning(inRDPSessionKeyStr, inProcessNameWEXEStr, inFilePathStr, inFlagGetAbsPathBool=True, inGSettings = None):
    """
    Выполнить запуск процесса на RDP сессии через графические UI инструменты (без Агента). 

    !ВНИМАНИЕ! Данная функция может работать нестабильно из-за использования графических элементов UI при работе с RDP. Мы рекомендуем использовать конструкцию взаимодействия Оркестратора с Агентом.

        # ПРИМЕР

        Orchestrator.RDPSessionProcessStartIfNotRunning(
            inProcessNameWEXEStr = 'Notepad.exe',
            inFilePathStr = "path\\to\the\\executable\\file.exe"
            inFlagGetAbsPathBool = True)
        # Orchestrator will start the process in RDP session

    :param inProcessNameWEXEStr: Наименование процесса с расширением .exe (WEXE - WITH EXE - С EXE). Параметр позволяет не допустить повторного запуска процесса, если он уже был запущен. Example: "Notepad.exe"
    :param inFilePathStr: Путь к файлу запуска процесса на стороне RDP сессии
    :param inFlagGetAbsPathBool: True - Преобразовать относительный путь inFilePathStr в абсолютный с учетом рабочей директории Оркестратора; False - Не выполнять преобразований
    """
        lActivityItem = {
            "Def": RDPSessionProcessStartIfNotRunning, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inProcessNameWEXEStr": inProcessNameWEXEStr, "inFilePathStr": inFilePathStr, "inFlagGetAbsPathBool": inFlagGetAbsPathBool },  # Args dictionary
        }
        inGSettings["ProcessorDict"]["ActivityList"].append(lActivityItem)
    else:
        lCMDStr = CMDStr.ProcessStartIfNotRunning(inProcessNameWEXEStr, inFilePathStr, inFlagGetAbsPath= inFlagGetAbsPathBool)
        # Run CMD
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="CROSSCHECK", inLogger=inGSettings["Logger"],
                                    inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])

def RDPSessionCMDRun(inRDPSessionKeyStr, inCMDStr, inModeStr="CROSSCHECK", inGSettings = None):
    """
    Отправить CMD команду на удаленную сесиию через RDP окно (без Агента).

        # ПРИМЕР

        lResultDict = Orchestrator.RDPSessionCMDRun(
            inModeStr = 'LISTEN')
        # Orchestrator will send CMD to RDP and return the result (see return section)
    
    :param inCMDStr: Команда CMD, которую отправить на удаленную сессию
    :param inModeStr: По умолчанию "CROSSCHECK". Варианты:
        "LISTEN" - Получить результат выполнения операции. Внимание! необходим общий буфер обмена с сессией Оркестратора!
        "CROSSCHECK" - Выполнить проверку, что операция была выполнена (без получения результата отработки CMD команды). Внимание! необходим общий буфер обмена с сессией Оркестратора!
        "RUN" - Не получать результат и не выполнять проверку
    :return: Результат выполнения операции в соответсвии с параметрами инициализации функции. Выходная структура:
         {
          "OutStr": <> # Результат выполнения CMD (если inModeStr = "LISTEN")
          "IsResponsibleBool": True|False # True - RDP приняло команду; False - обратная связь не была получена (если inModeStr = "CROSSCHECK")  
        }
    """
    lResult = {
        "OutStr": None,  # Result string
        "IsResponsibleBool": False  # Flag is RDP is responsible - works only when inModeStr = CROSSCHECK
    }
            "Def": RDPSessionCMDRun, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inCMDStr": inCMDStr, "inModeStr": inModeStr },  # Args dictionary
        }
    else:
        #lResult = True
        # Run CMD
            lResult = Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=inCMDStr, inModeStr=inModeStr, inLogger=inGSettings["Logger"],

def RDPSessionProcessStop(inRDPSessionKeyStr, inProcessNameWEXEStr, inFlagForceCloseBool, inGSettings = None):
    """
    Отправка CMD команды в RDP окне на остановку процесса (без Агента).

        # ПРИМЕР

        lResultDict = Orchestrator.RDPSessionProcessStop(
            inProcessNameWEXEStr = 'notepad.exe',
            inFlagForceCloseBool = True)

    :param inProcessNameWEXEStr: Наименование процесса, который требуется выключить с расширением .exe (WEXE - WITH EXE - С EXE). Пример: "Notepad.exe"
    :param inFlagForceCloseBool: True - Принудительное отключение. False - Отправка сигнала на безопасное отключение (если процесс поддерживает)
    """
        lResult = {
            "Def": RDPSessionProcessStop, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inProcessNameWEXEStr": inProcessNameWEXEStr, "inFlagForceCloseBool": inFlagForceCloseBool },  # Args dictionary
        }
    else:
        lResult = True
        lCMDStr = f'taskkill /im "{inProcessNameWEXEStr}" /fi "username eq %USERNAME%"'
        if inFlagForceCloseBool:
            lCMDStr+= " /F"
        # Run CMD
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="CROSSCHECK", inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])

def RDPSessionFileStoredSend(inRDPSessionKeyStr, inHostFilePathStr, inRDPFilePathStr, inGSettings = None):
    """
    Отправка файла со стороны Оркестратора на сторону RDP сессии через UI инструменты RDP окна (без Агента).

    !ВНИМАНИЕ! Для работы функции требуется открыть доступ по RDP к тем дискам, с которых будет производится копирование файла.

        # ПРИМЕР

        lResultDict = Orchestrator.RDPSessionFileStoredSend(
            inHostFilePathStr = "TESTDIR\\Test.py",
            inRDPFilePathStr = "C:\\RPA\\TESTDIR\\Test.py")

    :param inHostFilePathStr: Откуда взять файл. Относительный или абсолютный путь к файлу на стороне Оркестратора. Пример: "TESTDIR\\Test.py"
    :param inRDPFilePathStr: Куда скопировать файл. !Абсолютный! путь на стороне RDP сессии. Пример: "C:\\RPA\\TESTDIR\\Test.py"
    """
        lResult = {
            "Def": RDPSessionFileStoredSend, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inHostFilePathStr": inHostFilePathStr, "inRDPFilePathStr": inRDPFilePathStr },  # Args dictionary
        }
    else:
        lCMDStr = CMDStr.FileStoredSend(inHostFilePath = inHostFilePathStr, inRDPFilePath = inRDPFilePathStr)
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr, {}).get("SessionHex", None)
        #lSessionHex = inGlobalDict["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr]["SessionHex"]
        # Run CMD
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="LISTEN", inClipboardTimeoutSec = 120, inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])

def RDPSessionFileStoredRecieve(inRDPSessionKeyStr, inRDPFilePathStr, inHostFilePathStr, inGSettings = None):
    """
    Получение файла со стороны RDP сессии на сторону Оркестратора через UI инструменты RDP окна (без Агента).

        # ПРИМЕР

        lResultDict = Orchestrator.RDPSessionFileStoredRecieve(

    :param inRDPFilePathStr: Откуда скопировать файл. !Абсолютный! путь на стороне RDP сессии. Пример: "C:\\RPA\\TESTDIR\\Test.py"
    :param inHostFilePathStr: Куда скопировать файл. Относительный или абсолютный путь к файлу на стороне Оркестратора. Пример: "TESTDIR\\Test.py"
    """
        lResult = {
            "Def": RDPSessionFileStoredRecieve, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inRDPFilePathStr": inRDPFilePathStr, "inHostFilePathStr": inHostFilePathStr },  # Args dictionary
        }
    else:
        lCMDStr = CMDStr.FileStoredRecieve(inRDPFilePath = inRDPFilePathStr, inHostFilePath = inHostFilePathStr)
        # Run CMD

# # # # # Start orchestrator

#HIDDEN Interval gSettings auto cleaner def to clear some garbage.
def GSettingsAutocleaner(inGSettings=None):
    while True:
        time.sleep(inGSettings["Autocleaner"]["IntervalSecFloat"])  # Wait for the next iteration
        lNowDatetime = datetime.datetime.now() # Get now time
        # Clean old items in Client > Session > TechnicalSessionGUIDCache
        lTechnicalSessionGUIDCacheNew = {}
        for lItemKeyStr in inGSettings["Client"]["Session"]["TechnicalSessionGUIDCache"]:
            lItemValue = inGSettings["Client"]["Session"]["TechnicalSessionGUIDCache"][lItemKeyStr]
            if (lNowDatetime - lItemValue["InitDatetime"]).total_seconds() < inGSettings["Client"]["Session"]["LifetimeSecFloat"]: # Add if lifetime is ok
                lTechnicalSessionGUIDCacheNew[lItemKeyStr]=lItemValue # Lifetime is ok - set
            else:
                if lL: lL.debug(f"Client > Session > TechnicalSessionGUIDCache > lItemKeyStr: Lifetime is expired. Remove from gSettings")  # Info
        inGSettings["Client"]["Session"]["TechnicalSessionGUIDCache"] = lTechnicalSessionGUIDCacheNew # Set updated Cache
        # Clean old items in AgentActivityReturnDict > GUIDStr > ReturnedByDatetime
        lTechnicalAgentActivityReturnDictNew = {}
        for lItemKeyStr in inGSettings["AgentActivityReturnDict"]:
            lItemValue = inGSettings["AgentActivityReturnDict"][lItemKeyStr]
            if (lNowDatetime - lItemValue["ReturnedByDatetime"]).total_seconds() < inGSettings["Autocleaner"]["AgentActivityReturnLifetimeSecFloat"]: # Add if lifetime is ok
                lTechnicalAgentActivityReturnDictNew[lItemKeyStr]=lItemValue # Lifetime is ok - set
            else:
                if lL: lL.debug(f"AgentActivityReturnDict lItemKeyStr: Lifetime is expired. Remove from gSettings")  # Info
        inGSettings["AgentActivityReturnDict"] = lTechnicalAgentActivityReturnDictNew # Set updated Cache
    # # # # # # # # # # # # # # # # # # # # # # # # # #

from .. import __version__ # Get version from the package

def Start(inDumpRestoreBool = True, inRunAsAdministratorBool = True):
    Orchestrator(inDumpRestoreBool = True, inRunAsAdministratorBool = True)

def Orchestrator(inGSettings=None, inDumpRestoreBool = True, inRunAsAdministratorBool = True):
    """
    Инициализация ядра Оркестратора (всех потоков)

    :param inDumpRestoreBool: True - Восстановить информацию о RDP сессиях и StorageDict; False - не восстанавливать
    :param inRunAsAdministratorBool: True - Проверить права администратратора и обеспечить их; False - Не обеспечивать права администратора
    """
    # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script
    if not OrchestratorIsAdmin() and inRunAsAdministratorBool==True and CrossOS.IS_WINDOWS_BOOL:
        OrchestratorRerunAsAdmin()
    else:
        # Code of your program here
        inGSettings = GSettingsGet(inGSettings=inGSettings)  # Set the global settings
        #mGlobalDict = Settings.Settings(sys.argv[1])
        global gSettingsDict
        gSettingsDict = inGSettings # Alias for old name in alg
        inGSettings["VersionStr"] = __version__
        #Logger alias
        lL = gSettingsDict["Logger"]

        if lL: lL.info("Link the gSettings in submodules")  #Logging
        Processor.gSettingsDict = gSettingsDict
        Timer.gSettingsDict = gSettingsDict
        Timer.Processor.gSettingsDict = gSettingsDict
        Server.gSettingsDict = gSettingsDict
        Server.ProcessorOld.gSettingsDict = gSettingsDict # Backward compatibility

        #Backward compatibility - restore in Orc def if old def
        if inDumpRestoreBool == True:
            OrchestratorSessionRestore(inGSettings=inGSettings)

        # Init SettingsUpdate defs from file list (after RDP restore)
        lSettingsUpdateFilePathList = gSettingsDict.get("OrchestratorStart", {}).get("DefSettingsUpdatePathList",[])
        lSubmoduleFunctionName = "SettingsUpdate"
        lSettingsPath = "\\".join(os.path.join(os.getcwd(), __file__).split("\\")[:-1])
        for lModuleFilePathItem in lSettingsUpdateFilePathList:  # Import defs with try catch
            try:  # Try to init - go next if error and log in logger
                lModuleName = lModuleFilePathItem[0:-3]
                lFileFullPath = os.path.join(lSettingsPath, lModuleFilePathItem)
                lTechSpecification = importlib.util.spec_from_file_location(lModuleName, lFileFullPath)
                lTechModuleFromSpec = importlib.util.module_from_spec(lTechSpecification)
                lTechSpecificationModuleLoader = lTechSpecification.loader.exec_module(lTechModuleFromSpec)
                if lSubmoduleFunctionName in dir(lTechModuleFromSpec):
                    # Run SettingUpdate function in submodule
                    getattr(lTechModuleFromSpec, lSubmoduleFunctionName)(gSettingsDict)
            except Exception as e:
                if lL: lL.exception(f"Error when init .py file in orchestrator '{lModuleFilePathItem}'. Exception is below:")

        # Turn on backward compatibility
        BackwardCompatibility.Update(inGSettings= gSettingsDict)

        # Append Orchestrator def to ProcessorDictAlias
        lModule = sys.modules[__name__]
        lModuleDefList = dir(lModule)
        for lItemDefNameStr in lModuleDefList:
            # Dont append alias for defs Orchestrator and ___deprecated_orchestrator_start__
            if lItemDefNameStr not in ["Orchestrator", "___deprecated_orchestrator_start__"]:
                lItemDef = getattr(lModule,lItemDefNameStr)
                if callable(lItemDef): inGSettings["ProcessorDict"]["AliasDefDict"][lItemDefNameStr]=lItemDef

        #Load all defs from sys.modules
        ActivityItemDefAliasModulesLoad()

        #Инициализация настроечных параметров
        gSettingsDict["ServerDict"]["WorkingDirectoryPathStr"] = os.getcwd() # Set working directory in g settings

        #Инициализация сервера (инициализация всех интерфейсов)
        lListenDict = gSettingsDict.get("ServerDict",{}).get("ListenDict",{})
        for lItemKeyStr in lListenDict:
            lItemDict = lListenDict[lItemKeyStr]
            lThreadServer = Server.RobotDaemonServer(lItemKeyStr, gSettingsDict)
            lThreadServer.start()
            gSettingsDict["ServerDict"]["ServerThread"] = lThreadServer
            lItemDict["ServerInstance"] = lThreadServer

        # Init the RobotScreenActive in another thread
        lRobotScreenActiveThread = threading.Thread(target= Monitor.CheckScreen)
        lRobotScreenActiveThread.daemon = True # Run the thread in daemon mode.
        lRobotScreenActiveThread.start() # Start the thread execution.
        if lL: lL.info("Robot Screen active has been started")  #Logging

        # Init the RobotRDPActive in another thread
        lRobotRDPThreadControlDict = {"ThreadExecuteBool":True} # inThreadControlDict = {"ThreadExecuteBool":True}
        lRobotRDPActiveThread = threading.Thread(target= RobotRDPActive.RobotRDPActive, kwargs={"inGSettings":gSettingsDict, "inThreadControlDict":lRobotRDPThreadControlDict})
        lRobotRDPActiveThread.daemon = True # Run the thread in daemon mode.
        lRobotRDPActiveThread.start() # Start the thread execution.
        if lL: lL.info("Robot RDP active has been started")  #Logging

        # Init autocleaner in another thread
        lAutocleanerThread = threading.Thread(target= GSettingsAutocleaner, kwargs={"inGSettings":gSettingsDict})
        lAutocleanerThread.daemon = True # Run the thread in daemon mode.
        lAutocleanerThread.start() # Start the thread execution.
        if lL: lL.info("Autocleaner thread has been started")  #Logging

        # Set flag that orchestrator has been initialized
        inGSettings["HiddenIsOrchestratorInitializedBool"] = True

        # Orchestrator start activity
        if lL: lL.info("Orchestrator start activity run")  #Logging
        for lActivityItem in gSettingsDict["OrchestratorStart"]["ActivityList"]:
            # Processor.ActivityListOrDict(lActivityItem)
            Processor.ActivityListExecute(inGSettings=gSettingsDict,inActivityList=[BackwardCompatibility.v1_2_0_ProcessorOld2NewActivityDict(lActivityItem)])
        # Processor thread
        lProcessorThread = threading.Thread(target= Processor.ProcessorRunSync, kwargs={"inGSettings":gSettingsDict, "inRobotRDPThreadControlDict":lRobotRDPThreadControlDict})
        lProcessorThread.daemon = True # Run the thread in daemon mode.
        lProcessorThread.start() # Start the thread execution.
        if lL: lL.info("Processor has been started (ProcessorDict)")  #Logging

        # Processor monitor thread
        lProcessorMonitorThread = threading.Thread(target= Processor.ProcessorMonitorRunSync, kwargs={"inGSettings":gSettingsDict})
        lProcessorMonitorThread.daemon = True # Run the thread in daemon mode.
        lProcessorMonitorThread.start() # Start the thread execution.
        if lL: lL.info("Processor monitor has been started")  #Logging

        # Scheduler loop
        lSchedulerThread = threading.Thread(target= __deprecated_orchestrator_loop__)
        lSchedulerThread.daemon = True # Run the thread in daemon mode.
        lSchedulerThread.start() # Start the thread execution.
        if lL: lL.info("Scheduler (old) loop start")  #Logging

        # Schedule (new) loop
        lScheduleThread = threading.Thread(target= __schedule_loop__)
        lScheduleThread.daemon = True # Run the thread in daemon mode.
        lScheduleThread.start() # Start the thread execution.
        if lL: lL.info("Schedule module (new) loop start")  #Logging

        # Restore state for process
        for lProcessKeyTuple in inGSettings["ManagersProcessDict"]:
            lProcess = inGSettings["ManagersProcessDict"][lProcessKeyTuple]
            lProcess.StatusCheckIntervalRestore()
            lThread = threading.Thread(target= lProcess.StatusRestore)

        # Init debug thread (run if "init_dubug" file exists)
        Debugger.LiveDebugCheckThread(inGSettings=GSettingsGet())

def __schedule_loop__():
        schedule.run_pending()
        time.sleep(3)

# Backward compatibility below to 1.2.7
def __deprecated_orchestrator_loop__():
    inGSettings = GSettingsGet()
    lDaemonLoopSeconds = gSettingsDict["SchedulerDict"]["CheckIntervalSecFloat"]
    lDaemonActivityLogDict = {}  # Словарь отработанных активностей, ключ - кортеж (<activityType>, <datetime>, <processPath || processName>, <processArgs>)
    lDaemonLastDateTime = datetime.datetime.now()
    gDaemonActivityLogDictRefreshSecInt = 10  # The second period for clear lDaemonActivityLogDict from old items
    gDaemonActivityLogDictLastTime = time.time()  # The second perioad for clean lDaemonActivityLogDict from old items
        try:
            lCurrentDateTime = datetime.datetime.now()
            # Циклический обход правил
            lFlagSearchActivityType = True
            # Periodically clear the lDaemonActivityLogDict
            if time.time() - gDaemonActivityLogDictLastTime >= gDaemonActivityLogDictRefreshSecInt:
                gDaemonActivityLogDictLastTime = time.time()  # Update the time
                for lIndex, lItem in enumerate(lDaemonActivityLogDict):
                    if lItem["ActivityEndDateTime"] and lCurrentDateTime <= lItem["ActivityEndDateTime"]:
                        pass
                        # Activity is actual - do not delete now
                    else:
                        # remove the activity - not actual
                        lDaemonActivityLogDict.pop(lIndex, None)
            lIterationLastDateTime = lDaemonLastDateTime  # Get current datetime before iterator (need for iterate all activities in loop)
            # Iterate throught the activity list
            for lIndex, lItem in enumerate(gSettingsDict["SchedulerDict"]["ActivityTimeList"]):
                try:
                    # Prepare GUID of the activity
                    lGUID = None
                    if "GUID" in lItem and lItem["GUID"]:
                        lGUID = lItem["GUID"]
                    else:
                        lGUID = str(uuid.uuid4())
                        lItem["GUID"] = lGUID

                    # Проверка дней недели, в рамках которых можно запускать активность
                    lItemWeekdayList = lItem.get("WeekdayList", [0, 1, 2, 3, 4, 5, 6])
                    if lCurrentDateTime.weekday() in lItemWeekdayList:
                        if lFlagSearchActivityType:
                            #######################################################################
                            # Branch 1 - if has TimeHH:MM
                            if "TimeHH:MMStr" in lItem:
                                # Вид активности - запуск процесса
                                # Сформировать временной штамп, относительно которого надо будет проверять время
                                # часовой пояс пока не учитываем
                                lActivityDateTime = datetime.datetime.strptime(lItem["TimeHH:MMStr"], "%H:%M")
                                lActivityDateTime = lActivityDateTime.replace(year=lCurrentDateTime.year,
                                                                              month=lCurrentDateTime.month,
                                                                              day=lCurrentDateTime.day)
                                # Убедиться в том, что время наступило
                                if (
                                        lActivityDateTime >= lDaemonLastDateTime and
                                        lCurrentDateTime >= lActivityDateTime):
                                    # Log info about activity
                                    if lL: lL.info(
                                        f"Scheduler:: Activity list is started in new thread. Parameters are not available to see.")  # Logging
                                    # Do the activity
                                    lThread = threading.Thread(target=Processor.ActivityListExecute,
                                                               kwargs={"inGSettings": inGSettings,
                                                                       "inActivityList": lItem["ActivityList"]})
                                    lThread.start()
                                    lIterationLastDateTime = datetime.datetime.now()  # Set the new datetime for the new processor activity
                    if lL: lL.exception(
                        f"Scheduler: Exception has been catched in Scheduler module when activity time item was initialising. ActivityTimeItem is {lItem}")
            lDaemonLastDateTime = lIterationLastDateTime  # Set the new datetime for the new processor activity
            # Уснуть до следующего прогона
            time.sleep(lDaemonLoopSeconds)
            if lL: lL.exception(f"Scheduler: Exception has been catched in Scheduler module. Global error")

# Backward compatibility below to 1.2.0
def __deprecated_orchestrator_start__():
    lSubmoduleFunctionName = "Settings"
    lFileFullPath = sys.argv[1]
    lModuleName = (lFileFullPath.split("\\")[-1])[0:-3]
    lTechSpecification = importlib.util.spec_from_file_location(lModuleName, lFileFullPath)
    lTechModuleFromSpec = importlib.util.module_from_spec(lTechSpecification)
    lTechSpecificationModuleLoader = lTechSpecification.loader.exec_module(lTechModuleFromSpec)
    gSettingsDict = None
    if lSubmoduleFunctionName in dir(lTechModuleFromSpec):
        # Run SettingUpdate function in submodule
        gSettingsDict = getattr(lTechModuleFromSpec, lSubmoduleFunctionName)()
    #################################################
    Orchestrator(inGSettings=gSettingsDict) # Call the orchestrator

if CrossOS.IS_WINDOWS_BOOL:
    from pywinauto import win32defines, win32structures, win32functions
    import pywinauto
    import win32api

import ctypes
import struct

import time
from .Utils import ProcessCommunicator
from . import Utils #For ProcessBitness
from pyOpenRPA.Tools import Usage
from pyOpenRPA.Tools import License
import re
import copy

############################################
# When import UIDesktop init the other bitness python
# For this type
# UIDesktop.Utils.ProcessBitness.SettingsInit(inSettingsDict)
# inSettingsDict = {
#    "Python32FullPath": None, #Set from user: "..\\Resources\\WPy32-3720\\python-3.7.2\\OpenRPARobotGUIx32.exe"
#    "Python64FullPath": None, #Set from user
#    "Python32ProcessName": "OpenRPAUIDesktopX32.exe", #Config set once
#    "Python64ProcessName": "OpenRPAUIDesktopX64.exe" #Config set once
#}

#logging.basicConfig(filename="Reports\ReportRobotGUIRun_"+datetime.datetime.now().strftime("%Y_%m_%d")+".log", level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")

#####Внимание#######
#TODO В перспективе нужно реализовать алгоритм определения разрядности не в Robot.py, а в UIDesktop.py, тк начинают появляться функции, на входе в которые еще неизвестна разрядность элемента + селектор может охватить сразу два элемента из 2-х разных разрядностей - обрабатываться это должно непосредственно при выполнении

####################################
#Info: GUI module of the Robot app (pyOpenRPA - Robot)
# GUI Module - interaction with Desktop application

#GUI Naming convention
#<InArgument>_<ActivityName>_<OutArgument - if exist>

#UIO - UI Object (class of pywinauto UI object)
#UIOSelector - List of dict (key attributes)
#PWA - PyWinAuto
#PWASpecification - List of dict (key attributes in pywinauto.find_window notation)
#UIOTree - Recursive Dict of Dict ... (UI Parent -> Child hierarchy)
#UIOInfo - Dict of UIO attributes
#UIOActivity - Activity of the UIO (UI object) from the Pywinauto module
#UIOEI - UI Object info object

#inActivitySpecificationDict:
#{
#   ModuleName: <"GUI", str>, - optional
#   ActivityName: <Function or procedure name in module, str>,
#   ArgumentList: [<Argument 1, any type>, ...] - optional,
#   ArgumentDict: {<Argument 1 name, str>:<Argument 1 value, any type>, ...} - optional
#}

#outActivityResultDict:
#{
#   ActivitySpecificationDict: {
#       ModuleName: <"GUI", str>, -optional
#       ActivityName: <Function or procedure name in module, str>,
#       ArgumentList: [<Argument 1, any type>, ...] - optional,
#       ArgumentDict: {<Argument 1 name, str>: <Argument 1 value, any type>, ...} - optional
#   },
#   ErrorFlag: <Boolean flag - Activity result has error (true) or not (false), boolean>,
#   ErrorMessage: <Error message, str> - required if ErrorFlag is true,
#   ErrorTraceback: <Error traceback log, str> - required if ErrorFlag is true,
#   Result: <Result, returned from the Activity, int, str, boolean, list, dict> - required if ErrorFlag is false
#}

#inUIOSelector:
#[
#   {
#       "index":<Позиция элемента в родительском объекте>,
#       "depth_start" - глубина, с которой начинается поиск (по умолчанию 1),
#       "depth_end" - глубина, до которой ведется поиск (по умолчанию 1),
#       "class_name" - наименование класса, который требуется искать,
#       "title" - наименование заголовка,
#       "rich_text" - наименование rich_text,
#       "backend": <"win32"||"uia", only for the 1-st list element> - if not specified, use mDefaultPywinautoBackend
#   },
#   { ... }
#
#]

#Default parameters
mDefaultPywinautoBackend="win32"

############################
#Новая версия
#Получить список элементов, который удовлетворяет условиям через расширенный движок поиска
#[
#   {
#       "depth_start" - глубина, с которой начинается поиск (по умолчанию 1)
#       "depth_end" - глубина, до которой ведется поиск (по умолчанию 1)
#       "class_name" - наименование класса, который требуется искать
#       "title" - наименование заголовка
#       "rich_text" - наименование rich_text
#   }
#]

#old:PywinautoExtElementsGet
def UIOSelector_Get_UIOList (inSpecificationList,inElement=None,inFlagRaiseException=True):
    '''L-,W+: Получить список UIO объектов по UIO селектору
    
        # UIDesktop: Взаимодействие с UI объектами приложений
        from pyOpenRPA.Robot import UIDesktop
        # 1С: UIO Селектор выбора базы
        lDemoBaseUIOSelector = [{"title":"Запуск 1С:Предприятия","class_name":"V8TopLevelFrameTaxiStarter","backend":"uia"},{"title":"DEMO", "depth_start": 5, "depth_end": 5}]		
        lDemoBaseUIOList = UIDesktop.UIOSelector_Get_UIOList(lDemoBaseUIOSelector) #Получить список UIO объектов, которые удовлетворяют требованиям UIO селектора. В нашем примере либо [], либо [UIO объект]

    :param inSpecificationList: UIO Селектор, который определяет критерии поиска UI элементов
    :type inSpecificationList: list, обязательный
    :param inElement: Родительский элемент, от которого выполнить поиск UIO объектов по заданному UIO селектору. Если аргумент не задан, платформа выполнит поиск UIO объектов среди всех доступных приложений windows, которые запущены на текущей сессии
    :type inElement: UIO объект, опциональный
    :param inFlagRaiseException: True - формировать ошибку exception, если платформа не обнаружина ни одного UIO объекта по заданному UIO селектору. False - обратный случай. По умолчанию True
    :type inFlagRaiseException: bool, опциональный
    :return: Список UIO объектов, которые удовлетворяют условиям UIO селектора
    '''
    #Создать копию входного листа, чтобы не менять массив в других верхнеуровневых функциях
    inSpecificationList=copy.deepcopy(inSpecificationList)
    lResultList=[]
    lChildrenList=[]
    #Получить родительский объект если на вход ничего не поступило
    if inElement is None:
        #сформировать спецификацию на получение элемента
        lRootElementSpecification=[inSpecificationList[0]]
        lRootElementList=PWASpecification_Get_UIO(lRootElementSpecification)
        for lRootItem in lRootElementList:
            if lRootItem is not None:
                lChildrenList.append(lRootItem.wrapper_object())
    #Елемент на вход поступил - выполнить его анализ
    else:
        #Получить список элементов
        lElementChildrenList=inElement.children()
        #Поступил index - точное добавление
        if 'index' in inSpecificationList[0]:
            if inSpecificationList[0]['index']<len(lElementChildrenList):
                #Получить дочерний элемент - точное добавление
                lChildrenList.append(lElementChildrenList[inSpecificationList[0]['index']])
            else:
                if inFlagRaiseException:
                    raise ValueError('Object has no children with index: ' + str(inSpecificationList[0]['index']))
        #Поступил ctrl_index - точное добавление
        elif 'ctrl_index' in inSpecificationList[0]:
            if inSpecificationList[0]['ctrl_index']<len(lElementChildrenList):
                #Получить дочерний элемент
                lChildrenList.append(lElementChildrenList[inSpecificationList[0]['ctrl_index']])
            else:
                    raise ValueError('Object has no children with index: ' + str(inSpecificationList[0]['ctrl_index']))
        #Если нет точного обозначения элемента
        else:
            lFlagGoCheck=True
            #Учесть поле depth_start (если указано)
            if 'depth_start' in inSpecificationList[0]:
                if inSpecificationList[0]["depth_start"]>1:
                    lFlagGoCheck=False
            #Циклический обход по детям, на предмет соответствия всем условиям
            for lChildrenItem in lElementChildrenList:
                #Обработка глубины depth (рекурсивный вызов для всех детей с занижением индекса глубины)
                #По умолчанию значение глубины 1
                if 'depth_end' in inSpecificationList[0]:
                    if inSpecificationList[0]['depth_end']>1:
                        #Подготовка новой версии спецификации
                        lChildrenItemNewSpecificationList=inSpecificationList.copy()
                        lChildrenItemNewSpecificationList[0]=lChildrenItemNewSpecificationList[0].copy()
                        lChildrenItemNewSpecificationList[0]["depth_end"]=lChildrenItemNewSpecificationList[0]["depth_end"]-1
                        if 'depth_start' in lChildrenItemNewSpecificationList[0]:
                            lChildrenItemNewSpecificationList[0]["depth_start"]=lChildrenItemNewSpecificationList[0]["depth_start"]-1
                        #Циклический вызов для всех детей со скорректированной спецификацией
                        lResultList.extend(UIOSelector_Get_UIOList(lChildrenItemNewSpecificationList,lChildrenItem,inFlagRaiseException))
                #Фильтрация
                #TODO Сделать поддержку этих атрибутов для первого уровня селектора
                if lFlagGoCheck:
                    lFlagAddChild=True
                    #Фильтрация по title
                    if 'title' in inSpecificationList[0]:
                        if lChildrenItem.element_info.name != inSpecificationList[0]["title"]:
                            lFlagAddChild=False
                    #Фильтрация по title_re (regexp)
                    if 'title_re' in inSpecificationList[0]:
                        if re.fullmatch(inSpecificationList[0]["title_re"],lChildrenItem.element_info.name) is None:
                    #Фильтрация по rich_text
                    if 'rich_text' in inSpecificationList[0]:
                        if lChildrenItem.element_info.rich_text != inSpecificationList[0]["rich_text"]:
                    #Фильтрация по rich_text_re (regexp)
                    if 'rich_text_re' in inSpecificationList[0]:
                        if re.fullmatch(inSpecificationList[0]["rich_text_re"],lChildrenItem.element_info.rich_text) is None:
                    #Фильтрация по class_name
                    if 'class_name' in inSpecificationList[0]:
                        if lChildrenItem.element_info.class_name != inSpecificationList[0]["class_name"]:
                    #Фильтрация по class_name_re (regexp)
                    if 'class_name_re' in inSpecificationList[0]:
                        if re.fullmatch(inSpecificationList[0]["class_name_re"],lChildrenItem.element_info.class_name) is None:
                    #Фильтрация по friendly_class_name
                    if 'friendly_class_name' in inSpecificationList[0]:
                        if lChildrenItem.friendly_class_name() != inSpecificationList[0]["friendly_class_name"]:
                    #Фильтрация по friendly_class_name_re (regexp)
                    if 'friendly_class_name_re' in inSpecificationList[0]:
                        if re.fullmatch(inSpecificationList[0]["friendly_class_name_re"],lChildrenItem.friendly_class_name) is None:
                    #Фильтрация по control_type
                    if 'control_type' in inSpecificationList[0]:
                        if lChildrenItem.element_info.control_type != inSpecificationList[0]["control_type"]:
                    #Фильтрация по control_type_re (regexp)
                    if 'control_type_re' in inSpecificationList[0]:
                        if re.fullmatch(inSpecificationList[0]["control_type_re"],lChildrenItem.element_info.control_type) is None:
                    #Фильтрация по is_enabled (bool)
                    if 'is_enabled' in inSpecificationList[0]:
                        if lChildrenItem.is_enabled()!=inSpecificationList[0]["is_enabled"]:
                    #Фильтрация по is_visible (bool)
                    if 'is_visible' in inSpecificationList[0]:
                        if lChildrenItem.is_visible()!=inSpecificationList[0]["is_visible"]:
                    #####
                    #Все проверки пройдены - флаг добавления
                    if lFlagAddChild:
                        lChildrenList.append(lChildrenItem)
    #Выполнить рекурсивный вызов (уменьшение количества спецификаций), если спецификация больше одного элемента
    #????????Зачем в условии ниже is not None ???????????
    if len(inSpecificationList)>1 and len(lChildrenList)>0:
        #Вызвать рекурсивно функцию получения следующего объекта, если в спецификации есть следующий объект
        for lChildElement in lChildrenList:
            lResultList.extend(UIOSelector_Get_UIOList(inSpecificationList[1:],lChildElement,inFlagRaiseException))
    else:
        lResultList.extend(lChildrenList)
    #Условие, если результирующий список пустой и установлен флаг создания ошибки (и inElement is None - не следствие рекурсивного вызова)
    if inElement is None and len(lResultList)==0 and inFlagRaiseException:
        raise pywinauto.findwindows.ElementNotFoundError("Robot can't find element by the UIOSelector")

#old:PywinautoExtElementGet
def UIOSelector_Get_UIO (inSpecificationList,inElement=None,inFlagRaiseException=True):
    '''L-,W+: Получить список UIO объект по UIO селектору. Если критериям UIO селектора удовлетворяет несколько UIO объектов - вернуть первый из списка
    
        lDemoBaseUIOList = UIDesktop.UIOSelector_Get_UIO(lDemoBaseUIOSelector) #Получить 1-й UIO объект, которые удовлетворяет требованиям UIO селектора. В нашем примере либо None, либо UIO объект

    :return: UIO объект, которые удовлетворяют условиям UIO селектора, или None
    '''
    lResult=None
    lResultList=UIOSelector_Get_UIOList(inSpecificationList,inElement,False)
    if len(lResultList)>0:
        lResult=lResultList[0]
    if lResult is None and inFlagRaiseException:
    
#old:-
def UIOSelector_Exist_Bool (inUIOSelector):
    '''L-,W+: Проверить существование хотя бы 1-го UIO объекта по заданному UIO селектору
    
    !ВНИМАНИЕ! ДАННАЯ ФУНКЦИОНАЛЬНОСТЬ В АВТОМАТИЧЕСКОМ РЕЖИМЕ ПОДДЕРЖИВАЕТ ВСЕ РАЗРЯДНОСТИ ПРИЛОЖЕНИЙ (32|64), КОТОРЫЕ ЗАПУЩЕНЫ В СЕСИИ. PYTHON x64 ИМЕЕТ ВОЗМОЖНОСТЬ ВЗЗАИМОДЕЙСТВИЯ С x32 UIO ОБЪЕКТАМИ, НО МЫ РЕКОМЕНДУЕМ ДОПОЛНИТЕЛЬНО ИСПОЛЬЗОВАТЬ ИНТЕРПРЕТАТОР PYTHON x32 (ПОДРОБНЕЕ СМ. ФУНКЦИЮ Configure())
    
        lDemoBaseUIOExistBool = UIDesktop.UIOSelector_Exist_Bool(lDemoBaseUIOSelector) # Получить булевый результат проверки существования UIO объекта

    :param inUIOSelector: UIO Селектор, который определяет критерии поиска UIO объектов
    :type inUIOSelector: list, обязательный
    :return: True - существует хотя бы 1 UIO объект. False - не существует ни одного UIO объекта по заданному UIO селектору
    '''
    lResult=False
    #Check the bitness
    lSafeOtherProcess = UIOSelector_SafeOtherGet_Process(inUIOSelector)
    if lSafeOtherProcess is None:
        #Получить родительский объект если на вход ничего не поступило
        lResultList=UIOSelector_Get_UIOList(inUIOSelector, None, False)
        if len(lResultList)>0:
            lResult=True
    else:
        # Run function from other process with help of PIPE
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_Exist_Bool",
                            "ArgumentList": [inUIOSelector],
                            "ArgumentDict": {}}
        # Отправить запрос в дочерний процесс, который отвечает за работу с Windows окнами
        ProcessCommunicator.ProcessChildSendObject(lSafeOtherProcess, lPIPEResuestDict)
        # Get answer from child process
        lPIPEResponseDict = ProcessCommunicator.ProcessChildReadWaitObject(lSafeOtherProcess)
        if lPIPEResponseDict["ErrorFlag"]:
            raise Exception(
                f"Exception was occured in child process (message): {lPIPEResponseDict['ErrorMessage']}, (traceback): {lPIPEResponseDict['ErrorTraceback']}")
        else:
            lResult = lPIPEResponseDict["Result"]

#old: -
def UIOSelectorsSecs_WaitAppear_List (inSpecificationListList,inWaitSecs=86400.0,inFlagWaitAllInMoment=False):
    '''L-,W+: Ожидать появление хотя бы 1-го / всех UIO объектов по заданным UIO селекторам
    
        lNotepadOKSelector = [{"title":"notepad"},{"title":"OK"}]
        lNotepadCancelSelector = [{"title":"notepad"},{"title":"Cancel"}]
        lDemoBaseUIOExistList = UIDesktop.UIOSelectorsSecs_WaitAppear_List([lDemoBaseUIOSelector, lNotepadOKSelector, lNotepadCancelSelector]) # Ожидать появление UIO объекта

    :param inSpecificationListList: Список UIO селекторов, которые определяют критерии поиска UIO объектов
            Пример: [
            [{"title":"notepad"},{"title":"OK"}],
            [{"title":"notepad"},{"title":"Cancel"}]
        ]
    :type inSpecificationListList: list, обязательный
    :param inWaitSecs: Количество секунд, которые отвести на ожидание UIO объектов. По умолчанию 24 часа (86400 секунд)
    :type inWaitSecs: float, необязательный
    :param inFlagWaitAllInMoment: True - Ожидать до того момента, пока не появятся все запрашиваемые UIO объекты на рабочей области
    :return: Список индексов, которые указывают на номер входящих UIO селекторов, которые были обнаружены на рабочей области. Пример: [0,2]
    '''
    lResultFlag=False
    lSecsSleep = 1 #Настроечный параметр
    lSecsDone = 0
    lResultList = None
    #Цикл проверки
    while lResultFlag == False and lSecsDone<inWaitSecs:
        lResultList=[]
        #Итерация проверки
        lIndex = 0
        for lItem in inSpecificationListList:
            lItemResultFlag=UIOSelector_Exist_Bool(lItem)
            #Если обнаружен элемент - добавить его индекс в массив
            if lItemResultFlag:
                lResultList.append(lIndex)
            #Инкремент индекса
            lIndex=lIndex + 1
        #Проверка в зависимости от флага
        if inFlagWaitAllInMoment and len(lResultList)==len(inSpecificationListList):
            #Условие выполнено
            lResultFlag=True
        elif not inFlagWaitAllInMoment and len(lResultList)>0:
        #Если флаг не изменился - увеличить время и уснуть
        if lResultFlag == False:
            lSecsDone=lSecsDone+lSecsSleep
            time.sleep(lSecsSleep)

#old: -
def UIOSelectorsSecs_WaitDisappear_List (inSpecificationListList,inWaitSecs=86400.0,inFlagWaitAllInMoment=False):
    '''L-,W+:  Ожидать исчезновение хотя бы 1-го / всех UIO объектов по заданным UIO селекторам
    
        lDemoBaseUIOExistList = UIDesktop.UIOSelectorsSecs_WaitDisappear_List([lDemoBaseUIOSelector, lNotepadOKSelector, lNotepadCancelSelector]) # Ожидать исчезновение UIO объектов

            Пример: [
        ]
    :param inWaitSecs: Количество секунд, которые отвести на ожидание исчезновения UIO объектов. По умолчанию 24 часа (86400 секунд)
    :param inFlagWaitAllInMoment: True - Ожидать до того момента, пока не исчезнут все запрашиваемые UIO объекты на рабочей области
    '''
    
        lIndex = 0
            if not lItemResultFlag:

#old: -
def UIOSelectorSecs_WaitAppear_Bool (inSpecificationList,inWaitSecs):
    '''L-,W+: Ожидать появление 1-го UIO объекта по заданному UIO селектору
    
        lDemoBaseUIOExistBool = UIDesktop.UIOSelectorSecs_WaitAppear_Bool(lDemoBaseUIOSelector) # Ожидать появление UIO объекта

    :param inSpecificationList: UIO селектор, который определяет критерии поиска UIO объекта
    :param inWaitSecs: Количество секунд, которые отвести на ожидание UIO объекта. По умолчанию 24 часа (86400 секунд)
    :return: True - UIO объект был обнаружен. False - обратная ситуациая
    '''
    lWaitAppearList=UIOSelectorsSecs_WaitAppear_List([inSpecificationList],inWaitSecs)
    if len(lWaitAppearList)>0:
        lResult=True

#old name - -
def UIOSelectorSecs_WaitDisappear_Bool (inSpecificationList,inWaitSecs):
    '''L-,W+: Ожидать исчезновение 1-го UIO объекта по заданному UIO селектору
    
        lDemoBaseUIOExistBool = UIDesktop.UIOSelectorSecs_WaitDisappear_Bool(lDemoBaseUIOSelector) # Ожидать исчезновение UIO объекта

    :param inWaitSecs: Количество секунд, которые отвести на исчезновение UIO объекта. По умолчанию 24 часа (86400 секунд)
    '''
    lWaitDisappearList=UIOSelectorsSecs_WaitDisappear_List([inSpecificationList],inWaitSecs)
    if len(lWaitDisappearList)>0:

#old: -
def UIOSelector_Get_BitnessInt (inSpecificationList):
    '''L-,W+: Определить разрядность приложения по UIO селектору. Вернуть результат в формате целого числа (64 или 32)

        lDemoBaseBitInt = UIDesktop.UIOSelector_Get_BitnessInt(lDemoBaseUIOSelector) # Определить разрядность приложения, в котором обнаружен UIO объект по селектору

    :return: None - UIO объект не обнаружен; 64 (int) - разрядность приложения равна 64 битам; 32 (int) - разрядность приложения равна 32 битам
    '''
    #Получить объект Application (Для проверки разрядности)
    lRootElement=PWASpecification_Get_PWAApplication(inSpecificationList)
    if lRootElement is not None:
        if lRootElement.is64bit():
            lResult=64
        else:
            lResult=32

#old: -
def UIOSelector_Get_BitnessStr (inSpecificationList):
    """L-,W+: Определить разрядность приложения по UIO селектору. Вернуть результат в формате строки ("64" или "32")

        lDemoBaseBitStr = UIDesktop.UIOSelector_Get_BitnessStr(lDemoBaseUIOSelector) # Определить разрядность приложения, в котором обнаружен UIO объект по селектору

    :return: None - UIO объект не обнаружен; "64" (str) - разрядность приложения равна 64 битам; "32" (str) - разрядность приложения равна 32 битам
    """
            lResult="64"
        else:
            lResult="32"

#old: -
def Get_OSBitnessInt ():
    '''L-,W+: Определить разрядность робота, в котором запускается данная функция

        lRobotBitInt = UIDesktop.Get_OSBitnessInt() # Определить разрядность робота, в котором была вызвана это функция
    
    :return: 64 (int) - разрядность приложения равна 64 битам; 32 (int) - разрядность приложения равна 32 битам
    '''
    lResult=32
    if pywinauto.sysinfo.is_x64_OS():
        lResult=64
#old: -
def UIOSelector_SafeOtherGet_Process(inUIOSelector):
    """L-,W+: Получить процесс робота другой разрядности (если приложение UIO объекта выполняется в другой разрядности). Функция возвращает None, если разрядность робота совпадает с разрядностью приложения UIO объекта, либо если при инициализации робота не устанавливался интерпретатор другой разрядности.

        lOtherBitnessProcess = UIDesktop.UIOSelector_SafeOtherGet_Process(lDemoBaseUIOSelector) # Вернуть процесс робота, схожей разрядности

    :param inUIOSelector: UIO селектор, который определяет критерии поиска UIO объекта
    :return: Процесс робота схожей разрядности
    """
    #Default value
    lResult = None
    #Go check bitness if selector exists
    if inUIOSelector:
        #Get selector bitness
        lUIOSelectorAppBitness = UIOSelector_Get_BitnessStr(inUIOSelector)
        if lUIOSelectorAppBitness and Utils.ProcessBitness.mSettingsDict["BitnessProcessCurrent"] != lUIOSelectorAppBitness:
            lResult = Utils.ProcessBitness.OtherProcessGet()
#old: GetControl
def PWASpecification_Get_UIO(inControlSpecificationArray):
    """L-,W+: Получить UIO объект по PWA (pywinauto) селектору. (https://pywinauto.readthedocs.io/en/latest/code/pywinauto.findwindows.html). Мы рекомендуем использовать метод UIOSelector_UIO_Get, так как UIO селектор обладает большей функциональностью.

        lDemoBaseUIOSelector = [{"title":"Запуск 1С:Предприятия","class_name":"V8TopLevelFrameTaxiStarter","backend":"uia"}]		
        lUIOObject = UIDesktop.PWASpecification_Get_UIO(lDemoBaseUIOSelector) # Получить UIO объект по PWA селектору

    :param inControlSpecificationArray: PWA селектор, который определяет критерии поиска UIO объекта
        Допустимые ключи PWA селектора:

        - class_name содержимое атрибута class UIO объекта
        - class_name_re содержимое атрибута class UIO объекта, которое удовлетворяет установленному рег. выражению
        - process идентификатор процесса, в котором находится UIO объект
        - title содержимое атрибута title UIO объекта
        - title_re содержимое атрибута title UIO объекта, которое удовлетворяет установленному рег. выражению
        - top_level_only признак поиска только на верхнем уровне приложения. По умолчанию True
        - visible_only признак поиска только среди видимых UIO объектов. По умолчанию True
        - enabled_only признак поиска только среди разблокированных UIO объектов. По умолчанию False
        - best_match содержимое атрибута title UIO объекта максимально приближено к заданному
        - handle идентификатор handle искомого UIO объекта 
        - ctrl_index индекс UIO объекта среди всех дочерних объектов в списке родительского
        - found_index индекс UIO объекта среди всех обнаруженных
        - predicate_func пользовательская функция проверки соответсвия UIO элемента
        - active_only признак поиска только среди активных UIO объектов. По умолчанию False
        - control_id идентификатор control_id искомого UIO объекта 
        - control_type тип элемента (применимо, если backend == "uia")
        - auto_id идентификатор auto_id искомого UIO объекта (применимо, если backend == "uia")
        - framework_id идентификатор framework_id искомого UIO объекта (применимо, если backend == "uia")
        - backend вид технологии подключения к поиску UIO объекта ("uia" или "win32")
    :type inControlSpecificationArray: list, обязательный
    :return: UIO объект
    """
    #Определение backend
    lBackend=mDefaultPywinautoBackend
    if "backend" in inControlSpecificationArray[0]:
        lBackend=inControlSpecificationArray[0]["backend"]
        inControlSpecificationArray[0].pop("backend")
    #Подготовка входного массива
    inControlSpecificationOriginArray=copy.deepcopy(inControlSpecificationArray)
    inControlSpecificationArray=UIOSelector_SearchProcessNormalize_UIOSelector(inControlSpecificationArray)
    #Выполнить идентификацию объектов, если передан массив
    lTempObject=None
    if len(inControlSpecificationArray) > 0:
        #Сформировать выборку элементов, которые подходят под первый уровень спецификации
        lSpecDeepCopy = copy.deepcopy(inControlSpecificationArray)
        lSpecDeepCopy[0]["backend"]=lBackend
        lSpecificationLvL1List = pywinauto.findwindows.find_elements(**lSpecDeepCopy[0])
        for lItem in lSpecificationLvL1List:
            #Сделать независимую копию и установить информацию о process_id и handle
            lItemControlSpecificationArray=copy.deepcopy(inControlSpecificationArray)
            lItemControlSpecificationArray[0]["process_id"]=lItem.process_id
            lItemControlSpecificationArray[0]["handle"]=lItem.handle
            lItemControlSpecificationOriginArray=copy.deepcopy(inControlSpecificationOriginArray)
            lItemControlSpecificationOriginArray[0]["process_id"]=lItem.process_id
            lItemControlSpecificationOriginArray[0]["handle"]=lItem.handle
            #Выполнить подключение к объекту
            lRPAApplication = pywinauto.Application(backend=lBackend)
            #Проверка разрядности
            try:
                lRPAApplication.connect(**lItemControlSpecificationArray[0])
                UIOSelector_TryRestore_Dict(lItemControlSpecificationArray)
                try:
                    lRPAApplication.connect(**lItemControlSpecificationArray[0])
                    lRPAApplication = None
            if lRPAApplication is not None:
                #lTempObject=lRPAApplication.window(**lItemControlSpecificationArray[0])
                #Скорректировано из-за недопонимания структуры
                lTempObject=lRPAApplication
                #Нормализация массива для целей выборки объекта (удаление конфликтующих ключей)
                lItemControlSpecificationArray=UIOSelector_SearchUIONormalize_UIOSelector(lItemControlSpecificationOriginArray)
                #Циклическое прохождение к недрам объекта
                for lWindowSpecification in lItemControlSpecificationArray[0:]:
                    lTempObject=lTempObject.window(**lWindowSpecification)
                #Добавить объект в результирующий массив
                lResultList.append(lTempObject)

def PWASpecification_Get_PWAApplication(inControlSpecificationArray):
    """L-,W+: Получить значение атрибута backend по PWA (pywinauto) селектору. Мы рекомендуем использовать метод UIOSelector_UIO_Get, так как UIO селектор обладает большей функциональностью.

        lBackendStr = UIDesktop.PWASpecification_Get_PWAApplication(lDemoBaseUIOSelector) # Получить backend по PWA селектору

    :return: "win32" или "uia"
    """
    inControlSpecificationArray=copy.deepcopy(inControlSpecificationArray)
    inControlSpecificationOriginArray=inControlSpecificationArray
        #Выполнить подключение к объекту
        lRPAApplication = pywinauto.Application(backend=lBackend)
        #Проверка разрядности
        try:
            lRPAApplication.connect(**inControlSpecificationArray[0])
            UIOSelector_TryRestore_Dict(inControlSpecificationArray)
            try:
                lRPAApplication.connect(**inControlSpecificationArray[0])
                lRPAApplication = None
        if lRPAApplication is not None:
            #lTempObject=lRPAApplication.window(**inControlSpecificationArray[0])
            #Скорректировано из-за недопонимания структуры
            lTempObject=lRPAApplication
    return lTempObject
#old: AutomationSearchMouseElement
def UIOSelector_SearchChildByMouse_UIO(inElementSpecification):
    """L-,W+: Инициировать визуальный поиск UIO объекта с помощью указателя мыши. При наведении указателя мыши UIO объект выделяется зеленой рамкой. Остановить режим поиска можно с помощью зажима клавиши ctrl left на протяжении нескольких секунд. После этого в веб окне студии будет отображено дерево расположения искомого UIO объекта.

        lUIO = UIDesktop.UIOSelector_SearchChildByMouse_UIO(lDemoBaseUIOSelector) # Инициировать поиск дочернего UIO объекта, который расположен внутри lDemoBaseUIOSelector.

    :param inElementSpecification: UIO селектор, который определяет критерии поиска родительского UIO объекта, в котором будет производиться поиск дочернего UIO объекта
    :type inElementSpecification: list, обязательный
    :return: UIO объект или None (если UIO не был обнаружен)
    """
    lGUISearchElementSelected=None
    #Настройка - частота обновления подсвечивания
    lTimeSleepSeconds=0.4
    lElementFoundedList=[]
    #Ветка поиска в режиме реального времени
    #Сбросить нажатие Ctrl, если оно было
    bool(win32api.GetAsyncKeyState(16))
    bool(win32api.GetAsyncKeyState(17))
    bool(win32api.GetAsyncKeyState(18))
    #Оптимизация - получить объект для опроса единажды
    lUIORoot=UIOSelector_Get_UIO(inElementSpecification)
    lFlagLoop = True
    (lX,lY) = win32api.GetCursorPos()
    while lFlagLoop:
        #Проверить, нажата ли клавиша Ctrl (код 17)
        lFlagKeyPressedCtrl=bool(win32api.GetAsyncKeyState(17))
        lAltBool=bool(win32api.GetAsyncKeyState(18)) or bool(win32api.GetAsyncKeyState(16))
        #Подсветить объект, если мышка наведена над тем объектом, который не подсвечивался в прошлый раз
        if not lFlagKeyPressedCtrl:
            #Получить координаты мыши
            if lAltBool == False: # СВЕТИТЬ, НО НЕ ВЫБИРАТЬ
                (lX,lY) = win32api.GetCursorPos()
            lElementFounded={}
            #Создать карту пикселей и элементов
            #####Внимание! Функция UIOXY_SearchChild_ListDict не написана
            lElementFoundedList=UIOXY_SearchChild_ListDict(lUIORoot,lX,lY)
            lElementFounded=lElementFoundedList[-1]["element"]
            #Подсветить объект, если он мышь раньше стояла на другом объекте
            if lGUISearchElementSelected != lElementFounded:
                lGUISearchElementSelected = lElementFounded
            #Доработанная функция отрисовки
            if lElementFounded is not None:
                UIO_Highlight(lElementFounded)
        else:
            #Была нажата клавиша Ctrl - выйти из цикла
            lFlagLoop=False
        #Заснуть до следующего цикла
        time.sleep(lTimeSleepSeconds)
    #Вернуть результат поиска
    return lElementFoundedList

#old: - AutomationSearchMouseElementHierarchy
def UIOSelector_SearchChildByMouse_UIOTree(inUIOSelector):
    """L-,W+: Получить список уровней UIO объекта с указнием всех имеющихся атрибутов по входящему UIO селектору.

        lBackendStr = UIDesktop.UIOSelector_SearchChildByMouse_UIOTree(lDemoBaseUIOSelector) # Получить список атрибутов всех родительских элементов lDemoBaseUIOSelector.

    :param inUIOSelector: UIO селектор, который определяет UIO объект, для которого будет произведено извлечение всех атрибутов на всех уровнях.
    :return: list, список атрибутов на каждом уровне UIO объекта
    """

    lItemInfo = []
        #Запустить функцию поиска элемента по мыши
        lElementList = UIOSelector_SearchChildByMouse_UIO(inUIOSelector)
        lElement = lElementList[-1]['element']
        #Detect backend of the elements
        lFlagIsBackendWin32 = True
        #Если объект имеется (не None), то выполнить построение иерархии
        if lElement is not None:
            if lElement.backend.name == 'uia':
                lFlagIsBackendWin32 = False
            #Циклическое создание дерева
            #while lElement is not None:
            lListIterator=0
            lItemInfo2=lItemInfo
            for lListItem in lElementList:
                lElement = lListItem["element"]
                #Продолжать построение иерархии во всех случаях кроме бэк uia & parent() is None
                #if not lFlagIsBackendWin32 and lElement.parent() is None:
                #    lElement = None
                #else:
                #Получить информацию про объект
                lItemInfo2.append(UIOEI_Convert_UIOInfo(lElement.element_info))
                #Дообогатить информацией об индексе ребенка в родительском объекте
                if "index" in lListItem:
                    if lListItem["index"] is not None:
                        lItemInfo2[-1]['ctrl_index']=lListItem["index"]
                    else:
                        if "ctrl_index" in lListItem:
                            lItemInfo2[-1]['ctrl_index']=lListItem["ctrl_index"]
                else:
                    if "ctrl_index" in lListItem:
                        lItemInfo2[-1]['ctrl_index']=lListItem["ctrl_index"]
                #Оборачиваем потомка в массив, потому что у родителя по структуре интерфейса может быть больше одного наследников
                lItemInfo2[-1]['SpecificationChild']=[]
                lItemInfo2=lItemInfo2[-1]['SpecificationChild']
                #Переход на родительский объект
                #lElement = lElement.parent()
                lListIterator=lListIterator+1
            #Добавить информацию о Backend в первый объект
            lItemInfo[0]["backend"]=lElement.backend.name
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_SearchChildByMouse_UIOTree",
        else:
            lItemInfo = lPIPEResponseDict["Result"]
    #Вернуть результат
    return lItemInfo
#old name - PywinautoExtElementCtrlIndexGet
def UIO_GetCtrlIndex_Int(inElement):
    """L-,W+: Получить индекс UIO объекта inElement в списке родительского UIO объекта.

        lUIO = UIDesktop.UIOSelector_Get_UIO(lDemoBaseUIOSelector) # Получить UIO объект по UIO селектору.
        lUIOIndexInt = UIDesktop.UIO_GetCtrlIndex_Int(lUIO) # Получить индекс UIO объекта в списке у родительского UIO объекта.

    :param inElement: UIO объект, для которого требуется определить индекс в списке родительского UIO объекта.
    :type inElement: list, обязательный
    :return: int, индекс UIO объекта в списке родительского UIO объекта
    """
    #Выполнить алгоритм, если есть Element
    if inElement is not None:
        lElementParent = inElement.parent()
        if lElementParent is not None:
            lResult = 0
            lFlagFind = True
            #Получить список потомков
            lElementParentChildrenList = lElementParent.children()
            #Циклический поиск до того момента, пока не упремся в текущий элемент
            while lFlagFind:
                if lResult<len(lElementParentChildrenList):
                    #Прекратить поиск, если элемент был обнаружен
                    if inElement == lElementParentChildrenList[lResult]:
                        lFlagFind = False
                    else:
                        #Прекратить поиски, если итератор вышел за пределы списка
                        if lResult>=len(lElementParentChildrenList):
                            lResult = None
                            lFlagFind = False
                        else:
                            lResult = lResult + 1
                else:
                    lResult=-1
                    lFlagFind=False
    #Вернуть результат    

#old: - PywinautoExtElementsGetInfo
def UIOSelector_Get_UIOInfoList (inUIOSelector, inElement=None):
    """L-,W+: Техническая функция: Получить список параметров последних уровней UIO селектора по UIO объектам, которые удовлетворяют входящим inUIOSelector, поиск по которым будет производится от уровня inElement.

        lUIOInfoList = UIDesktop.UIOSelector_Get_UIOInfoList(lDemoBaseUIOSelector) # Получить словарь параметров по UIO селектору.

    :param inElement: UIO объект, от которого выполнить поиск дочерних UIO объектов по UIO селектору inUIOSelector. По умолчанию None - поиск среди всех приложений.
    :type inElement: UIO объект, необязательный
    :return: dict, пример: {"title":None,"rich_text":None,"process_id":None,"process":None,"handle":None,"class_name":None,"control_type":None,"control_id":None,"rectangle":{"left":None,"top":None,"right":None,"bottom":None}, 'runtime_id':None}
    """
        lResultList=UIOSelector_Get_UIOList(inUIOSelector, inElement)
        lIterator = 0
        for lItem in lResultList:
            lResultList[lIterator]=UIOEI_Convert_UIOInfo(lResultList[lIterator].element_info)
            lIterator = lIterator + 1
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_Get_UIOInfoList",
                            "ArgumentList": [inUIOSelector, inElement],
        else:
            lResultList = lPIPEResponseDict["Result"]

#old: - PywinautoExtTryToRestore
def UIOSelector_TryRestore_Dict(inSpecificationList):
    """L-,W+: Восстановить окно приложения на экране по UIO селектору inSpecificationList, если оно было свернуто. Функция обернута в try .. except - ошибок не возникнет.

    !ВНИМАНИЕ! ДАННАЯ ФУНКЦИОНАЛЬНОСТЬ УЖЕ ИСПОЛЬЗУЕТСЯ В РЯДЕ ДРУГИХ ФУНКЦИЙ ТАК КАК АДРЕССАЦИЯ ПО UIA FRAMEWORK НЕДОСТУПНА, ЕСЛИ ПРИЛОЖЕНИЕ СВЕРНУТО.

        UIDesktop.UIOSelector_TryRestore_Dict(lDemoBaseUIOSelector) # Попытка восстановления свернутого окна по UIO селектору.

    :param inSpecificationList: UIO селектор, который определяет UIO объект, для которого будет произведено извлечение всех атрибутов на всех уровнях.
    """
    lResult={}
    try:
        #Подготовка взодного массива
        inControlSpecificationArray=UIOSelector_SearchUIONormalize_UIOSelector(inSpecificationList)
        #Выполнить подключение к объекту. Восстановление необходимо только в бэке win32,
        #так как в uia свернутое окно не распознается
        lRPAApplication = pywinauto.Application(backend="win32")
        lRPAApplication.connect(**inSpecificationList[0])
        lRPAApplication.top_window().restore()
    except Exception:
        True==False

#old: - ElementActionGetList
def UIOSelector_Get_UIOActivityList (inUIOSelector):
    """L-,W+: Получить список доступных действий/функций по UIO селектору inUIOSelector. Описание возможных активностей см. ниже.

        lActivityList = UIDesktop.UIOSelector_Get_UIOActivityList(lDemoBaseUIOSelector) # Получить список активностей по UIO селектору.

    :param inUIOSelector: UIO селектор, который определяет UIO объект, для которого будет представлен перечень доступных активностей.
    """
        #Получить объект
        lObject=UIOSelector_Get_UIO(inUIOSelector)
        lActionList=dir(lObject)
        lResult=dir(lObject)
        #Выполнить чистку списка от неактуальных методов
        for lActionItem in lActionList:
            #Удалить те, которые начинаются на _
            if lActionItem[0]=='_':
                lResult.remove(lActionItem)
            #Удалить те, которые начинаются с символа верхнего регистра
            if lActionItem[0].isupper():
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_Get_UIOActivityList",
        else:

#old: - ElementRunAction
def UIOSelectorUIOActivity_Run_Dict(inUIOSelector, inActionName, inArgumentList=None, inkwArgumentObject=None):
    """L-,W+: Выполнить активность inActionName над UIO объектом, полученным с помощью UIO селектора inUIOSelector. Описание возможных активностей см. ниже.

        lActivityResult = UIDesktop.UIOSelectorUIOActivity_Run_Dict(lDemoBaseUIOSelector, "click") # выполнить действие над UIO объектом с помощью UIO селектора.

    :param inActionName: наименование активности, которую требуется выполнить над UIO объектом
    :type inActionName: str, обязательный
    :param inArgumentList: список передаваемых неименованных аргументов в функцию inActionName
    :type inArgumentList: list, необязательный
    :param inkwArgumentObject: словарь передаваемых именованных аргументов в функцию inActionName
    :type inkwArgumentObject: dict, необязательный
    :return: возвращает результат запускаемой функции с наименованием inActionName над UIO объектом
    """
    if inArgumentList is None: inArgumentList=[] # 2021 02 22 Minor fix by Ivan Maslov
    if inkwArgumentObject is None: inkwArgumentObject={} # 2021 02 22 Minor fix by Ivan Maslov
    #Run activity if SafeOtherProcess is None
        #Определить объект
        #Получить метод для вызова
        lFunction = getattr(lObject, inActionName)
        #Выполнить действие
        #Обернуто в безопасную обработку, тк для некоторых объектов метод не работает и может выдавать ошибку типа: NotImplementedError: This method not work properly for WinForms DataGrid, use cells()
        try:
            return lFunction(*inArgumentList,**inkwArgumentObject)
            #Если ошибка возникла на action get_properties
            if inActionName=="get_properties":
                lResult={}
                #Ручное формирование
                lResult["class_name"]=lObject.class_name()
                lResult["friendly_class_name"]=lObject.friendly_class_name()
                lResult["texts"]=lObject.texts()
                lResult["control_id"]=lObject.control_id()
                lResult["control_count"]=lObject.control_count()
                lResult["automation_id"]=lObject.automation_id()
                return lResult
            else:
                raise e
    else:
        #Run function from other process with help of PIPE
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelectorUIOActivity_Run_Dict",
         "ArgumentList": [inUIOSelector, inActionName, inArgumentList, inkwArgumentObject],
         "ArgumentDict": {}}
            raise Exception(f"Exception was occured in child process (message): {lPIPEResponseDict['ErrorMessage']}, (traceback): {lPIPEResponseDict['ErrorTraceback']}")
        else:

#old name - ElementGetInfo
def UIOSelector_Get_UIOInfo(inUIOSelector):
    """L-,W+: Получить свойства UIO объекта (element_info), по заданному UIO селектору. Ниже представлен перечень возвращаемых свойств.

    Для backend = win32:
    
    - automation_id (int)
    - class_name (str)
    - control_id (int)
    - control_type (str)
    - full_control_type (str)
    - enabled (bool)
    - handle (int)
    - name (str)
    - parent (object/UIO)
    - process_id (int)
    - rectangle (object/rect)
    - rich_text (str)
    - visible (bool)

    Для backend = uia:

    - framework_id (int)
    - runtime_id (int)

        lUIOElementInfoDict = UIDesktop.UIOSelector_Get_UIOInfo(lDemoBaseUIOSelector) #Получить свойства над UIO объектом с помощью UIO селектора.

    :return: словарь свойств element_info: Пример {"control_id": ..., "process_id": ...}
    """
        #Подготовка входного массива
        inUIOSelector=UIOSelector_SearchUIONormalize_UIOSelector(inUIOSelector)
        #Выполнить идентификацию объектов, если передан массив
        if len(inUIOSelector) > 0:
            #Получить объект
            lTempObject=UIOSelector_Get_UIO(inUIOSelector)
            #Получить инфо объект
            lTempObjectInfo = lTempObject.element_info
            #Добавить информацию об обнаруженом объекте
            lResultList.append(UIOEI_Convert_UIOInfo(lTempObjectInfo))
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_Get_UIOInfo",
        else:
#old: - GUISearchElementByRootXY
def UIOXY_SearchChild_ListDict(inRootElement,inX,inY,inHierarchyList=None):
    """L-,W+: Техническая функция: Получить иерархию вложенности UIO объекта по заданным корневому UIO объекту, координатам X и Y.

        lUIO = UIDesktop.UIOSelector_Get_UIO(lDemoBaseUIOSelector) # Получить UIO объект с помощью UIO селектора
        lUIOHierarchyList = UIDesktop.UIOXY_SearchChild_ListDict(lUIO, 100, 200) # Получить UIO объект с помощью UIO селектора родительского элемента и координат X / Y

    :param inRootElement: родительский UIO объект, полученный ранее с помощью UIO селектора.
    :type inRootElement: object UIO, обязательный
    :param inX: родительский UIO объект, полученный ранее с помощью UIO селектора.
    :type inX: int, обязательный
    :param inY: родительский UIO объект, полученный ранее с помощью UIO селектора.
    :type inY: int, обязательный
    :return: Список словарей - уровней UIO объектов
    """
    if inHierarchyList is None: inHierarchyList = []
    #Инициализация результирующего значения
    lResultElement = None
    lResultElementX1 = None
    lResultElementX2 = None
    lResultElementY1 = None
    lResultElementY2 = None
    lResultHierarchyList=[{'index':None,'element':None}]
    #Получить координаты текущего объекта
    try:
        lRootElementRectX1=inRootElement.element_info.rectangle.left
        lRootElementRectX2=inRootElement.element_info.rectangle.right
        lRootElementRectY1=inRootElement.element_info.rectangle.top
        lRootElementRectY2=inRootElement.element_info.rectangle.bottom
        #Добавить объект в результирующий, если координаты попадают в него
        if inX>=lRootElementRectX1 and inX<=lRootElementRectX2 and inY>=lRootElementRectY1 and inY<=lRootElementRectY2:
            lResultElement = inRootElement
            lResultElementX1 = lRootElementRectX1
            lResultElementX2 = lRootElementRectX2
            lResultElementY1 = lRootElementRectY1
            lResultElementY2 = lRootElementRectY2
            #Сформировать результирующий обьъект
            lParentHierarchy = inHierarchyList
            if len(lParentHierarchy)==0:
                lParentHierarchy.append({"index":None,"element":lResultElement})
            else:
                lParentHierarchy[-1]["element"] = lResultElement
            lResultHierarchyList=lParentHierarchy
            #Получить список детей и добавить в карту
            lChildIterator=0
            for lChildElement in inRootElement.children():
                #Сформировать результирующий массив
                lChildFoundedHierarchyList = lParentHierarchy.copy()
                lChildFoundedHierarchyList.append({'index': lChildIterator})
                lChildFoundedHierarchyList = UIOXY_SearchChild_ListDict(lChildElement,inX,inY, lChildFoundedHierarchyList)
                lChildFoundedElement = lChildFoundedHierarchyList[-1]["element"]
                #Установить обнаруженный элемент, если текущий результат пустой
                if lResultElement is None and lChildFoundedElement is not None:
                    lResultElement = lChildFoundedElement
                    lResultElementX1 = lResultElement.element_info.rectangle.left
                    lResultElementX2 = lResultElement.element_info.rectangle.right
                    lResultElementY1 = lResultElement.element_info.rectangle.top
                    lResultElementY2 = lResultElement.element_info.rectangle.bottom
                    lResultHierarchyList = lChildFoundedHierarchyList
                #Выполнить сверку lChildFoundedElement и lResultElement если оба имеются
                elif lResultElement is not None and lChildFoundedElement is not None: 
                    #Правила перезатирания карты, если имеется старый объект
                    #[Накладываемый объект] - НО - ElementNew
                    #[Имеющийся объект] - ИО - ElementOld
                    #3 типа вхождения объектов
                    #тип 1 - [имеющийся объект] полностью входит в [накладываемый объект] (ИО X1 Y1 >= НО X1 Y1; ИО X2 Y2 <= НО X2 Y2) - не вносить НО в bitmap в эти диапазоны
                    #тип 2 - [имеющийся объект] полностью выходит за пределы [накладываемого объекта] (ИО X1 Y1 < НО X1 Y1; ИО X2 Y2 > НО X2 Y2) - вносить НО в bitmap
                    #тип 3 - [имеющийся объект] частично входит в [накладываемый объект] (все остальные случаи)- вносить НО в bitmap
                    #Получить координаты ИО
                    lChildFoundedElementInfo = lChildFoundedElement.element_info
                    #lElementNew = inElement
                    lChildFoundedElementX1 = lChildFoundedElementInfo.rectangle.left
                    lChildFoundedElementX2 = lChildFoundedElementInfo.rectangle.right
                    lChildFoundedElementY1 = lChildFoundedElementInfo.rectangle.top
                    lChildFoundedElementY2 = lChildFoundedElementInfo.rectangle.bottom
                    #Проверка вхождения по типу 1
                    if (lResultElementX1>=lChildFoundedElementX1) and (lResultElementY1>=lChildFoundedElementY1) and (lResultElementX2<=lChildFoundedElementX2) and (lResultElementY2<=lChildFoundedElementY2):
                        False == True
                    #Проверка вхождения по типу 3
                    elif (lResultElementX1<lChildFoundedElementX1) and (lResultElementY1<lChildFoundedElementY1) and (lResultElementX2>lChildFoundedElementX2) and (lResultElementY2>lChildFoundedElementY2):
                        lResultElement = lChildFoundedElement
                        lResultElementX1 = lChildFoundedElementX1
                        lResultElementX2 = lChildFoundedElementX2
                        lResultElementY1 = lChildFoundedElementY1
                        lResultElementY2 = lChildFoundedElementY2
                        lResultHierarchyList = lChildFoundedHierarchyList
                    #Проверка вхождения по типу 2
                    else:
                lChildIterator=lChildIterator+1
        False == False
    return lResultHierarchyList

#old: - ElementGetChildElementList
def UIOSelector_GetChildList_UIOList(inUIOSelector=None, inBackend=mDefaultPywinautoBackend):
    """L-,W+: Получить список дочерних UIO объектов по входящему UIO селектору inUIOSelector.
    
        lUIOList = UIDesktop.UIOSelector_GetChildList_UIOList(lDemoBaseUIOSelector) # Получить список дочерних UIO объектов с помощью UIO селектора

    :param inUIOSelector: родительский UIO объект, полученный ранее с помощью UIO селектора.
    :param inBackend: вид backend "win32" или "uia". По умолчанию mDefaultPywinautoBackend ("win32")
    :type inBackend: str, необязательный
    :return: список дочерних UIO объектов
    """
    if inUIOSelector is None: inUIOSelector = []
    #mRobotLogger.info(f"File!!!!")
    #mRobotLogger.info(f"inSelector:{str(inUIOSelector)}, inBackend:{str(inBackend)}")
    #pdb.set_trace()
        #ctypes.windll.user32.MessageBoxW(0, str(inControlSpecificationArray), "Your title", 1)
            lTempObject = UIOSelector_Get_UIO(inUIOSelector)
            #Получить список дочерних объектов
            lTempChildList = lTempObject.children()
            lIterator=0
            #Подготовить результирующий объект
            for lChild in lTempChildList:
                lTempObjectInfo=lChild.element_info
                #Добавить информацию об обнаруженом объекте
                lObjectInfoItem=UIOEI_Convert_UIOInfo(lTempObjectInfo)
                #Итератор внутри объекта (для точной идентификации)
                lObjectInfoItem['ctrl_index']=lIterator
                lResultList.append(lObjectInfoItem)
                #Инкремент счетчика
                lIterator=lIterator+1
        else:
            lResultList=BackendStr_GetTopLevelList_UIOInfo(inBackend)
            #Установка бэк-енда на первый элемент
            for lItem in lResultList:
                lItem["backend"]=inBackend
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_GetChildList_UIOList",
                            "ArgumentList": [inUIOSelector, inBackend],
        else:

#old1: - ElementSpecificationArraySearchPrepare
#old2: - ElementSpecificationListNormalize
def UIOSelector_SearchUIONormalize_UIOSelector (inControlSpecificationArray):
    """L-,W+: Нормализовать UIO селектор для дальнейшего использования в функциях поиск UIO объекта. Если недопустимых атрибутов не присутствует, то оставить как есть.

        lDemoBaseUIOSelectorDitry = [{"title":"Запуск 1С:Предприятия","class_name":"V8TopLevelFrameTaxiStarter","backend":"uia"}]		
        lDemoBaseUIOSelectorClean = UIDesktop.UIOSelector_SearchUIONormalize_UIOSelector(lDemoBaseUIOSelectorDitry) # Очистить UIO селектор от недопустимых ключей для дальнейшего использования

    :param inControlSpecificationArray: UIO селектор, который определяет UIO объект, для которого будет представлен перечень доступных активностей.
    :return: нормализованный UIO селектор
    """
    lResult=[]
    #Циклический обход
    for lSpecificationItem in inControlSpecificationArray:
        lSpecificationItemNew=lSpecificationItem.copy()
        #Перебор всех элементов
        for lItemKey,lItemValue in lSpecificationItem.items():
            #Флаг удаления атрибута
            lFlagRemoveAttribute=False
            #############################
            #Если является вложенным словарем - удалить
            if type(lItemValue) is dict:
                lFlagRemoveAttribute=True
            #Является типом None
            if lItemValue is None:
            #Проверка допустимого ключевого слова
            if (
                lItemKey == "class_name" or
                lItemKey == "class_name_re" or
                lItemKey == "parent" or
                lItemKey == "process" or
                lItemKey == "title" or
                lItemKey == "title_re" or
                lItemKey == "top_level_only" or
                lItemKey == "visible_only" or
                lItemKey == "enabled_only" or
                lItemKey == "best_match" or
                lItemKey == "handle" or
                lItemKey == "ctrl_index" or
                lItemKey == "found_index" or
                lItemKey == "predicate_func" or
                lItemKey == "active_only" or
                lItemKey == "control_id" or
                lItemKey == "control_type" or
                lItemKey == "auto_id" or
                lItemKey == "framework_id" or
                lItemKey == "backend"):
                True == True
            else:

            #Конструкция по удалению ключа из словаря
            if lFlagRemoveAttribute:
                lSpecificationItemNew.pop(lItemKey)
        #Проверит наличие ctrl_index - если он есть, то удалить control_id и control_type из-за того, что они мешают друг другу
        if 'ctrl_index' in lSpecificationItemNew:
            if "control_id" in lSpecificationItemNew:
                lSpecificationItemNew.pop("control_id")
            if "control_type" in lSpecificationItemNew:
                lSpecificationItemNew.pop("control_type")
        #Проверить наличие handle - если он есть, то удалить process, control_id и control_type из-за того, что они мешают друг другу
        if 'handle' in lSpecificationItemNew:
            if "process" in lSpecificationItemNew:
                lSpecificationItemNew.pop("process")
        #Иначе Проверить наличие process - если он есть, то удалить тк он нужен только при подключении к процессу
        if 'process' in lSpecificationItemNew:
            lSpecificationItemNew.pop("process")
        #Добавить строку в результирующий массив
        lResult.append(lSpecificationItemNew)

#old name 1 - ElementSpecificationArraySearchPrepare
#old name 2 - ElementSpecificationListNormalize
def UIOSelector_SearchProcessNormalize_UIOSelector (inControlSpecificationArray):
    """L-,W+: Нормализовать UIO селектор для дальнейшего использования в функциях поиска процесса, в котором находится искомый UIO объект. Если недопустимых атрибутов не присутствует, то оставить как есть.

        lDemoBaseUIOSelectorClean = UIDesktop.UIOSelector_SearchProcessNormalize_UIOSelector(lDemoBaseUIOSelectorDitry) # Очистить UIO селектор от недопустимых ключей для дальнейшего использования

    """
            if (
            else:

        #Иначе Проверить наличие process - если он есть, то удалить title, control_id и control_type из-за того, что они мешают друг другу
        elif 'process' in lSpecificationItemNew:
            if "title" in lSpecificationItemNew:
                lSpecificationItemNew.pop("title")

#old: - ElementInfoExportObject
def UIOEI_Convert_UIOInfo(inElementInfo):
    """L-,W+: Техническая функция: Дообогащение словаря с параметрами UIO объекта по заданному UIO.element_info

        lUIOProcessInfoDict = UIDesktop.UIOEI_Convert_UIOInfo(lUIO.element_info)

    :param inElementInfo: экземпляр класса UIO.element_info, для которого требуется дообогатить словарь с параметрами (в дальнейшем можно использовать как элемент UIO селектора).
    :type inElementInfo: object, обязательный
    """
    #Подготовить выходную структуру данных
    lResult = {"title":None,"rich_text":None,"process_id":None,"process":None,"handle":None,"class_name":None,"control_type":None,"control_id":None,"rectangle":{"left":None,"top":None,"right":None,"bottom":None}, 'runtime_id':None}
    #Проверка name
    try:
        lResult['title']=inElementInfo.name
        True == False
    #Проверка rich_text
    try:
        lResult['rich_text']=inElementInfo.rich_text
    #Проверка process_id
    try:
        lResult['process_id']=inElementInfo.process_id
        lResult['process']=inElementInfo.process_id
    #Проверка handle
    try:
        lResult['handle']=inElementInfo.handle
    #Проверка class_name
    try:
        lResult['class_name']=inElementInfo.class_name
    #Проверка control_type
    try:
        lResult['control_type']=inElementInfo.control_type
    #Проверка control_id
    try:
        if inElementInfo.control_id!=0:
            lResult['control_id']=inElementInfo.control_id
    #Проверка rectangle left
    try:
        lResult['rectangle']['left']=inElementInfo.rectangle.left
    #Проверка rectangle right
    try:
        lResult['rectangle']['right']=inElementInfo.rectangle.right
    #Проверка rectangle top
    try:
        lResult['rectangle']['top']=inElementInfo.rectangle.top
    #Проверка rectangle bottom
    try:
        lResult['rectangle']['bottom']=inElementInfo.rectangle.bottom
    #Проверка runtime_id
    try:
        lResult['runtime_id']=inElementInfo.runtime_id

#old: - GetRootElementList
def BackendStr_GetTopLevelList_UIOInfo(inBackend=mDefaultPywinautoBackend):
    """L-,W+: Получить список UIOInfo словарей - процессы, которые запущены в рабочей сессии и готовы для взаимодействия с роботом через backend inBackend

        lAppList = UIDesktop.BackendStr_GetTopLevelList_UIOInfo() # Очистить UIO селектор от недопустимых ключей для дальнейшего использования

    :param inBackend: вид backend, который планируется использовать для взаимодействия с UIO объектами
    :type inBackend: list, обязательный
    :return: список UIOInfo словарей
    """
    #Получить список объектов
    lResultList=pywinauto.findwindows.find_elements(top_level_only=True,backend=inBackend)    
    lResultList2=[]
    for lI in lResultList:
        lTempObjectInfo=lI
        lResultList2.append(UIOEI_Convert_UIOInfo(lI))
    return lResultList2

#old: - ElementDrawOutlineNew
def UIOSelector_Highlight(inUIOSelector):
    """L-,W+: Подсветить на несколько секунд на экране зеленой рамкой UIO объект, который соответствует входящему UIO селектору inUIOSelector

        UIDesktop.UIOSelector_Highlight(lDemoBaseUIOSelector) # Подсветить UIO объект по UIO селектору

    """
        UIO_Highlight(UIOSelector_Get_UIO(inUIOSelector))
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_Highlight",
        else:
            return lPIPEResponseDict["Result"]
#old: - ElementDrawOutlineNewFocus
def UIOSelector_FocusHighlight(inUIOSelector):
    """L-,W+: Установить фокус и подсветить на несколько секунд на экране зеленой рамкой UIO объект, который соответствует входящему UIO селектору inUIOSelector

        UIDesktop.UIOSelector_FocusHighlight(lDemoBaseUIOSelector) # Установить фокус и подсветить UIO объект по UIO селектору

    """

        UIO_FocusHighlight(UIOSelector_Get_UIO(inUIOSelector))
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_FocusHighlight",
        else:

#old: - draw_outline_new
def UIO_Highlight(lWrapperObject,colour='green',thickness=2,fill=None,rect=None,inFlagSetFocus=False):
    """L-,W+: Выполнить подсветку UIO объекта на экране

        lUIO = UIDesktop.UIOSelector_Get_UIO(lDemoBaseUIOSelector) # Получить UIO объект по UIO селектору
        UIDesktop.UIO_Highlight(lUIO) # Подсветить UIO объект по UIO селектору зеленым цветом с толщиной подсветки 2 px.

    :param lWrapperObject: UIO объект, который будет подсвечен
    :type lWrapperObject: object UIO, обязательный
    :param colour: цвет подсветки UIO объекта. Варианты: 'red', 'green', 'blue'. По умолчанию 'green'
    :type colour: str, необязательный
    :param thickness: толщина подсветки UIO объекта. По умолчанию 2
    :type thickness: int, необязательный
    :param inFlagSetFocus: признак установки фокуса на UIO объект перед подсветкой. По умолчанию False
    :type inFlagSetFocus: bool, необязательный
    """
    if fill is None: fill = win32defines.BS_NULL
    if lWrapperObject is not None:
        """
        Draw an outline around the window.
        * **colour** can be either an integer or one of 'red', 'green', 'blue'
        (default 'green')
        * **thickness** thickness of rectangle (default 2)
        * **fill** how to fill in the rectangle (default BS_NULL)
        * **rect** the coordinates of the rectangle to draw (defaults to
          the rectangle of the control)
        """
        if inFlagSetFocus:
            #Установить фокус на объект, чтобы было видно выделение
            lWrapperObject.set_focus()
            time.sleep(0.5)
        # don't draw if dialog is not visible
        #if not lWrapperObject.is_visible():
        #    return
        colours = {
            "green": 0x00ff00,
            "blue": 0xff0000,
            "red": 0x0000ff,
        }
        # if it's a known colour
        if colour in colours:
            colour = colours[colour]
        if rect is None:
            rect = lWrapperObject.rectangle()
        # create the pen(outline)
        pen_handle = win32functions.CreatePen(
                win32defines.PS_SOLID, thickness, colour)
        # create the brush (inside)
        brush = win32structures.LOGBRUSH()
        brush.lbStyle = fill
        brush.lbHatch = win32defines.HS_DIAGCROSS
        brush_handle = win32functions.CreateBrushIndirect(ctypes.byref(brush))
        # get the Device Context
        dc = win32functions.CreateDC("DISPLAY", None, None, None )
        # push our objects into it
        win32functions.SelectObject(dc, brush_handle)
        win32functions.SelectObject(dc, pen_handle)
        # draw the rectangle to the DC
        win32functions.Rectangle(
            dc, rect.left, rect.top, rect.right, rect.bottom)
        # Delete the brush and pen we created
        win32functions.DeleteObject(brush_handle)
        win32functions.DeleteObject(pen_handle)
        # delete the Display context that we created
        win32functions.DeleteDC(dc)

#old: - draw_outline_new_focus
def UIO_FocusHighlight(lWrapperObject,colour='green',thickness=2,fill=None,rect=None):
    """L-,W+: Установить фокус и выполнить подсветку UIO объекта на экране

        UIDesktop.UIO_FocusHighlight(lUIO) # Установить фокус и подсветить UIO объект по UIO селектору зеленым цветом с толщиной подсветки 2 px.

    """
    UIO_Highlight(lWrapperObject,'green',2,fill,None,True)

#Определить разрядность процесса
lProcessBitnessStr = str(struct.calcsize("P") * 8)
Usage.Process(inComponentStr="Robot")
License.ConsoleVerify()

from selenium import *
from selenium import webdriver, common
from selenium.webdriver.common.by import By
import os
import sys

# XPATH CSS CHEAT CHEET: https://devhints.io/xpath
# XPATH CSS CHEAT CHEET: https://devhints.io/css

UIO_WAIT_SEC_FLOAT = 60
UIO_WAIT_INTERVAL_SEC_FLOAT = 1.0

gBrowser:webdriver.Chrome = None

def BrowserChromeStart(inDriverExePathStr:str = None, inChromeExePathStr:str = None, inExtensionPathList:list = None, inProfilePathStr:str=None) -> webdriver.Chrome:
    """L+,W+: Выполнить запуск браузера Chrome. Если вы скачали pyOpenRPA вместе с репозиторием, то будет использоваться встроенный браузер Google Chrome. Если установка pyOpenRPA производилась другим способом, то требуется указать расположение браузера Google Chrome и соответствующего WebDriver.

        # UIWeb: Взаимодействие с ui web
        from pyOpenRPA.Robot import UIWeb
        UIWeb.BrowserChromeStart()
        UIWeb.BrowserClose()
		
    :param inDriverExePathStr: Путь до компонента webdriver.exe, по умолчанию None (путь до webdriver.exe, который расположен в репозитории pyOpenRPA)
    :type inDriverExePathStr: str, опционально
    :param inChromeExePathStr:Путь до компонента chrome.exe, по умолчанию None (путь до chrome.exe, который расположен в репозитории pyOpenRPA)
    :type inChromeExePathStr: str, опционально
    :param inExtensionPathList: Список путей, по которым располагаются расширения Chrome, по умолчанию None
    :type inExtensionPathList: list, опционально
    :param inProfilePathStr: Путь, по которому выполнить сохранения профиля Chrome (история, куки и т.д.), по умолчанию None (профиль не сохраняется)
    :type inProfilePathStr: str, опционально
    :return: Объект браузера Google Chrome
    :rtype: webdriver.Chrome
    """
    global gBrowser
    inDriverExePathStr = CrossOS.PathStr(inPathStr=inDriverExePathStr)
    inChromeExePathStr = CrossOS.PathStr(inPathStr=inChromeExePathStr)
    lExtensionPathList = []
    if inExtensionPathList is not None:
        for lItemStr in inExtensionPathList:
            lExtensionPathList.append(CrossOS.PathStr(inPathStr=lItemStr))
    inExtensionPathList = lExtensionPathList
    inProfilePathStr = CrossOS.PathStr(inPathStr=inProfilePathStr)
    lResourcePathStr = os.path.abspath(os.path.join(sys.executable, "..","..", ".."))
    # Путь по умолчанию к портативному браузеру и драйверу (если скачивался репозиторий pyOpenRPA
    if inDriverExePathStr == None: 
        if CrossOS.IS_WINDOWS_BOOL: inDriverExePathStr = os.path.join(lResourcePathStr, "SeleniumWebDrivers", "Chrome", "chromedriver_win32 v84.0.4147.30", "chromedriver.exe")
        elif CrossOS.IS_LINUX_BOOL: inDriverExePathStr = os.path.join(lResourcePathStr, "SeleniumWebDrivers", "Chrome", "chromedriver_lin64 v103.0.5060.53", "chromedriver")
    if inChromeExePathStr == None: 
        if CrossOS.IS_WINDOWS_BOOL: inChromeExePathStr = os.path.join(lResourcePathStr, "WChrome64-840414730", "App", "Chrome-bin", "chrome.exe")
        elif CrossOS.IS_LINUX_BOOL: inChromeExePathStr = os.path.join(lResourcePathStr, "LChrome64-10305060114", "data", "chrome")
    if inExtensionPathList == None: inExtensionPathList = []
    # Set full path to exe of the chrome
    lWebDriverChromeOptionsInstance = webdriver.ChromeOptions()
    lWebDriverChromeOptionsInstance.binary_location = inChromeExePathStr
    #lWebDriverChromeOptionsInstance2 = webdriver.ChromeOptions()
    if inProfilePathStr is not None:
        inProfilePathStr = os.path.abspath(inProfilePathStr)
        lWebDriverChromeOptionsInstance.add_argument(f"user-data-dir={os.path.abspath(inProfilePathStr)}")
    # Add extensions
    for lExtensionItemFullPath in inExtensionPathList:
        lWebDriverChromeOptionsInstance.add_extension (os.path.abspath(lExtensionItemFullPath))
    #if inDriverExePathStr == "built-in":
    # Run with specified web driver path
    gBrowser = webdriver.Chrome(executable_path = inDriverExePathStr, options=lWebDriverChromeOptionsInstance)
    #else:
    #    lWebDriverInstance = webdriver.Chrome(options = lWebDriverChromeOptionsInstance)
    return gBrowser

def BrowserChange(inBrowser):
    """L+,W+: Выполнить смену активного браузера (при необходимости).

        lBrowser1 = UIWeb.BrowserChromeStart()
        UIWeb.BrowserChange(inBrowser=None)
        lBrowser2 = UIWeb.BrowserChromeStart()
        UIWeb.BrowserChange(inBrowser=lBrowser1)

    :param inBrowser: Объект браузера
    :type inBrowser: webdriver.Chrome
    """
    gBrowser = inBrowser

def PageOpen(inURLStr: str):
    """L+,W+: Открыть страницу inURLStr в браузере и дождаться ее загрузки.

        UIWeb.PageOpen("https://mail.ru")

    :param inURLStr: URL адрес страницы
    :type inURLStr: str
    """
    if gBrowser is not None: gBrowser.get(inURLStr)

def PageScrollTo(inVerticalPxInt=0, inHorizontalPxInt=0):
    """L+,W+: Выполнить прокрутку страницы (по вертикали или по горизонтали)

        UIWeb.PageScrollTo(inVerticalPxInt=100)

    :param inVerticalPxInt: Величина вертикальной прокрутки страницы в пикселях, по умолчанию 0
    :type inVerticalPxInt: int, опционально
    :param inHorizontalPxInt: Величина горизонтальной прокрутки страницы в пикселях, по умолчанию 0
    :type inHorizontalPxInt: int, опционально
    """
    PageJSExecute(inJSStr=f"scroll({inHorizontalPxInt},{inVerticalPxInt})")

def PageJSExecute(inJSStr, *inArgList):
    """L+,W+: Отправить на выполнение на сторону браузера код JavaScript.

    !ВНИМАНИЕ! Данная функция поддерживает передачу переменных в область кода JavaScript (*inArgList). Обратиться к переданным переменным из JavaScript можно с помощью ключевого слова: arguments[i], где i - это порядковый номер переданной переменной

        UIWeb.PageJSExecute(alert('arguments[0]);", "hello world!")

    :param inJSStr: Код JavaScript, отправляемый на сторону браузера
    :type inJSStr: str
    :param *inArgList: Перечисление аргументов, отправляемых на сторону браузера
    :type *inArgList: str
    :return: Результат отработки кода JavaScript, если он заканчивался оператором "return"
    :rtype: str | int | bool | float
    """
    # arguments[0], arguments[1] etc
    if gBrowser is not None: return gBrowser.execute_script(inJSStr, *inArgList)
    
def BrowserClose():
    """L+,W+: Закрыть браузер

    """
    if gBrowser is not None: gBrowser.quit()

def UIOSelectorList(inUIOSelectorStr, inUIO=None) -> list:
    """L+,W+: Получить список UIO объектов по UIO селектору.

        lUIOSelectorStr = "//*[@id=\"grid\"]/div[2]/div[2]/div[3]/div[1]/ul/li[5]/div/a"
        lUIOList = UIOSelectorList(inUIOSelectorStr = lUIOSelectorStr)

    :param inUIOSelectorStr: XPATH или CSS селектор UI объекта на web странице. Подсказки по CSS: https://devhints.io/css Подсказки по XPath: https://devhints.io/xpath 
    :type inUIOSelectorStr: str
    :param inUIO: Объект UIO, от которого выполнить поиск UIO объектов по селектору, по умолчанию None
    :type inUIO: WebElement, опционально
    :return: Список UIO объектов
    :rtype: list
    """
    if inUIO is None:
        global gBrowser
        if gBrowser is not None:
            if UIOSelectorDetect(inUIOSelectorStr=inUIOSelectorStr) == "CSS":
                lResultList = gBrowser.find_elements(By.CSS_SELECTOR, inUIOSelectorStr)
            else:
                lResultList = gBrowser.find_elements(By.XPATH,inUIOSelectorStr)
    else: 
        if UIOSelectorDetect(inUIOSelectorStr=inUIOSelectorStr) == "CSS":
            lResultList = inUIO.find_elements(By.CSS_SELECTOR, inUIOSelectorStr)
        else:
            lResultList = inUIO.find_elements(By.XPATH,inUIOSelectorStr)

def UIOSelectorFirst(inUIOSelectorStr, inUIO=None) -> list:
    """L+,W+: Получить UIO объект по UIO селектору.

        lUIO = UIOSelectorFirst(inUIOSelectorStr = lUIOSelectorStr)

    :return: Первый подходящий UIO объект
    :rtype: UIO объект
    """
    lUIOList = UIOSelectorList(inUIOSelectorStr=inUIOSelectorStr, inUIO=inUIO)
    if len(lUIOList) > 0: lResult = lUIOList[0]

def UIOTextGet(inUIO) -> str:
    """L+,W+: Получить текст UI элемента.

        lUIO = UIOSelectorList(inUIOSelectorStr = lUIOSelectorStr)[0]
        lTextStr = UIWeb.UIOTextGet(inUIO=lUIO)

    :param inUIO: UIO элемент. Получить его можно с помощью функций UIOSelectorList или UIOSelectorFirst
    :type inUIO: WebElement
    :return: Текст UI элемента
    :rtype: str
    """
    return inUIO.text

def UIOAttributeGet(inUIO, inAttributeStr) -> str:
    """L+,W+: Получить обычный (нестилевой) атрибут у UI элемента.

        UIWeb.UIOAttributeGet(inUIO=lUIO, inAttributeStr = "href")

    :param inAttributeStr: Наименование обычного (нестилевого) атрибута
    :type inAttributeStr: str
    :return: Значение обычного (нестилевого) атрибута
    """
    return inUIO.get_attribute(inAttributeStr)

def UIOAttributeStyleGet(inUIO, inAttributeStr) -> str:
    """L+,W+: Получить стилевой атрибут у UI элемента.

        UIWeb.UIOAttributeStyleGet(inUIO=lUIO, inAttributeStr = "href")

    :param inAttributeStr: Наименование стилевого атрибута
    :return: Значение стилевого атрибута
    """
    return inUIO.value_of_css_property(inAttributeStr)

def UIOAttributeSet(inUIO, inAttributeStr, inValue):
    """L+,W+: Установить обычный (нестилевой) атрибут у UI элемента.

        UIWeb.UIOAttributeSet(inUIO=lUIO, inAttributeStr = "href", inValue = "https://mail.ru")

    :param inValue: Устанавливаемое значение обычного (нестилевого) атрибута
    :type inValue: str
    """
    lJSStr = \
        f"arguments[0].setAttribute(arguments[1], arguments[2]);"
    gBrowser.execute_script(lJSStr,inUIO, inAttributeStr, inValue)

def UIOAttributeRemove(inUIO, inAttributeStr):
    """L+,W+: Удалить обычный (нестилевой) атрибут у UI элемента.

        UIWeb.UIOAttributeRemove(lUIO, "href")

    """
    lJSStr = \
        f"arguments[0].removeAttribute(arguments[1]);"
    gBrowser.execute_script(lJSStr,inUIO, inAttributeStr)

def UIOAttributeStyleSet(inUIO, inAttributeStr, inValue):
    """L+,W+: Установить стилевой атрибут у UI элемента.

        UIWeb.UIOAttributeStyleSet(inUIO=lUIO, inAttributeStr = "color", inValue = "grey")

    :param inValue: Устанавливаемое значение стилевого атрибута
    """
    lJSStr = \
        f"arguments[0].style[arguments[1]]=arguments[2];"

def UIOAttributeStyleRemove(inUIO, inAttributeStr:str):
    """L+,W+: Удалить стилевой атрибут у UI элемента.

        UIWeb.UIOAttributeStyleRemove(lUIO, "color")

    """
    lJSStr = \
        f"arguments[0].style[arguments[1]]=\"\";"

def UIOClick(inUIO):
    """L+,W+: Выполнить нажатие по элементу inUIO.

        UIOClick(inUIO = lUIO)

    """
    inUIO.click()

def UIOSelectorHighlight(inUIOSelectorStr: str, inIsFirst:bool=False, inDurationSecFloat:float=3.0, inColorStr:str="green"):
    """L+,W+: Выполнить подсвечивание UI элемента с селектором inUIOSelectorStr.

        UIOSelectorHighlight(inUIOSelectorStr = lUIOSelectorStr)

    :param inUIOSelectorStr: XPATH или CSS селектор UI элемента на web странице. Подсказки по CSS: https://devhints.io/css Подсказки по XPath: https://devhints.io/xpath 
    :param inIsFirst: True - подсветить только первый элемент, который удовлетворяет селектору. По умолчанию False
    :type inIsFirst: bool, опционально
    :param inDurationSecFloat: Длительность подсвечивания. По умолчанию 3.0 сек.
    :type inDurationSecFloat: float, опционально
    :param inColorStr: Цвет подсвечания Варианты: "red", "blue", "grey", "yellow". По умолчанию "green" (зеленый)
    :type inColorStr: str, опционально
    """
    if inIsFirst == True:
        lUIOList = [UIOSelectorFirst(inUIOSelectorStr=inUIOSelectorStr)]
        lJSStr = \
            f"var lElementList = arguments[0];" \
            f"if (lElementList.length>0) {{ lElementList=[lElementList[0]]; }}" \
            f"for (var lIndexInt=0; lIndexInt<lElementList.length;lIndexInt++) {{" \
            f"  lElement=lElementList[lIndexInt];" \
            f"  lElement.ORPABackupStyleOutline = lElement.style[\"outline\"];" \
            f"  lElement.style[\"outline\"]=\"2px solid {inColorStr}\";" \
            f"}}" \
            f"window.ORPAOutlineList = lElementList;"
        PageJSExecute(lJSStr, lUIOList)
    else:
        lUIOList = UIOSelectorList(inUIOSelectorStr=inUIOSelectorStr)
        lJSStr = \
            f"}}" \
    time.sleep(inDurationSecFloat)
    lJSStr = \
        f"var lElementList = window.ORPAOutlineList;" \
        f"for (var lIndexInt=0; lIndexInt<lElementList.length;lIndexInt++) {{" \
        f"  lElement=lElementList[lIndexInt];" \
        f"  lElement.style[\"outline\"]=lElement.ORPABackupStyleOutline;" \
        f"}}" \
        f"delete window.ORPAOutlineList;"
    PageJSExecute(inJSStr=lJSStr)

def UIOSelectorClick(inUIOSelectorStr: str):
    """L+,W+: Выполнить нажатие по элементу с селектором inUIOSelectorStr.

        UIOSelectorClick(inUIOSelectorStr = lUIOSelectorStr)

    """
    PageJSExecute(inJSStr=f"document.querySelector('{inUIOSelectorStr}').click()")

def UIOSelectorWaitAppear(inUIOSelectorStr:str, inWaitSecFloat:float=UIO_WAIT_SEC_FLOAT, inWaitIntervalSecFloat:float = UIO_WAIT_INTERVAL_SEC_FLOAT):
    """L+,W+: Ожидать появление UI элемента на веб странице (блокирует выполнение потока), заданного по UIO селектору inUIOSelectorStr. Выполнять ожидание на протяжении inWaitSecFloat (по умолчанию 60 сек.). Проверка производится с интервалом inWaitIntervalSecFloat (по умолчанию 1 сек.)

        lAppearUIOList = UIOSelectorWaitAppear(inUIOSelectorStr = lUIOSelectorStr)
    
    :param inWaitSecFloat: Время ожидания на исчезновение UI элемента, по умолчанию UIO_WAIT_SEC_FLOAT (60 сек)
    :type inWaitSecFloat: float, опциональный
    :param inWaitIntervalSecFloat: Интервал проверки исчезновения, по умолчанию UIO_WAIT_INTERVAL_SEC_FLOAT (1 сек)
    :type inWaitIntervalSecFloat: float, опциональный
    :raises Exception: Время ожидания превышено
    :return: Список UI элементов, которые удовлетворяют селектору и появились на странице
    """
    lStartSecFloat = time.time()
    while time.time() - lStartSecFloat < inWaitSecFloat:
        lResultList = UIOSelectorList(inUIOSelectorStr=inUIOSelectorStr)
        if len(lResultList)>0: break
        time.sleep(inWaitIntervalSecFloat)
    if time.time() - lStartSecFloat > inWaitSecFloat: raise Exception(f"Wait time is over. No element has been appear")

def UIOSelectorWaitDisappear(inUIOSelectorStr:str, inWaitSecFloat:float=UIO_WAIT_SEC_FLOAT, inWaitIntervalSecFloat:float = UIO_WAIT_INTERVAL_SEC_FLOAT):
    """L+,W+: Ожидать исчезновение UI элемента с веб страницы (блокирует выполнение потока), заданного по UIO селектору inUIOSelectorStr. Выполнять ожидание на протяжении inWaitSecFloat (по умолчанию 60 сек.). Проверка производится с интервалом inWaitIntervalSecFloat (по умолчанию 1 сек.)

        UIOSelectorWaitDisappear(inUIOSelectorStr = lUIOSelectorStr)
        
    """
        if len(lResultList)==0: break
    if time.time() - lStartSecFloat > inWaitSecFloat: raise Exception(f"Wait time is over. No element has been disappear")

from lxml import etree
from io import StringIO
gXML = etree.parse(StringIO('<foo><bar></bar></foo>'))

def UIOSelectorDetect(inUIOSelectorStr:str) -> str:
    """L+,W+: Идентифицировать стиль селектора (CSS или XPATH)

        lUIOSelectorStr = "#grid > div.grid-middle > div.grid__main-col.svelte-2y66pa > div.grid_newscol.grid_newscol__more-pulse.svelte-1yvqfic > div.grid__ccol.svelte-1yvqfic > ul > li:nth-child(5) > div > a"
        lResultStr = UIOSelectorDetect(inUIOSelectorStr = lUIOSelectorStr)

    :return: "CSS" или "XPATH"
    """
    global gXML
    lResultStr = "CSS"
    try:
        gXML.xpath(inUIOSelectorStr)
        lResultStr = "XPATH"
    except etree.XPathEvalError as e:
        lResultStr = "CSS"

def UIOMouseSearchInit():
    """L+,W+: Инициализирует процесс поиска UI элемента с помощью мыши. Для прекращения поиска необходимо использовать функцию: UIOMouseSearchReturn

        import time
        UIWeb.UIOMouseSearchInit()
        UIWeb.UIOMouseSearchReturn()
    """    
    lJSStr = """

        document.ORPASearch = function(e){
            document.ORPAMouseXInt = e.clientX;
            document.ORPAMouseYInt = e.clientY;
        }

        document.addEventListener('mousemove', document.ORPASearch, {
            passive: true})
    """
    PageJSExecute(lJSStr)

def UIOMouseSearchReturn():
    """L+,W+: Возвращает UIO объект, над которым находится указатель мыши. Предварительно должна быть вызвана функция UIWeb.UIOMouseSearchInit

    :rtype: webelement
    """    
        document.removeEventListener('mousemove', document.ORPASearch);    
        return document.elementFromPoint(document.ORPAMouseXInt,document.ORPAMouseYInt);
    """
    return PageJSExecute(lJSStr)
    
if CrossOS.IS_WINDOWS_BOOL: from keyboard import *
elif CrossOS.IS_LINUX_BOOL: 
    import pyautogui, sys, os
    press = pyautogui.keyDown
    release = pyautogui.keyUp
    def send(hotkey, do_press=True, do_release=True):
        if do_press and do_release: pyautogui.press(hotkey); return
        if do_press: press(hotkey)
        if do_release: release(hotkey)

WIN = CrossOS.IS_WINDOWS_BOOL
LIN = CrossOS.IS_LINUX_BOOL

# Настройки модуля Keyboard
WAIT_AFTER_SEC_FLOAT = 0.4 # Время, которое ожидать после выполнения любой операции модуля Keyboard. Настройка является единой для всех участов кода, использующих модуль Keyboard. Если для некоторой функции требуется изменить данное время ожидания, то в отношении этой функции можно применить соответсвующий аргумент.

# ШЕСТНАДЦАТИРИЧНЫЙ СКАН-КОД В РУССКОЙ РАСКЛАДКЕ (НЕЗАВИСИМО ОТ ВЫБРАННОГО ЯЗЫКА НА КЛАВИАТУРЕ) 
# ОТОБРАЖЕНИЕ СКАН КОДОВ НА КЛАВИАТУРЕ https://snipp.ru/handbk/scan-codes

KEY_RUS_LAYOUT = "ru" # NEED FOR LINUX (FOR LAYOUT SWITCH)
KEY_RUS_Ф = 0x1E if WIN else 'a' #A
KEY_RUS_И = 0x30 if WIN else 'b' #B
KEY_RUS_С = 0x2E if WIN else 'c' #C
KEY_RUS_В = 0x20 if WIN else 'd' #D
KEY_RUS_У = 0x12 if WIN else 'e' #E
KEY_RUS_А = 0x21 if WIN else 'f' #F
KEY_RUS_П = 0x22 if WIN else 'g' #G
KEY_RUS_Р = 0x23 if WIN else 'h' #H
KEY_RUS_Ш = 0x17 if WIN else 'i' #I
KEY_RUS_О = 0x24 if WIN else 'j' #J
KEY_RUS_Л = 0x25 if WIN else 'k' #K
KEY_RUS_Д = 0x26 if WIN else 'l' #L
KEY_RUS_Ь = 0x32 if WIN else 'm' #M
KEY_RUS_Т = 0x31 if WIN else 'n' #N
KEY_RUS_Щ = 0x18 if WIN else 'o' #O
KEY_RUS_З = 0x19 if WIN else 'p' #P
KEY_RUS_Й = 0x10 if WIN else 'q' #Q
KEY_RUS_К = 0x13 if WIN else 'r' #R
KEY_RUS_Ы = 0x1F if WIN else 's' #S
KEY_RUS_Е = 0x14 if WIN else 't' #T
KEY_RUS_Г = 0x16 if WIN else 'u' #U
KEY_RUS_М = 0x2F if WIN else 'v' #V
KEY_RUS_Ц = 0x11 if WIN else 'w' #W
KEY_RUS_Ч = 0x2D if WIN else 'x' #X
KEY_RUS_Н = 0x15 if WIN else 'y' #Y
KEY_RUS_Я = 0x2C if WIN else 'z' #Z
KEY_RUS_Ё = 0x29 if WIN else '~' #~
KEY_RUS_Ж = 0x27 if WIN else ':' #:
KEY_RUS_Б = 0x33 if WIN else '<' #<
KEY_RUS_Ю = 0x34 if WIN else '>' #>
KEY_RUS_Х = 0x1A if WIN else '[' #[
KEY_RUS_Ъ = 0x1B if WIN else ']' #]
KEY_RUS_Э = 0x28 if WIN else "'" #'
 
KEY_ENG_LAYOUT = "us" # NEED FOR LINUX (FOR LAYOUT SWITCH)
KEY_ENG_A = 0x1E if WIN else 'a' #A
KEY_ENG_B = 0x30 if WIN else 'b' #B
KEY_ENG_C = 0x2E if WIN else 'c' #C
KEY_ENG_D = 0x20 if WIN else 'd' #D
KEY_ENG_E = 0x12 if WIN else 'e' #E
KEY_ENG_F = 0x21 if WIN else 'f' #F
KEY_ENG_G = 0x22 if WIN else 'g' #G
KEY_ENG_H = 0x23 if WIN else 'h' #H
KEY_ENG_I = 0x17 if WIN else 'i' #I
KEY_ENG_J = 0x24 if WIN else 'j' #J
KEY_ENG_K = 0x25 if WIN else 'k' #K
KEY_ENG_L = 0x26 if WIN else 'l' #L
KEY_ENG_M = 0x32 if WIN else 'm' #M
KEY_ENG_N = 0x31 if WIN else 'n' #N
KEY_ENG_O = 0x18 if WIN else 'o' #O
KEY_ENG_P = 0x19 if WIN else 'p' #P
KEY_ENG_Q = 0x10 if WIN else 'q' #Q
KEY_ENG_R = 0x13 if WIN else 'r' #R
KEY_ENG_S = 0x1F if WIN else 's' #S
KEY_ENG_T = 0x14 if WIN else 't' #T
KEY_ENG_U = 0x16 if WIN else 'u' #U
KEY_ENG_V = 0x2F if WIN else 'v' #V
KEY_ENG_W = 0x11 if WIN else 'w' #W
KEY_ENG_X = 0x2D if WIN else 'x' #X
KEY_ENG_Y = 0x15 if WIN else 'y' #Y
KEY_ENG_Z = 0x2C if WIN else 'z' #Z

KEY_ENG_TILDE = 0x29 if WIN else '~' #~
KEY_ENG_COLON = 0x27 if WIN else ':' #:
KEY_ENG_PLUS = 0x0D if WIN else '+' #+
KEY_ENG_MINUS = 0x0C if WIN else '-' #-
KEY_ENG_LESS_THAN = 0x33 if WIN else '<' #< ,
KEY_ENG_GREATER_THAN = 0x34 if WIN else '>' #> .
KEY_ENG_SOLIDUS = 0x35 if WIN else '/' #/ ?
KEY_ENG_SQUARE_BRACKET_LEFT = 0x1A if WIN else '['  #[
KEY_ENG_SQUARE_BRACKET_RIGHT = 0x1B if WIN else ']' #]
KEY_ENG_APOSTROPHE = 0x28 if WIN else "'" #' "
KEY_ENG_VERTICAL_LINE = 0x2B if WIN else '|' #| \

KEY_ENG_NUMPAD_0 = 0x52 if WIN else 'num0' 
KEY_ENG_NUMPAD_1 = 0x4F if WIN else 'num1' 
KEY_ENG_NUMPAD_2 = 0x50 if WIN else 'num2' 
KEY_ENG_NUMPAD_3 = 0x51 if WIN else 'num3' 
KEY_ENG_NUMPAD_4 = 0x4B if WIN else 'num4' 
KEY_ENG_NUMPAD_5 = 0x4C if WIN else 'num5' 
KEY_ENG_NUMPAD_6 = 0x4D if WIN else 'num6' 
KEY_ENG_NUMPAD_7 = 0x47 if WIN else 'num7' 
KEY_ENG_NUMPAD_8 = 0x48 if WIN else 'num8' 
KEY_ENG_NUMPAD_9 = 0x49 if WIN else 'num9' 
KEY_ENG_NUMPAD_ASTERISK = 0x37 if WIN else '*'  #*
KEY_ENG_NUMPAD_PLUS = 0x4E if WIN else '+' 
KEY_ENG_NUMPAD_MINUS = 0x4A if WIN else '-' 
KEY_ENG_NUMPAD_DELETE = 0x53 if WIN else 'delete' 
KEY_ENG_NUMPAD_SOLIDUS = 0x35 if WIN else '/' #/ 
KEY_ENG_NUMPAD_ENTER = 0x11c if WIN else 'enter' 

KEY_ENG_0 = 0xB if WIN else '0' 
KEY_ENG_1 = 0x2 if WIN else '1' 
KEY_ENG_2 = 0x3 if WIN else '2' 
KEY_ENG_3 = 0x4 if WIN else '3' 
KEY_ENG_4 = 0x5 if WIN else '4' 
KEY_ENG_5 = 0x6 if WIN else '5' 
KEY_ENG_6 = 0x7 if WIN else '6' 
KEY_ENG_7 = 0x8 if WIN else '7' 
KEY_ENG_8 = 0x9 if WIN else '8' 
KEY_ENG_9 = 0xA if WIN else '9' 

KEY_HOT_NUMPAD_0 = 0x52 if WIN else 'num0' 
KEY_HOT_NUMPAD_1 = 0x4F if WIN else 'num1' 
KEY_HOT_NUMPAD_2 = 0x50 if WIN else 'num2' 
KEY_HOT_NUMPAD_3 = 0x51 if WIN else 'num3' 
KEY_HOT_NUMPAD_4 = 0x4B if WIN else 'num4' 
KEY_HOT_NUMPAD_5 = 0x4C if WIN else 'num5' 
KEY_HOT_NUMPAD_6 = 0x4D if WIN else 'num6' 
KEY_HOT_NUMPAD_7 = 0x47 if WIN else 'num7' 
KEY_HOT_NUMPAD_8 = 0x48 if WIN else 'num8' 
KEY_HOT_NUMPAD_9 = 0x49 if WIN else 'num9' 
KEY_HOT_NUMPAD_ASTERISK = 0x37 if WIN else '*'  #*
KEY_HOT_NUMPAD_PLUS = 0x4E if WIN else '+' 
KEY_HOT_NUMPAD_MINUS = 0x4A if WIN else '-' 
KEY_HOT_NUMPAD_DELETE = 0x53 if WIN else 'delete' 
KEY_HOT_NUMPAD_SOLIDUS = 0x35 if WIN else '/' #/ 
KEY_HOT_NUMPAD_ENTER = 0x11c if WIN else 'enter' 

KEY_HOT_F1 = 0x3B if WIN else 'f1' 
KEY_HOT_F2 = 0x3C if WIN else 'f2' 
KEY_HOT_F3 = 0x3D if WIN else 'f3' 
KEY_HOT_F4 = 0x3E if WIN else 'f4' 
KEY_HOT_F5 = 0x3F if WIN else 'f5' 
KEY_HOT_F6 = 0x40 if WIN else 'f6' 
KEY_HOT_F7 = 0x41 if WIN else 'f7' 
KEY_HOT_F8 = 0x42 if WIN else 'f8' 
KEY_HOT_F9 = 0x43 if WIN else 'f9' 
KEY_HOT_F10 = 0x44 if WIN else 'f10' 
KEY_HOT_F11 = 0x57 if WIN else 'f11' 
KEY_HOT_F12 = 0x58 if WIN else 'f12' 
KEY_HOT_F13 = 0x7C if WIN else 'f13' 
KEY_HOT_F14 = 0x7D if WIN else 'f14' 
KEY_HOT_F15 = 0x7E if WIN else 'f15' 
KEY_HOT_F16 = 0x7F if WIN else 'f16' 
KEY_HOT_F17 = 0x80 if WIN else 'f17' 
KEY_HOT_F18 = 0x81 if WIN else 'f18' 
KEY_HOT_F19 = 0x82 if WIN else 'f19'  
KEY_HOT_F20 = 0x83 if WIN else 'f20' 
KEY_HOT_F21 = 0x84 if WIN else 'f21' 
KEY_HOT_F22 = 0x85 if WIN else 'f22' 
KEY_HOT_F23 = 0x86 if WIN else 'f23' 
KEY_HOT_F24 = 0x87 if WIN else 'f24' 

KEY_HOT_TILDE = 0x29 if WIN else '~' #~
KEY_HOT_COLON = 0x27 if WIN else ':' #:
KEY_HOT_PLUS = 0x0D if WIN else '+' #+
KEY_HOT_MINUS = 0x0C if WIN else '-' #-
KEY_HOT_LESS_THAN = 0x33 if WIN else '<' #< ,
KEY_HOT_GREATER_THAN = 0x34 if WIN else '>' #> .
KEY_HOT_SOLIDUS = 0x35 if WIN else '/' #/ ?
KEY_HOT_SQUARE_BRACKET_LEFT = 0x1A if WIN else '['  #[
KEY_HOT_SQUARE_BRACKET_RIGHT = 0x1B if WIN else ']' #]
KEY_HOT_APOSTROPHE = 0x28 if WIN else "'" #' "
KEY_HOT_VERTICAL_LINE = 0x2B if WIN else '|' #| \

KEY_HOT_ESC = 0x1 if WIN else 'esc' 
KEY_HOT_BACKSPACE = 0x0E if WIN else 'backspace' 
KEY_HOT_TAB = 0x0F if WIN else 'tab' 
KEY_HOT_ENTER = 0x1C if WIN else 'enter' 
KEY_HOT_CONTEXT_MENU = 0x15D if WIN else 'apps' 
KEY_HOT_SHIFT_LEFT = 0x2A if WIN else 'shiftleft'
KEY_HOT_SHIFT_RIGHT = 0x36 if WIN else 'shiftright'
KEY_HOT_CTRL_LEFT = 0x1D if WIN else 'ctrlleft'
KEY_HOT_CTRL_RIGHT = 0x11D if WIN else 'ctrlright'
KEY_HOT_ALT_LEFT = 0x38  if WIN else 'altleft'
KEY_HOT_ALT_RIGHT = 0x138  if WIN else 'altright'
KEY_HOT_WIN_LEFT = 57435 if WIN else 'winleft' #OLD AND DONT WORK 0x5B
KEY_HOT_WIN_RIGHT = 57436  if WIN else 'winright' #OLD AND DONT WORK 0x5C
KEY_HOT_CAPS_LOCK = 0x3A  if WIN else 'capslock'
KEY_HOT_NUM_LOCK = 0x45  if WIN else 'numlock'
KEY_HOT_SCROLL_LOCK = 0x46  if WIN else 'scrolllock'
KEY_HOT_END = 0x4F  if WIN else 'end'
KEY_HOT_HOME = 0x47 if WIN else 'home'
KEY_HOT_SPACE = 0x39 if WIN else 'space'
KEY_HOT_PAGE_UP = 0x49  if WIN else 'pageup'
KEY_HOT_PAGE_DOWN = 0x51 if WIN else 'pagedown'
KEY_HOT_CLEAR = 0x4C if WIN else 'clear'
KEY_HOT_LEFT = 0x4B if WIN else 'left'
KEY_HOT_UP = 0x48  if WIN else 'up'
KEY_HOT_RIGHT = 0x4D  if WIN else 'right'
KEY_HOT_DOWN = 0x50  if WIN else 'down'
KEY_HOT_PRINT_SCREEN = 0x137  if WIN else 'printscreen'
KEY_HOT_INSERT = 0x52 if WIN else 'insert'
KEY_HOT_DELETE = 0x53 if WIN else 'delete'

KEY_HOT_0 = 0xB if WIN else '0' 
KEY_HOT_1 = 0x2 if WIN else '1' 
KEY_HOT_2 = 0x3 if WIN else '2' 
KEY_HOT_3 = 0x4 if WIN else '3' 
KEY_HOT_4 = 0x5 if WIN else '4' 
KEY_HOT_5 = 0x6 if WIN else '5' 
KEY_HOT_6 = 0x7 if WIN else '6' 
KEY_HOT_7 = 0x8 if WIN else '7' 
KEY_HOT_8 = 0x9 if WIN else '8' 
KEY_HOT_9 = 0xA if WIN else '9' 

MAP_RUS_ENG = { # FOR LINUX PURPOSE
"Ё":"`",
"Й":"q",
"Ц":"w",
"У":"e",
"К":"r",
"Е":"t",
"Н":"y",
"Г":"u",
"Ш":"i",
"Щ":"o",
"З":"p",
"Х":"[",
"Ъ":"]",
"Ф":"a",
"Ы":"s",
"В":"d",
"А":"f",
"П":"g",
"Р":"h",
"О":"j",
"Л":"k",
"Д":"l",
"Ж":";",
"Э":"'",
"Я":"z",
"Ч":"x",
"С":"c",
"М":"v",
"И":"b",
"Т":"n",
"Ь":"m",
"Б":",",
"Ю":"."
}

def Write(inTextStr:str, inDelayFloat:float=0, inRestoreStateAfterBool:bool=True, inExactBool:bool=None, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Печатает текст, который был передан в переменной inTextStr (поддерживает передачу в одной строке символов разного языка). Не зависит от текущей раскладки клавиатуры! Посылает искусственные клавишные события в ОС, моделируя печать данного текста. Знаки, не доступные на клавиатуре, напечатаны как явный unicode знаки, использующие определенную для ОС функциональность, такие как alt+codepoint.
    Чтобы гарантировать текстовую целостность, все в настоящее время нажатые ключи выпущены прежде текст напечатан, и модификаторы восстановлены впоследствии.

    ВНИМАНИЕ! ПЕЧАТАЕТ ЛЮБУЮ СТРОКУ, ДАЖЕ В СОЧЕТАНИИ НЕСКОЛЬКИХ ЯЗЫКОВ ОДНОВРЕМЕННО. ДЛЯ РАБОТЫ С ГОРЯЧИМИ КЛАВИШАМИ ИСПОЛЬЗУЙ ФУНКЦИЮ Send, Up, Down, HotkeyCombination

    ВНИМАНИЕ! В LINUX НЕ ДЕЙСТВУЮТ СЛЕДУЮЩИЕ ПАРАМЕТРЫ: inRestoreStateAfterBool, inExactBool

        # Keyboard: Взаимодействие с клавиатурой
        from pyOpenRPA.Robot import Keyboard
        Keyboard.Write("Привет мой милый мир! Hello my dear world!")

    :param inTextStr: Текст, отправляемый на печать. Не зависит от текущей раскладки клавиатуры! 
    :type inTextStr: str
    :param inDelayFloat: Число секунд, которое ожидать между нажатиями. По умолчанию 0
    :type inDelayFloat: float, опциональный
    :param inRestoreStateAfterBool: Может использоваться, чтобы восстановить регистр нажатых ключей после того, как текст напечатан, т.е. нажимает ключи, которые были выпущены в начало.
    :type inRestoreStateAfterBool: bool, опциональный
    :param inExactBool: Печатает все знаки как явный unicode. Необязательный параметр
    :type inExactBool: bool, опциональный
    :param inWaitAfterSecFloat: Количество секунд, которые ожидать после выполнения операции. По умолчанию установлено в настройках модуля Keyboard (базовое значение 0.4)
    :type inWaitAfterSecFloat: float, опциональный
    """
        write(text=inTextStr, delay=inDelayFloat, restore_state_after=inRestoreStateAfterBool, exact=inExactBool)
        _WriteLinux(inTextStr=inTextStr, inDelayFloat=inDelayFloat)
    time.sleep(inWaitAfterSecFloat)

def _WriteLinux(inTextStr, inDelayFloat=0.01):
    lModuleKeyboard = sys.modules[__name__]
    lVarList = dir(lModuleKeyboard)
    lLayoutStr = None
    for lCharStr in inTextStr:
        # Check rus (ru)
        lVarNameStr = f"KEY_RUS_{lCharStr.upper()}"
        lFoundBool = False
        lShiftBool = False
        if lVarNameStr in lVarList: 
            lNewLayoutStr=getattr(lModuleKeyboard, "KEY_RUS_LAYOUT")
            lCapBool = lCharStr.isupper()
            lCharStr = MAP_RUS_ENG[lCharStr.upper()]
            #if lCapBool == True: lCharStr=lCharStr.upper()
            if lCapBool == True: lShiftBool=True            
            lFoundBool=True
        # Check eng (us)
        lVarNameStr = f"KEY_ENG_{lCharStr.upper()}"
        if lVarNameStr in lVarList and lFoundBool == False: 
            lNewLayoutStr=getattr(lModuleKeyboard, "KEY_ENG_LAYOUT")
        if lFoundBool == False: lNewLayoutStr=getattr(lModuleKeyboard, "KEY_ENG_LAYOUT")
        # CHECK LAST LAYOUT
        if lNewLayoutStr != lLayoutStr: 
            #print(f"setxkbmap -layout {lNewLayoutStr}")
            os.system(f"setxkbmap -layout {lNewLayoutStr}")
            lLayoutStr = lNewLayoutStr
        if lShiftBool == True: pyautogui.keyDown('shift')  # hold down the shift key
        pyautogui.write(lCharStr, interval = inDelayFloat)
        if lShiftBool == True: pyautogui.keyUp('shift')  # hold down the shift key
    
    # Set 2 layout with alt+shift switch
    os.system(f"setxkbmap -layout {getattr(lModuleKeyboard, 'KEY_ENG_LAYOUT')},{getattr(lModuleKeyboard, 'KEY_RUS_LAYOUT')} -option grp:alt_shift_toggle")

def HotkeyCombination(*inKeyList, inDelaySecFloat = 0.3,inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Получает перечень клавиш для одновременного нажатия. Между нажатиями программа ожидания время inDelaySecFloat
    ВНИМАНИЕ! НЕ ЗАВИСИТ ОТ ТЕКУЩЕЙ РАСКЛАДКИ КЛАВИАТУРЫ

        Keyboard.HotkeyCombination(Keyboard.KEY_HOT_CTRL_LEFT,Keyboard.KEY_ENG_A) # Ctrl + a
        Keyboard.HotkeyCombination(Keyboard.KEY_HOT_CTRL_LEFT,Keyboard.KEY_ENG_C) # Ctrl + c
        Keyboard.HotkeyCombination(Keyboard.KEY_HOT_CTRL_LEFT,Keyboard.KEY_ENG_A)
        Keyboard.HotkeyCombination(Keyboard.KEY_HOT_ALT_LEFT,Keyboard.KEY_HOT_TAB, Keyboard.KEY_HOT_TAB)
    
    :param inKeyList: Список клавиш для одновременного нажатия. Перечень клавиш см. в разделе "Коды клавиш". Пример: KEY_HOT_CTRL_LEFT,KEY_ENG_A
    :param inDelaySecFloat: Интервал между нажатиями. Необходим в связи с тем, что операционной системе требуется время на реакцию на нажатие клавиш, по умолчанию: 0.3
    :type inDelaySecFloat: float, опциональный
    """
    for l_key_item in inKeyList:
        if l_key_item == inKeyList[-1]:
            send(l_key_item)
        else:
            press(l_key_item)
        time.sleep(inDelaySecFloat)
    lRevKeyList = list(reversed(inKeyList))
    for l_key_item in lRevKeyList:
        if l_key_item == lRevKeyList[0]:
            pass
        else:
            release(l_key_item)

def HotkeyCtrlV(inWaitAfterSecFloat:float=0.4) -> None:
    """L+,W+: Выполнить вставку текста из буфера обмена
    
        Keyboard.HotkeyCtrlV()
    
    """
    HotkeyCombination(KEY_HOT_CTRL_LEFT,KEY_ENG_V) # Ctrl + v

def HotkeyCtrlA_CtrlC(inWaitAfterSecFloat:float=0.4) -> None:
    """L+,W+: Выполнить выделение текста, после чего скопировать его в буфер обмена
    
        Keyboard.HotkeyCtrlA_CtrlC() # Отправить команды: выделить все, скопировать в буфер обмена
    
    """
    HotkeyCombination(KEY_HOT_CTRL_LEFT,KEY_ENG_A) # Ctrl + a
    HotkeyCombination(KEY_HOT_CTRL_LEFT,KEY_ENG_C) # Ctrl + c

def Send(inKeyInt:int, inDoPressBool:bool=True, inDoReleaseBool:bool=True,inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT) -> None:
    """L+,W+: Имитация нажатия/отпускания любой физической клавиши. Посылает событие в операционную систему, которые выполняет нажатие и отпускание данной клавиши
    
    ВНИМАНИЕ! ПРИ ПОПЫТКЕ ПЕЧАТИ ТЕКСТА БУДЕТ УЧИТЫВАТЬ ТЕКУЩУЮ РАСКЛАДКУ КЛАВИАТУРЫ. ДЛЯ ПЕЧАТИ ТЕКСТА ИСПОЛЬЗУЙ Write!

        Keyboard.Send(Keyboard.KEY_ENG_A) # Нажать клавишу A. Если будет активна русская раскладка, то будет выведена буква ф.

    :param inKeyInt: Перечень клавиш см. в разделе "Коды клавиш". Пример: KEY_HOT_CTRL_LEFT,KEY_ENG_A
    :type inKeyInt: int
    :param inDoPressBool: Выполнить событие нажатия клавиши, По умолчанию True
    :type inDoPressBool: bool, опциональный
    :param inDoReleaseBool: Выполнить событие отпускания клавиши, По умолчанию True
    :type inDoReleaseBool: bool, опциональный
    """
    send(hotkey=inKeyInt, do_press=inDoPressBool, do_release=inDoReleaseBool)
    
def Up(inKeyInt:int, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT) -> None:
    """L+,W+: Отпустить (поднять) клавишу. Если клавиша уже была поднята, то ничего не произойдет.
    
        Keyboard.Up(Keyboard.KEY_ENG_A) # Отпустить клавишу A.

    :param inKeyInt: Перечень клавиш см. в разделе "Коды клавиш". Пример: KEY_HOT_CTRL_LEFT, KEY_ENG_A
    """
    send(hotkey=inKeyInt, do_press=False, do_release=True)
    
def Down(inKeyInt:int, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT) -> None:
    """L+,W+: Нажать (опустить) клавишу. Если клавиша уже была опущена, то ничего не произойдет.
    
        Keyboard.Down(Keyboard.KEY_ENG_A) # Отпустить клавишу A.

    """
    send(hotkey=inKeyInt, do_press=True, do_release=False)

def IsDown(inKeyInt:int) -> bool:
    """L+,W+: Проверить, опущена ли клавиша. Вернет True если опущена; False если поднята.
    
        lKeyAIsPressedBool = Keyboard.IsDown(Keyboard.KEY_ENG_A) # Проверить, опущена ли клавиша A.

    """
    return is_pressed(inKeyInt)

def Wait(inKeyInt:int,inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L-,W+: Блокирует осуществление программы, пока данная обозначенная клавиша не будет нажата.
    ВНИМАНИЕ! НЕ ЗАВИСИТ ОТ ТЕКУЩЕЙ РАСКЛАДКИ КЛАВИАТУРЫ. ОЖИДАЕТ НАЖАТИЕ СООТВЕТСВУЮЩЕЙ ФИЗИЧЕСКОЙ КЛАВИШИ
    
        Keyboard.Wait(Keyboard.KEY_ENG_A) # Ждать нажатие клавиши A.

    """
    wait(hotkey=inKeyInt)

if CrossOS.IS_WINDOWS_BOOL: key_to_scan_codes("win") # 2022 06 10 Люблю смотреть скан код клавиши Виндовс :)

if CrossOS.IS_WINDOWS_BOOL: import win32clipboard #CrossOS
if CrossOS.IS_LINUX_BOOL: import pyclip #CrossOS
#Info: Clipboard module of the Robot app (OpenRPA - Robot)
# GUI Module - interaction with Windows clipboard

def Get():
    """L+,W+: Получить текстовое содержимое буфера обмена.

        # Clipboard: Взаимодействие с буфером
        from pyOpenRPA.Robot import Clipboard
        lClipStr = Clipboard.Get()

    :return: Текстовое содержимое буфера обмена
    """
    return ClipboardGet()

def Set(inTextStr:str):
    """L+,W+: Установить текстовое содержимое в буфер обмена.

        lClipStr = Clipboard.Set(inTextStr="HELLO WORLD")

    :param inTextStr: Текстовое содержимое для установки в буфера обмена
    """
    ClipboardSet(inText=inTextStr)

def ClipboardGet():
        win32clipboard.OpenClipboard()
        lResult = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
        win32clipboard.CloseClipboard()
        lResult = pyclip.paste(text=True)

def ClipboardSet(inText):
        win32clipboard.EmptyClipboard()
        win32clipboard.SetClipboardData(win32clipboard.CF_UNICODETEXT,inText)
        pyclip.copy(inText) # copy data to the clipboard

from pyautogui import *

WAIT_AFTER_SEC_FLOAT = 0.4 # Настройка модуля Mouse: Время, которое ожидать после выполнения любой операции модуля Mouse. Настройка является единой для всех участов кода, использующих модуль Mouse. Если для некоторой функции требуется изменить данное время ожидания, то в отношении этой функции можно применить соответсвующий аргумент.

def Click(inXInt:int=None, inYInt:int=None, inClickCountInt:int=1, inIntervalSecFloat:float=0.0, inButtonStr:str='left', inMoveDurationSecFloat:float=0.0, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Нажатие (вниз) кнопки мыши и затем немедленно выпуск (вверх) её. Допускается следующая параметризация. Если не указаны inXInt и inYInt - клик производится по месту нахождения указателя мыши.

    !ВНИМАНИЕ! Отсчет координат inXInt, inYInt начинается с левого верхнего края рабочей области (экрана).
    
        # Mouse: Взаимодействие с мышью
        from pyOpenRPA.Robot import Mouse
        Mouse.Click(100,150) #Выполнить нажатие левой клавиши мыши на экране по координатам: X(гор) 100px, Y(вер) 150px.

    :param inXInt: Целевая позиция указателя мыши по оси X (горизонтальная ось). 
    :type inXInt: int, опциональный
    :param inYInt: Целевая позиция указателя мыши по оси Y (вертикальная ось). 
    :type inYInt: int, опциональный
    :param inClickCountInt: Количество нажатий (вниз и вверх) кнопкой мыши, По умолчанию 1
    :type inClickCountInt: int, опциональный
    :param inIntervalSecFloat: Интервал ожидания в секундах между нажатиями, По умолчанию 0.0
    :type inIntervalSecFloat: float, опциональный 
    :param inButtonStr: Номер кнопки, которую требуется нажать. Возможные варианты: 'left', 'middle', 'right' или 1, 2, 3. В остальных случаях инициирует исключение ValueError. По умолчанию 'left'
    :type inButtonStr: str, опциональный
    :param inMoveDurationSecFloat: Время перемещения указателя мыши, По умолчанию 0.0 (моментальное перемещение)
    :type inMoveDurationSecFloat: float, опциональный
    :param inWaitAfterSecFloat: Количество секунд, которые ожидать после выполнения операции. По умолчанию установлено в настройках модуля Mouse (базовое значение 0.4)
    """
    if inWaitAfterSecFloat == None: inWaitAfterSecFloat = WAIT_AFTER_SEC_FLOAT
    click(x=inXInt, y=inYInt, clicks=inClickCountInt, interval=inIntervalSecFloat, button=inButtonStr, duration=inMoveDurationSecFloat)

def ClickDouble(inXInt:int=None, inYInt:int=None, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Двойное нажатие левой клавиши мыши. Данное действие аналогично вызову функции (см. ниже).
    
        Mouse.ClickDouble(100,150) #Выполнить двойное нажатие левой клавиши мыши на экране по координатам: X(гор) 100px, Y(вер) 150px.

    """
    doubleClick(x=inXInt, y=inYInt)

def Down(inXInt:int=None, inYInt:int=None, inButtonStr:str='left', inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Переместить указатель по координатам inXInt, inYInt, после чего нажать (вниз) клавишу мыши и не отпускать до выполнения соответсвующей команды (см. Up). Если координаты inXInt, inYInt не переданы - нажатие происходит на тех координатах X/Y, на которых указатель мыши находится.

        Mouse.Down() #Опустить левую клавишу мыши

    """
    mouseDown(x=inXInt, y=inYInt, button = inButtonStr)

def Up(inXInt:int=None, inYInt:int=None, inButtonStr:str='left', inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Отпустить (вверх) клавишу мыши. Если координаты inXInt, inYInt не переданы - нажатие происходит на тех координатах X/Y, на которых указатель мыши находится.

    !ВНИМАНИЕ! Отсчет координат inXInt, inYInt начинается с левого верхнего края рабочей области.

        Mouse.Up(inButtonStr:str='right') #Поднять правую клавишу мыши

    :param inButtonStr: Номер кнопки, которую требуется поднять. Возможные варианты: 'left', 'middle', 'right' или 1, 2, 3. В остальных случаях инициирует исключение ValueError. По умолчанию 'left'
    """
    mouseUp(x=inXInt, y=inYInt, button = inButtonStr)

def MoveTo(inXInt=None, inYInt=None, inMoveDurationSecFloat:float=0.0, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Переместить указатель мыши на позицию inXInt, inYInt за время inMoveDurationSecFloat.

        Mouse.MoveTo(inXInt=100, inYInt=200) #Переместить указатель мыши по координатам: X(гор) 100, Y(вер) 200

    """
    moveTo(x=inXInt, y=inYInt, duration=inMoveDurationSecFloat)
    
def ScrollVertical(inScrollClickCountInt, inXInt=None, inYInt=None, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W+: Переместить указатель мыши на позицию inXInt, inYInt и выполнить вертикальную прокрутку (скроллинг) колесом мыши на количество щелчков inScrollClickCountInt.

        Mouse.ScrollVertical(100, inXInt=100, inYInt=200) #Крутить колесо мыши вниз на 100 кликов по координатам: X(гор) 100, Y(вер) 200
        Mouse.ScrollVertical(-100) #Крутить колесо мыши вверх на 100 кликов по текущим координатам указателя мыши.

    :param inScrollClickCountInt: Количество щелчок колеса мыши, которое требуется !вертикально! прокрутить. Аргумент может принимать как положительное (прокрутка вниз), так и отрицательное (прокрутка вверх) значения
    :type inScrollClickCountInt: int, обязательный
    """
    inScrollClickCountInt = inScrollClickCountInt*-1
    vscroll(inScrollClickCountInt, x=inXInt, y=inYInt)
    
def ScrollHorizontal(inScrollClickCountInt, inXInt=None, inYInt=None, inWaitAfterSecFloat:float=WAIT_AFTER_SEC_FLOAT):
    """L+,W-: Переместить указатель мыши на позицию inXInt, inYInt и выполнить горизонтальную прокрутку (скроллинг) виртуальным колесом мыши на количество щелчков inScrollClickCountInt.

        Mouse.ScrollHorizontal(100, inXInt=100, inYInt=200) #Крутить колесо мыши вниз на 100 кликов по координатам: X(гор) 100, Y(вер) 200
        Mouse.ScrollHorizontal(-100) #Крутить колесо мыши вверх на 100 кликов по текущим координатам указателя мыши.

    :param inScrollClickCountInt: Количество щелчок колеса мыши, которое требуется !горизонтально! прокрутить. Аргумент может принимать как положительное (прокрутка вправо), так и отрицательное (прокрутка влево) значения
    """
    hscroll(inScrollClickCountInt, x=inXInt, y=inYInt)

import pyautogui
import pyscreeze
from . import Mouse
    import tkinter as tk # Python 3

IMAGE_WAIT_SEC_FLOAT = 60
IMAGE_WAIT_INTERVAL_SEC_FLOAT = 1.0

def BoxCreate(inTopInt:int, inLeftInt:int, inHeightInt:int, inWidthInt:int) -> pyscreeze.Box:
    """L+,W+: Создать экземпляр прямоугольной области.

    !ВНИМАНИЕ! Координаты inTopInt, inLeftInt определяют верхний левый край прямоугольной области.

    :param inTopInt: Координата левой верхней точки в пикселях по оси X (горизонталь)
    :type inTopInt: int
    :param inLeftInt: Координата левой верхней точки в пикселях по оси Y (вертикаль)
    :type inLeftInt: int
    :param inHeightInt: Расстояние вниз от левой верхней точки в пикселях
    :type inHeightInt: int
    :param inWidthInt: Расстояние вправо от левой верхней точки в пикселях
    :type inWidthInt: int
    """
    return pyscreeze.Box(top = inTopInt, left = inLeftInt, height = inHeightInt, width = inWidthInt)

def BoxNormalize(*inArgList, **inAgrDict) -> list:
    pass

def BoxMoveTo(inBox, inDXInt=None, inDYInt=None):
    """L+,W+: Переместить прямоугольную область (сохранить длину/ширину).

    !ВНИМАНИЕ! ПОДДЕРЖИВАЕТ ПАКЕТНУЮ ОБРАТКУ ПРИ ПЕРЕДАЧЕ СПИСКА ЭКЗЕМПЛЯРОВ BOX

        # Screen: Взаимодействие с экраном
        from pyOpenRPA.Robot import Screen
        # Вариант изменения 1-го элемента
        # Создать пробную прямоугольную область
        lBox = Screen.BoxCreate(inTopInt=10, inLeftInt=10, inHeightInt=10, inWidthInt=10)
        # Переместить пробную прямоугольную область
        lBox = Screen.BoxMoveTo(lBox, inDXInt=100, inDYInt=200)
        
    :param inBox: Экземпляр класса прямоугольной области Box
    :type inBox: pyscreeze.Box
    :param inDXInt: Смещение левой верхней координаты по оси X в пикселях (горизонтальная ось). 
    :type inDXInt: int, опциональный
    :param inDYInt: Смещение левой верхней координаты по оси Y в пикселях (вертикальная ось). 
    :type inDYInt: int, опциональный
    :return: Экземпляр класса прямоугольной области Box
    :rtype: pyscreeze.Box
    """
    if type(inBox) is list:
        lResult = []
        for lBox in inBox:
            lResult.append(BoxMoveTo(lBox, inDXInt=inDXInt, inDYInt=inDYInt))
        return lResult
    else:
        lTopInt = inBox.top
        lLeftInt = inBox.left
        if inDXInt: lLeftInt = inBox.left + inDXInt
        if inDYInt: lTopInt = inBox.top + inDYInt
        return BoxCreate(inTopInt=lTopInt, inLeftInt=lLeftInt,
            inHeightInt=inBox.height, inWidthInt=inBox.width)

def BoxModify(inBox, inDWidthInt=None, inDHeightInt=None, inPointRuleStr="CC"):
    """L+,W+: Изменить ширину / высоту прямоугольной области.

    !ВНИМАНИЕ! ЕСЛИ СМЕЩЕНИЕ ПРИВЕДЕТ К ОБРАЗОВАНИЮ ДРОБНОГО ЧИСЛА, БУДЕТ ВЫПОЛНЕНО ОКРУГЛЕНИЕ ПО МАТЕМАТИЧЕСКИМ ПРАВИЛАМ

        # Скорректировать пробную прямоугольную область
        lBox2 = Screen.BoxModify(lBox,10,10,"CC"); print(lBox2)
        lBox2 = Screen.BoxModify(lBox,10,10,"LU"); print(lBox2)
        lBox2 = Screen.BoxModify(lBox,10,10,"LD"); print(lBox2)
        lBox2 = Screen.BoxModify(lBox,10,10,"RU"); print(lBox2)
        lBox2 = Screen.BoxModify(lBox,10,10,"RD"); print(lBox2)
        
    :param inPointRuleStr: Символьное указание точки (подробнее см. выше), относительно которой выполнить изменение прямоугольной области. Допустимые значения: "CC" (по умолчанию), "LU", "LD", "RD", "RU"
    :type inPointRuleStr: str, опциональный
    """
            lResult.append(BoxModify(lBox, inDWidthInt=inDWidthInt, inDHeightInt=inDHeightInt, inPointRuleStr=inPointRuleStr))
    else:
        lWidthInt = inBox.width + inDWidthInt
        lHeightInt = inBox.height + inDHeightInt
        inPointRuleStr = inPointRuleStr.upper() # ВЕРХНИЙ РЕГИСТР
        if inDWidthInt: # Изменения по ширине
            if "C" in inPointRuleStr:
                lLeftInt = round(lLeftInt - inDWidthInt / 2)
            elif "R" in inPointRuleStr:
                lLeftInt = lLeftInt - inDWidthInt
        if inDHeightInt: # Изменения по высоте
                lTopInt = round(lTopInt - inDHeightInt / 2)
            elif "D" in inPointRuleStr:
                lTopInt = lTopInt - inDHeightInt
            inHeightInt=lHeightInt, inWidthInt=lWidthInt)

def BoxDraw(inBox, inColorStr='green',inThicknessInt = 2):
    """L+,W+: Выполнить подсветку прямоугольной области inBox на экране

    !ВНИМАНИЕ! ЦВЕТ inColorStr ПОДДЕРЖИВАЕТСЯ ТОЛЬКО НА ОС WINDOWS

        # ВАРИАНТ ОТРИСОВКИ 1ГО ЭЛЕМЕНТА
        Screen.BoxDraw(lBox)

        # ВАРИАНТ ПАКЕТНОЙ ОТРИСОВКИ
        lBox = Screen.BoxCreate(inTopInt=10, inLeftInt=10, inHeightInt=100, inWidthInt=100)
        lBox2 = Screen.BoxCreate(inTopInt=60, inLeftInt=60, inHeightInt=100, inWidthInt=100)
        Screen.BoxDraw([lBox, lBox2])

    :param inColorStr: цвет подсветки прямоугольной области. Варианты: 'red', 'green', 'blue'. По умолчанию 'green'
    :type inColorStr: str, необязательный
    :param inThicknessInt: толщина подсветки прямоугольной области. По умолчанию 2
    :type inThicknessInt: int, необязательный
    """
            BoxDraw(inBox=lBox, inColorStr=inColorStr,inThicknessInt = inThicknessInt)
    else:
        # Windows case
        if CrossOS.IS_WINDOWS_BOOL:
            fill = win32defines.BS_NULL
            if inBox is not None:
                """
                Draw an outline around the window.
                * **inColorStr** can be either an integer or one of 'red', 'green', 'blue'
                (default 'green')
                * **inThicknessInt** inThicknessInt of rectangle (default 2)
                * **fill** how to fill in the rectangle (default BS_NULL)
                """
                # don't draw if dialog is not visible
                #if not lWrapperObject.is_visible():
                #    return
                colours = {
                    "green": 0x00ff00,
                    "blue": 0xff0000,
                    "red": 0x0000ff,
                }
                # if it's a known colour
                if inColorStr in colours:
                    inColorStr = colours[inColorStr]
                # create the pen(outline)
                pen_handle = win32functions.CreatePen(
                        win32defines.PS_SOLID, inThicknessInt, inColorStr)
                # create the brush (inside)
                brush = win32structures.LOGBRUSH()
                brush.lbStyle = fill
                brush.lbHatch = win32defines.HS_DIAGCROSS
                brush_handle = win32functions.CreateBrushIndirect(ctypes.byref(brush))
                # get the Device Context
                dc = win32functions.CreateDC("DISPLAY", None, None, None )
                # push our objects into it
                win32functions.SelectObject(dc, brush_handle)
                win32functions.SelectObject(dc, pen_handle)
                # draw the rectangle to the DC
                win32functions.Rectangle(
                    dc, inBox.left, inBox.top, inBox.left+inBox.width, inBox.top+inBox.height)
                # Delete the brush and pen we created
                win32functions.DeleteObject(brush_handle)
                win32functions.DeleteObject(pen_handle)
                # delete the Display context that we created
                win32functions.DeleteDC(dc)
        elif CrossOS.IS_LINUX_BOOL:
                root = tk.Tk()
                # The image must be stored to Tk or it will be garbage collected.
                label = tk.Label(root, bg='red')
                root.overrideredirect(True)
                root.geometry(f"{inBox.width}x{inBox.height}+{inBox.left}+{inBox.top}") # WxH+X+Y
                root.lift()
                root.wm_attributes("-topmost", True)
                #root.wm_attributes("-disabled", True)
                root.wm_attributes("-alpha", 0.85)
                root.wait_visibility(root)
                # Create a canvas widget
                #canvas=tk.Canvas(root, width=500, height=300)
                #canvas.pack()
                # Add a line in canvas widget
                #canvas.create_line(100,200,500,350, fill="green", width=5)
                #label.pack()
                #label.mainloop()
                #root.mainloop()
                time.sleep(1)
                root.destroy()

def BoxAnchorRuleCheck(inBox, inAnchorBox=None, inAnchorRuleStr=None) -> bool:
    """L+,W+: Выполнить проверку соответствия всем условиям вхождения inBox в inAnchorBox с учетом правил inAnchorRule
    
        lBox1 = Screen.BoxCreate(inTopInt=265, inLeftInt=62, inHeightInt=100, inWidthInt=90)
        lBox2 = Screen.BoxCreate(inTopInt=160, inLeftInt=160, inHeightInt=100, inWidthInt=100)
        lBox3 = Screen.BoxCreate(inTopInt=460, inLeftInt=60, inHeightInt=100, inWidthInt=100)

        l = Screen.BoxAnchorRuleCheck(inBox=lBox1, inAnchorBox=[lBox2,lBox3], inAnchorRuleStr=["LD","CUS"])
        Screen.BoxDraw([lBox1,lBox2,lBox3])

    :param inAnchorBox: Экземпляр класса прямоугольной области Box
    :type inAnchorBox: pyscreeze.Box или list из pyscreeze.Box
    :param inAnchorRuleStr: Символьное указание области проверки соответствия. Может принимать единственное значение (единый формат для всех inAnchorBox), или список форматов для каждого inAnchorBox (если inAnchorBox является списком Box)
    :type inAnchorRuleStr: str или list из str
    :return: True - соответствует всем правилам
    """
    # Формирование стартовых переменных
    if inAnchorBox is None: inAnchorBox = []
    if type(inAnchorBox) is not list:
        inAnchorBox = [inAnchorBox]
    lAnchorRuleStr = "CC,S"
    if inAnchorRuleStr is None or inAnchorRuleStr=="": inAnchorRuleStr = [lAnchorRuleStr]
    if type(inAnchorRuleStr) is not list:
        inAnchorRuleStr = [inAnchorRuleStr]

    # Дополнение списка правил до длины якорей, если они расходятся и список правил равен длине 1 или 0 (по умолчанию CC,S)
    if len(inAnchorRuleStr)==1 and len(inAnchorBox)==1:
        if inAnchorRuleStr[0]=="" or inAnchorRuleStr[0] is None: 
            inAnchorRuleStr = [lAnchorRuleStr]
    elif len(inAnchorRuleStr)==1 and len(inAnchorBox)!=1:
        if inAnchorRuleStr[0]!="" and inAnchorRuleStr[0] is not None: 
            lAnchorRuleStr = inAnchorRuleStr[0]
    
    if len(inAnchorRuleStr) != len(inAnchorBox):
        inAnchorRuleStr = []
        for lItem in inAnchorBox:
            inAnchorRuleStr.append(lAnchorRuleStr)

    for lIndexInt, lItemBox in enumerate(inAnchorBox): # Остановиться, если итог False
        lItemRuleStr = inAnchorRuleStr[lIndexInt].upper()
        print(lItemRuleStr)
        # Подготовка вспомогательных областей
        lScreenWidthPXInt = 9999
        lScreenHeightPXInt = 5555
        lAnchorLUBox = BoxCreate(inTopInt=0, inLeftInt=0, inHeightInt=lItemBox.top, inWidthInt=lItemBox.left)
        lAnchorRUBox = BoxCreate(inTopInt=0, inLeftInt=lItemBox.left+lItemBox.width, inHeightInt=lItemBox.top, inWidthInt=lScreenWidthPXInt)
        lAnchorCUBox = BoxCreate(inTopInt=0, inLeftInt=lItemBox.left, inHeightInt=lItemBox.top, inWidthInt=lItemBox.width)
        lAnchorLCBox = BoxCreate(inTopInt=lItemBox.top, inLeftInt=0, inHeightInt=lItemBox.height, inWidthInt=lItemBox.left)
        lAnchorRCBox = BoxCreate(inTopInt=lItemBox.top, inLeftInt=lItemBox.left+lItemBox.width, inHeightInt=lItemBox.height, inWidthInt=lScreenWidthPXInt)
        lAnchorLDBox = BoxCreate(inTopInt=lItemBox.top+lItemBox.height, inLeftInt=0, inHeightInt=lScreenHeightPXInt, inWidthInt=lItemBox.left)
        lAnchorRDBox = BoxCreate(inTopInt=lItemBox.top+lItemBox.height, inLeftInt=lItemBox.left+lItemBox.width, inHeightInt=lScreenHeightPXInt, inWidthInt=lScreenWidthPXInt)
        lAnchorCDBox = BoxCreate(inTopInt=lItemBox.top+lItemBox.height, inLeftInt=lItemBox.left, inHeightInt=lScreenHeightPXInt, inWidthInt=lItemBox.width)
        if "S" not in lItemRuleStr: # Проверка без S - Strict
            lResult = False
            # Алгоритм проверки соответствия хотя бы на одно вхождение
            if ("CC" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lItemBox)==True: lResult = True
            elif ("LU" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorLUBox)==True: lResult = True
            elif ("RU" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorRUBox)==True: lResult = True
            elif ("CU" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorCUBox)==True: lResult = True
            elif ("LC" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorLCBox)==True: lResult = True
            elif ("RC" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorRCBox)==True: lResult = True
            elif ("LD" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorLDBox)==True: lResult = True
            elif ("RD" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorRDBox)==True: lResult = True
            elif ("CD" in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorCDBox)==True: lResult = True
            if lResult == False: break # Остановиться, если итог False
        else: # Проверка с S - Strict
            lResult = True
            # Алгоритм проверки соответствия хотя бы на одно вхождение для того сегмента, который недоступен
            if ("CC" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lItemBox)==True: lResult = False
            elif ("LU" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorLUBox)==True: lResult = False
            elif ("RU" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorRUBox)==True: lResult = False
            elif ("CU" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorCUBox)==True: lResult = False
            elif ("LC" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorLCBox)==True: lResult = False
            elif ("RC" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorRCBox)==True: lResult = False
            elif ("LD" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorLDBox)==True: lResult = False
            elif ("RD" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorRDBox)==True: lResult = False
            elif ("CD" not in lItemRuleStr) and BoxOverlay(inBox1=inBox, inBox2=lAnchorCDBox)==True: lResult = False

def BoxOverlay(inBox1, inBox2) -> bool:
    """L+,W+:Проверить наложение 2-х прямоугольных областей друг на друга.
    
        lBox1 = Screen.BoxCreate(inTopInt=10, inLeftInt=10, inHeightInt=100, inWidthInt=1000)
        Screen.BoxDraw([lBox1, lBox2])
        Screen.BoxOverlay(lBox1,lBox2)

    :param inBox1: Экземпляр класса прямоугольной области Box
    :type inBox1: pyscreeze.Box
    :param inBox2: Экземпляр класса прямоугольной области Box
    :type inBox2: pyscreeze.Box
    :return: True - inBox1 наложен на inBox2
    """
    return not ((inBox1.left>inBox2.left + inBox2.width or inBox2.left>inBox1.left + inBox1.width) or (inBox1.top>inBox2.top + inBox2.height or inBox2.top>inBox1.top + inBox1.height))

import re
def BoxGetPoint(inBox, inPointRuleStr="CC") -> pyscreeze.Point:
    """L+,W+:Получить точку pyscreeze.Point по заданной прямоугольной области pyscreeze.Box и строковому параметру расположения inPointRuleStr.

        # Screen: Взаимодействие с мышью объектами экрана
        lPoint = Screen.BoxGetPoint(inBox=lBox1, inPointRuleStr="LC")

    :param inBox: Прямоугольная область на экране
    :type inBox: pyscreeze.Box, обязательный
    :param inPointRuleStr: Правило идентификации точки на прямоугольной области (правила формирования см. выше). Варианты: "LU","CU","RU","LC","CC","RC","LD","CD","RD".  По умолчанию "CC"
    :return: Точка на экране
    :rtype: pyscreeze.Point
    """
    lPoint = None
    inPointRuleStr = inPointRuleStr.upper()
    if "CC" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left + inBox.width/2,y=inBox.top + inBox.height/2)
    elif "LU" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left,y=inBox.top)
    elif "RU" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left + inBox.width,y=inBox.top)
    elif "CU" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left + inBox.width/2,y=inBox.top)
    elif "LC" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left,y=inBox.top + inBox.height/2)
    elif "RC" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left + inBox.width,y=inBox.top + inBox.height/2)
    elif "LD" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left,y=inBox.top + inBox.height)
    elif "CD" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left + inBox.width/2,y=inBox.top + inBox.height)
    elif "RD" in inPointRuleStr: lPoint = pyscreeze.Point(x=inBox.left + inBox.width,y=inBox.top + inBox.height)
    # Корректировка при необходимости
    lDXInt=0
    lDYInt=0
    lMatchY = re.search(r'.*Y([-+]?\d*).*', inPointRuleStr)
    if lMatchY is not None: lDYInt=int(lMatchY.group(1))
    lMatchX = re.search(r'.*X([-+]?\d*).*', inPointRuleStr)
    if lMatchX is not None: lDXInt=int(lMatchX.group(1))
    lPoint = PointModify(inPoint=lPoint,inDXInt=lDXInt,inDYInt=lDYInt)
    return lPoint

def PointModify(inPoint, inDXInt, inDYInt) -> pyscreeze.Point:
    """L+,W+:Скорректировать точку pyscreeze.Point.

        lPoint = Screen.PointCreate(inXInt=10, inYInt=10)
        lPoint = Screen.PointModify(inPoint=lPoint, inDXInt=90, inDYInt=10)

    :param inPoint: Точка на экране, по которой выполнить нажатие мыши
    :type inPoint: pyscreeze.Point, обязательный
    :param inDXInt: Смещение указателя мыши по оси X (горизонтальная ось). 
    :param inDYInt: Смещение указателя мыши по оси Y (вертикальная ось). 
    """
    return PointCreate(inXInt=inPoint.x+inDXInt, inYInt=inPoint.y+inDYInt)

def PointCreate(inXInt, inYInt):
    """L+,W+:Создать точку pyscreeze.Point.

    :param inXInt: Смещение указателя мыши по оси X (горизонтальная ось). 
    :param inYInt: Смещение указателя мыши по оси Y (вертикальная ось). 
    """
    return pyscreeze.Point(x=inXInt,y=inYInt)

def PointClick(inPoint:pyscreeze.Point, inClickCountInt:int=1, inIntervalSecFloat:float=0.0, inButtonStr:str='left', inMoveDurationSecFloat:float=0.0, inWaitAfterSecFloat:float=None):
    """L+,W+:Нажатие (вниз) кнопки мыши и затем немедленно выпуск (вверх) её. Допускается следующая параметризация.

        lPoint = Screen.PointCreate(100,150)
        Screen.PointClick(lPoint) #Выполнить нажатие левой клавиши мыши

    """
    Mouse.Click(inXInt=inPoint.x, inYInt=inPoint.y, inClickCountInt=inClickCountInt, inIntervalSecFloat=inIntervalSecFloat, inButtonStr=inButtonStr, inMoveDurationSecFloat=inMoveDurationSecFloat, inWaitAfterSecFloat=inWaitAfterSecFloat)

def PointClickDouble(inPoint:pyscreeze.Point, inWaitAfterSecFloat:float=None):
    """L+,W+:Двойное нажатие левой клавиши мыши. Данное действие аналогично вызову функции (см. ниже).

        Screen.PointClickDouble(lPoint) #Выполнить двойное нажатие левой клавиши мыши

    """
    Mouse.ClickDouble(inXInt=inPoint.x, inYInt=inPoint.y, inWaitAfterSecFloat=inWaitAfterSecFloat)

def PointDown(inPoint:pyscreeze.Point, inButtonStr:str='left', inWaitAfterSecFloat:float=None):
    """L+,W+:Переместить указатель по координатам inPoint, после чего нажать (вниз) клавишу мыши и не отпускать до выполнения соответсвующей команды (см. Up). 

        Screen.PointDown(lPoint)

    """
    Mouse.Down(inXInt=inPoint.x, inYInt=inPoint.y,inButtonStr=inButtonStr, inWaitAfterSecFloat=inWaitAfterSecFloat)

def PointUp(inPoint:pyscreeze.Point, inButtonStr:str='left', inWaitAfterSecFloat:float=None):
    """L+,W+:Отпустить (вверх) клавишу мыши.

        Screen.PointUp(lPoint)

    """
    Mouse.Up(inXInt=inPoint.x, inYInt=inPoint.y,inButtonStr=inButtonStr, inWaitAfterSecFloat=inWaitAfterSecFloat)

def PointMoveTo(inPoint:pyscreeze.Point, inWaitAfterSecFloat:float=None):
    """L+,W+:Переместить указатель мыши на позицию inXInt, inYInt за время inMoveDurationSecFloat.

        Screen.PointMoveTo(inXInt=100, inYInt=200)

    """
    Mouse.MoveTo(inXInt=inPoint.x, inYInt=inPoint.y, inWaitAfterSecFloat=inWaitAfterSecFloat)

def ImageLocateAll(inImgPathStr:str, inIsGrayModeBool:bool=False, inConfidenceFloat:float=None) -> list:
    """L+W+: Искать на экране графические объекты, которые похожи на inImgPathStr. Вернуть список прямоугольных областей на экране (pyscreeze.Box)

    !ВНИМАНИЕ! Для использования параметра точности inConfidenceFloat необходим пакет Python opencv-python (python -m pip install opencv-python)

        # Screen: Взаимодействие с объектами экрана
        Screen.ImageLocateAll(inImgPathStr="Button.png",inConfidenceFloat=0.9)

    :param inImgPathStr: Путь к изображению, которое требуется искать на экране
    :type inImgPathStr: str, относительный или абсолютный
    :param inIsGrayModeBool: True - выполнить поиск изображения в режиме серых оттенков (ускоряет производительность, если допускается искажение цвета). По умолчанию False
    :type inIsGrayModeBool: bool, опционально
    :param inConfidenceFloat: Показатель точности. 1.0 - идентичное соответствие, 0.0 - полное несоответствие. По умолчанию 1.0 (None)
    :type inConfidenceFloat: float, опционально
    :return: Список из pyscreeze.Box
    """
    if inConfidenceFloat is None: lLocateList = pyautogui.locateAllOnScreen(image = inImgPathStr, grayscale = inIsGrayModeBool)
    else: lLocateList = pyautogui.locateAllOnScreen(image = inImgPathStr, grayscale = inIsGrayModeBool, confidence = inConfidenceFloat)
    for lItem in lLocateList:
        lResult.append(lItem)

def ImageExists(inImgPathStr:str, inIsGrayModeBool:bool=False, inConfidenceFloat:float=None) -> list:
    """L+,W+:Проверить, имеется ли на экране хотя бы один подходящий объект. Вернуть булево значение

        lResult = Screen.ImageExists(inImgPathStr="Button.png",inConfidenceFloat=0.9)

    """
    lLocateList = ImageLocateAll(inImgPathStr=inImgPathStr,inIsGrayModeBool=inIsGrayModeBool, inConfidenceFloat=inConfidenceFloat)
    return len(lLocateList)>0

def ImageWaitAppear(inImgPathStr:str, inWaitSecFloat:float=IMAGE_WAIT_SEC_FLOAT, inWaitIntervalSecFloat:float = IMAGE_WAIT_INTERVAL_SEC_FLOAT, inIsGrayModeBool:bool=False, inConfidenceFloat:float=None) -> list:
    """L+,W+:Ожидать появление изображения на протяжении inWaitSecFloat секунд. Проверять с периодичностью inWaitIntervalSecFloat. Вернуть список прямоугольных областей, которые удовлетворяют условию

        lBoxList = Screen.ImageWaitAppear(inImgPathStr="Button.png",inConfidenceFloat=0.9)

    :param inWaitSecFloat: Время ожидания появления изображения в сек. По умолчанию IMAGE_WAIT_SEC_FLOAT (60)
    :param inWaitIntervalSecFloat: Интервал повторной проверки наличия изображения. По умолчанию IMAGE_WAIT_INTERVAL_SEC_FLOAT (1)
    :return: Список из pyscreeze.Box или [] если прошло время ожидания.
    """
        lResultList = ImageLocateAll(inImgPathStr=inImgPathStr,inIsGrayModeBool=inIsGrayModeBool, inConfidenceFloat=inConfidenceFloat)

def ImageWaitDisappear(inImgPathStr:str, inWaitSecFloat:float=IMAGE_WAIT_SEC_FLOAT, inWaitIntervalSecFloat:float = IMAGE_WAIT_INTERVAL_SEC_FLOAT, inIsGrayModeBool:bool=False, inConfidenceFloat:float=None):
    """L+,W+:Ожидать исчезновение изображения на протяжении inWaitSecFloat секунд. Проверять с периодичностью inWaitIntervalSecFloat.

        Screen.ImageWaitDisappear(inImgPathStr="Button.png",inConfidenceFloat=0.9)

    """

def ImageClick(inImgPathStr:str,inBoxIndexInt:int = 0, inPointRuleStr:str="CC",  inIsGrayModeBool:bool=False, inConfidenceFloat:float=None, inWaitSecFloat:float=0, inWaitIntervalSecFloat:float = 0):
    """L+,W+:Выполнить поиск прямоугольной области по изображению.

        Screen.ImageClick(inImgPathStr="Button.png",inConfidenceFloat=0.9)

    :param inBoxIndexInt: Индекс прямоугольной области, по которой выполнить клик (если обнаружено несколько областей Box), По умолчанию 0
    :type inBoxIndexInt: int, опционально
    :param inWaitSecFloat: Время ожидания появления изображения в сек. По умолчанию 0
    :param inWaitIntervalSecFloat: Интервал повторной проверки наличия изображения. По умолчанию 0
    """
    if inWaitSecFloat > 0:
        lBoxList = ImageWaitAppear(inImgPathStr=inImgPathStr,inWaitSecFloat=inWaitSecFloat,inWaitIntervalSecFloat=inWaitIntervalSecFloat,inIsGrayModeBool=inIsGrayModeBool,inConfidenceFloat=inConfidenceFloat)
        if len(lBoxList)>0: PointClick(inPoint=BoxGetPoint(lBoxList[inBoxIndexInt],inPointRuleStr=inPointRuleStr))
    else:
        PointClick(BoxGetPoint(inBox=ImageLocateAll(inImgPathStr=inImgPathStr,inIsGrayModeBool=inIsGrayModeBool,inConfidenceFloat=inConfidenceFloat)[inBoxIndexInt],inPointRuleStr=inPointRuleStr))

"""
# How to use
# from pyOpenRPA.Tools import StopSafe
# StopSafe.Init(inLogger=None)
# StopSafe.IsSafeStop() # True - WM_CLOSE SIGNAL has come. taskkill /im someprocess.exe
"""

import win32con
import win32gui
import os
gIsSignalCloseBool = False
gLogger = None
gWindowTitleStr = "PythonTerminator" # Title of the phantom window
gWindowDescriptionStr = "pyOpenRPA library for safe turn off the program (by send the WM_CLOSE signal from task kill)" # Description of the phantom window

def Init(inLogger=None):
    """
    Init the StopSafe module. After that you can use def IsStopSafe() to check if close signal has come.

    :param inLogger: Logger to log messages about StopSafe
    :return:
    """
    global gLogger
    global gIsSignalCloseBool
    gIsSignalCloseBool = False # Init default
    gLogger = inLogger
    import threading
    if gLogger: gLogger.info(f"StopSafe: Init termination catch thread")
    shutdown_thread = threading.Thread(target=_shutdown_monitor)
    shutdown_thread.start()
    #shutdown_thread.join()
    #shutdown_monitor()
    
def IsStopSafe():
    """
    Check if stop signal has come.

    :return:
    """
    global gIsSignalCloseBool # Init the global variable
    return gIsSignalCloseBool # Return the result

def _shutdown_monitor():
    def wndproc(hwnd, msg, wparam, lparam):
        if msg == win32con.WM_CLOSE:
            win32gui.DestroyWindow(hwnd)
            return 0
        elif msg == win32con.WM_DESTROY:
            win32gui.PostQuitMessage(0)
            return 0
        return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
    wc = win32gui.WNDCLASS()
    wc.lpszClassName = gWindowTitleStr
    wc.lpfnWndProc = wndproc
    win32gui.RegisterClass(wc)
    hwnd = win32gui.CreateWindow(gWindowTitleStr, gWindowDescriptionStr,
                0, 0, 0, 0, 0, 0, 0, 0, None)
    win32gui.PumpMessages()
    gIsSignalCloseBool = True # WM_CLOSE message has come
    if gLogger:
        gLogger.info(f"StopSafe: Program has catch VM_CLOSE signal - do safe exit")
    
import os
import threading
import pdb

"""Module wait file "init_debug" in working directory
"""
    
gKWARGS = None

def LiveDebugCheckLoop():
        if os.path.exists("init_debug"):
            pdb.set_trace()
        time.sleep(30.0)

def LiveDebugCheckThread(**inKWARGS):
    """Create thread to wait file appear "init_debug" in the working directory. 
    """
    global gKWARGS
    gKWARGS = inKWARGS
    lThread = threading.Thread(target=LiveDebugCheckLoop)
    lThread.start()