import subprocess, json, psutil, time, os, win32security, sys, base64, logging, ctypes #Get input argument
from . import Server
from . import Timer
from . import Processor
from . import BackwardCompatibility # Backward compatibility from v1.1.13
from . import Core
from .Utils import LoggerHandlerDumpLogList

# 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

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

# AGENT DEFS

def AgentActivityItemAdd(inGSettings, inHostNameStr, inUserStr, inActivityItemDict):
    """
    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: None
    """
    lAgentDictItemKeyTurple = (inHostNameStr.upper(),inUserStr.upper())
    if lAgentDictItemKeyTurple not in inGSettings["AgentDict"]:
        inGSettings["AgentDict"][lAgentDictItemKeyTurple] = SettingsTemplate.__AgentDictItemCreate__()
    lThisAgentDict = inGSettings["AgentDict"][lAgentDictItemKeyTurple]
    lThisAgentDict["ActivityList"].append(inActivityItemDict)


def AgentOSCMD(inGSettings, inHostNameStr, inUserStr, inCMDStr, inRunAsyncBool=True):
    """
    Send CMD to OS thought the pyOpenRPA.Agent daemon. Result return to log + Orchestrator by the A2O connection

    :param inGSettings: Global settings dict (singleton)
    :param inHostNameStr:
    :param inUserStr:
    :param inCMDStr:
    :param inRunAsyncBool:
    """
    lActivityItemDict = {
        "Def":"OSCMD", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgList":[], # Args list
        "ArgDict":{"inCMDStr":inCMDStr,"inRunAsyncBool":inRunAsyncBool}, # 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
    AgentActivityItemAdd(inGSettings=inGSettings, inHostNameStr=inHostNameStr, inUserStr=inUserStr, inActivityItemDict=lActivityItemDict)

def AgentOSFileBinaryDataBytesCreate(inGSettings, inHostNameStr, inUserStr, inFilePathStr, inFileDataBytes):
    """
    Create binary file by the base64 string by the pyOpenRPA.Agent daemon process (safe for JSON transmition)

    :param inGSettings: Global settings dict (singleton)
    :param inHostNameStr:
    :param inUserStr:
    :param inFilePathStr:
    :param inFileDataBytes:
    """

    lFileDataBase64Str = base64.b64encode(inFileDataBytes).decode("utf-8")
    lActivityItemDict = {
        "Def":"OSFileBinaryDataBase64StrCreate", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgList":[], # Args list
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataBase64Str":lFileDataBase64Str}, # 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
    AgentActivityItemAdd(inGSettings=inGSettings, inHostNameStr=inHostNameStr, inUserStr=inUserStr, inActivityItemDict=lActivityItemDict)


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

    :param inGSettings: Global settings dict (singleton)
    :param inHostNameStr:
    :param inUserStr:
    :param inFilePathStr:
    :param inFileDataBase64Str:
    """

    lActivityItemDict = {
        "Def":"OSFileBinaryDataBase64StrCreate", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgList":[], # Args list
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataBase64Str":inFileDataBase64Str}, # 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
    AgentActivityItemAdd(inGSettings=inGSettings, inHostNameStr=inHostNameStr, inUserStr=inUserStr, inActivityItemDict=lActivityItemDict)

# Send text file to Agent (string)
def AgentOSFileTextDataStrCreate(inGSettings, inHostNameStr, inUserStr, inFilePathStr, inFileDataStr, inEncodingStr = "utf-8"):
    """
    Create text file by the string by the pyOpenRPA.Agent daemon process

    :param inGSettings: Global settings dict (singleton)
    :param inHostNameStr:
    :param inUserStr:
    :param inFilePathStr:
    :param inFileDataStr:
    :param inEncodingStr:
    """

    lActivityItemDict = {
        "Def":"OSFileTextDataStrCreate", # def alias (look pyOpeRPA.Agent gSettings["ProcessorDict"]["AliasDefDict"])
        "ArgList":[], # Args list
        "ArgDict":{"inFilePathStr":inFilePathStr,"inFileDataStr":inFileDataStr, "inEncodingStr": inEncodingStr}, # 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
    AgentActivityItemAdd(inGSettings=inGSettings, inHostNameStr=inHostNameStr, inUserStr=inUserStr, inActivityItemDict=lActivityItemDict)

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

    :param inUserStr:
    :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 OSCMD(inCMDStr, inRunAsyncBool=True, inLogger = None):
    """
    OS send command in shell locally

    :param inCMDStr:
    :param inRunAsyncBool:
    :param inLogger:
    :return: CMD result string
    """
    lResultStr = ""
    # Subdef to listen OS result
    def _CMDRunAndListenLogs(inCMDStr, inLogger):
        lResultStr = ""
        lOSCMDKeyStr = str(uuid.uuid4())[0:4].upper()
        lCMDProcess = subprocess.Popen(f'cmd /c {inCMDStr}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        if inLogger:
            lListenBool = True
            inLogger.info(f"{lOSCMDKeyStr}: # # # # CMD Process has been STARTED # # # # ")
            inLogger.info(f"{lOSCMDKeyStr}: {inCMDStr}")
            while lListenBool:
                lOutputLineBytes = lCMDProcess.stdout.readline()
                if lOutputLineBytes == b"":
                    lListenBool = False
                lStr = lOutputLineBytes.decode('cp866')
                if lStr.endswith("\n"): lStr = lStr[:-1]
                inLogger.info(f"{lOSCMDKeyStr}: {lStr}")
                lResultStr+=lStr
            inLogger.info(f"{lOSCMDKeyStr}: # # # # CMD Process has been FINISHED # # # # ")
        return lResultStr
    # New call
    if inRunAsyncBool:
        lThread = threading.Thread(target=_CMDRunAndListenLogs, kwargs={"inCMDStr":inCMDStr, "inLogger":inLogger})
        lThread.start()
        lResultStr="ActivityList has been started in async mode - no output is available here."
    else:
        lResultStr = _CMDRunAndListenLogs(inCMDStr=inCMDStr, inLogger=inLogger)
    #lCMDCode = "cmd /c " + inCMDStr
    #subprocess.Popen(lCMDCode)
    #lResultCMDRun = 1  # os.system(lCMDCode)
    return lResultStr

def OrchestratorRestart(inGSettings=None):
    """
    Orchestrator restart

    :param inGSettings: Global settings dict (singleton)
    """
    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 OrchestratorSessionSave(inGSettings=None):
    """
    Orchestrator session save in file _SessionLast_RDPList.json (encoding = "utf-8")

    :param inGSettings: Global settings dict (singleton)
    :return: True every time
    """
    # 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:
        lL = inGSettings["Logger"]
        if lL: lL.info(
            f"Orchestrator has dump the RDP list before the restart.")
    return True

def UACKeyListCheck(inRequest, inRoleKeyList) -> bool:
    """
    Check is client is has access for the key list

    :param inRequest:
    :param inRoleKeyList:
    :return: bool
    """
    return inRequest.UACClientCheck(inRoleKeyList=inRoleKeyList)

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

    :param inGSettings: Global settings dict (singleton)
    :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(inGSettings, inSuperTokenStr):
    """
    Add supertoken for the all access (it is need for the robot communication without human)

    :param inGSettings: Global settings dict (singleton)
    :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 WebURLConnectDef(inGSettings, inMethodStr, inURLStr, inMatchTypeStr, inDef, inContentTypeStr="application/octet-stream"):
    """
     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 inGSettings: Global settings dict (singleton)
    :param inMethodStr:
    :param inURLStr:
    :param inMatchTypeStr:
    :param inDef:
    :param inContentTypeStr:
    """
    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
    }
    inGSettings["ServerDict"]["URLList"].append(lURLItemDict)


def WebURLConnectFolder(inGSettings, inMethodStr, inURLStr, inMatchTypeStr, inFolderPathStr):
    """
    Connect URL to Folder
        "inMethodStr":"GET|POST",
        "inURLStr": "/Folder/", #URL of the request
        "inMatchTypeStr": "", #"BeginWith|Contains|Equal|EqualCase",
        "inFolderPathStr": "", #Absolute or relative path

    :param inGSettings: Global settings dict (singleton)
    :param inMethodStr:
    :param inURLStr:
    :param inMatchTypeStr:
    :param inFolderPathStr:
    """
    # Check if last symbol is "/" - append if not exist
    lFolderPathStr = os.path.abspath(inFolderPathStr)
    if lFolderPathStr[-1]!="/":lFolderPathStr+="/"
    # Prepare URLItem
    lURLItemDict = {
        "Method": inMethodStr.upper(),
        "URL": inURLStr,  # URL of the request
        "MatchType": inMatchTypeStr,  # "BeginWith|Contains|Equal|EqualCase",
        # "ResponseFilePath": "", #Absolute or relative path
        "ResponseFolderPath": lFolderPathStr, # Absolute or relative path
        "ResponseContentType": "application/octet-stream", #HTTP Content-type
        #"ResponseDefRequestGlobal": inDef #Function with str result
    }
    inGSettings["ServerDict"]["URLList"].append(lURLItemDict)


def WebURLConnectFile(inGSettings, inMethodStr, inURLStr, inMatchTypeStr, inFilePathStr, inContentTypeStr="application/octet-stream"):
    """
    Connect URL to File
        "inMethodStr":"GET|POST",
        "inURLStr": "/index", #URL of the request
        "inMatchTypeStr": "", #"BeginWith|Contains|Equal|EqualCase",
        "inFolderPathStr": "", #Absolute or relative path

    :param inGSettings: Global settings dict (singleton)
    :param inMethodStr:
    :param inURLStr:
    :param inMatchTypeStr:
    :param inFilePathStr:
    :param inContentTypeStr:
    """
    lURLItemDict = {
        "Method": inMethodStr.upper(),
        "URL": inURLStr,  # URL of the request
        "MatchType": inMatchTypeStr,  # "BeginWith|Contains|Equal|EqualCase",
        "ResponseFilePath": os.path.abspath(inFilePathStr), #Absolute or relative path
        #"ResponseFolderPath": os.path.abspath(inFilePathStr), # Absolute or relative path
        "ResponseContentType": inContentTypeStr, #HTTP Content-type
        #"ResponseDefRequestGlobal": inDef #Function with str result
    }
    inGSettings["ServerDict"]["URLList"].append(lURLItemDict)

def WebCPUpdate(inGSettings, inCPKeyStr, inHTMLRenderDef=None, inJSONGeneratorDef=None, inJSInitGeneratorDef=None):
    """
    Add control panel HTML, JSON generator or JS when page init

    :param inGSettings: Global settings dict (singleton)
    :param inCPKeyStr:
    :param inHTMLRenderDef:
    :param inJSONGeneratorDef:
    :param inJSInitGeneratorDef:
    """
    # Create Struct if the re is current key
    if inCPKeyStr not in inGSettings["CPDict"]:
        inGSettings["CPDict"][inCPKeyStr] = {"HTMLRenderDef": None,"JSONGeneratorDef": None, "JSInitGeneratorDef": None}
    # CASE HTMLRender
    if inHTMLRenderDef is not None:
        inGSettings["CPDict"][inCPKeyStr]["HTMLRenderDef"]=inHTMLRenderDef
    # CASE JSONGenerator
    if inJSONGeneratorDef is not None:
        inGSettings["CPDict"][inCPKeyStr]["JSONGeneratorDef"] = inJSONGeneratorDef
    # CASE JSInitGeneratorDef
    if inJSInitGeneratorDef is not None:
        inGSettings["CPDict"][inCPKeyStr]["JSInitGeneratorDef"] = inJSInitGeneratorDef

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

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

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

    :param inRequest:
    :param inGSettings: Global settings dict (singleton)
    :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):
    """
    Return User UAC Hierarchy DICT Return {...}

    :param inRequest:
    :return: UAC Dict {}
    """
    return inRequest.UserRoleHierarchyGet()

