From 46d14b3baa8dff43ad5d5d9c7940f7ee28309f41 Mon Sep 17 00:00:00 2001 From: Arslan Date: Thu, 21 Sep 2023 12:24:09 +0200 Subject: [PATCH] add subprocess/ fix captcha / annotate code --- app.py | 52 +------ assets/default-params.json | 3 +- "pages/0_\360\237\223\201_File_Upload.py" | 114 +++++++------- "pages/1_\360\237\221\200_View_Raw_Data.py" | 155 +++++++++++--------- pages/2_Workflow.py | 131 +++++++++++++++-- src/captcha_.py | 77 +++++++++- src/ini2dec.py | 60 ++++++++ src/run_subprocess.py | 50 +++++++ 8 files changed, 457 insertions(+), 185 deletions(-) create mode 100644 src/ini2dec.py create mode 100644 src/run_subprocess.py diff --git a/app.py b/app.py index 81e426e8..c1c4e37c 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,5 @@ import streamlit as st from captcha.image import ImageCaptcha -import random, string from src.common import * from streamlit.web import cli from pathlib import Path @@ -26,60 +25,21 @@ def main(): if "local" in sys.argv: main() -else: - - length_captcha = 5 - width = 400 - height = 180 +##if docker or online mode +else: - # define the function for the captcha control - def captcha_control(): - #control if the captcha is correct - if 'controllo' not in st.session_state or st.session_state['controllo'] == False: - st.title("Makesure you are not a robotπŸ€–") - - # define the session state for control if the captcha is correct - st.session_state['controllo'] = False - col1, col2 = st.columns(2) - - # define the session state for the captcha text because it doesn't change during refreshes - if 'Captcha' not in st.session_state: - st.session_state['Captcha'] = ''.join(random.choices(string.ascii_uppercase + string.digits, k=length_captcha)) - - #setup the captcha widget - image = ImageCaptcha(width=width, height=height) - data = image.generate(st.session_state['Captcha']) - col1.image(data) - capta2_text = col2.text_area('Enter captcha text', height=20) - - - if st.button("Verify the code"): - capta2_text = capta2_text.replace(" ", "") - # if the captcha is correct, the controllo session state is set to True - if st.session_state['Captcha'].lower() == capta2_text.lower().strip(): - del st.session_state['Captcha'] - col1.empty() - col2.empty() - st.session_state['controllo'] = True - st.experimental_rerun() - else: - # if the captcha is wrong, the controllo session state is set to False and the captcha is regenerated - st.error("🚨 Captch is wrong") - del st.session_state['Captcha'] - del st.session_state['controllo'] - st.experimental_rerun() - else: - #wait for the button click - st.stop() - # WORK LIKE MULTIPAGE APP if 'controllo' not in st.session_state or st.session_state['controllo'] == False: + #delete pages delete_page("app", "File_Upload") delete_page("app", "View_Raw_Data") delete_page("app", "Workflow") + #apply captcha captcha_control() else: + #run main main() + #add all pages back add_page("app", "File_Upload") add_page("app", "View_Raw_Data") add_page("app", "Workflow") diff --git a/assets/default-params.json b/assets/default-params.json index dac180ee..513a11cf 100644 --- a/assets/default-params.json +++ b/assets/default-params.json @@ -4,5 +4,6 @@ "2D-map-intensity-cutoff": 5000, "example-x-dimension": 10, - "example-y-dimension": 5 + "example-y-dimension": 5, + "controllo": false } \ No newline at end of file diff --git "a/pages/0_\360\237\223\201_File_Upload.py" "b/pages/0_\360\237\223\201_File_Upload.py" index 4841e075..eca873bc 100755 --- "a/pages/0_\360\237\223\201_File_Upload.py" +++ "b/pages/0_\360\237\223\201_File_Upload.py" @@ -5,67 +5,81 @@ from src.common import * from src.fileupload import * +from src.captcha_ import * params = page_setup() -# Make sure "selected-mzML-files" is in session state -if "selected-mzML-files" not in st.session_state: - st.session_state["selected-mzML-files"] = params["selected-mzML-files"] +#if local no need captcha +if st.session_state.location == "local": + params["controllo"] = True + st.session_state["controllo"] = True -st.title("File Upload") +#if controllo is false means not captcha applied +if 'controllo' not in st.session_state or params["controllo"] == False or st.session_state.location != "local": + #apply captcha + captcha_control() + +else: + ### main content of page -tabs = ["File Upload", "Example Data"] -if st.session_state.location == "local": - tabs.append("Files from local folder") + # Make sure "selected-mzML-files" is in session state + if "selected-mzML-files" not in st.session_state: + st.session_state["selected-mzML-files"] = params["selected-mzML-files"] -tabs = st.tabs(tabs) + st.title("File Upload") -with tabs[0]: - with st.form("mzML-upload", clear_on_submit=True): - files = st.file_uploader( - "mzML files", accept_multiple_files=(st.session_state.location == "local")) - cols = st.columns(3) - if cols[1].form_submit_button("Add files to workspace", type="primary"): - save_uploaded_mzML(files) + tabs = ["File Upload", "Example Data"] + if st.session_state.location == "local": + tabs.append("Files from local folder") -# Example mzML files -with tabs[1]: - st.markdown("Short information text about the example data.") - cols = st.columns(3) - if cols[1].button("Load Example Data", type="primary"): - load_example_mzML_files() + tabs = st.tabs(tabs) -# Local file upload option: via directory path -if st.session_state.location == "local": - with tabs[2]: - # with st.form("local-file-upload"): - local_mzML_dir = st.text_input( - "path to folder with mzML files") - # raw string for file paths - local_mzML_dir = r"{}".format(local_mzML_dir) + with tabs[0]: + with st.form("mzML-upload", clear_on_submit=True): + files = st.file_uploader( + "mzML files", accept_multiple_files=(st.session_state.location == "local")) + cols = st.columns(3) + if cols[1].form_submit_button("Add files to workspace", type="primary"): + save_uploaded_mzML(files) + + # Example mzML files + with tabs[1]: + st.markdown("Short information text about the example data.") cols = st.columns(3) - if cols[1].button("Copy files to workspace", type="primary", disabled=(local_mzML_dir == "")): - copy_local_mzML_files_from_directory(local_mzML_dir) + if cols[1].button("Load Example Data", type="primary"): + load_example_mzML_files() + + # Local file upload option: via directory path + if st.session_state.location == "local": + with tabs[2]: + # with st.form("local-file-upload"): + local_mzML_dir = st.text_input( + "path to folder with mzML files") + # raw string for file paths + local_mzML_dir = r"{}".format(local_mzML_dir) + cols = st.columns(3) + if cols[1].button("Copy files to workspace", type="primary", disabled=(local_mzML_dir == "")): + copy_local_mzML_files_from_directory(local_mzML_dir) -if any(Path(mzML_dir).iterdir()): - v_space(2) - # Display all mzML files currently in workspace - df = pd.DataFrame( - {"file name": [f.name for f in Path(mzML_dir).iterdir()]}) - st.markdown("##### mzML files in current workspace:") - show_table(df) - v_space(1) - # Remove files - with st.expander("πŸ—‘οΈ Remove mzML files"): - to_remove = st.multiselect("select mzML files", - options=[f.stem for f in sorted(mzML_dir.iterdir())]) - c1, c2 = st.columns(2) - if c2.button("Remove **selected**", type="primary", disabled=not any(to_remove)): - remove_selected_mzML_files(to_remove) - st.experimental_rerun() + if any(Path(mzML_dir).iterdir()): + v_space(2) + # Display all mzML files currently in workspace + df = pd.DataFrame( + {"file name": [f.name for f in Path(mzML_dir).iterdir()]}) + st.markdown("##### mzML files in current workspace:") + show_table(df) + v_space(1) + # Remove files + with st.expander("πŸ—‘οΈ Remove mzML files"): + to_remove = st.multiselect("select mzML files", + options=[f.stem for f in sorted(mzML_dir.iterdir())]) + c1, c2 = st.columns(2) + if c2.button("Remove **selected**", type="primary", disabled=not any(to_remove)): + remove_selected_mzML_files(to_remove) + st.experimental_rerun() - if c1.button("⚠️ Remove **all**", disabled=not any(mzML_dir.iterdir())): - remove_all_mzML_files() - st.experimental_rerun() + if c1.button("⚠️ Remove **all**", disabled=not any(mzML_dir.iterdir())): + remove_all_mzML_files() + st.experimental_rerun() save_params(params) diff --git "a/pages/1_\360\237\221\200_View_Raw_Data.py" "b/pages/1_\360\237\221\200_View_Raw_Data.py" index 3376af39..d9efb4ce 100755 --- "a/pages/1_\360\237\221\200_View_Raw_Data.py" +++ "b/pages/1_\360\237\221\200_View_Raw_Data.py" @@ -3,88 +3,103 @@ from src.common import * from src.view import * +from src.captcha_ import * params = page_setup() -st.title("View raw MS data") -selected_file = st.selectbox( - "choose file", - [f.name for f in Path(st.session_state.workspace, - "mzML-files").iterdir()], -) -if selected_file: - df = get_df( - Path(st.session_state.workspace, "mzML-files", selected_file)) - df_MS1, df_MS2 = ( - df[df["mslevel"] == 1], - df[df["mslevel"] == 2], - ) +#if local no need captcha +if st.session_state.location == "local": + params["controllo"] = True + st.session_state["controllo"] = True - if not df_MS1.empty: - tabs = st.tabs(["πŸ“ˆ Base peak chromatogram and MS1 spectra", - "πŸ“ˆ Peak map and MS2 spectra"]) - with tabs[0]: - # BPC and MS1 spec - st.markdown("πŸ’‘ Click a point in the BPC to show the MS1 spectrum.") - bpc_fig = plot_bpc(df_MS1) +#if controllo is false means not captcha applied +if 'controllo' not in st.session_state or params["controllo"] == False or st.session_state.location != "local": + #apply captcha + captcha_control() + +else: - # Determine RT positions from clicks in BPC to show MS1 at this position - bpc_points = plotly_events(bpc_fig) - if bpc_points: - ms1_rt = bpc_points[0]["x"] - else: - ms1_rt = df_MS1.loc[0, "RT"] + ### main content of page - spec = df_MS1.loc[df_MS1["RT"] == ms1_rt].squeeze() + st.title("View raw MS data") + selected_file = st.selectbox( + "choose file", + [f.name for f in Path(st.session_state.workspace, + "mzML-files").iterdir()], + ) + if selected_file: + df = get_df( + Path(st.session_state.workspace, "mzML-files", selected_file)) + df_MS1, df_MS2 = ( + df[df["mslevel"] == 1], + df[df["mslevel"] == 2], + ) - title = f"MS1 spectrum @RT {spec['RT']}" - fig = plot_ms_spectrum( - spec, - title, - "#EF553B", - ) - show_fig(fig, title.replace(" ", "_")) + if not df_MS1.empty: + tabs = st.tabs(["πŸ“ˆ Base peak chromatogram and MS1 spectra", + "πŸ“ˆ Peak map and MS2 spectra"]) + with tabs[0]: + # BPC and MS1 spec + st.markdown("πŸ’‘ Click a point in the BPC to show the MS1 spectrum.") + bpc_fig = plot_bpc(df_MS1) - with tabs[1]: - c1, c2 = st.columns(2) - c1.number_input( - "2D map intensity cutoff", - 1000, - 1000000000, - params["2D-map-intensity-cutoff"], - 1000, - key="2D-map-intensity-cutoff" - ) - v_space(1, c2) - c2.markdown("πŸ’‘ Click anywhere to show the closest MS2 spectrum.") - map2D = plot_2D_map( - df_MS1, - df_MS2, - st.session_state["2D-map-intensity-cutoff"], - ) - map_points = plotly_events(map2D) - # Determine RT and mz positions from clicks in the map to get closest MS2 spectrum - if not df_MS2.empty: - if map_points: - rt = map_points[0]["x"] - prec_mz = map_points[0]["y"] + # Determine RT positions from clicks in BPC to show MS1 at this position + bpc_points = plotly_events(bpc_fig) + if bpc_points: + ms1_rt = bpc_points[0]["x"] else: - rt = df_MS2.iloc[0, 2] - prec_mz = df_MS2.iloc[0, 0] - spec = df_MS2.loc[ - ( - abs(df_MS2["RT"] - rt) + - abs(df_MS2["precursormz"] - prec_mz) - ).idxmin(), - :, - ] - title = f"MS2 spectrum @precursor m/z {round(spec['precursormz'], 4)} @RT {round(spec['RT'], 2)}" + ms1_rt = df_MS1.loc[0, "RT"] - ms2_fig = plot_ms_spectrum( + spec = df_MS1.loc[df_MS1["RT"] == ms1_rt].squeeze() + + title = f"MS1 spectrum @RT {spec['RT']}" + fig = plot_ms_spectrum( spec, title, - "#00CC96" + "#EF553B", ) - show_fig(ms2_fig, title.replace(" ", "_")) + show_fig(fig, title.replace(" ", "_")) + + with tabs[1]: + c1, c2 = st.columns(2) + c1.number_input( + "2D map intensity cutoff", + 1000, + 1000000000, + params["2D-map-intensity-cutoff"], + 1000, + key="2D-map-intensity-cutoff" + ) + v_space(1, c2) + c2.markdown("πŸ’‘ Click anywhere to show the closest MS2 spectrum.") + map2D = plot_2D_map( + df_MS1, + df_MS2, + st.session_state["2D-map-intensity-cutoff"], + ) + map_points = plotly_events(map2D) + # Determine RT and mz positions from clicks in the map to get closest MS2 spectrum + if not df_MS2.empty: + if map_points: + rt = map_points[0]["x"] + prec_mz = map_points[0]["y"] + else: + rt = df_MS2.iloc[0, 2] + prec_mz = df_MS2.iloc[0, 0] + spec = df_MS2.loc[ + ( + abs(df_MS2["RT"] - rt) + + abs(df_MS2["precursormz"] - prec_mz) + ).idxmin(), + :, + ] + title = f"MS2 spectrum @precursor m/z {round(spec['precursormz'], 4)} @RT {round(spec['RT'], 2)}" + + ms2_fig = plot_ms_spectrum( + spec, + title, + "#00CC96" + ) + show_fig(ms2_fig, title.replace(" ", "_")) save_params(params) diff --git a/pages/2_Workflow.py b/pages/2_Workflow.py index dce783a4..363a0997 100755 --- a/pages/2_Workflow.py +++ b/pages/2_Workflow.py @@ -1,29 +1,128 @@ import streamlit as st +import threading from src.common import * from src.workflow import * +from src.run_subprocess import * +from src.captcha_ import * # Page name "workflow" will show mzML file selector in sidebar params = page_setup(page="workflow") -st.title("Workflow") -# Define two widgets with values from paramter file -# To save them as parameters use the same key as in the json file +#if local no need captcha +if st.session_state.location == "local": + params["controllo"] = True + st.session_state["controllo"] = True -# We access the x-dimension via local variable -xdimension = st.number_input( - label="x dimension", min_value=1, max_value=20, value=params["example-x-dimension"], step=1, key="example-x-dimension") +#if controllo is false means not captcha applied +if 'controllo' not in st.session_state or params["controllo"] == False: + #apply captcha + captcha_control() + +else: + ### main content of page -st.number_input(label="y dimension", min_value=1, max_value=20, - value=params["example-y-dimension"], step=1, key="example-y-dimension") + st.title("Workflow") + + tabs = ["Table example", "Run Subprocess example"] -# Get a dataframe with x and y dimensions via time consuming (sleep) cached function -# If the input has been given before, the function does not run again -# Input x from local variable, input y from session state via key -df = generate_random_table(xdimension, st.session_state["example-y-dimension"]) + tabs = st.tabs(tabs) -# Display dataframe via custom show_table function, which will render a download button as well -show_table(df, download_name="random-table") + # Define two widgets with values from paramter file + # To save them as parameters use the same key as in the json file -# At the end of each page, always save parameters (including any changes via widgets with key) -save_params(params) + with tabs[0]: + # We access the x-dimension via local variable + xdimension = st.number_input( + label="x dimension", min_value=1, max_value=20, value=params["example-x-dimension"], step=1, key="example-x-dimension") + + st.number_input(label="y dimension", min_value=1, max_value=20, + value=params["example-y-dimension"], step=1, key="example-y-dimension") + + # Get a dataframe with x and y dimensions via time consuming (sleep) cached function + # If the input has been given before, the function does not run again + # Input x from local variable, input y from session state via key + df = generate_random_table(xdimension, st.session_state["example-y-dimension"]) + + # Display dataframe via custom show_table function, which will render a download button as well + show_table(df, download_name="random-table") + + with tabs[1]: + ###### Here just example for run subprocess + mzML_dir: Path = Path(st.session_state.workspace, "mzML-files") + col1, col2 = st.columns(2) + + ## here can be make form to take all user parameters for OpenMS TOPP tools + # for make more simple already write function; please see ini2dic.py + + mzML_file = col1.text_area('Enter mzML file name', height=10, placeholder="mzML file name", help="provide mzML file name without .mzML extension") + mzML_file_path = os.path.join(mzML_dir, mzML_file+'.mzML') + + #result dictionary to capture output of subprocess + result_dict = {} + result_dict["success"] = False + result_dict["log"] = " " + + #create terminate flag from even function + terminate_flag = threading.Event() + terminate_flag.set() + + #terminate subprocess by terminate flag + def terminate_subprocess(): + global terminate_flag + terminate_flag.set() + + # run analysis + if st.button("Extract ids"): + + # To terminate subprocess and clear form + if st.button("Terminate/Clear"): + #terminate subprocess + terminate_subprocess() + st.warning("Process terminated. The analysis may not be complete.") + #reset page + st.experimental_rerun() + + #with st.spinner("Running analysis... Please wait until analysis done πŸ˜‘"): #without status/ just spinner button + with st.status("Please wait until fetch all ids from mzML πŸ˜‘"): + + #If session state is local + if st.session_state.location == "local": + + #If local the OpenMS executable in bin e-g bin/OpenNuXL + #exec_command = os.path.join(os.getcwd(),'bin', "exec_name") + + #example of run subprocess + args = ["grep", "idRef", mzML_file_path] + + + #If session state is online/docker + else: + + #example of run subprocess + args = ["grep", "idRef", mzML_file_path] + + # Add any additional variables needed for the subprocess (if any) + variables = [] + + #want to see the command values and argues + message = f"Running '{' '.join(args)}'" + st.code(message) + + #run subprocess command + run_subprocess(args, variables, result_dict) + + #if run_subprocess success (no need if not success because error will show/display in run_subprocess command) + if result_dict["success"]: + + # Save the log to a text file in the result_dir + #log_file_path = result_dir / f"{protocol_name}_log.txt" + #with open(log_file_path, "w") as log_file: + #log_file.write(result_dict["log"]) + + #do something probably display results etc + pass + + + # At the end of each page, always save parameters (including any changes via widgets with key) + save_params(params) diff --git a/src/captcha_.py b/src/captcha_.py index d2478d12..a3d11b5e 100644 --- a/src/captcha_.py +++ b/src/captcha_.py @@ -6,21 +6,46 @@ get_pages, _on_pages_changed ) +from captcha.image import ImageCaptcha +import random, string def delete_page(main_script_path_str, page_name): + """ + delete page from app + Args: + main_script_path_str: main page (e-g app) + page_name: Page want to delete (e-g Analyze) + + Returns: + None + """ + #get all pages current_pages = get_pages(main_script_path_str) + #iterate over all pages and del if desire page found for key, value in current_pages.items(): if value['page_name'] == page_name: del current_pages[key] break else: pass + + #refresh the pages config _on_pages_changed.send() def add_page(main_script_path_str, page_name): - + """ + add page in app + + Args: + main_script_path_str: main page (e-g app) + page_name: Page want to delete (e-g Analyze) + + Returns: + None + """ + #get all pages pages = get_pages(main_script_path_str) main_script_path = Path(main_script_path_str) pages_dir = main_script_path.parent / "pages" @@ -30,10 +55,58 @@ def add_page(main_script_path_str, page_name): psh = calc_md5(script_path_str) + #add new page config pages[psh] = { "page_script_hash": psh, "page_name": pn, "icon": pi, "script_path": script_path_str, } - _on_pages_changed.send() \ No newline at end of file + + #refresh the page config + _on_pages_changed.send() + + +length_captcha = 5 +width = 400 +height = 180 + +# define the function for the captcha control +def captcha_control(): + #control if the captcha is correct + if 'controllo' not in st.session_state or st.session_state['controllo'] == False: + st.title("Makesure you are not a robotπŸ€–") + + # define the session state for control if the captcha is correct + st.session_state['controllo'] = False + col1, col2 = st.columns(2) + + # define the session state for the captcha text because it doesn't change during refreshes + if 'Captcha' not in st.session_state: + st.session_state['Captcha'] = ''.join(random.choices(string.ascii_uppercase + string.digits, k=length_captcha)) + + #setup the captcha widget + image = ImageCaptcha(width=width, height=height) + data = image.generate(st.session_state['Captcha']) + col1.image(data) + capta2_text = col2.text_area('Enter captcha text', height=20) + + + if st.button("Verify the code"): + capta2_text = capta2_text.replace(" ", "") + # if the captcha is correct, the controllo session state is set to True + if st.session_state['Captcha'].lower() == capta2_text.lower().strip(): + del st.session_state['Captcha'] + col1.empty() + col2.empty() + st.session_state['controllo'] = True + st.experimental_rerun() + else: + # if the captcha is wrong, the controllo session state is set to False and the captcha is regenerated + st.error("🚨 Captch is wrong") + del st.session_state['Captcha'] + del st.session_state['controllo'] + st.experimental_rerun() + else: + #wait for the button click + st.stop() diff --git a/src/ini2dec.py b/src/ini2dec.py new file mode 100644 index 00000000..afcbaa4e --- /dev/null +++ b/src/ini2dec.py @@ -0,0 +1,60 @@ +import xml.etree.ElementTree as ET + +def ini2dict(path: str, sections: list): + # Parse the XML configuration + tree = ET.parse(path) + root = tree.getroot() + + # Initialize an empty dictionary to store the extracted information + config_dict = {} + + # Iterate through sections and store information in the dictionary + for section_name in sections: + + for node in root.findall(f".//ITEMLIST[@name='{section_name}']") or root.findall(f".//ITEM[@name='{section_name}']"): + + #can adapt depends on tool + node_name = str(node.get("name")) + node_default = str(node.get("value")) + node_desc = str(node.get("description")) + node_rest = str(node.get("restrictions")) + + #generate list + restrictions_list = node_rest.split(',') if node_rest else [] + + entry = { + "name": node_name, + "default": node_default, + "description": node_desc, + "restrictions": restrictions_list + } + + # Store the entry in the section dictionary + config_dict[section_name] = entry + + return config_dict + +######################### Usage example ################################## +######################## Take parameters values from tool config file (.ini) ################################# + +# Define the sections you want to extract +#sections = ["missed_cleavages"]#let suppose we extract tool parameter: missed cleavages + +# path of .ini file (# placed executable .ini file in assets) +#config_path = os.path.join(os.getcwd(), 'assets', 'exec.ini') + +# take dictionary of parameters +#exec_config=ini2dict(config_path, sections) + +# (will give every section as 1 entry: +# entry = { + #"name": node_name, + #"default": node_default, + #"description": node_desc, + #"restrictions": restrictions_list + # }) + +# take all variables settings from config dictionary +# by create form take parameter values +# for example missed_cleavages +#Missed_cleavages = str(st.number_input("Missed_cleavages",value=int(exec_config['missed_cleavages']['default']), help=exec_config['missed_cleavages']['description'] + " default: "+ exec_config['missed_cleavages']['default'])) diff --git a/src/run_subprocess.py b/src/run_subprocess.py new file mode 100644 index 00000000..534c6c26 --- /dev/null +++ b/src/run_subprocess.py @@ -0,0 +1,50 @@ +import streamlit as st +import subprocess + +def run_subprocess(args, variables, result_dict): + """ + run subprocess + Args: + args: command with args + variables: variable if any + result_dict: contain success (success flag) and log (capture long log) + should contain result_dict["success"], result_dict["log"] + Returns: + None + """ + + # run subprocess and get every line of executable log in same time + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + + stdout_ = [] + stderr_ = [] + + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + #print every line of exec on page + st.text(output.strip()) + #append line to store log + stdout_.append(output.strip()) + + while True: + error = process.stderr.readline() + if error == '' and process.poll() is not None: + break + if error: + #print every line of exec on page even error + st.error(error.strip()) + #append line to store log of error + stderr_.append(error.strip()) + + #check if process run successfully + if process.returncode == 0: + result_dict["success"] = True + #save in to log all lines + result_dict["log"] = " ".join(stdout_) + else: + result_dict["success"] = False + #save in to log all lines even process cause error + result_dict["log"] = " ".join(stderr_) \ No newline at end of file