1111
1212import concore_base
1313
14- logger = logging .getLogger (' concore' )
14+ logger = logging .getLogger (" concore" )
1515logger .addHandler (logging .NullHandler ())
1616
17- #these lines mute the noisy library
18- logging .getLogger (' matplotlib' ).setLevel (logging .WARNING )
19- logging .getLogger (' PIL' ).setLevel (logging .WARNING )
20- logging .getLogger (' urllib3' ).setLevel (logging .WARNING )
21- logging .getLogger (' requests' ).setLevel (logging .WARNING )
17+ # these lines mute the noisy library
18+ logging .getLogger (" matplotlib" ).setLevel (logging .WARNING )
19+ logging .getLogger (" PIL" ).setLevel (logging .WARNING )
20+ logging .getLogger (" urllib3" ).setLevel (logging .WARNING )
21+ logging .getLogger (" requests" ).setLevel (logging .WARNING )
2222
2323
2424# ===================================================================
@@ -51,28 +51,40 @@ def _register_pid():
5151
5252
5353def _cleanup_pid ():
54- """Remove the current process PID from the registry on exit."""
54+ """Remove the current process PID from the registry on exit.
55+
56+ Uses file locking on Windows (msvcrt.locking) to prevent race
57+ conditions when multiple nodes exit concurrently.
58+ """
5559 pid = str (os .getpid ())
5660 try :
5761 if not os .path .exists (_PID_REGISTRY_FILE ):
5862 return
59- with open (_PID_REGISTRY_FILE , "r" ) as f :
63+ with open (_PID_REGISTRY_FILE , "r+" ) as f :
64+ # Acquire an exclusive lock on Windows to prevent concurrent
65+ # read-modify-write races between exiting nodes.
66+ if hasattr (sys , "getwindowsversion" ):
67+ import msvcrt
68+
69+ msvcrt .locking (f .fileno (), msvcrt .LK_LOCK , 1 )
6070 pids = [line .strip () for line in f if line .strip ()]
61- remaining = [p for p in pids if p != pid ]
62- if remaining :
63- with open (_PID_REGISTRY_FILE , "w" ) as f :
71+ remaining = [p for p in pids if p != pid ]
72+ if remaining :
73+ f .seek (0 )
74+ f .truncate ()
6475 for p in remaining :
6576 f .write (p + "\n " )
66- else :
67- # No PIDs left — clean up both files
68- try :
69- os .remove (_PID_REGISTRY_FILE )
70- except OSError :
71- pass
72- try :
73- os .remove (_KILL_SCRIPT_FILE )
74- except OSError :
75- pass
77+ else :
78+ f .close ()
79+ # No PIDs left — clean up both files
80+ try :
81+ os .remove (_PID_REGISTRY_FILE )
82+ except OSError :
83+ pass
84+ try :
85+ os .remove (_KILL_SCRIPT_FILE )
86+ except OSError :
87+ pass
7688 except OSError :
7789 pass # Non-fatal: best-effort cleanup
7890
@@ -88,28 +100,34 @@ def _write_kill_script():
88100 """
89101 try :
90102 script = "@echo off\r \n "
91- script += " if not exist \ " %~dp0" + _PID_REGISTRY_FILE + " \" (\r \n "
103+ script += ' if not exist "%~dp0' + _PID_REGISTRY_FILE + '" (\r \n '
92104 script += " echo No PID registry found. Nothing to kill.\r \n "
93105 script += " exit /b 0\r \n "
94106 script += ")\r \n "
95- script += "for /f \" tokens=*\" %%p in (%~dp0" + _PID_REGISTRY_FILE + ") do (\r \n "
96- script += " tasklist /FI \" PID eq %%p\" 2>nul | find /i \" python\" >nul\r \n "
107+ script += (
108+ 'for /f "usebackq tokens=*" %%p in ("%~dp0'
109+ + _PID_REGISTRY_FILE
110+ + '") do (\r \n '
111+ )
112+ script += ' tasklist /FI "PID eq %%p" 2>nul | find /i "python" >nul\r \n '
97113 script += " if not errorlevel 1 (\r \n "
98114 script += " echo Killing Python process %%p\r \n "
99115 script += " taskkill /F /PID %%p >nul 2>&1\r \n "
100116 script += " ) else (\r \n "
101- script += " echo Skipping PID %%p - not a Python process or not running\r \n "
117+ script += (
118+ " echo Skipping PID %%p - not a Python process or not running\r \n "
119+ )
102120 script += " )\r \n "
103121 script += ")\r \n "
104- script += " del /q \ " %~dp0" + _PID_REGISTRY_FILE + " \" 2>nul\r \n "
105- script += " del /q \ " %~dp0" + _KILL_SCRIPT_FILE + " \" 2>nul\r \n "
106- with open (_KILL_SCRIPT_FILE , "w" ) as f :
122+ script += ' del /q "%~dp0' + _PID_REGISTRY_FILE + '" 2>nul\r \n '
123+ script += ' del /q "%~dp0' + _KILL_SCRIPT_FILE + '" 2>nul\r \n '
124+ with open (_KILL_SCRIPT_FILE , "w" , newline = "" ) as f :
107125 f .write (script )
108126 except OSError :
109127 pass # Non-fatal: best-effort script generation
110128
111129
112- if hasattr (sys , ' getwindowsversion' ):
130+ if hasattr (sys , " getwindowsversion" ):
113131 _register_pid ()
114132 _write_kill_script ()
115133 atexit .register (_cleanup_pid )
@@ -125,17 +143,19 @@ def _write_kill_script():
125143
126144last_read_status = "SUCCESS"
127145
128- s = ''
129- olds = ''
146+ s = ""
147+ olds = ""
130148delay = 1
131149retrycount = 0
132- inpath = "./in" # must be rel path for local
150+ inpath = "./in" # must be rel path for local
133151outpath = "./out"
134152simtime = 0
135153
154+
136155def _port_path (base , port_num ):
137156 return base + str (port_num )
138157
158+
139159concore_params_file = os .path .join (_port_path (inpath , 1 ), "concore.params" )
140160concore_maxtime_file = os .path .join (_port_path (inpath , 1 ), "concore.maxtime" )
141161
@@ -145,16 +165,19 @@ def _port_path(base, port_num):
145165
146166_mod = sys .modules [__name__ ]
147167
168+
148169# ===================================================================
149170# ZeroMQ Communication Wrapper
150171# ===================================================================
151172def init_zmq_port (port_name , port_type , address , socket_type_str ):
152173 concore_base .init_zmq_port (_mod , port_name , port_type , address , socket_type_str )
153174
175+
154176def terminate_zmq ():
155177 """Clean up all ZMQ sockets and contexts before exit."""
156178 concore_base .terminate_zmq (_mod )
157179
180+
158181def signal_handler (sig , frame ):
159182 """Handle interrupt signals gracefully."""
160183 print (f"\n Received signal { sig } , shutting down gracefully..." )
@@ -165,20 +188,23 @@ def signal_handler(sig, frame):
165188 concore_base .terminate_zmq (_mod )
166189 sys .exit (0 )
167190
191+
168192# Register cleanup handlers
169193atexit .register (terminate_zmq )
170- signal .signal (signal .SIGINT , signal_handler ) # Handle Ctrl+C
171- if not hasattr (sys , ' getwindowsversion' ):
194+ signal .signal (signal .SIGINT , signal_handler ) # Handle Ctrl+C
195+ if not hasattr (sys , " getwindowsversion" ):
172196 signal .signal (signal .SIGTERM , signal_handler ) # Handle termination (Unix only)
173197
174198params = concore_base .load_params (concore_params_file )
175199
176- #9/30/22
200+
201+ # 9/30/22
177202def tryparam (n , i ):
178203 """Return parameter `n` from params dict, else default `i`."""
179204 return params .get (n , i )
180205
181- #9/12/21
206+
207+ # 9/12/21
182208# ===================================================================
183209# Simulation Time Handling
184210# ===================================================================
@@ -187,12 +213,15 @@ def default_maxtime(default):
187213 global maxtime
188214 maxtime = safe_literal_eval (concore_maxtime_file , default )
189215
216+
190217default_maxtime (100 )
191218
219+
192220def unchanged ():
193221 """Check if global string `s` is unchanged since last call."""
194222 return concore_base .unchanged (_mod )
195223
224+
196225# ===================================================================
197226# I/O Handling (File + ZMQ)
198227# ===================================================================
@@ -228,5 +257,6 @@ def read(port_identifier, name, initstr_val):
228257def write (port_identifier , name , val , delta = 0 ):
229258 concore_base .write (_mod , port_identifier , name , val , delta )
230259
231- def initval (simtime_val_str ):
260+
261+ def initval (simtime_val_str ):
232262 return concore_base .initval (_mod , simtime_val_str )
0 commit comments