		.. |release| replace:: v1.2.12
		.. |author| replace:: Ivan Maslov <ivan.maslov@unicodelabs.ru>
	
Action: Turn mouse on the UI object you are interested and hold the «Ctrl» key for 3 seconds

Action: Turn mouse on the UI object you are interested and hold the "Ctrl" key for 3 seconds

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__)

GSettingsAutocleaner() (в модуле 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__)

RDPSessionDublicatesResolve() (в модуле 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__)

RDPSessionResponsibilityCheck() (в модуле 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__)

JSActivityListExecute() (в модуле pyOpenRPA.Orchestrator.Web.Basic)

JSProcessorActivityListAdd() (в модуле pyOpenRPA.Orchestrator.Web.Basic)

KeyTurpleGet() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

Manual2Auto() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

ManualStopListClear() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

ManualStopTriggerSet() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

MuteWait() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

RestartForce() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

RestartSafe() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StartCheck() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusChangeLog() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusCheck() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusCheckStart() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusCheckStopForce() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusCheckStopSafe() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusRestore() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StatusSave() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StopForce() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StopForceCheck() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StopSafe() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

StopSafeCheck() (метод pyOpenRPA.Orchestrator.Managers.Process.Process)

ProcessExists() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessInitSafe() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessManual2Auto() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessManualStopListClear() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessManualStopTriggerSet() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessScheduleStatusCheckEverySeconds() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

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

ProcessStatusCheck() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessStatusRestore() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessStatusSave() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessStatusStrGet() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessStopForce() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ProcessStopSafe() (в модуле pyOpenRPA.Orchestrator.Managers.Process)

ControlPanel (класс в pyOpenRPA.Orchestrator.Managers.ControlPanel)

OnInitJSStr() (метод pyOpenRPA.Orchestrator.Managers.ControlPanel.ControlPanel)

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_SafeOtherGet_Process() (в модуле pyOpenRPA.Robot.UIDesktop)

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

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

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

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

UIOSelectorsSecs_WaitDisappear_List() (в модуле 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 subprocess import CREATE_NEW_CONSOLE # Flag to create new process in another CMD
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:
            lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=None, stderr=None,
                                           creationflags=CREATE_NEW_CONSOLE)
        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, win32security, 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
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):
    """
    Add activity in AgentDict. Check if item is created

    :param inGSettings: Global settings dict (singleton)
    :param inHostNameStr: Agent host name
    :param inUserStr: User login, where agent is based
    :param inActivityItemDict: ActivityItem
    :return: GUID String of the ActivityItem - you can wait (sync or async) result by this guid!
    """

    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):
    """
    Check by GUID if ActivityItem has exists in request list. If exist - the result response has not been recieved from the agent

    :param inGUIDStr: GUID String of the ActivityItem
    :return: True - ActivityItem is exist in AgentDict ; False - else case
    """
    # 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):
    """
    Check by GUID if ActivityItem has been executed and result has come to the Orchestrator

    :param inGUIDStr: GUID String of the ActivityItem - you can wait (sync or async) result by this guid!
    :return: True - result has been received from the Agent to orc; False - else case
    """

    return inGUIDStr in inGSettings["AgentActivityReturnDict"]

def AgentActivityItemReturnGet(inGUIDStr, inCheckIntervalSecFloat = 0.5, inGSettings=None):
    """
    Work synchroniously! Wait while result will be recieved. Get the result of the ActivityItem execution on the Agent side. Before this please check by the def AgentActivityItemReturnExists that result has come to the Orchestrator

    !ATTENTION! Use only after Orchestrator initialization! Before orchestrator init exception will be raised.

    :param inCheckIntervalSecFloat: Interval in sec of the check Activity Item result
    :return: Result of the ActivityItem executed on the Agent side anr transmitted to the Orchestrator. IMPORTANT! ONLY JSON ENABLED Types CAN BE TRANSMITTED TO ORCHESTRATOR!
    """
    #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):
    """
    Send CMD to OS thought the pyOpenRPA.Agent daemon. Result return to log + Orchestrator by the A2O connection

    :param inHostNameStr: Agent host name in upper case (example "RPA01", "RPA_99" and so on). Active agent session you can see on the orchestrator dashboard as Orchestrator admin
    :param inUserStr: Agent user name in upper case (example "UserRPA"). Active agent session you can see on the orchestrator dashboard as Orchestrator admin
    """
    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 the agent user session

    """
    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):
    """
   Send the file from the Orchestrator to Agent (synchroniously) pyOpenRPA.Agent daemon process (safe for JSON transmition).
   Work safety with big files
   Thread safe - you can call def even if you dont init the orchestrator - def will be executed later

    :param inHostNameStr:
    :param inUserStr:
    :param inFilePathStr:
    :param inFileDataBytes:
    """

    # 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):
    """
    Create binary file by the base64 string by the pyOpenRPA.Agent daemon process (safe for JSON transmition)

    """
    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):
    """
    Create binary file by the base64 string by the pyOpenRPA.Agent daemon process (safe for JSON transmission)

    :param inFileDataBase64Str:
    """
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataBase64Str":inFileDataBase64Str}, # Args dictionary
    }

def AgentOSFileBinaryDataBase64StrAppend(inHostNameStr, inUserStr, inFilePathStr, inFileDataBase64Str, inGSettings = None):
    """
    Append binary file by the base64 string by the pyOpenRPA.Agent daemon process (safe for JSON transmission)

    """
        "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):
    """
    Create text file by the string by the pyOpenRPA.Agent daemon process

    :param inFileDataStr:
    :param inEncodingStr:
    """
        "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):
    """

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

def AgentOSFileBinaryDataReceive(inHostNameStr, inUserStr, inFilePathStr):
    """
    Read binary file from agent (synchronious)

    :return: file data bytes
    """
    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):
    """

    :param inEncodingStr: Text file encoding. Default 'utf-8'
    """
        "Def":"OSFileTextDataStrReceive", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgDict":{"inFilePathStr":inFilePathStr, "inEncodingStr": inEncodingStr}, # Args dictionary
    }

def AgentProcessWOExeUpperUserListGet(inHostNameStr, inUserStr, inGSettings = None):
    """

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

# OS DEFS

def OSLogoff():
    """
    Logoff the current orchestrator session
    :return:
    """
    os.system("shutdown /l")

def OSCredentialsVerify(inUserStr, inPasswordStr, inDomainStr=""): ##
    """
    Verify user credentials in windows. Return bool

    :param inPasswordStr:
    :param inDomainStr:
    :return: True - Credentials are actual; False - Credentials are not actual
    """
    try:
        hUser = win32security.LogonUser(
            inUserStr,inDomainStr, inPasswordStr,
            win32security.LOGON32_LOGON_NETWORK, win32security.LOGON32_PROVIDER_DEFAULT
        )
    except win32security.error:
        return False
    else:
        return True

def OSRemotePCRestart(inHostStr, inForceBool=True, inLogger = None):
    """
    Send signal via power shell to restart remote PC
    ATTENTION: Orchestrator user need to have restart right on the Remote machine to restart PC.

    :param inLogger: logger to log powershell result in logs
    :param inHostStr: PC hostname which you need to restart.
    :param inForceBool: True - send signal to force retart PC; False - else case
    :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 OSCMD(inCMDStr, inRunAsyncBool=True, inLogger = None):
    """
    OS send command in shell locally

    :param inCMDStr:
    :param inRunAsyncBool:
    :param inLogger:
    :return: CMD result string
    """
    if inRunAsyncBool == True:
    def _CMDRunAndListenLogs(inCMDStr, inLogger):
        lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=CREATE_NEW_CONSOLE)
        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):
    """
    Orchestrator restart

    """
    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
    os.execl(sys.executable, os.path.abspath(__file__), *sys.argv)
    sys.exit(0)

def OrchestratorLoggerGet() -> logging.Logger:
    """
    Get the logger from the Orchestrator

    :return:
    """
    return GSettingsGet().get("Logger",None)

def OrchestratorScheduleGet() -> schedule:
    """
    Get the schedule (schedule.readthedocs.io) from the Orchestrator

    Fro example you can use:

    .. code-block:: python
        # One schedule threaded
        Orchestrator.OrchestratorScheduleGet().every(5).seconds.do(lProcess.StatusCheckStart)

        #New schedule thread # See def description Orchestrator.OrchestratorThreadStart
        Orchestrator.OrchestratorScheduleGet().every(5).seconds.do(Orchestrator.OrchestratorThreadStart,lProcess.StatusCheckStart)

    :return: schedule module. Example see here https://schedule.readthedocs.io/en/stable/examples.html
    """
    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):
    """
    Execute def in new thread and pass some args with list and dict types

    :param inDef: Python Def
    :param inArgList: args as list
    :param inArgDict: args as dict
    :return: threading.Thread object
    """
    lDefThread = threading.Thread(target=inDef,args=inArgList,kwargs=inArgDict)
    lDefThread.start()
    return lDefThread

def OrchestratorIsAdmin():
    """
    Check if Orchestrator process is running as administrator

    :return: True - run as administrator; False - not as administrator
    """
    try:
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:

def OrchestratorIsInited() -> bool:
    """Check if Orchestrator initial actions were processed

    :return: True - orc is already inited; False - else
    :rtype: bool
    """    

    return Core.IsOrchestratorInitialized(inGSettings=GSettingsGet())

def OrchestratorInitWait() -> None:
    """Wait thread while orc will process initial action. 
    ATTENTION: DO NOT CALL THIS DEF IN THREAD WHERE ORCHESTRATOR MUST BE INITIALIZED - INFINITE LOOP
    """
    lIntervalSecFloat = 0.5
    while not OrchestratorIsInited():
        time.sleep(lIntervalSecFloat)
    
def OrchestratorRerunAsAdmin():
    """
    Check if not admin - then rerun orchestrator as administrator

    """
    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):
    """
    Search the py files by the glob and do the safe init (in try except). Also add inited module in sys.modules as imported (module name = file name without extension).
    You can init CP in async way!

        # USAGE VAR 1 (without the def  auto call)
        # Autoinit control panels starts with CP_
        Orchestrator.OrchestratorPySearchInit(inGlobPatternStr="ControlPanel\\CP_*.py")

        # USAGE VAR 2 (with the def auto call) - for the backward compatibility CP for the Orchestrator ver. < 1.2.7
        Orchestrator.OrchestratorPySearchInit(inGlobPatternStr="ControlPanel\\CP_*.py", inDefStr="SettingsUpdate", inDefArgNameGSettingsStr="inGSettings")

        # INFO: The code above will replace the code below
        ## !!! 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: example"..\\*\\*\\*X64*.cmd"
    :param inDefStr: OPTIONAL The string name of the def. For backward compatibility if you need to auto call some def from initialized module
    :param inDefArgNameGSettingsStr: OPTIONAL The name of the GSettings argument in def (if exists)
    :param inAsyncInitBool: OPTIONAL True - init py modules in many threads - parallel execution. False (default) - sequence execution
    :return: { "ModuleNameStr":{"PyPathStr": "", "Module": ...},  ...}
    """

    # # # # # # # #
    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 is not "":
                lDef = getattr(lTechModuleFromSpec, inDefStr)
                lArgDict = {}
                if inDefArgNameGSettingsStr is not None and inDefArgNameGSettingsStr is not "":
                    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):
    """
    Orchestrator session save in file
    (from version 1.2.7)
        _SessionLast_GSettings.pickle (binary)

    (above the version 1.2.7)
        _SessionLast_RDPList.json (encoding = "utf-8")
        _SessionLast_StorageDict.pickle (binary)

    :return: True every time
    """
    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):
    """
    Check _SessioLast... files in working directory. if exist - load into gsettings

    :return:
    """
    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:
    """
    Check is client is has access for the key list

    :param inRequest: request handler (from http.server import BaseHTTPRequestHandler)
    :param inRoleKeyList:
    :return: bool
    """
    return inRequest.UACClientCheck(inRoleKeyList=inRoleKeyList)

