Preventing "Event loop shut down?"

I have a script that performance a lengthy FTP transfer. Because of the time it takes the file transfer portion of the script is executed using ‘system.util.invokeAsynchronous’. Everything operates as intended but the output console and/or gateway log returns with the “rejectedExecution: Failed to submit a listener notification task. Event loop shut down?” statement each time.

Is there a way to avoid this from occurring each time?

Show your code and how you are calling it.

I suspect this has something to do with Jython's janky socket implementation, but sharing your code won't hurt.

1 Like

Omitting a couple of variables here, but general pattern:

Value Changed script on a UDT-

if currentValue.value == True:
	node_path = tagPath.replace("[default]","")
	nodeID = node_path.split("/")[0]
	
	
	Units.CCU_Update.CCUSoftwareUpdate(nodeID)
return

CCUSoftwareUpdate() ultimately calls the invokeAsynchronous

def CCUSoftwareUpdate(nodeID):


if model == 'V25':
	return
else:
	credentials		= {named query for credentials}
	
	arguments = (nodeID, model, credentials, progress_tag)
	message = "{} software update in progress".format(nodeID)
	system.util.invokeAsynchronous(Units.CCU_Update.file_transfer, args = arguments, description= message)
	return
return

Please show the code for this function as well.

def file_transfer(nodeID, model, credentials, progress_tag):

import ftplib
import os
import StringIO
import socket
import uuid


system.util.getLogger("FTPTransfer").info("Updating CCU software on {}".format(nodeID))
#Set a 10-minute timeout for FTP operations
socket.setdefaulttimeout(600) 

batchID = str(uuid.uuid4())

try:
    # Image File Source Directory
    source_dir = r"C:/Ignition/"+ model +"/"
    
    # Find the only .ci3 file
    ci3_files = [f for f in os.listdir(source_dir) if f.endswith(".ci3")]
    if len(ci3_files) != 1:
        raise Exception("Expected only one .ci3 file, found {}".format(len(ci3_files)))

    # Store the current database filename for logging to the unit as the tag CCU.Statuses.CCU_Software_Rev
    original_filename = ci3_files[0]
    source_filepath = os.path.join(source_dir, original_filename)
    source_filesize = os.path.getsize(source_filepath)
    
     # Extract credentials for the specified device
    host = credentials.getValueAt(0,0)
    password = credentials.getValueAt(0,1)
    user = 'admin'
    activeTag = '[default]' + nodeID + '/Version_Info/Software/CCU_Update_Active'
    
    # Location to copy the image file to on the unit (SD card)
    if model == "MAX":
    	directory = "/DRIVE-E/"
    	port = 21
    else:
    	directory = "/"
    	port = 2111

    if not all([host, user, password]):
        raise Exception("Missing FTP credentials for device '{}'.".format(nodeID)) 
    
    #Establish FTP connection
    ftp = ftplib.FTP()
    ftp.connect(host, port, timeout = 60) #Time out after 60 seconds
    ftp.login(user, password)
    #Set 'Passive' to false, ensuring it is an active connection
    ftp.set_pasv(False)
    #Set the working directory
    ftp.cwd(directory) 
    ftp.voidcmd("TYPE I")

    # Try to read CCU_Software_Rev.txt from the edge node
    #    This file contains the filename of the last database image file that was transferred to the unit via this script
    #    A mismatch between what is in this file and the image file saved at C:/Ignition/ will trigger a transfer
    #    The contents of this file are linked to the tag CCU.Statuses.CCU_Software_Rev
    rev_lines = []
    try:
        ftp.retrlines("RETR CCU_Software_Rev.txt", rev_lines.append)
        edge_rev = rev_lines[0].strip() if rev_lines else ""
    except:
        edge_rev = ""  # File doesn't exist or can't be read

    # Compare device CCU software rev against current released CCU software rev
    if edge_rev != original_filename:
        # Update status to 1 in the file 'CCU_Software_Update_Status.txt'
        # This initiates notification popups on the unit HMI for the user to acknowledge
        status = "1"
        status_file = StringIO.StringIO(status + "\n")
        ftp.storlines("STOR CCU_Software_Update_Status.txt", status_file)
        message = "Revision mismatch or missing. Updating CCU software on {} from {} to {}.".format(nodeID,edge_rev, original_filename)
        system.util.getLogger("FTPTransfer").info(message)
        
        #Set the Progress Active Flag to true
        	#This ensures the progress bar on the Unit Dashboard becomes visible
        
        system.tag.writeAsync(activeTag, True)
        
        #Inititate Manifest Log for the update instance
       
        newVersion = original_filename
        
        Units.CCU_Update.beginCCUmanifest(nodeID, batchID, newVersion, status, message)
		    		
        # Attempt the image file transfer
        try:
        	#set up the Callback function to be used for status updates on the file transfer
        	progress = Units.CCU_Update.progressCallback(source_filesize, progress_tag)
        	
        	with open(source_filepath, "rb") as file:
        		ftp.storbinary("STOR image.ci3", file, blocksize = 32768, callback = progress)

            # Transfer succeeded — update status to 2 and write the current revision to CCU_Software_Rev.txt
			status = "2"
			status_file = StringIO.StringIO(status + "\n")
			ftp.storlines("STOR CCU_Software_Update_Status.txt", status_file)
			
			txt_content = StringIO.StringIO(original_filename + "\n")
			ftp.storlines("STOR CCU_Software_Rev.txt", txt_content)
			message = "Transfer complete. {} CCU software updated to: {}.".format(nodeID, original_filename)
			system.util.getLogger("FTPTransfer").info(message)
			
			#Completed CCU Manifest
			Units.CCU_Update.updateCCUmanifest(status, message, nodeID, batchID)
			
        except socket.timeout as e:
            message = "CCU software update on {} timed out: {}".format(nodeID,e)
            system.util.getLogger("FTPTransfer").error(message)

            # Transfer failed due to timeout — set status to 3
            status = "3"
            status_file = StringIO.StringIO(status + "\n")
            try:
                ftp.storlines("STOR CCU_Software_Update_Status.txt", status_file)
                message = "Timeout occurred while updating CCU software on {}. Status set to '3'.".format(nodeID)
                system.util.getLogger("FTPTransfer").info(message)
                Units.CCU_Update.updateCCUmanifest(status, message, nodeID, batchID)
            except Exception as inner_e:
            	message = "Failed to reset status after timeout on {}: {} ".format(nodeID, inner_e)
            	system.util.getLogger("FTPTransfer").error(message)
            	Units.CCU_Update.updateCCUmanifest(status, message, nodeID, batchID)
    else:
        # Transfer not needed, software already up to date — reset status to 0
        status = "0"
        status_file = StringIO.StringIO(status + "\n")
        ftp.storlines("STOR CCU_Software_Update_Status.txt", status_file)
        message = "No update needed. {} already running {}.".format(nodeID,edge_rev)
        system.util.getLogger("FTPTransfer").info(message)
        Units.CCU_Update.updateCCUmanifest(status, message, nodeID, batchID)
    
    ftp.quit()
    
    #Set Progress Tag to False
    	#Ensure progress bar visibility is disabled on the Unit Dashboard
    system.tag.writeAsync(activeTag, False)
    
