1414import psutil
1515import traitlets as tr
1616from aiida import engine , get_profile , manage , orm
17+ from aiida .common .exceptions import NotExistent
1718from aiida .engine .daemon .client import DaemonException
19+ from aiida .engine .processes import control as process_control
1820from sqlalchemy import text
1921
2022from home import process
@@ -950,6 +952,7 @@ def worker():
950952class ProcessControlWidget (ipw .VBox ):
951953 def __init__ (self ):
952954 process_list = process .ProcessListWidget (path_to_root = "../" )
955+ self .process_list = process_list
953956 past_days_widget = ipw .IntText (value = 7 , description = "Past days:" )
954957 tr .link ((past_days_widget , "value" ), (process_list , "past_days" ))
955958
@@ -970,16 +973,187 @@ def __init__(self):
970973 disabled = False ,
971974 )
972975 tr .dlink ((process_state_widget , "value" ), (process_list , "process_states" ))
976+
977+ self .process_select = ipw .SelectMultiple (
978+ description = "Act on:" ,
979+ options = [],
980+ rows = 8 ,
981+ layout = ipw .Layout (width = "600px" ),
982+ style = {"description_width" : "initial" },
983+ )
984+ self .process_select .observe (self ._on_selection_change , names = "value" )
985+ process_list .observe (self ._on_process_list_updated , names = "updated" )
986+
987+ self .pause_button = ipw .Button (description = "Pause" , disabled = True )
988+ self .pause_button .on_click (self ._on_pause_clicked )
989+ self .play_button = ipw .Button (description = "Play" , disabled = True )
990+ self .play_button .on_click (self ._on_play_clicked )
991+ self .kill_button = ipw .Button (
992+ description = "Kill" , button_style = "danger" , disabled = True
993+ )
994+ self .kill_button .on_click (self ._on_kill_clicked )
995+ self ._action_status = ipw .HTML ()
996+
997+ self ._action_running = False
998+ self ._kill_armed = False
999+
9731000 process_list .update ()
9741001
9751002 super ().__init__ (
9761003 children = [
9771004 ipw .HBox ([past_days_widget , all_days_checkbox ]),
9781005 process_state_widget ,
9791006 process_list ,
1007+ self .process_select ,
1008+ ipw .HBox ([self .pause_button , self .play_button , self .kill_button ]),
1009+ self ._action_status ,
9801010 ]
9811011 )
9821012
1013+ def _on_process_list_updated (self , _ = None ):
1014+ self ._rebuild_options ()
1015+ self ._disarm_kill ()
1016+ self ._sync_action_buttons_disabled ()
1017+
1018+ def _rebuild_options (self ):
1019+ rows = self .process_list .current_rows .get ("rows" , [])
1020+ previous_selection = set (self .process_select .value )
1021+
1022+ options = []
1023+ for row in rows :
1024+ try :
1025+ pk = int (row [process .HEADER_PK ])
1026+ except (KeyError , ValueError , TypeError ):
1027+ continue
1028+ label = (
1029+ f"{ pk } | { row .get (process .HEADER_PROCESS_LABEL , '' )} | "
1030+ f"{ row .get (process .HEADER_STATE , '' )} "
1031+ )
1032+ options .append ((label , pk ))
1033+
1034+ self .process_select .options = options
1035+ self .process_select .value = tuple (
1036+ pk for _ , pk in options if pk in previous_selection
1037+ )
1038+
1039+ def _on_selection_change (self , _ = None ):
1040+ self ._disarm_kill ()
1041+ self ._sync_action_buttons_disabled ()
1042+
1043+ def _sync_action_buttons_disabled (self ):
1044+ disabled = self ._action_running or not self .process_select .value
1045+ self .pause_button .disabled = disabled
1046+ self .play_button .disabled = disabled
1047+ self .kill_button .disabled = disabled
1048+
1049+ def _disarm_kill (self ):
1050+ self ._kill_armed = False
1051+ self .kill_button .description = "Kill"
1052+
1053+ def _on_pause_clicked (self , _ = None ):
1054+ self ._disarm_kill ()
1055+ self ._dispatch_action (
1056+ action_name = "pause the process(es)" ,
1057+ control_func = process_control .pause_processes ,
1058+ verb = "Pause" ,
1059+ )
1060+
1061+ def _on_play_clicked (self , _ = None ):
1062+ self ._disarm_kill ()
1063+ self ._dispatch_action (
1064+ action_name = "play the process(es)" ,
1065+ control_func = process_control .play_processes ,
1066+ verb = "Play" ,
1067+ )
1068+
1069+ def _on_kill_clicked (self , _ = None ):
1070+ selected = self .process_select .value
1071+ if not selected :
1072+ self ._action_status .value = "Select at least one process."
1073+ return
1074+ if not self ._kill_armed :
1075+ self ._kill_armed = True
1076+ self .kill_button .description = f"Confirm kill ({ len (selected )} )"
1077+ return
1078+ self ._dispatch_action (
1079+ action_name = "kill the process(es)" ,
1080+ control_func = process_control .kill_processes ,
1081+ verb = "Kill" ,
1082+ )
1083+ self ._disarm_kill ()
1084+
1085+ def _dispatch_action (self , action_name , control_func , verb ):
1086+ selected_pks = list (self .process_select .value )
1087+ if not selected_pks :
1088+ self ._action_status .value = "Select at least one process."
1089+ return
1090+
1091+ if not engine .daemon .get_daemon_client ().is_daemon_running :
1092+ self ._action_status .value = (
1093+ "<span style='color:#b58900'>Process actions need the "
1094+ "running daemon (see the Daemon section above).</span>"
1095+ )
1096+ return
1097+
1098+ self ._action_running = True
1099+ self ._sync_action_buttons_disabled ()
1100+ self ._action_status .value = (
1101+ f"{ verb } request in progress... <i class='fa fa-spinner fa-spin'></i>"
1102+ )
1103+
1104+ def worker ():
1105+ try :
1106+ nodes = []
1107+ errors = []
1108+ for pk in selected_pks :
1109+ try :
1110+ nodes .append (orm .load_node (pk ))
1111+ except NotExistent as exc :
1112+ errors .append (f"PK { pk } : { exc } " )
1113+
1114+ if nodes :
1115+ control_func (nodes , timeout = 5.0 )
1116+
1117+ messages = []
1118+ if nodes :
1119+ messages .append (
1120+ f"<span style='color:green'>{ verb } request sent to "
1121+ f"{ len (nodes )} process(es). States may take a few "
1122+ "seconds to change.</span>"
1123+ )
1124+ if errors :
1125+ messages .append (
1126+ "<span style='color:red'>"
1127+ + "<br>" .join (html .escape (e ) for e in errors )
1128+ + "</span>"
1129+ )
1130+ self ._action_status .value = "<br>" .join (messages ) or "Nothing to do."
1131+ except process_control .ProcessTimeoutException as exc :
1132+ self ._action_status .value = (
1133+ f"<span style='color:red'>Timed out trying to "
1134+ f"{ action_name } : { html .escape (str (exc ))} </span>"
1135+ )
1136+ except Exception as exc :
1137+ self ._action_status .value = (
1138+ f"<span style='color:red'>Failed to { action_name } : "
1139+ f"{ html .escape (str (exc ))} </span>"
1140+ )
1141+ finally :
1142+ # Re-enable the buttons before refreshing the process list:
1143+ # if the refresh raises, the page must not be left with the
1144+ # buttons permanently disabled.
1145+ self ._action_running = False
1146+ self ._sync_action_buttons_disabled ()
1147+ try :
1148+ self .process_list .update ()
1149+ except Exception as exc :
1150+ self ._action_status .value += (
1151+ "<br><span style='color:red'>Failed to refresh the "
1152+ f"process list: { html .escape (str (exc ))} </span>"
1153+ )
1154+
1155+ threading .Thread (target = worker , daemon = True ).start ()
1156+
9831157
9841158class GroupControlWidget (ipw .VBox ):
9851159 def __init__ (self ):
0 commit comments