def UACUserDictGet(inRequest) -> dict:
    """
    Return user UAC hierarchy dict of the inRequest object. Empty dict - superuser access

    :return: user UAC hierarchy dict
    """
    return inRequest.UserRoleHierarchyGet() # get the Hierarchy

def UACUpdate(inADLoginStr, inADStr="", inADIsDefaultBool=True, inURLList=None, inRoleHierarchyAllowedDict=None, inGSettings = None):
    """
    Update user access (UAC)

    :param inADLoginStr:
    :param inADStr:
    :param inADIsDefaultBool:
    :param inURLList:
    :param inRoleHierarchyAllowedDict:
    """
    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):
    """
    Add supertoken for the all access (it is need for the robot communication without human)

    :param inSuperTokenStr:
    """
    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 ="/"):
    """Change the index page of the orchestrator if you dont want the '/' (main) path

    :param inURLIndexStr: New url for the index page of the orchestrator, defaults to "/"
    :type inURLIndexStr: str, optional
    """
    GSettingsGet()["ServerDict"]["URLIndexStr"] = inURLIndexStr

def WebURLConnectDef(inMethodStr, inURLStr, inMatchTypeStr, inDef, inContentTypeStr="application/octet-stream", inGSettings = None, inUACBool = None):
    """
     Connect URL to DEF
        "inMethodStr":"GET|POST",
        "inURLStr": "/index", #URL of the request
        "inMatchTypeStr": "", #"BeginWith|Contains|Equal|EqualCase",
        "inContentTypeStr": "", #HTTP Content-type
        "inDef": None #Function with str result

    :param inMethodStr: "GET|POST",
    :param inURLStr: example "/index", #URL of the request
    :param inMatchTypeStr: #"BeginWith|Contains|Equal|EqualCase",
    :param inDef: def arg allowed list: 2:[inRequest, inGSettings], 1: [inRequest], 0: []
    :param inContentTypeStr: default: "application/octet-stream"
    :param inUACBool: default: None; True - check user access before do this URL item. None - get Server flag if ask user
    """
    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):
    """
    Connect URL to Folder
        "inURLStr": "/Folder/", #URL of the request
        "inFolderPathStr": "", #Absolute or relative path
        "inUACBool"

    :param inMethodStr:
    :param inURLStr:
    :param inMatchTypeStr:
    :param inFolderPathStr:
    :param inUseCacheBool: True - cache this page - dont open ever
    """
    # 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):
    """
    Connect URL to File

    :param inContentTypeStr: If none - autodetect
    """
        "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):
    """
    Create listen interface for the web server

    :param inGSettings:  Global settings dict (singleton)
    :param inAddressStr: IP interface to listen
    :param inPortInt: Port int to listen for HTTP default is 80; for HTTPS default is 443
    :param inCertFilePEMPathStr: Path to .pem (base 64) certificate. Required for SSL connection. ATTENTION - do not use certificate with password
    :param inKeyFilePathStr: Path to the private key file
    :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):
    """
    Add control panel HTML, JSON generator or JS when page init

    :param inCPKeyStr:
    :param inHTMLRenderDef:
    :param inJSONGeneratorDef:
    :param inJSInitGeneratorDef:
    """
    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="-"):
    """
    Create message string with request user details (IP, Login etc...). Very actual for IT security in big company.

        # USAGE
        from pyOpenRPA import Orchestrator

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

        # Log the WebAudit message
        lLogger.info(lWebAuditMessageStr)

    :param inRequest: HTTP request handler. Optional if call def from request thread
    :param inOperationCodeStr: operation code in string format (actual for IT audit in control panels)
    :param inMessageStr: additional message after
    :return: format "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):
    """
    Extract the body in bytes from the request

    :param inRequest: inRequest from the server. Optional if call def from request thread
    :return: Bytes or 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):
    """
    Extract the body in str from the request

    :return: str or None
    """
    return WebRequestParseBodyBytes(inRequest=inRequest).decode('utf-8')

def WebRequestParseBodyJSON(inRequest=None):
    """
    Extract the body in dict/list from the request

    :return: dict or list
    """
    return json.loads(WebRequestParseBodyStr(inRequest=inRequest))

def WebRequestParsePath(inRequest=None):
    """
    Parse the request - extract the url. Example: /pyOpenRPA/Debugging/DefHelper/...

    :return: Str, Example: /pyOpenRPA/Debugging/DefHelper/...
    """
    return urllib.parse.unquote(inRequest.path)

def WebRequestParseFile(inRequest=None):
    """
    Parse the request - extract the file (name, body in bytes)

    :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):
    """
    Send response for the request

    :return:
    """
    inRequest.OpenRPAResponseDict["Body"] = bytes(inResponeStr, "utf8")

def WebRequestGet():
    """
    Return the web request instance if current thread was created by web request from client. else return None

    """
    lCurrentThread = threading.current_thread()
    if hasattr(lCurrentThread, "request"):
        return lCurrentThread.request

def WebUserInfoGet(inRequest=None):
    """
    Return User info about request

    :return: {"DomainUpperStr": "", "UserNameUpperStr": ""}
    """
    lDomainUpperStr = inRequest.OpenRPA["Domain"].upper()
    lUserUpperStr = inRequest.OpenRPA["User"].upper()
    return {"DomainUpperStr": lDomainUpperStr, "UserNameUpperStr": lUserUpperStr}

