11# --- For cmd.py
22import os
3+ import sys
34import subprocess
45import multiprocessing
56
67import collections
8+ from contextlib import contextmanager
79import glob
810import pandas as pd
911import numpy as np
1416# --- Fast libraries
1517from openfast_toolbox .io .fast_input_file import FASTInputFile
1618from openfast_toolbox .io .fast_output_file import FASTOutputFile
19+ from openfast_toolbox .tools .strings import FAIL , OK
1720
1821FAST_EXE = 'openfast'
1922
23+ @contextmanager
24+ def safe_cd (newdir ):
25+ prevdir = os .getcwd ()
26+ try :
27+ os .chdir (newdir )
28+ yield
29+ finally :
30+ os .chdir (prevdir )
31+
2032# --------------------------------------------------------------------------------}
2133# --- Tools for executing FAST
2234# --------------------------------------------------------------------------------{
@@ -72,10 +84,10 @@ def _report(p):
7284 # --- Giving a summary
7385 if len (Failed )== 0 :
7486 if verbose :
75- print ( '[ OK ] All simulations run successfully.' )
87+ OK ( ' All simulations run successfully.' )
7688 return True , Failed
7789 else :
78- print ( '[FAIL] {}/{} simulations failed:' .format (len (Failed ),len (inputfiles )))
90+ FAIL ( ' {}/{} simulations failed:' .format (len (Failed ),len (inputfiles )))
7991 for p in Failed :
8092 print (' ' ,p .input_file )
8193 return False , Failed
@@ -113,34 +125,178 @@ class Dummy():
113125 p .exe = exe
114126 return p
115127
116- def runBatch (batchfiles , showOutputs = True , showCommand = True , verbose = True ):
128+ def in_jupyter ():
129+ try :
130+ from IPython import get_ipython
131+ return 'ipykernel' in str (type (get_ipython ()))
132+ except :
133+ return False
134+
135+ def stream_output (std , buffer_lines = 5 , prefix = '|' , line_count = True ):
136+ if in_jupyter ():
137+ from IPython .display import display , update_display
138+ # --- Jupyter mode ---
139+ #handles = [display("DUMMY LINE FOR BUFFER", display_id=True) for _ in range(buffer_lines)]
140+ #buffer = []
141+ #for line in std:
142+ # line = line.rstrip()
143+ # buffer.append(line)
144+ # if len(buffer) > buffer_lines:
145+ # buffer.pop(0)
146+ # # update all display slots
147+ # for i, handle in enumerate(handles):
148+ # text = buffer[i] if i < len(buffer) else ""
149+ # update_display(text, display_id=handle.display_id)
150+
151+ # --- alternative using HTML
152+ from IPython .display import display , update_display , HTML
153+ import html as _html
154+
155+ # --- Jupyter mode with HTML ---
156+ handles = [display (HTML ("<pre style='margin:0'>{}DUMMY LINE FOR BUFFER</pre>" .format (prefix )), display_id = True )
157+ for _ in range (buffer_lines )]
158+ buffer = []
159+ iLine = 0
160+ for line in std :
161+ iLine += 1
162+ line = line .rstrip ("\r \n " )
163+ if line_count :
164+ line = f"{ iLine :>5} : { line } "
165+ line = f"{ prefix } { line } "
166+ buffer .append (line )
167+ if len (buffer ) > buffer_lines :
168+ buffer .pop (0 )
169+ # update all display slots
170+ for i , handle in enumerate (handles ):
171+ text = buffer [i ] if i < len (buffer ) else ""
172+
173+ html_text = "<pre style='margin:0'>{}</pre>" .format (_html .escape (text ) if text else " " )
174+ update_display (HTML (html_text ), display_id = handle .display_id )
175+
176+ else :
177+ import shutil
178+
179+ term_width = shutil .get_terminal_size ((80 , 20 )).columns
180+ for _ in range (buffer_lines ):
181+ print ('DummyLine' )
182+ # --- Terminal mode ---
183+ buffer = []
184+ iLine = 0
185+ for line in std :
186+ iLine += 1
187+ line = line .rstrip ()
188+ line = line .rstrip ()
189+ if line_count :
190+ line = f"{ iLine :>5} : { line } "
191+ line = f"{ prefix } { line } "
192+ line = line [:term_width ] # truncate to fit in one line
193+ buffer .append (line )
194+ if len (buffer ) > buffer_lines :
195+ buffer .pop (0 )
196+ sys .stdout .write ("\033 [F\033 [K" * len (buffer ))
197+ for l in buffer :
198+ print (l )
199+ sys .stdout .flush ()
200+
201+ def stdHandler (std , method = 'show' ):
202+ from collections import deque
203+ import sys
204+
205+ if method == 'show' :
206+ for line in std :
207+ print (line , end = '' )
208+ return None
209+
210+ elif method == 'store' :
211+ return std .read () # read everything
212+
213+ elif method .startswith ('buffer' ):
214+ buffer_lines = int (method .split ('_' )[1 ])
215+ buffer = deque (maxlen = buffer_lines )
216+ print ('------ Beginning of buffer outputs ----------------------------------' )
217+ stream_output (std , buffer_lines = buffer_lines )
218+ print ('------ End of buffer outputs ----------------------------------------' )
219+ return None
220+
221+
222+
223+
224+
225+ def runBatch (batchfiles , showOutputs = True , showCommand = True , verbose = True , newWindow = False , closeWindow = True , shell_cmd = 'bash' , nBuffer = 0 ):
117226 """
118227 Run one or several batch files
119228 TODO: error handling, status, parallel
229+
230+ showOutputs=True => stdout & stderr printed live
231+ showOutputs=False => stdout captured internally, stderr printed live
232+
233+ For output to show in a Jupyter notebook, we cannot use stdout=None, or stderr=None, we need to use Pipe
234+
120235 """
236+ import sys
237+ windows = (os .name == "nt" )
121238
122239 if showOutputs :
123240 STDOut = None
241+ std_method = 'show'
242+ if nBuffer > 0 :
243+ std_method = f'buffer_{ nBuffer } '
124244 else :
125- STDOut = open ( os . devnull , 'w' )
126-
127- curDir = os . getcwd ()
128- print ( 'Current directory' , curDir )
245+ std_method = 'store'
246+ #STDOut= open(os.devnull, 'w')
247+ #STDOut= subprocess.DEVNULL
248+ STDOut = subprocess . PIPE
129249
130250 def runOneBatch (batchfile ):
131251 batchfile = batchfile .strip ()
132252 batchfile = batchfile .replace ('\\ ' ,'/' )
133253 batchDir = os .path .dirname (batchfile )
254+ batchfileRel = os .path .relpath (batchfile , batchDir )
255+ if windows :
256+ command = [batchfileRel ]
257+ else :
258+ command = [shell_cmd , batchfileRel ]
259+
134260 if showCommand :
135- print ('>>>> Running batch file:' , batchfile )
136- print (' in directory:' , batchDir )
137- try :
138- os .chdir (batchDir )
139- returncode = subprocess .call ([batchfile ], stdout = STDOut , stderr = subprocess .STDOUT , shell = shell )
140- except :
141- os .chdir (curDir )
142- returncode = - 10
143- return returncode
261+ print ('[INFO] Running batch file:' , batchfileRel )
262+ print (' using command:' , command )
263+ print (' in directory:' , batchDir )
264+
265+ if newWindow :
266+ # --- Launch a new window (windows only for now)
267+ if windows :
268+ cmdflag = '/c' if closeWindow else '/k'
269+ subprocess .Popen (f'start cmd { cmdflag } { batchfileRel } ' , shell = True , cwd = batchDir )
270+ return 0
271+ else :
272+ raise NotImplementedError ('Running batch in `newWindow` only implemented on Windows.' )
273+ else :
274+ # --- We wait for outputs
275+ stdout_data = None
276+ with safe_cd (batchDir ): # Automatically go back to current directory
277+ try :
278+ # --- Option 2
279+ # Use Popen so we can print outputs live
280+ #proc = subprocess.Popen([batchfileRel], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell, text=True )
281+ proc = subprocess .Popen (command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , shell = shell , text = True )
282+ # Print or store stdout
283+ stdout_data = stdHandler (proc .stdout , method = std_method )
284+ # Always print errors output line by line
285+ for line in proc .stderr :
286+ print (line , end = '' )
287+ proc .wait ()
288+ returncode = proc .returncode
289+ # Dump stdout if there was an error
290+ if returncode != 0 and stdout_data :
291+ print ("\n --- Captured stdout ---" )
292+ print (stdout_data )
293+ except FileNotFoundError as e :
294+ print ('[FAIL] Running Batch failed, a file or command was not found see below:\n ' + str (e ))
295+ returncode = - 10
296+ except Exception as e :
297+ print ('[FAIL] Running Batch failed, see below:\n ' + str (e ))
298+ returncode = - 10
299+ return returncode
144300
145301 shell = False
146302 if isinstance (batchfiles ,list ):
@@ -152,20 +308,20 @@ def runOneBatch(batchfile):
152308 Failed .append (batchfile )
153309 if len (Failed )> 0 :
154310 returncode = 1
155- print ( '[FAIL] {}/{} Batch files failed.' .format (len (Failed ),len (batchfiles )))
311+ FAIL ( ' {}/{} Batch files failed.' .format (len (Failed ),len (batchfiles )))
156312 print (Failed )
157313 else :
158314 returncode = 0
159315 if verbose :
160- print ( '[ OK ] {} batch filse ran successfully.' .format (len (batchfiles )))
316+ OK ( ' {} batch files ran successfully.' .format (len (batchfiles )))
161317 # TODO
162318 else :
163319 returncode = runOneBatch (batchfiles )
164320 if returncode == 0 :
165321 if verbose :
166- print ( '[ OK ] Batch file ran successfully.' )
322+ OK ( ' Batch file ran successfully.' )
167323 else :
168- print ( '[FAIL] Batch file failed:' , batchfiles )
324+ FAIL ( ' Batch file failed: ' + str ( batchfiles ) )
169325
170326 return returncode
171327
@@ -200,6 +356,7 @@ def writeBatch(batchfile, fastfiles, fastExe=None, nBatches=1, pause=False, flag
200356 discard_if_ext_present = None ,
201357 dispatch = False ,
202358 stdOutToFile = False ,
359+ preCommands = None ,
203360 echo = True ):
204361 """ Write one or several batch file, all paths are written relative to the batch file directory.
205362 The batch file will consist of lines of the form:
@@ -255,6 +412,8 @@ def writeb(batchfile, fastfiles):
255412 if not echo :
256413 if os .name == 'nt' :
257414 f .write ('@echo off\n ' )
415+ if preCommands is not None :
416+ f .write (preCommands + '\n ' )
258417 for ff in fastfiles :
259418 ff_abs = os .path .abspath (ff )
260419 ff_rel = os .path .relpath (ff_abs , batchdir )
0 commit comments