## GSettings defs
def GSettingsKeyListValueSet(inGSettings, inValue, inKeyList=None):
    """
    Set value in GSettings by the key list

    :param inGSettings: Global settings dict (singleton)
    :param inValue:
    :param inKeyList:
    :return: bool
    """
    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
    return True

def GSettingsKeyListValueGet(inGSettings, inKeyList=None):
    """
    Get the value from the GSettings by the key list

    :param inGSettings: Global settings dict (singleton)
    :param inKeyList:
    :return: value any type
    """
    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]
    return lDict.get(inKeyList[-1],None)

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

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inGSettings: Global settings dict (singleton)
    :param inValue: Any value to be appended in gSettings Dict by the key list
    :param inKeyList: List of the nested keys (see example)
    :return: True every time
    """
    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]].append(inValue) #Set value
    return True

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

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inGSettings: Global settings dict (singleton)
    :param inValue: List with values to be merged with list in gSettings
    :param inKeyList: List of the nested keys (see example)
    :return: True every time
    """
    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
    return True

def ProcessorAliasDefCreate(inGSettings, inDef, inAliasStr=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)

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inGSettings: Global settings dict (singleton)
    :param inDef: Def
    :param inAliasStr: String alias for associated def
    :return: str Alias string (Alias can be regenerated if previous alias was occupied)
    """
    #TODO Pay attention - New alias can be used too - need to create more complex algorythm to create new alias!
    lL = inGSettings["Logger"]
    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 ProcessorAliasDefUpdate(inGSettings, inDef, inAliasStr):
    """
    Update 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)

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inGSettings: Global settings dict (singleton)
    :param inDef: Def
    :param inAliasStr: String alias for associated def
    :return: str Alias string
    """
    if callable(inDef): inGSettings["ProcessorDict"]["AliasDefDict"][inAliasStr] = inDef
    else: raise Exception(f"pyOpenRPA Exception: You can't use Orchestrator.ProcessorAliasDefUpdate with arg 'inDef' string value. inDef is '{inDef}', inAliasStr is '{inAliasStr}'")
    return inAliasStr

def ProcessorActivityItemCreate(inDef, inArgList=None, inArgDict=None, inArgGSettingsStr=None, inArgLoggerStr=None):
    """
    Create activity item. Activity item can be used as list item in ProcessorActivityItemAppend or in Processor.ActivityListExecute.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        # EXAMPLE 1
        def TestDef(inArg1Str, inGSettings, inLogger):
            pass
        lActivityItem = Orchestrator.ProcessorActivityItemCreate(
            inDef = TestDef,
            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.ProcessorAliasDefUpdate(
            inGSettings = gSettings,
            inDef = TestDef,
            inAliasStr="TestDefAlias")
        lActivityItem = Orchestrator.ProcessorActivityItemCreate(
            inDef = "TestDefAlias",
            inArgList=[],
            inArgDict={"inArg1Str": "ArgValueStr"},
            inArgGSettingsStr = None,
            inArgLoggerStr = None)
        # lActivityItem:
        #   {
        #       "Def":"TestDefAlias",
        #       "ArgList":inArgList,
        #       "ArgDict":inArgDict,
        #       "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
    :return: {}
    """
    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)
        }
    return lActivityItemDict

def ProcessorActivityItemAppend(inGSettings, inDef=None, inArgList=None, inArgDict=None, inArgGSettingsStr=None, inArgLoggerStr=None, inActivityItemDict=None):
    """
    Create and add activity item in processor queue.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        # EXAMPLE 1
        def TestDef(inArg1Str, inGSettings, inLogger):
            pass
        lActivityItem = Orchestrator.ProcessorActivityItemAppend(
            inGSettings = gSettingsDict,
            inDef = TestDef,
            inArgList=[],
            inArgDict={"inArg1Str": "ArgValueStr"},
            inArgGSettingsStr = "inGSettings",
            inArgLoggerStr = "inLogger")
        # Activity have been already append in the processor queue

        # EXAMPLE 2
        def TestDef(inArg1Str):
            pass
        Orchestrator.ProcessorAliasDefUpdate(
            inGSettings = gSettings,
            inDef = TestDef,
            inAliasStr="TestDefAlias")
        lActivityItem = Orchestrator.ProcessorActivityItemCreate(
            inDef = "TestDefAlias",
            inArgList=[],
            inArgDict={"inArg1Str": "ArgValueStr"},
            inArgGSettingsStr = None,
            inArgLoggerStr = None)
        Orchestrator.ProcessorActivityItemAppend(
            inGSettings = gSettingsDict,
            inActivityItemDict = lActivityItem)
        # Activity have been already append in the processor queue

    :param inGSettings: Global settings dict (singleton)
    :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 inActivityItemDict: Fill if you already have ActivityItemDict (don't fill inDef, inArgList, inArgDict, inArgGSettingsStr, inArgLoggerStr)
    """
    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]
    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.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        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
    for proc in psutil.process_iter():
        try:
            # Check if process name contains the given name string.
            if inProcessNameWOExeStr.lower() in proc.name().lower():
                return True
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            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.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        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.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inProcessNameWOExeStr: Process name WithOut (WO) '.exe' postfix. Example: "notepad" (not "notepad.exe")
    :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
    :return: None
    """
    # 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.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inProcessNameWOExeList:
    :return: {
    "ProcessWOExeList": ["notepad","..."],
    "ProcessWOExeUpperList": ["NOTEPAD","..."],
    "ProcessDetailList": [
        {
            'pid': 412,
            'username': "DESKTOP\\USER",
            'name': 'notepad.exe',
            'vms': 13.77767775,
            'NameWOExeUpperStr': 'NOTEPAD',
            'NameWOExeStr': "'notepad'"},
        {...}]

    """
    if inProcessNameWOExeList is None: inProcessNameWOExeList = []
    lMapUPPERInput = {} # Mapping for processes WO exe
    lResult = {"ProcessWOExeList":[], "ProcessWOExeUpperList":[],"ProcessDetailList":[]}
    # Create updated list for quick check
    lProcessNameWOExeList = []
    for lItem in inProcessNameWOExeList:
        if lItem is not None:
            lProcessNameWOExeList.append(f"{lItem.upper()}.EXE")
            lMapUPPERInput[f"{lItem.upper()}.EXE"]= lItem
    # Iterate over the list
    for proc in psutil.process_iter():
        try:
            # Fetch process details as dict
            pinfo = proc.as_dict(attrs=['pid', 'name', 'username'])
            pinfo['vms'] = proc.memory_info().vms / (1024 * 1024)
            pinfo['NameWOExeUpperStr'] = pinfo['name'][:-4].upper()
            # Add if empty inProcessNameWOExeList or if process in inProcessNameWOExeList
            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'])
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
           pass
    return lResult

# 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.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        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
    :return: None
    """
    if inArgList is None: inArgList=[]
    if inArgDict is None: inArgDict={}
    try:
        lModule=importlib.import_module(inModulePathStr) #Подключить модуль для вызова
        lFunction=getattr(lModule,inDefNameStr) #Найти функцию
        return lFunction(*inArgList,**inArgDict)
    except Exception as e:
        if inLogger: inLogger.exception("Loop activity error: module/function not founded")

# # # # # # # # # # # # # # # # # # # # # # #
# Scheduler
# # # # # # # # # # # # # # # # # # # # # # #

def SchedulerActivityTimeAddWeekly(inGSettings, inTimeHHMMStr="23:55:", inWeekdayList=None, inActivityList=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.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        # EXAMPLE 1
        def TestDef(inArg1Str):
            pass
        lActivityItem = Orchestrator.ProcessorActivityItemCreate(
            inDef = TestDef,
            inArgList=[],
            inArgDict={"inArg1Str": "ArgValueStr"},
            inArgGSettingsStr = None,
            inArgLoggerStr = None)
        Orchestrator.SchedulerActivityTimeAddWeekly(
            inGSettings = gSettingsDict,
            inTimeHHMMStr = "04:34",
            inWeekdayList=[2,3,4],
            inActivityList = [lActivityItem])
        # Activity will be executed at 04:34 Wednesday (2), thursday (3), friday (4)

    :param inGSettings: Global settings dict (singleton)
    :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
    :return: None
    """
    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):
    """
    Create RDP connect dict item/ Use it connect/reconnect (Orchestrator.RDPSessionConnect)

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        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"],
        #         ###### 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"]
    :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"]
            ###### 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"]
    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"]
        ###### 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!

    :param inGSettings: Global settings dict (singleton)
    :return:
    """
    pass
    #for lItemKeyStr in inGSettings["RobotRDPActive"]["RDPList"]:
    #   lItemDict = inGSettings["RobotRDPActive"]["RDPList"][lItemKeyStr]

def RDPSessionConnect(inGSettings, inRDPSessionKeyStr, inRDPTemplateDict=None, inHostStr=None, inPortStr=None, inLoginStr=None, inPasswordStr=None):
    """
    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

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        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)
        Orchestrator.RDPSessionConnect(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inRDPTemplateDict = lRDPItemDict)
        # Orchestrator will create RDP session by the lRDPItemDict configuration

    :param inGSettings: Global settings dict (singleton)
    :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 :)
    """
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inRDPTemplateDict":inRDPTemplateDict, "inHostStr": inHostStr, "inPortStr": inPortStr,
                    "inLoginStr": inLoginStr, "inPasswordStr": inPasswordStr},  # 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
        # 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))            # 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.")
    return True

def RDPSessionDisconnect(inGSettings, inRDPSessionKeyStr, inBreakTriggerProcessWOExeList = None):
    """
    Disconnect the RDP session and stop monitoring it.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :param inBreakTriggerProcessWOExeList: List of the processes, which will stop the execution. Example ["notepad"]

        .. note::

        Orchestrator look processes on the current machine
    :return: True every time
    """
    if inBreakTriggerProcessWOExeList is None: inBreakTriggerProcessWOExeList = []
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionDisconnect, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inBreakTriggerProcessWOExeList": inBreakTriggerProcessWOExeList },  # 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
        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
    return True

def RDPSessionReconnect(inGSettings, inRDPSessionKeyStr, inRDPTemplateDict=None):
    """
    Reconnect the RDP session

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        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)
        Orchestrator.RDPSessionReconnect(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inRDPTemplateDict = inRDPTemplateDict)
        # Orchestrator will reconnect RDP session and will continue to monitoring current RDP

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :param inRDPTemplateDict: RDP configuration dict with settings (see def Orchestrator.RDPTemplateCreate)
    :return:
    """
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionReconnect, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inRDPTemplateDict":inRDPTemplateDict },  # 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:
        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)
    return True

def RDPSessionMonitorStop(inGSettings, inRDPSessionKeyStr):
    """
    Stop monitoring the RDP session by the Orchestrator process. Current def don't kill RDP session - only stop to track it (it can give )

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        Orchestrator.RDPSessionMonitorStop(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey")
        # Orchestrator will stop the RDP monitoring

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :return: True every time :>
    """
    lResult = True
    inGSettings["RobotRDPActive"]["RDPList"].pop(inRDPSessionKeyStr,None) # Remove item from RDPList
    return lResult

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

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        Orchestrator.RDPSessionLogoff(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inBreakTriggerProcessWOExeList = ['Notepad'])
        # Orchestrator will logoff the RDP session

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :param inBreakTriggerProcessWOExeList: List of the processes, which will stop the execution. Example ["notepad"]
    :return: True - logoff is successful
    """
    if inBreakTriggerProcessWOExeList is None: inBreakTriggerProcessWOExeList = []
    lResult = True
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionLogoff, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inBreakTriggerProcessWOExeList": inBreakTriggerProcessWOExeList },  # 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:
        lCMDStr = "shutdown -L" # CMD logoff command
        # Calculate the session Hex
        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 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
    return lResult

def RDPSessionResponsibilityCheck(inGSettings, inRDPSessionKeyStr):
    """
    DEVELOPING, MAYBE NOT USEFUL Check RDP Session responsibility TODO NEED DEV + TEST

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :return: True every time
    """
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionResponsibilityCheck, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr },  # 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:
        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
    return True

def RDPSessionProcessStartIfNotRunning(inGSettings, inRDPSessionKeyStr, inProcessNameWEXEStr, inFilePathStr, inFlagGetAbsPathBool=True):
    """
    Start process in RDP if it is not running (check by the arg inProcessNameWEXEStr)

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

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

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :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
    :return: True every time :)
    """
    # Check thread
    lResult = True
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        if inGSettings["Logger"]: inGSettings["Logger"].warning(f"RDP def was called not from processor queue - activity will be append in the processor queue.")
        lActivityItem = {
            "Def": RDPSessionProcessStartIfNotRunning, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inProcessNameWEXEStr": inProcessNameWEXEStr, "inFilePathStr": inFilePathStr, "inFlagGetAbsPathBool": inFlagGetAbsPathBool },  # 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(lActivityItem)
    else:
        lCMDStr = CMDStr.ProcessStartIfNotRunning(inProcessNameWEXEStr, inFilePathStr, inFlagGetAbsPath= inFlagGetAbsPathBool)
        # Calculate the session Hex
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr,{}).get("SessionHex", None)
        # Run CMD
        if lSessionHex:
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="CROSSCHECK", inLogger=inGSettings["Logger"],
                                    inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
    return lResult

def RDPSessionCMDRun(inGSettings, inRDPSessionKeyStr, inCMDStr, inModeStr="CROSSCHECK"):
    """
    Send CMD command to the RDP session "RUN" window

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        lResultDict = Orchestrator.RDPSessionCMDRun(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inModeStr = 'LISTEN')
        # Orchestrator will send CMD to RDP and return the result (see return section)

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :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
    }
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        if inGSettings["Logger"]: inGSettings["Logger"].warning(f"RDP def was called not from processor queue - activity will be append in the processor queue.")
        lProcessorActivityDict = {
            "Def": RDPSessionCMDRun, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inCMDStr": inCMDStr, "inModeStr": inModeStr },  # 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(lProcessorActivityDict)
    else:
        #lResult = True
        # Calculate the session Hex
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr,{}).get("SessionHex", None)
        # Run CMD
        if lSessionHex:
            lResult = Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=inCMDStr, inModeStr=inModeStr, inLogger=inGSettings["Logger"],
                                    inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
    return lResult

def RDPSessionProcessStop(inGSettings, inRDPSessionKeyStr, inProcessNameWEXEStr, inFlagForceCloseBool):
    """
    Send CMD command to the RDP session "RUN" window.

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        lResultDict = Orchestrator.RDPSessionProcessStop(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inProcessNameWEXEStr = 'notepad.exe',
            inFlagForceCloseBool = True)
        # Orchestrator will send CMD to RDP and return the result (see return section)

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :param inProcessNameWEXEStr: Process name to kill. Example: 'notepad.exe'
    :param inFlagForceCloseBool: True - force close the process. False - safe close the process
    :return: True every time
    """
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionProcessStop, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inProcessNameWEXEStr": inProcessNameWEXEStr, "inFlagForceCloseBool": inFlagForceCloseBool },  # 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:
        lResult = True
        lCMDStr = f'taskkill /im "{inProcessNameWEXEStr}" /fi "username eq %USERNAME%"'
        if inFlagForceCloseBool:
            lCMDStr+= " /F"
        # Calculate the session Hex
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr,{}).get("SessionHex", None)
        # Run CMD
        if lSessionHex:
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="CROSSCHECK", inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
    return lResult

def RDPSessionFileStoredSend(inGSettings, inRDPSessionKeyStr, inHostFilePathStr, inRDPFilePathStr):
    """
    Send file from Orchestrator session to the RDP session using shared drive in RDP (see RDP Configuration Dict, Shared drive)

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        lResultDict = Orchestrator.RDPSessionFileStoredSend(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inHostFilePathStr = "TESTDIR\\Test.py",
            inRDPFilePathStr = "C:\\RPA\\TESTDIR\\Test.py")
        # Orchestrator will send CMD to RDP and return the result (see return section)

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :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"
    :return: True every time
    """
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionFileStoredSend, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr,  "inHostFilePathStr": inHostFilePathStr, "inRDPFilePathStr": inRDPFilePathStr },  # 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:
        lResult = True
        lCMDStr = CMDStr.FileStoredSend(inHostFilePath = inHostFilePathStr, inRDPFilePath = inRDPFilePathStr)
        # Calculate the session Hex
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr, {}).get("SessionHex", None)
        #lSessionHex = inGlobalDict["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr]["SessionHex"]
        # Run CMD
        if lSessionHex:
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="LISTEN", inClipboardTimeoutSec = 120, inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
    return lResult

def RDPSessionFileStoredRecieve(inGSettings, inRDPSessionKeyStr, inRDPFilePathStr, inHostFilePathStr):
    """
    Recieve file from RDP session to the Orchestrator session using shared drive in RDP (see RDP Configuration Dict, Shared drive)

    .. code-block:: python

        # USAGE
        from pyOpenRPA import Orchestrator

        lResultDict = Orchestrator.RDPSessionFileStoredRecieve(
            inGSettings = gSettings,
            inRDPSessionKeyStr = "RDPKey",
            inHostFilePathStr = "TESTDIR\\Test.py",
            inRDPFilePathStr = "C:\\RPA\\TESTDIR\\Test.py")
        # Orchestrator will send CMD to RDP and return the result (see return section)

    :param inGSettings: Global settings dict (singleton)
    :param inRDPSessionKeyStr: RDP Session string key - need for the further identification
    :param inRDPFilePathStr: !Absolute! path to the destination file location on the RDP side. Example: "C:\\RPA\\TESTDIR\\Test.py"
    :param inHostFilePathStr: Relative or absolute path to the file location on the Orchestrator side. Example: "TESTDIR\\Test.py"
    :return: True every time
    """
    # Check thread
    if not Core.IsProcessorThread(inGSettings=inGSettings):
        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": RDPSessionFileStoredRecieve, # def link or def alias (look gSettings["Processor"]["AliasDefDict"])
            "ArgList": [],  # Args list
            "ArgDict": {"inRDPSessionKeyStr": inRDPSessionKeyStr, "inRDPFilePathStr": inRDPFilePathStr, "inHostFilePathStr": inHostFilePathStr },  # 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:
        lResult = True
        lCMDStr = CMDStr.FileStoredRecieve(inRDPFilePath = inRDPFilePathStr, inHostFilePath = inHostFilePathStr)
        # Calculate the session Hex
        lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr,{}).get("SessionHex", None)
        # Run CMD
        if lSessionHex:
            Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=lCMDStr, inModeStr="LISTEN", inClipboardTimeoutSec = 120, inLogger=inGSettings["Logger"], inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr])
    return lResult

# # # # # # # # # # # # # # # # # # # # # # #
# # # # # Start orchestrator
# # # # # # # # # # # # # # # # # # # # # # #

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

    :param inGSettings: Global settings dict (singleton)
    :return: None
    """
    while True:
        time.sleep(inGSettings["Autocleaner"]["IntervalSecFloat"])  # Wait for the next iteration
        lL = inGSettings["Logger"]
        if lL: lL.info(f"Autocleaner is running") # Info
        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
    # # # # # # # # # # # # # # # # # # # # # # # # # #

from .. import __version__ # Get version from the package

def Orchestrator(inGSettings):
    lL = inGSettings["Logger"]
    # https://stackoverflow.com/questions/130763/request-uac-elevation-from-within-a-python-script
    def is_admin():
        try:
            return ctypes.windll.shell32.IsUserAnAdmin()
        except:
            return False
    if not is_admin():
        # Re-run the program with admin rights
        ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
    else:
        # Code of your program here
        #mGlobalDict = Settings.Settings(sys.argv[1])
        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

        # Check _SessionLast_RDPList.json in working directory. if exist - load into gsettings
        # GSettings
        #"RobotRDPActive": {
        #    "RDPList": {
        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
            gSettingsDict["RobotRDPActive"]["RDPList"]=lSessionLastRDPList # Set the last session dict
            if lL: lL.warning(f"RDP Session List was restored from previous Orchestrator session")

        # 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

        #Инициализация настроечных параметров
        lDaemonLoopSeconds=gSettingsDict["SchedulerDict"]["CheckIntervalSecFloat"]
        lDaemonActivityLogDict={} #Словарь отработанных активностей, ключ - кортеж (<activityType>, <datetime>, <processPath || processName>, <processArgs>)
        lDaemonLastDateTime=datetime.datetime.now()
        gSettingsDict["ServerDict"]["WorkingDirectoryPathStr"] = os.getcwd() # Set working directory in g settings

        #Инициализация сервера
        lThreadServer = Server.RobotDaemonServer("ServerThread", gSettingsDict)
        lThreadServer.start()
        if lL: lL.info("Web server has been started")  #Logging

        # 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

        # 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


        if lL: lL.info("Scheduler loop start")  #Logging
        gDaemonActivityLogDictRefreshSecInt = 10 # The second period for clear lDaemonActivityLogDict from old items
        gDaemonActivityLogDictLastTime = time.time() # The second perioad for clean lDaemonActivityLogDict from old items
        while True:
            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
                    except Exception as e:
                        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)
            except Exception as e:
                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