def WebUserIsSuperToken(inRequest = None, inGSettings = None):
    """
    Return bool if request is authentificated with supetoken (token which is never expires)

    :return: bool True - is supertoken; False - is not supertoken
    """
    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):
    """
    Return User UAC Hierarchy DICT Return {...}

    :return: UAC Dict {}
    """
    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):
    """
    Get the GSettings from the singleton module.

    :param inGSettings: You can pass some GSettings to check if it equal to base gsettings. If not equal - def will merge it
    :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):
    """
    Set value in GSettings by the key list

    :param inValue:
    :param inKeyList:
    """
    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):
    """
    Get the value from the GSettings by the key list

    :return: value any type
    """
            pass
        else:
    return lDict.get(inKeyList[-1],None)

def GSettingsKeyListValueAppend(inValue, inKeyList=None, inGSettings = None):
    """
    Append value in GSettings by the key list

        # USAGE

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

    :param inValue: Any value to be appended in gSettings Dict by the key list
    :param inKeyList: List of the nested keys (see example)
    """
            pass
        else:
    lDict[inKeyList[-1]].append(inValue) #Set value

def GSettingsKeyListValueOperatorPlus(inValue, inKeyList=None, inGSettings = None):
    """
    Execute plus operation between 2 lists (1:inValue and 2:gSettings by the inKeyList)

        # USAGE

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

    :param inValue: List with values to be merged with list in gSettings
    """
            pass
        else:
    lDict[inKeyList[-1]] += inValue #Set value

def StorageRobotExists(inRobotNameStr):
    """
    Check if robot storage exists

    :param inRobotNameStr: Robot name (case sensitive)
    :return: True - robot storage exist; False - does not exist
    """
    return inRobotNameStr in GSettingsGet()["StorageDict"]

def StorageRobotGet(inRobotNameStr):
    """
    Get the robot storage by the robot name. If Robot storage is not exist - function will create it

    :return: Dict
    """
    if inRobotNameStr not in GSettingsGet()["StorageDict"]:
        GSettingsGet()["StorageDict"][inRobotNameStr]={}
    return GSettingsGet()["StorageDict"][inRobotNameStr]

def ProcessorAliasDefCreate(inDef, inAliasStr=None, inGSettings = None):
    """
    Create alias for def (can be used in ActivityItem in field Def)
    !WHEN DEF ALIAS IS REQUIRED! - Def alias is required when you try to call Python def from the Orchestrator WEB side (because you can't transmit Python def object out of the Python environment)
    Deprecated. See 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: Def
    :param inAliasStr: String alias for associated def
    :return: str Alias string (Alias can be regenerated if previous alias was occupied)
    """
    return ActivityItemDefAliasCreate(inDef=inDef, inAliasStr=inAliasStr, inGSettings = inGSettings)

def ProcessorAliasDefUpdate(inDef, inAliasStr, inGSettings = None):
    """
    Update alias for def (can be used in ActivityItem in field Def).
    Deprecated. See ActivityItemDefAliasUpdate

        # USAGE

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

    :return: str Alias string
    """
    return ActivityItemDefAliasUpdate(inDef=inDef, inAliasStr=inAliasStr, inGSettings = inGSettings)

# ActivityItem defs
def ActivityItemHelperDefList(inDefQueryStr=None):
    """
     Create list of the available Def names in activity item. You can use query def filter via arg inDefQueryStr

    :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):
    """
    Detect def by the name and prepare the activity item dict with values.

    :param inDef:
    :return:
    """
    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):
    """
    Create activity item. Activity item can be used as list item in ProcessorActivityItemAppend or in Processor.ActivityListExecute.

        # USAGE

        # EXAMPLE 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"
        #   }

        # EXAMPLE 2
        def TestDef(inArg1Str):
            pass
        Orchestrator.ActivityItemDefAliasUpdate(
            inDef = "TestDefAlias",
            inArgGSettingsStr = None,
            inArgLoggerStr = None)
        #   {
        #       "Def":"TestDefAlias",
        #       "ArgGSettings": None,
        #       "ArgLogger": None
        #   }

    :param inDef: def link or def alias (look gSettings["Processor"]["AliasDefDict"])
    :param inArgList: Args list for the Def
    :param inArgDict: Args dict for the def
    :param inArgGSettingsStr: Name of def argument of the GSettings dict
    :param inArgLoggerStr: Name of def argument of the logging object
    :param inGUIDStr: GUID which you can specify. If None the GUID will be generated
    :param inThreadBool: True - execute ActivityItem in new thread; False - in processor thread
    :return: {}
    """
    # 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):
    """

        # USAGE

            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():
    """
    Load all def from sys.modules... in ActivityItem def alias dict

    :return: None
    """
    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):
    """
    Deprecated. See ActivityItemCreate

        # USAGE

        # EXAMPLE 1
            pass
        lActivityItem = Orchestrator.ProcessorActivityItemCreate(
        #   {
        #   }

        # EXAMPLE 2
            pass
        #   {
        #   }

    """
    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):
    """
    Create and add activity item in processor queue.

        # 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 inArgDict: Args dict for the Def
    :param inActivityItemDict: Fill if you already have ActivityItemDict (don't fill inDef, inArgList, inArgDict, inArgGSettingsStr, inArgLoggerStr)
    :return ActivityItem GUIDStr
    """
    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
    """
    Check if there is any running process that contains the given name processName.

        # USAGE

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

    :param inProcessNameWOExeStr: Process name WithOut (WO) '.exe' postfix. Example: "notepad" (not "notepad.exe")
    :return: True - process is running on the orchestrator machine; False - process is not running on the orchestrator machine
    """
    #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):
    """
    Start process locally. Extra feature: Use inStopProcessNameWOExeStr to stop the execution if current process is running.

        # USAGE

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

    :param inPathStr: Command to send in CMD
    :param inArgList: List of the arguments for the CMD command. Example: ["test.txt"]
    :param inStopProcessNameWOExeStr: Trigger: stop execution if process is running. Process name WithOut (WO) '.exe' postfix. Example: "notepad" (not "notepad.exe")
    :return: None - nothing is returned. If process will not start -exception will be raised
    """
    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%"):
    """
    Stop process on the orchestrator machine. You can set user session on the machine and set flag about to force close process.

        # USAGE

        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 inCloseForceBool: True - do force close. False - send signal to safe close (!ATTENTION! - Safe close works only in orchestrator session. Win OS doens't allow to send safe close signal between GUI sessions)
    :param inUserNameStr: User name which is has current process to close. Default value is close process on the Orchestrator session
    """
    # 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):
    """
    Return process list on the orchestrator machine sorted by Memory Usage. You can determine the list of the processes you are interested - def will return the list about it.

        # USAGE

        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.

    :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):
    """
    Use this procedure if you need to run periodically some def. Set def, args, interval and enjoy :)

    :param inGSettings: global settings
    :param inDef: def link, which will be called with interval inIntervalSecFloat
    :param inIntervalSecFloat: Interval in seconds between call
    :param inIntervalAsyncBool: False - wait interval before next call after the previous iteration result; True - wait interval after previous iteration call
    :param inDefArgList: List of the args in def. Default None (empty list)
    :param inDefArgDict: Dict of the args in def. Default None (empty dict)
    :param inDefArgGSettingsNameStr: Name of the GSettings arg name for def (optional)
    :param inDefArgLoggerNameStr: Name of the Logger arg name for def (optional). If Use - please check fill of the inLogger arg.
    :param inExecuteInNewThreadBool: True - create new thread for the periodic execution; False - execute in current thread. Default: True
    :param inLogger: logging def if some case is appear
    :return:
    """
    #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):
    """
    Import module and run def in the Orchestrator process.

    .. note::

        Import module will be each time when PythonStart def will be called.

        # USAGE

        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: Absolute or relative (working directory of the orchestrator process) path to the importing module .py
    :param inDefNameStr: Def name in module
    :param inArgList: List of the arguments for callable def
    :param inArgDict: Dict of the named arguments for callable def
    :param inLogger: Logger instance to log some information when PythonStart def is running
    """
    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):
    """
    Add activity item list in scheduler. You can set weekday list and set time when launch. Activity list will be executed at planned time/day.

        # USAGE

        # EXAMPLE 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: Activation time from "00:00" to "23:59". Example: "05:29"
    :param inWeekdayList: Week day list to initiate activity list. Use int from 0 (monday) to 6 (sunday) as list items. Example: [0,1,2,3,4]. Default value is everyday ([0,1,2,3,4,5,6])
    :param inActivityList: Activity list structure
    """
    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):
    """
    Create RDP connect dict item/ Use it connect/reconnect (Orchestrator.RDPSessionConnect)

        # USAGE

        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)
        #     lRDPTemplateDict= {  # Init the configuration item
        #         "Host": "127.0.0.1", "Port": "3389", "Login": "USER_99", "Password": "USER_PASS_HERE",
        #         "Screen": { "Width": 1680, "Height": 1050, "FlagUseAllMonitors": False, "DepthBit": "32" },
        #         "SharedDriveList": ["c"],
        #         "RedirectClipboardBool": True, # True - share clipboard to RDP; False - else
        #         ###### Will updated in program ############
        #         "SessionHex": "77777sdfsdf77777dsfdfsf77777777",  # Hex is created when robot runs, example ""
        #         "SessionIsWindowExistBool": False, "SessionIsWindowResponsibleBool": False, "SessionIsIgnoredBool": False
        #     }

    :param inLoginStr: User/Robot Login, example "USER_99"
    :param inPasswordStr: Password, example "USER_PASS_HERE"
    :param inHostStr: Host address, example "77.77.22.22"
    :param inPortInt: RDP Port, example "3389" (default)
    :param inWidthPXInt: Width of the remote desktop in pixels, example 1680
    :param inHeightPXInt: Height of the remote desktop in pixels, example 1050
    :param inUseBothMonitorBool: True - connect to the RDP with both monitors. False - else case
    :param inDepthBitInt: Remote desktop bitness. Available: 32 or 24 or 16 or 15, example 32
    :param inSharedDriveList: Host local disc to connect to the RDP session. Example: ["c", "d"]
    :param inRedirectClipboardBool: # True - share clipboard to RDP; False - else
    :return:
        {
            "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": True, # 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
        }

    """
    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):
    """
    DEVELOPING Search duplicates in GSettings RDPlist
    !def is developing!

    :return:
    """
    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):
    """
    Create new RDPSession in RobotRDPActive. Attention - activity will be ignored if RDP key is already exists
     2 way of the use
    Var 1 (Main stream): inGSettings, inRDPSessionKeyStr, inRDPTemplateDict
    Var 2 (Backward compatibility): inGSettings, inRDPSessionKeyStr, inHostStr, inPortStr, inLoginStr, inPasswordStr

        # USAGE

            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 Session string key - need for the further identification
    :param inRDPTemplateDict: RDP configuration dict with settings (see def Orchestrator.RDPTemplateCreate)
    :param inHostStr: Backward compatibility from Orchestrator v 1.1.20. Use inRDPTemplateDict
    :param inPortStr: Backward compatibility from Orchestrator v 1.1.20. Use inRDPTemplateDict
    :param inLoginStr: Backward compatibility from Orchestrator v 1.1.20. Use inRDPTemplateDict
    :param inPasswordStr: Backward compatibility from Orchestrator v 1.1.20. Use inRDPTemplateDict
    :return: True every time :)
    """
        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):
    """
    Disconnect the RDP session and stop monitoring it.

        # USAGE

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

    :param inBreakTriggerProcessWOExeList: List of the processes, which will stop the execution. Example ["notepad"]

        .. note::

        Orchestrator look processes on the current machine
    """
    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):
    """
    Reconnect the RDP session

        # USAGE

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

    :return:
    """
        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):
    """
    Stop monitoring the RDP session by the Orchestrator process. Current def don't kill RDP session - only stop to track it (it can give )

        # USAGE

        Orchestrator.RDPSessionMonitorStop(
        # Orchestrator will stop the RDP monitoring

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

def RDPSessionLogoff(inRDPSessionKeyStr, inBreakTriggerProcessWOExeList = None, inGSettings = None):
    """
    Logoff the RDP session from the Orchestrator process (close all apps in session when logoff)

        # USAGE

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

    :return: True - logoff is successful
    """
        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):
    """
    DEVELOPING, MAYBE NOT USEFUL Check RDP Session responsibility TODO NEED DEV + TEST

    """
        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):
    """
    Start process in RDP if it is not running (check by the arg inProcessNameWEXEStr)

        # USAGE

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

    :param inProcessNameWEXEStr: Process name with extension (.exe). This arg allow to check the process is running. Example: "Notepad.exe"
    :param inFilePathStr: Path to run process if it is not running.
    :param inFlagGetAbsPathBool: True - get abs path from the relative path in inFilePathStr. False - else case
    """
        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):
    """
    Send CMD command to the RDP session "RUN" window

        # USAGE

        lResultDict = Orchestrator.RDPSessionCMDRun(
            inModeStr = 'LISTEN')
        # Orchestrator will send CMD to RDP and return the result (see return section)

    :param inCMDStr: Any CMD string
    :param inModeStr: Variants:
        "LISTEN" - Get result of the cmd command in result;
        "CROSSCHECK" - Check if the command was successfully sent
        "RUN" - Run without crosscheck and get clipboard
    :return: # OLD > True - CMD was executed successfully
         {
          "OutStr": <> # Result string
          "IsResponsibleBool": True|False # Flag is RDP is responsible - works only when 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):
    """
    Send CMD command to the RDP session "RUN" window.

        # USAGE

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

    :param inProcessNameWEXEStr: Process name to kill. Example: 'notepad.exe'
    :param inFlagForceCloseBool: True - force close the process. False - safe close the process
    """
        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):
    """
    Send file from Orchestrator session to the RDP session using shared drive in RDP (see RDP Configuration Dict, Shared drive)

        # USAGE

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

    :param inHostFilePathStr: Relative or absolute path to the file location on the Orchestrator side. Example: "TESTDIR\\Test.py"
    :param inRDPFilePathStr: !Absolute! path to the destination file location on the RDP side. Example: "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):
    """
    Recieve file from RDP session to the Orchestrator session using shared drive in RDP (see RDP Configuration Dict, Shared drive)

        # USAGE

        lResultDict = Orchestrator.RDPSessionFileStoredRecieve(

    """
        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

def GSettingsAutocleaner(inGSettings=None):
    """
    HIDDEN Interval gSettings auto cleaner def to clear some garbage.

    """
    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):
    """
    Start the orchestrator threads execution

    :param inDumpRestoreBool: True - restore data from the dumo
    :param inRunAsAdministratorBool: True - rerun as admin if not
    :return:
    """
    Orchestrator(inDumpRestoreBool = True, inRunAsAdministratorBool = True)

def Orchestrator(inGSettings=None, inDumpRestoreBool = True, inRunAsAdministratorBool = True):
    """
    Main def to start orchestrator

    :param inGSettings:
    :param inDumpRestoreBool:
    :param inRunAsAdministratorBool:
    :return:
    """
    # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script
    if not OrchestratorIsAdmin() and inRunAsAdministratorBool==True:
        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

import json, copy
from .. import __Orchestrator__
from .. import Processor
# Escape JS to the safe JS for the inline JS in HTML tags ATTENTION! Use it only if want to paste JS into HTML tag - not in <script>
# USAGE: JSEscapeForHTMLInline(inJSStr="lTest=\"Hello World\"; alert(\"lTest\")")
def JSEscapeForHTMLInline(inJSStr):
    lResult = inJSStr.replace("\"","&quot;")

def JSProcessorActivityListAdd(inActivityList, inGUIDRemoveBool = True):
    """
        # Create JS for send activity list/ activity to the processor
        # USAGE: Orchestrator.Web.Basic.JSProcessorActivityListAdd(inActivityList)

    :param inActivityList: List of the activities (See __Orchestrator__.ProcessorActivityItemCreate)
    :param inGUIDRemoveBool: True - remove GUID before generate JS (if GUID is not important)
    :return: JavaScript string for the front end
    """
    Processor.__ActivityListVerify__(inActivityList=inActivityList)  # DO VERIFICATION FOR THE inActivityList
    inActivityList = copy.deepcopy(inActivityList)
    # Check if no def function is here - if exist - replace to alias
    for lActivityItem in inActivityList:
        if "GUIDStr" in lActivityItem and inGUIDRemoveBool == True: del lActivityItem["GUIDStr"] # Remove GUID from activity items if exists
        lDef = lActivityItem["Def"]
        if callable(lDef): raise Exception(f"pyOpenRPA Exception: You can't send ActivityList with def to JS. Use Def Alias (see Orchestrator.ProcessorAliasDefUpdate)")
    lJSStr = f"""mGlobal.pyOpenRPA.ProcessorQueueAdd({json.dumps(inActivityList)});"""
    return lJSStr

def JSActivityListExecute(inActivityList, inGUIDRemoveBool = True):
    """
        Create JS for execute activity list/ activity permanent
        USAGE: Orchestrator.Web.Basic.JSActivityListExecute(inActivityList, inGUIDRemoveBool = True)

    """
    lJSStr = f"""mGlobal.pyOpenRPA.ActivityListExecute({json.dumps(inActivityList)});"""

# Generate HTML code of the simple URL link by the URL
# USAGE: Orchestrator.Web.Basic.HTMLLinkURL(inURLStr="test",inColorStr="orange")
# USAGE: Basic.HTMLLinkURL(inURLStr="test",inColorStr="orange")
def HTMLLinkURL(inURLStr, inTitleStr=None, inColorStr=None):
    lCSSStyleStr = ""
    if not inTitleStr: inTitleStr = inURLStr
    if inColorStr: lCSSStyleStr=f"style=\"color:{inColorStr}\""
    lResult=f"<a {lCSSStyleStr} href=\"{inURLStr}\">{inTitleStr}</a>"

# Generate HTML code of the simple URL link by the JS when onclick
# USAGE: Orchestrator.Web.Basic.HTMLLinkJSOnClick(inJSOnClickStr="",inColorStr="orange")
# USAGE: Basic.HTMLLinkJSOnClick(inJSOnClickStr="test",inColorStr="orange")
def HTMLLinkJSOnClick(inJSOnClickStr, inTitleStr, inColorStr=None):
    inJSOnClickStr= JSEscapeForHTMLInline(inJSStr=inJSOnClickStr) # Escape some symbols for the inline JS
    lResult=f"<a {lCSSStyleStr} onclick=\"{inJSOnClickStr}\">{inTitleStr}</a>"

# Create HTMLLink by the def, argdict, arglist, gsettingsStr, logger Str titleStr, color, (add in processor queue)
def HTMLLinkDefProcessor(inGSettings, inDef, inArgDict=None, inArgList=None, inArgGSettingsStr=None, inArgLoggerStr=None, inLinkTitleStr=None, inLinkColorStr=""):
    lDefAliasStr = inDef
    if callable(inDef):
        lDefAliasStr = str(inDef)
        lDefAliasStr = __Orchestrator__.ProcessorAliasDefUpdate(inGSettings=inGSettings, inDef=inDef, inAliasStr=lDefAliasStr)
    if inLinkTitleStr is None: inLinkTitleStr = lDefAliasStr
    lActivityList = [__Orchestrator__.ProcessorActivityItemCreate(inDef=lDefAliasStr,inArgList=inArgList,inArgDict=inArgDict,inArgGSettingsStr=inArgGSettingsStr,inArgLoggerStr=inArgLoggerStr)]
    lJSStr = JSProcessorActivityListAdd(lActivityList)
    lHTMLStr = HTMLLinkJSOnClick(inJSOnClickStr=lJSStr,inTitleStr=inLinkTitleStr, inColorStr=inLinkColorStr)
    return lHTMLStr

# Create HTMLLink by the def, argdict, arglist, gsettingsStr, logger Str titleStr, color, (execute permanently)
def HTMLLinkDefExecute(inGSettings, inDef, inArgDict=None, inArgList=None, inArgGSettingsStr=None, inArgLoggerStr=None, inLinkTitleStr=None, inLinkColorStr=""):
    lJSStr = JSActivityListExecute(lActivityList)

# HTML Generator for the CP up to v.1.2.0
def HTMLControlPanelBC(inCPDict):
    # FooterButtonX2List generation
    lFooterButtonX2Str = ""
    for lItem in inCPDict["FooterButtonX2List"]:
        lFooterButtonX2Str+=f'<div class="ui basic {lItem.get("Color","")} button" onclick="{JSEscapeForHTMLInline(lItem.get("OnClick",""))}">{lItem.get("Text","")}</div>'
    # FooterButtonX1List generation
    lFooterButtonX1Str = ""
    for lItem in inCPDict["FooterButtonX1List"]:
        lFooterButtonX1Str+=f'<div class="ui basic {lItem.get("Color","")} button" onclick="{JSEscapeForHTMLInline(lItem.get("OnClick",""))}">{lItem.get("Text","")}</div>'
    # BodyKeyValue generation
    lBodyKeyValueStr = ""
    for lItem in inCPDict["BodyKeyValueList"]:
        lBodyKeyValueStr+=f"<li>{lItem['Key']}: {lItem['Value']}</li>"
    # Generate consolidated string
    lResultHTMLStr = f"""
    <div class="card">
        <div class="content">
            <div class="right floated mini ui ">
                {inCPDict['HeaderRightText']}
            </div>
            <div class="header">
                {inCPDict['HeaderLeftText']}

            </div>
            <div class="meta">
                {inCPDict['SubheaderText']}
            </div>
            <div class="description">
                <ul style="padding-inline-start:16px;margin:0px">
                    {lBodyKeyValueStr}
                </ul>
            </div>
        </div>
        <div class="extra content">
            {inCPDict['FooterText']}
        </div>
            <div class="ui two buttons">
            {lFooterButtonX2Str}
            </div>
            <div class="ui horizontal divider">Add. controls</div>
            <div class="ui one buttons">
            {lFooterButtonX1Str}
            </div>
        </div>
    </div>
    """

    return lResultHTMLStr

#from pyOpenRPA.Orchestrator import Managers
import os
import time

from pyOpenRPA import Orchestrator
class Process():
    """
    Manager process, which is need to be started / stopped / restarted

    With Process instance you can automate your process activity. Use schedule package to set interval when process should be active and when not.

    All defs in class are pickle safe! After orchestrator restart (if not the force stop of the orchestrator process) your instance with properties will be restored. But it not coverage the scheduler which is in __Orchestrator__ .
    After orc restart you need to reinit all schedule rules: Orchestrator.OrchestratorScheduleGet

    Process instance has the following statuses:
        - 0_STOPPED
        - 1_STOPPED_MANUAL
        - 2_STOP_SAFE
        - 3_STOP_SAFE_MANUAL
        - 4_STARTED
        - 5_STARTED_MANUAL

        # For the safe init class use ProcessInitSafe
        lProcess = Orchestrator.Managers.ProcessInitSafe(inAgentHostNameStr="PCNAME",inAgentUserNameStr="USER",
             inProcessNameWOExeStr="notepad",inStartCMDStr="notepad",inStopSafeTimeoutSecFloat=3)
        # Async way to run job
        lProcess.ScheduleStatusCheckEverySeconds(inIntervalSecondsInt=5)
        Orchestrator.OrchestratorScheduleGet().every(5).seconds.do(Orchestrator.OrchestratorThreadStart,
                                                                   lProcess.StartCheck)
        # OR (sync mode)
        Orchestrator.OrchestratorScheduleGet().every(5).seconds.do(lProcess.StartCheck)

    How to use StopSafe on the robot side

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

    mAgentHostNameStr = None
    mAgentUserNameStr = None
    mStartPathStr = None
    mStartCMDStr = None
    mStartArgDict = None
    mStatusCheckIntervalSecFloat = None
    mProcessNameWOExeStr = None
    mStopSafeTimeoutSecFloat = None
    mStatusStr = None # 0_STOPPED 1_STOPPED_MANUAL 2_STOP_SAFE 3_STOP_SAFE_MANUAL 4_STARTED 5_STARTED_MANUAL

    # MST - Manual Stop Trigger
    mMSTdTSecFloat: float = None
    mMSTdNInt = None
    mMSTStartTimeList = []

    mAgentMuteBool = False # Mute any sends to agent while some action is perfomed

    mStatusSavedStr = None # Saved status to the further restore

    def MuteWait(self):
        """
        Internal def. Wait when class is apply to send new activities to the agent

        :return:
        """
        lIntervalSecFloat = 0.3
        while self.mAgentMuteBool == True:
            time.sleep(lIntervalSecFloat)
        return None

    def KeyTurpleGet(self):
        """
        Get the key turple of the current process

        """
        return (self.mAgentHostNameStr.upper(), self.mAgentUserNameStr.upper(), self.mProcessNameWOExeStr.upper())

    def __init__(self, inAgentHostNameStr, inAgentUserNameStr, inProcessNameWOExeStr, inStartPathStr=None, inStartCMDStr = None, inStopSafeTimeoutSecFloat=300, inStartArgDict=None, inStatusCheckIntervalSecFloat=30):
        """
        Init the class instance.
        !ATTENTION! Function can raise exception if process with the same (inAgentHostNameStr, inAgentUserNameStr, inProcessNameWOExeStr) is already exists in GSettings (can be restored from previous Orchestrator session). See ProcessInitSafe to sefaty init the instance or restore previous
        !ATTENTION! Schedule options you must

        :param inAgentHostNameStr: Agent hostname in any case. Required to identify Process
        :param inAgentUserNameStr: Agent user name in any case. Required to identify Process
        :param inProcessNameWOExeStr: The process name without extension .exe (the key of the Process instance). Any case - will be processed to the upper case
        :param inStartPathStr: Path to start process (.cmd/ .exe or something else). Path can be relative (from orc working directory) or absolute
        :param inStartCMDStr: CMD script to start program (if no start file is exists)
        :param inStopSafeTimeoutSecFloat: Time to wait for stop safe. After that do the stop force (if process is not stopped)
        """
        lGS = __Orchestrator__.GSettingsGet()
        # Check if Process is not exists in GSettings
        if (inAgentHostNameStr.upper(), inAgentUserNameStr.upper(), inProcessNameWOExeStr.upper()) not in lGS["ManagersProcessDict"]:
            self.mStartArgDict = inStartArgDict
            self.mAgentHostNameStr = inAgentHostNameStr
            self.mAgentUserNameStr = inAgentUserNameStr
            self.mStartPathStr = inStartPathStr
            self.mStartCMDStr = inStartCMDStr
            self.mProcessNameWOExeStr = inProcessNameWOExeStr
            self.mStopSafeTimeoutSecFloat = inStopSafeTimeoutSecFloat
            lGS["ManagersProcessDict"][(inAgentHostNameStr.upper(), inAgentUserNameStr.upper(), inProcessNameWOExeStr.upper())]=self
            lActivityDict = __Orchestrator__.ProcessorActivityItemCreate(inDef=self.StatusCheck,inArgList=[], inThreadBool=True)
            __Orchestrator__.ProcessorActivityItemAppend(inActivityItemDict=lActivityDict)
            if inStatusCheckIntervalSecFloat is not None: __Orchestrator__.OrchestratorScheduleGet().every(inStatusCheckIntervalSecFloat).seconds.do(Orchestrator.OrchestratorThreadStart,self.StatusCheck)
            self.mStatusCheckIntervalSecFloat = inStatusCheckIntervalSecFloat
        else: raise Exception(f"Managers.Process ({inAgentHostNameStr}, {inAgentUserNameStr}, {inProcessNameWOExeStr}): Can't init the Process instance because it already inited in early (see ProcessInitSafe)")

    def ManualStopTriggerSet(self, inMSTdTSecFloat: float, inMSTdNInt: int) -> None:
        """
        Set ManualStopTrigger (MST) to switch to STOPPED MANUAL if specified count of start fails will be catched in specified time period

        :param inMSTdTSecFloat: Time perios in seconds
        :param inMSTdNInt: Counts of the start tries
        :return: None
        """

        # MST - Manual Stop Trigger
        self.mMSTdTSecFloat = inMSTdTSecFloat
        self.mMSTdNInt = inMSTdNInt

    def ManualStopTriggerNewStart(self):
        """
        Log new start event. Check if it is applicable. Change status if ManualStop trigger criteria is applied

        :return: # 0_STOPPED 1_STOPPED_MANUAL 2_STOP_SAFE 3_STOP_SAFE_MANUAL 4_STARTED 5_STARTED_MANUAL
        """
        if self.mMSTdTSecFloat is not None and self.mMSTdNInt is not None:
            lTimeNowSecFloat  = time.time()
            self.mMSTStartTimeList.append(lTimeNowSecFloat) # Append current time to MST list
            # Remove old items from list
            lMSTStartTimeList = []
            for lTimeItemSecFloat in self.mMSTStartTimeList:
                ldTSecFloat = lTimeNowSecFloat - lTimeItemSecFloat
                # Move to the new list if dT less
                if ldTSecFloat < self.mMSTdTSecFloat: lMSTStartTimeList.append(lTimeItemSecFloat)
            self.mMSTStartTimeList = lMSTStartTimeList # Set new list
            # Check count in list
            if len(lMSTStartTimeList) > self.mMSTdNInt:
                self.mStatusStr = "1_STOPPED_MANUAL"
                # Log info about process
                lL = __Orchestrator__.OrchestratorLoggerGet()
                lL.info(f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): ManualStopTrigger is activated. {self.mMSTdNInt} start tries in {self.mMSTdTSecFloat} sec.")
        return self.mStatusStr

    def ManualStopListClear(self) -> None:
        """
        Clear the last start tries list.

        """
        self.mMSTStartTimeList=[]

    def Manual2Auto(self) -> str:
        """
        Remove Manual flag from process (if exists) - it will allow the schedule operations via def StatusCheckStart(self):    def StatusCheckStorForce(self): def StatusCheckStopSafe(self):

        :return: Process status. See self.mStatusStr. 0_STOPPED 1_STOPPED_MANUAL 2_STOP_SAFE 3_STOP_SAFE_MANUAL 4_STARTED 5_STARTED_MANUAL
        """
        lLogBool = False
        if self.mStatusStr=="1_STOPPED_MANUAL": self.mStatusStr = "0_STOPPED"; lLogBool=True
        if self.mStatusStr=="3_STOP_SAFE_MANUAL": self.mStatusStr = "2_STOP_SAFE"; lLogBool=True
        if self.mStatusStr=="5_STARTED_MANUAL": self.mStatusStr = "4_STARTED"; lLogBool=True
        # Log info about process
        if lLogBool == True: self.StatusChangeLog()

    def Start(self, inIsManualBool = True, inStartArgDict=None) -> str:
        """
        Manual/Auto start. Manual start will block scheduling execution. To return schedule execution use def Manual2Auto.
        Will not start if STOP SAFE is now and don't start auto is stopped manual now

        :param inIsManualBool: Default is True - Mark this operation as manual - StatusCheckStart/Stop will be blocked - only StatusCheck will be working. False - Auto operation
        """
        if inIsManualBool == False: self.ManualStopTriggerNewStart() # Set the time
        if self.mStatusStr is not None and (self.mStatusStr == "1_STOPPED_MANUAL" or "STOP_SAFE" in self.mStatusStr) and inIsManualBool == False:
            lStr = f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): Process will not start because of stopped manual or stop safe is now."
            __Orchestrator__.OrchestratorLoggerGet().warning(lStr)
            return self.mStatusStr
        # Send activity item to agent - wait result
        if self.mStartPathStr is not None: lCMDStr = os.path.abspath(self.mStartPathStr)
        elif self.mStartCMDStr is not None: lCMDStr = self.mStartCMDStr
        # Append args
        if inStartArgDict is not None: self.mStartArgDict = inStartArgDict
        if self.mStartArgDict is not None:
            for lItemKeyStr in self.mStartArgDict:
                lItemValueStr = self.mStartArgDict[lItemKeyStr]
                lCMDStr = f"{lCMDStr} {lItemKeyStr} {lItemValueStr}"
        self.MuteWait()
        self.mAgentMuteBool=True
        lActivityItemStart = __Orchestrator__.ProcessorActivityItemCreate(inDef="OSCMD",
              inArgDict={"inCMDStr":lCMDStr,"inSendOutputToOrchestratorLogsBool":False, "inCaptureBool":False},
              inArgGSettingsStr="inGSettings")
        lGUIDStr = __Orchestrator__.AgentActivityItemAdd(inHostNameStr=self.mAgentHostNameStr,
                                                         inUserStr=self.mAgentUserNameStr,
                                                         inActivityItemDict=lActivityItemStart)
        lStartResult = __Orchestrator__.AgentActivityItemReturnGet(inGUIDStr=lGUIDStr)
        if inIsManualBool==True:
            self.mStatusStr = "5_STARTED_MANUAL"
        else:
            self.mStatusStr = "4_STARTED"
        self.StatusChangeLog()
        self.mAgentMuteBool = False

    def StartCheck(self) -> str:
        """
        Start program if auto stopped (0_STOPPED).

        """
        if self.mStatusStr == "0_STOPPED":
            self.Start(inIsManualBool=False)

    def StopSafe(self, inIsManualBool = True, inStopSafeTimeoutSecFloat = None) -> str:
        """
        Manual/Auto stop safe. Stop safe is the operation which send signal to process to terminate own work (send term signal to process). Managers.Process wait for the mStopSafeTimeoutSecFloat seconds. After that, if process is not terminated - self will StopForce it.
        Manual stop safe will block scheduling execution. To return schedule execution use def Manual2Auto

        :param inStopSafeTimeoutSecFloat: Default value goes from the instance. You can specify time is second to wait while safe stop. After that program will stop force
        """
        if inStopSafeTimeoutSecFloat is None: inStopSafeTimeoutSecFloat = self.mStopSafeTimeoutSecFloat
        lCMDStr = f'taskkill /im "{self.mProcessNameWOExeStr}.exe" /fi "username eq %USERNAME%"'
        lActivityItemStart = __Orchestrator__.ProcessorActivityItemCreate(
            inDef="OSCMD",inArgDict={"inCMDStr": lCMDStr,"inSendOutputToOrchestratorLogsBool":False, "inCaptureBool": False},inArgGSettingsStr="inGSettings")
            self.mStatusStr = "3_STOP_SAFE_MANUAL"
        else:
            self.mStatusStr = "2_STOP_SAFE"
        # Interval check is stopped
        lTimeStartFloat = time.time()
        lIntervalCheckSafeStatusFLoat = 15.0
        while "SAFE" in self.mStatusStr and (time.time() - lTimeStartFloat) < inStopSafeTimeoutSecFloat:
            self.StatusCheck()
            if "SAFE" not in self.mStatusStr: break
            time.sleep(lIntervalCheckSafeStatusFLoat)
        if "SAFE" in  self.mStatusStr:
            # Log info about process
            lL = __Orchestrator__.OrchestratorLoggerGet()
            lL.info(f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): Safe stop has been wait for {inStopSafeTimeoutSecFloat} sec. Now do the force stop.")
            self.StopForce(inIsManualBool=inIsManualBool,inMuteIgnoreBool=True)
        # self.StatusChangeLog() status check has already log status (see above)

    def StopSafeCheck(self, inStopSafeTimeoutSecFloat = None) -> str:
        """
        Stop safe program if auto started (4_STARTED).

        """
        if self.mStatusStr == "4_STARTED":
            self.StopSafe(inIsManualBool=False, inStopSafeTimeoutSecFloat = inStopSafeTimeoutSecFloat)

    def StopForce(self, inIsManualBool = True, inMuteIgnoreBool = False) -> str:
        """
        Manual/Auto stop force. Force stop don't wait process termination  - it just terminate process now.

        """
        if inMuteIgnoreBool == False: self.MuteWait()
        lMuteWorkBool = False
        if self.mAgentMuteBool==False: self.mAgentMuteBool=True; lMuteWorkBool=True
        lCMDStr = f'taskkill /F /im "{self.mProcessNameWOExeStr}.exe" /fi "username eq %USERNAME%"'
            self.mStatusStr = "1_STOPPED_MANUAL"
        else:
            self.mStatusStr = "0_STOPPED"
        if lMuteWorkBool == True:
            self.mAgentMuteBool=False

    def StopForceCheck(self) -> str:
        """
        Stop force program if auto started (4_STARTED).

        """
            self.StopForce(inIsManualBool=False)

    def RestartSafe(self, inIsManualBool = True):
        """
        Manual/Auto restart safe. Restart safe is the operation which send signal to process to terminate own work (send term signal to process). Then it run process. Managers.Process wait for the mStopSafeTimeoutSecFloat seconds. After that, if process is not terminated - self will StopForce it.

        """
        self.StopSafe(inIsManualBool=inIsManualBool)
        return self.Start(inIsManualBool=inIsManualBool)

    def RestartForce(self, inIsManualBool = True):
        """
        Manual/Auto restart force. Force restart dont wait process termination  - it just terminate process now ant then start it.
        Manual restart will block scheduling execution. To return schedule execution use def Manual2Auto

        """
        self.StopForce(inIsManualBool=inIsManualBool)

    def StatusSave(self):
        """
        Save current status of the process. After that you can restore process activity. Work when orchestrator is restarted. Don't save "STOP_SAFE" status > "STOPPED"

        """
        lWarnSafeBool = True
        if self.mStatusStr == "2_STOP_SAFE": self.mStatusSavedStr = "0_STOPPED"
        elif self.mStatusStr == "3_STOP_SAFE_MANUAL": self.mStatusSavedStr = "1_STOPPED_MANUAL"
        else: self.mStatusSavedStr = self.mStatusStr; lWarnSafeBool = False
        if lWarnSafeBool==True: __Orchestrator__.OrchestratorLoggerGet().warning(f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): Safe status has been catched when safe > change saved status to stopped.")

    def StatusCheckIntervalRestore(self):
        """Call from orchestrator when init
        """
        if self.mStatusCheckIntervalSecFloat is not None: 
            __Orchestrator__.OrchestratorLoggerGet().info(f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): Restore schedule to StatusCheck in interval of {self.mStatusCheckIntervalSecFloat} sec.")
            __Orchestrator__.OrchestratorScheduleGet().every(self.mStatusCheckIntervalSecFloat).seconds.do(Orchestrator.OrchestratorThreadStart,self.StatusCheck)

    def StatusRestore(self):
        """
        Execute the StatusCheck, after that restore status to the saved state (see StatusSave). Work when orchestrator is restarted.

        """
        self.StatusCheck() # check current status
        # Do some action
        if self.mStatusSavedStr != self.mStatusStr and self.mStatusSavedStr is not None:
            #lManualBool = False
            #if "MANUAL" in self.mStatusSavedStr:
            #    lManualBool = True
            if "STOPPED" in self.mStatusSavedStr and "STOPPED" not in self.mStatusStr:
                self.StopSafe(inIsManualBool=True)
            if "STARTED" in self.mStatusSavedStr and "STARTED" not in self.mStatusStr:
                self.Start(inIsManualBool=True)
            Orchestrator.OrchestratorLoggerGet().info(f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): Status has been restored to {self.mStatusSavedStr}")
            self.mStatusStr = self.mStatusSavedStr
            self.mStatusSavedStr = None

    def StatusChangeLog(self):
        """
        Lof information about status change

        :return:
        """
        lL = __Orchestrator__.OrchestratorLoggerGet()
        lL.info(f"Managers.Process ({self.mAgentHostNameStr}, {self.mAgentUserNameStr}, {self.mProcessNameWOExeStr}): Status has been changed to {self.mStatusStr})")

    def StatusCheck(self):
        """
        Check if process is alive. The def will save the manual flag is exists. Don't wait mute but set mute if it is not set.

        """
        lActivityItemUserProcessList = __Orchestrator__.ProcessorActivityItemCreate(inDef="ProcessWOExeUpperUserListGet")
        #self.MuteWait()
        lGUIDStr = __Orchestrator__.AgentActivityItemAdd(inHostNameStr=self.mAgentHostNameStr,inUserStr=self.mAgentUserNameStr,inActivityItemDict=lActivityItemUserProcessList)
        lUserProcessList = __Orchestrator__.AgentActivityItemReturnGet(inGUIDStr=lGUIDStr)
        if self.mProcessNameWOExeStr.upper() in lUserProcessList:
            if self.mStatusStr == "1_STOPPED_MANUAL": self.mStatusStr = "5_STARTED_MANUAL"; lLogBool=True
            if self.mStatusStr == "0_STOPPED": self.mStatusStr = "4_STARTED"; lLogBool=True
            if self.mStatusStr is None: self.mStatusStr = "4_STARTED"; lLogBool=True
        else:
            if self.mStatusStr == "2_STOP_SAFE": self.mStatusStr = "0_STOPPED"; lLogBool = True
            if self.mStatusStr == "3_STOP_SAFE_MANUAL": self.mStatusStr = "1_STOPPED_MANUAL"; lLogBool = True
            if self.mStatusStr == "5_STARTED_MANUAL": self.mStatusStr = "1_STOPPED_MANUAL"; lLogBool=True
            if self.mStatusStr == "4_STARTED": self.mStatusStr = "0_STOPPED"; lLogBool=True
            if self.mStatusStr is None: self.mStatusStr = "0_STOPPED"; lLogBool=True
    def StatusCheckStart(self):
        """
        Check process status and run it if auto stopped self.mStatusStr is "0_STOPPED"

        """
        lStatusStr = self.StatusCheck()
        if lStatusStr == "0_STOPPED":
    def StatusCheckStopForce(self):
        """
        Check process status and auto stop force it if self.mStatusStr is 4_STARTED

        """
        if lStatusStr == "4_STARTED":

    def StatusCheckStopSafe(self):
        """
        Check process status and auto stop safe it if self.mStatusStr is 4_STARTED

        :return:
        """
            self.StopSafe(inIsManualBool=False)