except Exception as e:
	status = '3'
	message = "CCU software update on {} failed: ".format(nodeID) + str(e)
    system.util.getLogger("FTPTransfer").error(message)
    Units.CCU_Update.updateCCUmanifest(status, message, nodeID, batchID)


#Close the socket
socket.close()
return

This can't possibly do anything but error out... at no point in your script do you have a socket instance or assign one to a socket variable...

The only reference to socket is your import, and setdefaulttimeout works because it's defined in _socket.py and not associated with an instance.

That is correct, and has since been removed. The main code for this was inherited and included the socket.setdefaulttimeout(). The socket.close() was my novice attempt at closing the socket before the defaulttimeout.

Please correct me if I am wrong, but I don't see any reason for socket module to even be imported, as none of the transfers are initiated with it and the FTP transfer has its own timeout for the transfer instance.

Without the setdefaulttimeout call I don’t see any reason either.

Unfortunately I don’t think any of this is related to your log message. It’s probably just Jython’s crappy stdlib.

1 Like

Not sure this is it either, but that function call should be system.tag.writeAsync([activeTag], [True])

Also take a look at this, according to the ftplib documentation, the callback is a callable that should take a single parameter, which is the data as bytes. I’m not 100% on how this line works:

progress = Units.CCU_Update.progressCallback(source_filesize, progress_tag)

But I believe Python tries calling it like progress(block), and that may be erroring out somehow since you’ve already provided parameters.

OK so my files are tiny but try this for starters, your architecture seems a bit more complex than mine….

Also I need to reply to my own thread because I got that issue of my last reply sorted.

I’ll be VPN’d into that Gateway tomorrow.

This is a custom function I wrote to pass the value of bytes sent as a percentage of the total file to a progress bar on my perspective dashboard.

image

The progress bar is made visible through the tag.writeAsync() you mentioned here:

If this is an improper use of its proper syntax than I got lucky because it is working appropriately