From bee96d3596a00f9fca3c706d67d83a3bc6070399 Mon Sep 17 00:00:00 2001 From: Ivan Maslov Date: Wed, 14 Oct 2020 22:30:58 +0300 Subject: [PATCH] # Create Orchestrator.RDP defs - need decorator @ProcessorOnly --- .../Settings/SettingsOrchestratorExample.py | 3 +- .../Orchestrator/BackwardCompatibility.py | 3 +- Sources/pyOpenRPA/Orchestrator/Core.py | 5 + .../pyOpenRPA/Orchestrator/Orchestrator.py | 232 +++++++++++++++++- Sources/pyOpenRPA/Orchestrator/Processor.py | 3 +- .../Orchestrator/SettingsTemplate.py | 3 +- changelog.md | 2 + 7 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 Sources/pyOpenRPA/Orchestrator/Core.py diff --git a/Orchestrator/Settings/SettingsOrchestratorExample.py b/Orchestrator/Settings/SettingsOrchestratorExample.py index e720dfff..813e09df 100644 --- a/Orchestrator/Settings/SettingsOrchestratorExample.py +++ b/Orchestrator/Settings/SettingsOrchestratorExample.py @@ -267,7 +267,8 @@ def Settings(): ], "AliasDefDict": {}, # Storage for def with Str alias. To use it see pyOpenRPA.Orchestrator.ControlPanel "CheckIntervalSecFloat": 1.0, # Interval for check gSettings in ProcessorDict > ActivityList - "ExecuteBool": True # Flag to execute thread processor + "ExecuteBool": True, # Flag to execute thread processor + "ThreadIdInt": None # Technical field - will be setup when processor init }, "ControlPanelDict": { "RefreshSeconds": 5, # deprecated parameter diff --git a/Sources/pyOpenRPA/Orchestrator/BackwardCompatibility.py b/Sources/pyOpenRPA/Orchestrator/BackwardCompatibility.py index cd89d5d0..c162c38c 100644 --- a/Sources/pyOpenRPA/Orchestrator/BackwardCompatibility.py +++ b/Sources/pyOpenRPA/Orchestrator/BackwardCompatibility.py @@ -172,6 +172,7 @@ def Update(inGSettings): ], "AliasDefDict": {} , # Storage for def with Str alias. To use it see pyOpenRPA.Orchestrator.ControlPanel "CheckIntervalSecFloat": 1.0, # Interval for check gSettings in ProcessorDict > ActivityList - "ExecuteBool": True # Flag to execute thread processor + "ExecuteBool": True, # Flag to execute thread processor + "ThreadIdInt": None # Fill thread id when processor will be inited } if lL: lL.warning(f"Backward compatibility (v1.1.20 to v1.2.0): Create new structure 'ProcessorDict'") # Log about compatibility \ No newline at end of file diff --git a/Sources/pyOpenRPA/Orchestrator/Core.py b/Sources/pyOpenRPA/Orchestrator/Core.py new file mode 100644 index 00000000..bd4136fb --- /dev/null +++ b/Sources/pyOpenRPA/Orchestrator/Core.py @@ -0,0 +1,5 @@ +import threading + +# Check if current execution is in Processor thread +def IsProcessorThread(inGSettings): + return inGSettings["ProcessorDict"]["ThreadIdInt"] == threading.get_ident() \ No newline at end of file diff --git a/Sources/pyOpenRPA/Orchestrator/Orchestrator.py b/Sources/pyOpenRPA/Orchestrator/Orchestrator.py index 7281efe5..eea2f1f2 100644 --- a/Sources/pyOpenRPA/Orchestrator/Orchestrator.py +++ b/Sources/pyOpenRPA/Orchestrator/Orchestrator.py @@ -1,15 +1,16 @@ -import subprocess, json, psutil -import datetime -import time -import codecs -import os, win32security -import signal -import sys #Get input argument -import pdb +import subprocess, json, psutil, time, os, win32security, sys #Get input argument from . import Server from . import Timer from . import Processor from . import BackwardCompatibility # Backward compatibility from v1.1.13 +from . import Core + +# 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 .RobotRDPActive import ConnectorExceptions # Exceptions #from .Settings import Settings import importlib @@ -53,7 +54,7 @@ def OrchestratorRestart(inGSettings=None): ## Orchestrator restart def OrchestratorSessionSave(inGSettings=None): ## Orchestrator session save # Dump RDP List in file json lFile = open("_SessionLast_RDPList.json", "w", encoding="utf-8") - lFile.write(json.dumps(gSettingsDict["RobotRDPActive"]["RDPList"])) # dump json to file + lFile.write(json.dumps(inGSettings["RobotRDPActive"]["RDPList"])) # dump json to file lFile.close() # Close the file if inGSettings is not None: lL = inGSettings["Logger"] @@ -159,6 +160,34 @@ def ProcessStop(inProcessNameWOExeStr, inCloseForceBool, inUserNameStr = "%usern # Kill process os.system(lActivityCloseCommand) +#Check activity of the list of processes +def ProcessListGet(inProcessNameWOExeList=[]): + '''Get list of running process sorted by Memory Usage and filtered by inProcessNameWOExeList''' + lMapUPPERInput = {} # Mapping for processes WO exe + lResult = {"ProcessWOExeList":[],"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: + pinfo['NameWOExeStr'] = lMapUPPERInput[pinfo['name'].upper()] + lResult["ProcessDetailList"].append(pinfo) # Append dict to list + lResult["ProcessWOExeList"].append(pinfo['NameWOExeStr']) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + return lResult + # Python def - start module function def PythonStart(inModulePathStr, inDefNameStr, inArgList=[], inArgDict={}, inLogger = None): # Python import module and start def try: @@ -168,6 +197,191 @@ def PythonStart(inModulePathStr, inDefNameStr, inArgList=[], inArgDict={}, inLog except Exception as e: if inLogger: inLogger.exception("Loop activity error: module/function not founded") + +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # +# RDPSession +# # # # # # # # # # # # # # # # # # # # # # # + +# Create new RDPSession in RobotRDPActive +def RDPSessionConnect(inGSettings, inRDPSessionKeyStr, inHostStr, inPortStr, inLoginStr, inPasswordStr): + # 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, "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 + # ATTENTION - dont connect if RDP session is exist + if inRDPSessionKeyStr not in inGSettings["RobotRDPActive"]["RDPList"]: + lRDPConfigurationItem = { # Init the configuration item + "Host": inHostStr, # Host address, example "77.77.22.22" + "Port": inPortStr, # RDP Port, example "3389" + "Login": inLoginStr, # Login, example "test" + "Password": inPasswordStr, # Password, example "test" + "Screen": { + "Width": 1680, # Width of the remote desktop in pixels, example 1680 + "Height": 1050, # Height of the remote desktop in pixels, example 1050 + # "640x480" or "1680x1050" or "FullScreen". If Resolution not exists set full screen, example + "FlagUseAllMonitors": False, # True or False, example False + "DepthBit": "32" # "32" or "24" or "16" or "15", example "32" + }, + "SharedDriveList": ["c"], # 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 + } + inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr] = lRDPConfigurationItem # Add item in RDPList + Connector.Session(lRDPConfigurationItem) # Create the RDP session + Connector.SystemRDPWarningClickOk() # Click all warning messages + return True + +# Disconnect the RDP session +def RDPSessionDisconnect(inGSettings, inRDPSessionKeyStr, 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": RDPSessionConnect, # 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 + +# RDP Session reconnect +def RDPSessionReconnect(inGSettings, inRDPSessionKeyStr): + lRDPConfigurationItem = inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr] + RDPSessionDisconnect(inRDPSessionKeyStr=inRDPSessionKeyStr) # Disconnect the RDP + # Add item in RDPList + inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr] = lRDPConfigurationItem + # Create the RDP session + Connector.Session(lRDPConfigurationItem) + return True + +# Stop track the RDP session. Current def dont kill RDP session - only stop to track it (it can give ) +def RDPSessionMonitorStop(inGSettings, inRDPSessionKeyStr): + lResult = True + inGSettings["RobotRDPActive"]["RDPList"].pop(inRDPSessionKeyStr,None) # Remove item from RDPList + return lResult + +# Logoff the RDP session +def RDPSessionLogoff(inGSettings, inRDPSessionKeyStr, inBreakTriggerProcessWOExeList = []): + lResult = True + 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 + +# Check RDP Session responsibility TODO NEED DEV + TEST +def RDPSessionResponsibilityCheck(inGSettings, inRDPSessionKeyStr): + 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 + +# Start process if it is not running +def RDPSessionProcessStartIfNotRunning(inGSettings, inRDPSessionKeyStr, inProcessNameWEXEStr, inFilePathStr, inFlagGetAbsPathBool=True): + lResult = True + 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"): + lResult = True + # Calculate the session Hex + lSessionHex = inGSettings["RobotRDPActive"]["RDPList"].get(inRDPSessionKeyStr,{}).get("SessionHex", None) + # Run CMD + if lSessionHex: + Connector.SessionCMDRun(inSessionHex=lSessionHex, inCMDCommandStr=inCMDStr, inModeStr=inModeStr, inLogger=inGSettings["Logger"], + inRDPConfigurationItem=inGSettings["RobotRDPActive"]["RDPList"][inRDPSessionKeyStr]) + return lResult +# Create CMD str to stop process +def RDPSessionProcessStop(inGSettings, inRDPSessionKeyStr, inProcessNameWEXEStr, inFlagForceCloseBool): + 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 +# Send file from Host to Session RDP using shared drive in RDP +def RDPSessionFileStoredSend(inGSettings, inRDPSessionKeyStr, inHostFilePathStr, inRDPFilePathStr): + 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 +# Recieve file from Session RDP to Host using shared drive in RDP +def RDPSessionFileStoredRecieve(inGSettings, inRDPSessionKeyStr, inRDPFilePathStr, inHostFilePathStr): + 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 # # # # # # # # # # # # # # # # # # # # # # # diff --git a/Sources/pyOpenRPA/Orchestrator/Processor.py b/Sources/pyOpenRPA/Orchestrator/Processor.py index b465c9b1..c34c3493 100644 --- a/Sources/pyOpenRPA/Orchestrator/Processor.py +++ b/Sources/pyOpenRPA/Orchestrator/Processor.py @@ -1,5 +1,5 @@ # 1.2.0 - general processor - contains old orchestrator processor + RDPActive processor -import time, copy +import time, copy, threading # Run processor synchronious def ProcessorRunSync(inGSettings): """ @@ -17,6 +17,7 @@ def ProcessorRunSync(inGSettings): "CheckIntervalSecFloat": 1.0 # Interval for check gSettings in ProcessorDict > ActivityList "ExecuteBool": True # Flag to execute thread processor """ + inGSettings["ProcessorDict"]["ThreadIdInt"] = threading.get_ident() # fill Processor thread id while inGSettings["ProcessorDict"]["ExecuteBool"]: lActivityItem = inGSettings["ProcessorDict"]["ActivityList"].pop(0, None) # Extract the first item from processor queue while lActivityItem is not None: diff --git a/Sources/pyOpenRPA/Orchestrator/SettingsTemplate.py b/Sources/pyOpenRPA/Orchestrator/SettingsTemplate.py index f17cd3cd..ee608416 100644 --- a/Sources/pyOpenRPA/Orchestrator/SettingsTemplate.py +++ b/Sources/pyOpenRPA/Orchestrator/SettingsTemplate.py @@ -204,7 +204,8 @@ def __Create__(): ], "AliasDefDict": {}, # Storage for def with Str alias. To use it see pyOpenRPA.Orchestrator.ControlPanel "CheckIntervalSecFloat": 1.0, # Interval for check gSettings in ProcessorDict > ActivityList - "ExecuteBool": True # Flag to execute thread processor + "ExecuteBool": True, # Flag to execute thread processor + "ThreadIdInt": None # Technical field - will be setup when processor init }, "ControlPanelDict": { "RefreshSeconds": 5, # deprecated parameter diff --git a/changelog.md b/changelog.md index fd994eca..a7e37758 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,8 @@ - - def ProcessStart(inPathStr, inArgList, inStopProcessNameWOExeStr=None): # Start process locally [optional: if task name is not started] - - def ProcessStop(inProcessNameWOExeStr, inCloseForceBool, inUserNameStr = "%username%"): # Stop process - - def PythonStart(inModulePathStr, inDefNameStr, inArgList=[], inArgDict={}, inLogger = None): # Python import module and start def +- - Add pyOpenRPA.Orchestrator.Core module technical defs +- - - def IsProcessorThread() return True or False [1.1.0] After 2 month test prefinal with new improovements (+RobotRDPActive in Orchestrator + Easy ControlPanelTemplate) Beta before 1.1.0 (new way of OpenRPA with improvements. Sorry, but no backward compatibility)/ Backward compatibility will start from 1.0.1