def ProcessInitSafe(inAgentHostNameStr, inAgentUserNameStr, inProcessNameWOExeStr, inStartPathStr=None, inStartCMDStr = None, inStopSafeTimeoutSecFloat=300) -> Process:
    """
    Exception safe function. Check if process instance is not exists in GSettings (it can be after restart because Orchestrator restore objects from dump of the previous Orchestrator session)
    Return existing instance (if exists) or create new instance and return it.

    :param inAgentHostNameStr: Agent hostname in any case. Required to identify Process
    :param inAgentUserNameStr: Agent user name in any case. Required to identify Process
    :param inProcessNameWOExeStr: The process name without extension .exe (the key of the Process instance). Any case - will be processed to the upper case
    :param inStartPathStr: Path to start process (.cmd/ .exe or something else). Path can be relative (from orc working directory) or absolute
    :param inStartCMDStr: CMD script to start program (if no start file is exists)
    :param inStopSafeTimeoutSecFloat: Time to wait for stop safe. After that do the stop force (if process is not stopped)
    :return: Process instance
    """
    lProcess = ProcessGet(inAgentHostNameStr = inAgentHostNameStr, inAgentUserNameStr = inAgentUserNameStr, inProcessNameWOExeStr=inProcessNameWOExeStr)
    if lProcess is not None: return lProcess
    else: return Process(inAgentHostNameStr=inAgentHostNameStr,inAgentUserNameStr=inAgentUserNameStr,inProcessNameWOExeStr=inProcessNameWOExeStr,
                  inStartPathStr=inStartPathStr,inStartCMDStr=inStartCMDStr,inStopSafeTimeoutSecFloat=inStopSafeTimeoutSecFloat)

def ProcessExists(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str) -> bool:
    """
    Check if the Process instance is exists in GSettings by the (inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str)

    :return: True - process exists in gsettings; False - else
    """
    return (inAgentHostNameStr.upper(), inAgentUserNameStr.upper(), inProcessNameWOExeStr.upper()) in __Orchestrator__.GSettingsGet()["ManagersProcessDict"]

def ProcessGet(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str) -> Process:
    """
    Return the process instance by the inProcessNameWOExeStr

    :return: Process instance (if exists) Else None
    """
    return __Orchestrator__.GSettingsGet()["ManagersProcessDict"].get((inAgentHostNameStr.upper(), inAgentUserNameStr.upper(), inProcessNameWOExeStr.upper()),None)

def ProcessStatusStrGet(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str) -> str:
    """
    Get the status of the Process instance.

    :return: Process status. See self.mStatusStr.
        Process instance has the following statuses:
        - None (if Process instance not exists)
    """
    if lProcess is not None: return lProcess.mStatusStr

def ProcessStart(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str, inIsManualBool: bool = True) -> str:
    """
    Manual/Auto start. Manual start will block scheduling execution. To return schedule execution use def Manual2Auto

    :param inIsManualBool: Default is True - Mark this operation as manual - StatusCheckStart/Stop will be blocked - only StatusCheck will be working. False - Auto operation
    """
    if lProcess is not None: return lProcess.Start(inIsManualBool=inIsManualBool)

def ProcessStopSafe(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str, inIsManualBool: bool = True, inStopSafeTimeoutSecFloat = None) -> str:
    """
    Manual/Auto stop safe. Stop safe is the operation which send signal to process to terminate own work (send term signal to process). Managers.Process wait for the mStopSafeTimeoutSecFloat seconds. After that, if process is not terminated - self will StopForce it.
    Manual stop safe will block scheduling execution. To return schedule execution use def Manual2Auto

    :param inStopSafeTimeoutSecFloat: Default value goes from the instance. You can specify time is second to wait while safe stop. After that program will stop force
    """
    if lProcess is not None: return lProcess.StopSafe(inIsManualBool=inIsManualBool)

def ProcessStopForce(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str, inIsManualBool: bool = True) -> str:
    """
    Manual/Auto stop force. Force stop dont wait process termination  - it just terminate process now.

    """
    if lProcess is not None: return lProcess.StopForce(inIsManualBool=inIsManualBool)

def ProcessStatusSave(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str):
    """
    Save current status of the process. After that you can restore process activity. Work when orchestrator is restarted. Don't save "STOP_SAFE" status > "STOPPED"

    :return: Process status. See self.mStatusStr. 0_STOPPED 1_STOPPED_MANUAL 2_STOP_SAFE 3_STOP_SAFE_MANUAL 4_STARTED 5_STARTED_MANUAL
    """
    lProcess = ProcessGet(inAgentHostNameStr=inAgentHostNameStr, inAgentUserNameStr=inAgentUserNameStr,
                          inProcessNameWOExeStr=inProcessNameWOExeStr)
    if lProcess is not None:
        lProcess.StatusSave()
        return lProcess.mStatusStr

def ProcessStatusRestore(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str):
    """
    Execute the StatusCheck, after that restore status to the saved state (see StatusSave). Work when orchestrator is restarted.

    """
        lProcess.StatusRestore()

def ProcessStatusCheck(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str) -> str:
    """
    Check if process is alive. The def will save the manual flag is exists.

    """
        lProcess.StatusCheck()

def ProcessManual2Auto(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str) -> str:
    """
    Remove Manual flag from process (if exists) - it will allow the schedule operations via def StatusCheckStart(self):    def StatusCheckStorForce(self): def StatusCheckStopSafe(self):

    """
    if lProcess is not None: return lProcess.Manual2Auto()

def ProcessManualStopTriggerSet(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str,  inMSTdTSecFloat: float, inMSTdNInt: int) -> None:
    """

    :param inMSTdTSecFloat: Time periods in seconds
    :param inMSTdNInt: Counts of the start tries
    """
    if lProcess is not None: lProcess.ManualStopTriggerSet(inMSTdTSecFloat = inMSTdTSecFloat, inMSTdNInt = inMSTdNInt)

def ProcessManualStopListClear(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str) -> None:
    """
    Clear the last start tries list.

    """
    if lProcess is not None: lProcess.ManualStopListClear()

def ProcessScheduleStatusCheckEverySeconds(inAgentHostNameStr: str, inAgentUserNameStr: str, inProcessNameWOExeStr: str,inIntervalSecondsInt: int = 120):
    """
    Run status check every interval in second you specify.

    :param inIntervalSecondsInt: Interval in seconds. Default is 120
    """
    # Check job in threaded way
    __Orchestrator__.OrchestratorScheduleGet().every(inIntervalSecondsInt).seconds.do(__Orchestrator__.OrchestratorThreadStart,lProcess.StatusCheck)

from ... import Orchestrator
import jinja2
import os
from inspect import signature # For detect count of def args
from ..Web import Basic
import operator

class ControlPanel():
    """
    Manage your control panel on the orchestrator

    Control panel has 3 events types:
    - onRefreshHTML - run every n (see settings) second to detect changes in HTML control panel.
    - onRefreshJSON - run every n (see settings) second to detect changes in JSON data container to client side.
    - onInitJS - run when client reload the Orchestrator web page

        # Usage example:
        lCPManager = Orchestrator.Managers.ControlPanel(inControlPanelNameStr="TestControlPanel",
            inRefreshHTMLJinja2TemplatePathStr="ControlPanel\\test.html", inJinja2TemplateRefreshBool = True)
        
    If you use Jinja2 you can use next data context:
    StorageRobotDict: Orchestrator.StorageRobotGet(inRobotNameStr=self.mRobotNameStr),
    ControlPanelInstance: self,
    OrchestratorModule:Orchestrator,
    RequestInstance: inRequest,
    UserInfoDict: Orchestrator.WebUserInfoGet(inRequest=inRequest),
    UserUACDict: Orchestrator.UACUserDictGet(inRequest=inRequest),
    UserUACCheckDef: inRequest.UACClientCheck,
    EnumerateDef: enumerate,
    OperatorModule: operator,
    MathModule: math

    You can modify jinja context by use the function:
    Jinja2DataUpdateDictSet 

    .. code-block:: html
        Hello my control panel!
        You can use any def from Orchestrator module here in Jinja2 HTML template:
        Example: OrchestratorModule.OSCMD(inCMDStr="notepad")
        {{MathModule.pi}}
        {% if UserInfoDict['UserNameUpperStr']=="ND" %}
        YES - IT IS ND
        {% endif %}

    """
    mControlPanelNameStr = None
    # Jinja2 consolidated
    mJinja2TemplateRefreshBool = None
    mJinja2DataUpdateDict = None

    # RefreshHTML block
    mRefreshHTMLJinja2TemplatePathStr = None
    mRefreshHTMLJinja2TemplateFileNameStr = None
    mRefreshHTMLJinja2Loader = None
    mRefreshHTMLJinja2Env = None
    mRefreshHTMLJinja2Template = None

    # InitJS block
    mInitJSJinja2TemplatePathStr = None
    mInitJSJinja2TemplateFileNameStr = None
    mInitJSJinja2Loader = None
    mInitJSJinja2Env = None
    mInitJSJinja2Template = None

    mBackwardCompatibilityHTMLDef = None
    mBackwardCompatibilityJSDef = None
    mBackwardCompatibilityJSONDef = None

    mRobotNameStr = None

    def __init__(self, inControlPanelNameStr, inRefreshHTMLJinja2TemplatePathStr = None, inJinja2TemplateRefreshBool = False, inRobotNameStr = None):
        """
        Constructor of the control panel manager

        :param inControlPanelNameStr:
        :param inJinja2TemplatePathStr:
        """
        # Connect self witch pyOpenRPA via ControlPanelNameStr
        if inControlPanelNameStr in Orchestrator.GSettingsGet()["ServerDict"]["ControlPanelDict"]:
            raise Exception(f"Another control panel with name {inControlPanelNameStr} is already exists. Please resolve the error and restart")
        Orchestrator.GSettingsGet()["ServerDict"]["ControlPanelDict"][inControlPanelNameStr] = self
        self.RefreshHTMLJinja2TemplatePathSet(inJinja2TemplatePathStr = inRefreshHTMLJinja2TemplatePathStr)
        self.mJinja2TemplateRefreshBool = inJinja2TemplateRefreshBool
        self.mControlPanelNameStr = inControlPanelNameStr # Set the name of the control panel
        self.mRobotNameStr = inRobotNameStr # Set the robot name for robot it execute

    def Jinja2DataUpdateDictSet(self, inJinja2DataUpdateDict):
        """
        Set the data dict from the Jinja2 context (you can add some new params)

        :param inJinja2DataUpdateDict: dict, which will be appended to main data context
        """
        self.mJinja2DataUpdateDict = inJinja2DataUpdateDict

    def RefreshHTMLJinja2TemplatePathSet(self, inJinja2TemplatePathStr):
        """
        Create Jinja2 env and load the template html

        :return:
        """
        try:
            if inJinja2TemplatePathStr is not None:
                lSystemLoaderPathStr = "/".join(inJinja2TemplatePathStr.split("\\")[0:-1])
                lTemplateFileNameStr = inJinja2TemplatePathStr.split("\\")[-1]
                self.mRefreshHTMLJinja2TemplateFileNameStr = lTemplateFileNameStr
                self.mRefreshHTMLJinja2Loader = jinja2.FileSystemLoader(lSystemLoaderPathStr)
                self.mRefreshHTMLJinja2Env = jinja2.Environment(loader=self.mRefreshHTMLJinja2Loader, trim_blocks=True)
                self.mRefreshHTMLJinja2Template = self.mRefreshHTMLJinja2Env.get_template(lTemplateFileNameStr)
            Orchestrator.OrchestratorLoggerGet().exception("EXCEPTION WHEN INIT Jinja2")

    def RefreshHTMLJinja2StrGenerate(self, inDataDict):
        """
        Generate the HTML str from the Jinja2. Pass the context inDataDict
        :param inDataDict:
        :return:
        """
        if self.mJinja2TemplateRefreshBool == True:
            self.mRefreshHTMLJinja2Template = self.mRefreshHTMLJinja2Env.get_template(self.mRefreshHTMLJinja2TemplateFileNameStr)
        lHTMLStr = self.mRefreshHTMLJinja2Template.render(**inDataDict) # Render the template into str
        return lHTMLStr

    def InitJSJinja2TemplatePathSet(self, inJinja2TemplatePathStr):
        """

        :return:
        """
        try:
                self.mInitJSJinja2TemplateFileNameStr = lTemplateFileNameStr
                self.mInitJSJinja2Loader = jinja2.FileSystemLoader(lSystemLoaderPathStr)
                self.mInitJSJinja2Env = jinja2.Environment(loader=self.mInitJSJinja2Loader, trim_blocks=True)
                self.mInitJSJinja2Template = self.mInitJSJinja2Env.get_template(lTemplateFileNameStr)

    def InitJSJinja2StrGenerate(self, inDataDict):
        """
        :return:
        """
            self.mInitJSJinja2Template = self.mInitJSJinja2Env.get_template(self.mInitJSJinja2TemplateFileNameStr)
        lHTMLStr = self.mInitJSJinja2Template.render(**inDataDict) # Render the template into str

    def DataDictGenerate(self, inRequest):
        """

        :param inRequest: request handler (from http.server import BaseHTTPRequestHandler)
        :return:
        """
        lData = {
            "StorageRobotDict": None,
            "ControlPanelInstance":self,
            "OrchestratorModule":Orchestrator,
            "RequestInstance": inRequest,
            "UserInfoDict": Orchestrator.WebUserInfoGet(inRequest=inRequest),
            "UserUACDict": Orchestrator.UACUserDictGet(inRequest=inRequest),
            "UserUACCheckDef": inRequest.UACClientCheck,
            "EnumerateDef": enumerate,
            "OperatorModule": operator,
            "MathModule": math
        }
        # Get the robot storage by the robot name (if you set robot name when init)
        if self.mRobotNameStr is not None:
            lData["StorageRobotDict"] = Orchestrator.StorageRobotGet(inRobotNameStr=self.mRobotNameStr)
        # Checkj Jinja2DataUpdateDict
        if self.mJinja2DataUpdateDict is not None:
            lData.update(self.mJinja2DataUpdateDict)
        return lData

    def OnRefreshHTMLStr(self, inRequest):
        """
        Event to generate HTML code of the control panel when refresh time is over.
        Support backward compatibility for previous versions.

        :return:
        """
        lHTMLStr = None
        lL = Orchestrator.OrchestratorLoggerGet()
        if self.mBackwardCompatibilityHTMLDef is None:
            if self.mRefreshHTMLJinja2Template is not None or (self.mJinja2TemplateRefreshBool == True and self.mRefreshHTMLJinja2TemplateFileNameStr is not None):
                lDataDict = self.OnRefreshHTMLDataDict(inRequest = inRequest)
                # Jinja code
                lHTMLStr = self.RefreshHTMLJinja2StrGenerate(inDataDict=lDataDict)
        else:
            lHTMLStr = self.BackwardAdapterHTMLDef(inRequest=inRequest)
        # return the str

    def OnRefreshHTMLDataDict(self, inRequest):
        """
        Event to prepare data context for the futher Jinja2 HTML generation. You can override this def if you want some thing more data

        :return: dict
        """
        return self.DataDictGenerate(inRequest=inRequest)

    def OnRefreshHTMLHashStr(self, inRequest):
        """
        Generate the hash the result output HTML. You can override this function if you know how to optimize HTML rendering.
        TODO NEED TO MODIFY ServerSettings to work with Hash because of all defs are need do use Hash

        :return: None - default, hash function is not determined. Str - hash function is working on!
        """

    def OnRefreshJSONDict(self, inRequest):
        """
        Event to transmit some data from server side to the client side in JSON format. Call when page refresh is initialized

        :return: Dict type
        """
        lResultDict = None
        if self.mBackwardCompatibilityJSONDef is None:
            pass
        else:
            lResultDict = self.BackwardAdapterJSONDef(inRequest=inRequest)
        return lResultDict

    def OnInitJSStr(self, inRequest):
        """
        Event when orchestrator web page is init on the client side - you can transmit some java script code is str type to execute it once.

        :return: ""
        """
        lJSStr = ""
        if self.mBackwardCompatibilityJSDef is None:
            if self.mInitJSJinja2Template is not None or (self.mJinja2TemplateRefreshBool == True and self.mInitJSJinja2TemplateFileNameStr is not None):
                lDataDict = self.OnInitJSDataDict(inRequest = inRequest)
                lJSStr = self.InitJSJinja2StrGenerate(inDataDict=lDataDict)
        else:
            lJSStr = self.BackwardAdapterJSDef(inRequest=inRequest)
        return lJSStr

    def OnInitJSDataDict(self, inRequest):
        """
        Event to prepare data context for the futher Jinja2 JS init generation. You can override this def if you want some thing more data

        """

    def BackwardAdapterHTMLDef(self,inRequest):
        lGS = Orchestrator.GSettingsGet()
        # HTMLRenderDef
        lItemHTMLRenderDef = self.mBackwardCompatibilityHTMLDef
        if lItemHTMLRenderDef is not None:  # Call def (inRequest, inGSettings) or def (inGSettings)
            lHTMLResult = None
            lDEFSignature = signature(lItemHTMLRenderDef)  # Get signature of the def
            lDEFARGLen = len(lDEFSignature.parameters.keys())  # get count of the def args
            try:
                if lDEFARGLen == 1:  # def (inGSettings)
                    lHTMLResult = lItemHTMLRenderDef(lGS)
                elif lDEFARGLen == 2:  # def (inRequest, inGSettings)
                    lHTMLResult = lItemHTMLRenderDef(inRequest, lGS)
                elif lDEFARGLen == 0:  # def ()
                    lHTMLResult = lItemHTMLRenderDef()
                # RunFunction
                # Backward compatibility up to 1.2.0 - call HTML generator if result has no "HTMLStr"
                if type(lHTMLResult) is str:
                    lResultStr = lHTMLResult
                elif "HTMLStr" in lHTMLResult or "JSONDict" in lHTMLResult:
                    lResultStr = lHTMLResult["HTMLStr"]
                else:
                    # Call backward compatibility HTML generator
                    lResultStr = Basic.HTMLControlPanelBC(inCPDict=lHTMLResult)
                if lL: lL.exception(f"Error in control panel HTMLRenderDef. CP Key {self.mControlPanelNameStr}. Exception are below")

    def BackwardAdapterJSONDef(self,inRequest):
        lItemJSONGeneratorDef = self.mBackwardCompatibilityJSONDef
        lResultDict = {}
        if lItemJSONGeneratorDef is not None:  # Call def (inRequest, inGSettings) or def (inGSettings)
            lJSONResult = None
            lDEFSignature = signature(lItemJSONGeneratorDef)  # Get signature of the def
            try:
                    lJSONResult = lItemJSONGeneratorDef(lGS)
                    lJSONResult = lItemJSONGeneratorDef(inRequest, lGS)
                    lJSONResult = lItemJSONGeneratorDef()
                lType = type(lJSONResult)
                if lType is str or lJSONResult is None or lType is int or lType is list or lType is dict or lType is bool or lType is float:
                    lResultDict = lJSONResult
                else:
                    if lL: lL.warning(f"JSONGenerator return bad type: {str(type(lJSONResult))}, CP Key {self.mControlPanelNameStr}")
                if lL: lL.exception(
                    f"Error in control panel JSONGeneratorDef. CP Key {self.mControlPanelNameStr}. Exception are below")

    def BackwardAdapterJSDef(self,inRequest):
        lJSInitGeneratorDef = self.mBackwardCompatibilityJSDef
        if lJSInitGeneratorDef is not None:  # Call def (inRequest, inGSettings) or def (inGSettings)
            lJSResult = ""
            lDEFSignature = signature(lJSInitGeneratorDef)  # Get signature of the def
            try:
                    lJSResult = lJSInitGeneratorDef(lGS)
                    lJSResult = lJSInitGeneratorDef(inRequest, lGS)
                    lJSResult = lJSInitGeneratorDef()
                if type(lJSResult) is str:
                    lResultStr = lJSResult  # Add delimiter to some cases
                else:
                    if lL: lL.warning(f"JSInitGenerator return bad type: {str(type(lJSResult))}, CP Key {self.mControlPanelNameStr}")
                    f"Error in control panel JSInitGeneratorDef. CP Key {self.mControlPanelNameStr}. Exception are below")

from pywinauto import win32defines, win32structures, win32functions
import pywinauto
import ctypes
import struct
import win32api
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 (OpenRPA - 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
#   }
#]
################
#return: List of UI Object
#inElement - Входной элемент - показатель, что не требуется выполнять коннект к процессу
#inFlagRaiseException - Флаг True - выкинуть ошибку в случае обнаружении пустого списка
#old name - PywinautoExtElementsGet
def UIOSelector_Get_UIOList (inSpecificationList,inElement=None,inFlagRaiseException=True):
    '''
    Get the UIO list by the selector

    :param inSpecificationList: UIO Selector
    :param inElement: Входной элемент - показатель, что не требуется выполнять коннект к процессу
    :param inFlagRaiseException: Флаг True - выкинуть ошибку в случае обнаружении пустого списка
    :return:
    '''
    #Создать копию входного листа, чтобы не менять массив в других верхнеуровневых функциях
    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 is not None:
        #Вызвать рекурсивно функцию получения следующего объекта, если в спецификации есть следующий объект
        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")

#################################################################################################
#Get first (in more than one) UIO (UI Object)
#inSpecificationList - UIOSelector
#old name - PywinautoExtElementGet
def UIOSelector_Get_UIO (inSpecificationList,inElement=None,inFlagRaiseException=True):
    '''
    Get the pywinauto object by the UIO selector.

    :param inSpecificationList:
    :param inElement:
    :param inFlagRaiseException:
    :return:
    '''
    lResult=None
    lResultList=UIOSelector_Get_UIOList(inSpecificationList,inElement,False)
    if len(lResultList)>0:
        lResult=lResultList[0]
    if lResult is None and inFlagRaiseException:
#Check if UIO exist (Identified by the UIOSelector)
#!!!!!Safe call is included (you can set activity and UIDesktop will choose the bitness and return the result)!!!!!
#UIOSelector
#old name - -
def UIOSelector_Exist_Bool (inUIOSelector):
    '''
    Check if object is exist by the UIO selector.

    :param inUIOSelector:
    :return: True - Object is exist. False - else case
    '''
    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"]
#Wait for UIO is appear (at least one of them or all at the same time)
#inSpecificationListList - List of the UIOSelector
#inWaitSecs - Время ожидания объекта в секундах
#inFlagWaitAllInMoment - доп. условие - ожидать появление всех UIOSelector одновременно
#return: [0,1,2] - index of UIOSpecification, which is appear
#####Внимание#####
##Функция ожидания появления элементов (тк элементы могут быть недоступны, неизвестно в каком фреймворке каждый из них может появиться)
def UIOSelectorsSecs_WaitAppear_List (inSpecificationListList,inWaitSecs,inFlagWaitAllInMoment=False):
    '''
    Wait for many UI object will appear in GUI for inWaitSecs seconds.

    :param inSpecificationListList: UIOSelector list.
        Example: [
            [{"title":"notepad"},{"title":"OK"}],
            [{"title":"notepad"},{"title":"Cancel"}]
        ]
    :param inWaitSecs: Float value (seconds) for wait UI element appear in GUI
    :param inFlagWaitAllInMoment: True - Wait all UI objects from the UIOSelector list to be appeared
    :return: List of index, which UI object UIO will be appeared. Example: [1] # Appear only UI object with UIO selector: [{"title":"notepad"},{"title":"Cancel"}]
    '''
    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)
#Wait for UIO is Disappear (at least one of them or all at the same time)
#inWaitSecs - Время ожидания пропажи объекта в секундах
#inFlagWaitAllInMoment - доп. условие - ожидать пропажу всех UIOSelector одновременно
#return: [0,1,2] - index of UIOSpecification, which is Disappear
##Функция ожидания пропажи элементов (тк элементы могут быть недоступны, неизвестно в каком фреймворке каждый из них может появиться)
def UIOSelectorsSecs_WaitDisappear_List (inSpecificationListList,inWaitSecs,inFlagWaitAllInMoment=False):
    '''
    Wait for many UI object will disappear in GUI for inWaitSecs seconds.

        Example: [
        ]
    :param inWaitSecs: Float value (seconds) for wait UI element disappear in GUI
    :param inFlagWaitAllInMoment: True - Wait all UI objects from the UIOSelector list to be disappeared.
    :return: List of index, which UI object UIO will be disappeared. Example: [1] # Disappear only UI object with UIO selector: [{"title":"notepad"},{"title":"Cancel"}]
    :return:
    '''
        lIndex = 0
            if not lItemResultFlag:
#return: Bool - True - UIO is appear
def UIOSelectorSecs_WaitAppear_Bool (inSpecificationList,inWaitSecs):
    '''
    Wait for UI object will appear in GUI for inWaitSecs seconds.

    :param inSpecificationList: UIOSelector. Example: [{"title":"notepad"},{"title":"OK"}]
    :return: True - UI object will appear. False - else case
    '''
    lWaitAppearList=UIOSelectorsSecs_WaitAppear_List([inSpecificationList],inWaitSecs)
    if len(lWaitAppearList)>0:
        lResult=True
#Wait for UIO is disappear (at least one of them or all at the same time)
#return: Bool - True - UIO is Disappear
def UIOSelectorSecs_WaitDisappear_Bool (inSpecificationList,inWaitSecs):
    '''
    Wait for UI object will disappear in GUI for inWaitSecs seconds.

    :param inSpecificationList: UIOSelector.
        Example: [{"title":"notepad"},{"title":"OK"}]
    :return: True - UI object will disappear. False - else case
    '''
    lWaitDisappearList=UIOSelectorsSecs_WaitDisappear_List([inSpecificationList],inWaitSecs)
    if len(lWaitDisappearList)>0:
#Get process bitness (32 or 64)
#old name - None
#return None (if Process not found), int 32, or int 64
def UIOSelector_Get_BitnessInt (inSpecificationList):
    '''
    Detect process bitness by the UI Object UIO Selector.

    :return: int 32 or int 64
    '''
    #Получить объект Application (Для проверки разрядности)
    lRootElement=PWASpecification_Get_PWAApplication(inSpecificationList)
    if lRootElement is not None:
        if lRootElement.is64bit():
            lResult=64
        else:
            lResult=32
#Get process bitness ("32" or "64")
def UIOSelector_Get_BitnessStr (inSpecificationList):
    '''

    :return: str "32" or str "64"
    '''
            lResult="64"
        else:
            lResult="32"
#Get OS bitness (32 or 64)
#return int 32, or int 64
def Get_OSBitnessInt ():
    '''
    Detect OS bitness.

    '''
    lResult=32
    if pywinauto.sysinfo.is_x64_OS():
        lResult=64
def UIOSelector_SafeOtherGet_Process(inUIOSelector):
    '''
    Safe get other process or None if destination app is the other/same bitness

    :param inUIOSelector: UIO Selector of the UI object
    :return: None or process (of the other bitness)
    '''
    #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()
##################################################################################################
def PWASpecification_Get_UIO(inControlSpecificationArray):
    '''
    #Backend def selection - attribute "backend" ("win32" || "uia") in 1-st list element
    #old name - GetControl

    :param inControlSpecificationArray: List of dict, dict in pywinauto.find_windows notation
    :return: list of UIO object
    '''
    #Определение 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:
        #Сформировать выборку элементов, которые подходят под первый уровень спецификации
        lSpecificationLvL1List = pywinauto.findwindows.find_elements(**inControlSpecificationArray[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):
    '''
    #Backend selection - attribute "backend" ("win32" || "uia") in 1-st list element

    :return: process application object
    '''
    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

###########################################################################################################
def UIOSelector_SearchChildByMouse_UIO(inElementSpecification):
    '''
    UIOSelector (see description on the top of the document)
    #old name - AutomationSearchMouseElement

    :param inElementSpecification: UIOSelector of the UI Object
    :return: pywinauto element wrapper instance or None
    '''
    lGUISearchElementSelected=None
    #Настройка - частота обновления подсвечивания
    lTimeSleepSeconds=0.4
    lElementFoundedList=[]
    #Ветка поиска в режиме реального времени
    #Сбросить нажатие Ctrl, если оно было
    bool(win32api.GetAsyncKeyState(17))
    #Оптимизация - получить объект для опроса единажды
    lUIORoot=UIOSelector_Get_UIO(inElementSpecification)
    lFlagLoop = True
    while lFlagLoop:
        #Проверить, нажата ли клавиша Ctrl (код 17)
        lFlagKeyPressedCtrl=bool(win32api.GetAsyncKeyState(17))
        #Подсветить объект, если мышка наведена над тем объектом, который не подсвечивался в прошлый раз
        if not lFlagKeyPressedCtrl:
            #Получить координаты мыши
            (lX,lY) = win32api.GetCursorPos()
            lElementFounded={}
            #Создать карту пикселей и элементов
            #####Внимание! Функция UIOXY_SearchChild_ListDict не написана
            lElementFoundedList=UIOXY_SearchChild_ListDict(lUIORoot,lX,lY)
            #print(lElementFoundedList)
            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 name - AutomationSearchMouseElementHierarchy
def UIOSelector_SearchChildByMouse_UIOTree(inUIOSelector):
    '''
    !!!!Safe call is included (you can set activity and UIDesktop will choose the bitness and return the result)!!!!!

    :param inUIOSelector: UIOSelector of the UI Object
    :return: ?
    '''
    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
#inElement- UIO (UI Object)
#old name - PywinautoExtElementCtrlIndexGet
def UIO_GetCtrlIndex_Int(inElement):
    #Выполнить алгоритм, если есть 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
    #Вернуть результат    

# Get the UIO Info list for the selected criteria
#old name - PywinautoExtElementsGetInfo
def UIOSelector_Get_UIOInfoList (inUIOSelector, inElement=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"]

#Try to restore (maximize) window, if it's was minimized
#(особенность uia backend - он не может прицепиться к окну, если оно свернуто)
#old name - PywinautoExtTryToRestore
def UIOSelector_TryRestore_Dict(inSpecificationList):
    """
    Try to restore (maximize) window, if it's minimized. (!IMPORTANT! When use UIA framework minimized windows doesn't appear by the UIOSelector. You need to try restore windows and after that try to get UIO)

    :param inSpecificationList: UIOSelector - List of items, which contains condition attributes
    :return:
    """
    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
#Get the list of the UI object activities
#inControlSpecificationArray - UIOSelector
#old name - ElementActionGetList
def UIOSelector_Get_UIOActivityList (inUIOSelector):
    """
    Get the list of the UI object activities

    :param inUIOSelector: UIOSelector - List of items, which contains condition attributes
    :return:
    """
        #Получить объект
        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:

#Run the activity in UIO (UI Object)
#inUIOSelector
#inActionName - UIOActivity (name) from Pywinauto
#old name - ElementRunAction
def UIOSelectorUIOActivity_Run_Dict(inUIOSelector, inActionName, inArgumentList=None, inkwArgumentObject=None):
    """
    Run the activity in UIO (UI Object)

    :param inActionName: UIOActivity (name) activity name string from Pywinauto
    :param inArgumentList:
    :param inkwArgumentObject:
    :return:
    """
    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:

#Get the UIO dict of the attributes
#old name - ElementGetInfo
def UIOSelector_Get_UIOInfo(inUIOSelector):
    """
    Get the UIO dict of the attributes

    :return:
    """
        #Подготовка входного массива
        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:
#Search child UIO by the: Parent UIO, X, Y
#inHierarchyList: [{"index":<>,"element":<>}] - technical argument for internal purpose
#result -List of dict [{"index":<>,"element":<>}] -- list of element hierarchy specifications
#old name - GUISearchElementByRootXY
def UIOXY_SearchChild_ListDict(inRootElement,inX,inY,inHierarchyList=None):
    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

###################################################################################################
#Get list of child UIO's by Parent UIOSelector
#inControlSpecificationArray- UIOSelector
#old name - ElementGetChildElementList
def UIOSelector_GetChildList_UIOList(inUIOSelector=None, inBackend=mDefaultPywinautoBackend):
    """
    Get list of child UIO's by the parent UIOSelector

    :param inBackend: "win32" or "uia"
    :return:
    """

    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:

#Подготовить массив для обращшения к поиску элемементов
#inControlSpecificationArray - UIOSelector (can be dirty)
#old name 1 - ElementSpecificationArraySearchPrepare
#old name 2 - ElementSpecificationListNormalize
def UIOSelector_SearchUIONormalize_UIOSelector (inControlSpecificationArray):
    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)
#Подготовить массив для обращшения к поиску процесса (отличается от поиска элемента, тк данная функция нужна для нормализации спецификации для подключения к процессу с окнами)
def UIOSelector_SearchProcessNormalize_UIOSelector (inControlSpecificationArray):
            if (
            else:

        #Иначе Проверить наличие process - если он есть, то удалить title, control_id и control_type из-за того, что они мешают друг другу
        elif 'process' in lSpecificationItemNew:
            if "title" in lSpecificationItemNew:
                lSpecificationItemNew.pop("title")
#Transfer UI object element info (pywinauto) to UIOInfo (dict of attributes)
#inElementInfo - UIOEI
#old name - ElementInfoExportObject
def UIOEI_Convert_UIOInfo(inElementInfo):
    #Подготовить выходную структуру данных
    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

#Get list of top level
#old name - GetRootElementList
def BackendStr_GetTopLevelList_UIOInfo(inBackend=mDefaultPywinautoBackend):
    #Получить список объектов
    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

#Highlight the UI object
#old name - ElementDrawOutlineNew
def UIOSelector_Highlight(inUIOSelector):
    """
    Highlight (draw outline) the element (in app) by the UIO selector.

    :return:
    """
        UIO_Highlight(UIOSelector_Get_UIO(inUIOSelector))
    else:
        lPIPEResuestDict = {"ModuleName": "UIDesktop", "ActivityName": "UIOSelector_Highlight",
        else:
            return lPIPEResponseDict["Result"]

#inSpecificationArray - UIOSelector
#old name - ElementDrawOutlineNewFocus
def UIOSelector_FocusHighlight(inUIOSelector):
    """
    Set focus and highlight (draw outline) the element (in app) by the UIO selector.

    :return:
    """

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

#old name - draw_outline_new
def UIO_Highlight(lWrapperObject,colour='green',thickness=2,fill=win32defines.BS_NULL,rect=None,inFlagSetFocus=False):
    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 name - draw_outline_new_focus
def UIO_FocusHighlight(lWrapperObject,colour='green',thickness=2,fill=win32defines.BS_NULL,rect=None):
    UIO_Highlight(lWrapperObject,'green',2,win32defines.BS_NULL,None,True)

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

"""
# 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()