diff --git a/CfdOF/CfdConsoleProcess.py b/CfdOF/CfdConsoleProcess.py index 980c77f2..abb06718 100644 --- a/CfdOF/CfdConsoleProcess.py +++ b/CfdOF/CfdConsoleProcess.py @@ -51,6 +51,7 @@ def __init__(self, finished_hook=None, stdout_hook=None, stderr_hook=None): def __del__(self): self.terminate() + #TODO document what the format of cmd is for developers def start(self, cmd, env_vars=None, working_dir=None): """ Start process and return immediately """ self.print_next_error_lines = 0 @@ -79,12 +80,18 @@ def terminate(self): self.process.write(b"terminate\n") self.process.waitForBytesWritten() # 'flush' else: + print("Process has been terminated") self.process.terminate() self.process.waitForFinished() def finished(self, exit_code): + print("Process has finished") if self.finishedHook: + #print("finished hook was called with exit code:" + str(exit_code)) self.finishedHook(exit_code) + else: + pass + #print("Error: finished wasn't hooked to a handler") def readStdout(self): # Ensure only complete lines are passed on diff --git a/CfdOF/CfdPreferencePage.py b/CfdOF/CfdPreferencePage.py index d9f9ed21..1e4dfefb 100644 --- a/CfdOF/CfdPreferencePage.py +++ b/CfdOF/CfdPreferencePage.py @@ -114,6 +114,7 @@ def __init__(self): self.form.tb_choose_output_dir.clicked.connect(self.chooseOutputDir) self.form.le_output_dir.textChanged.connect(self.outputDirChanged) + self.form.cb_add_filename_to_output.clicked.connect(self.addFilenameChanged) self.form.cb_docker_sel.clicked.connect(self.dockerCheckboxClicked) self.form.pb_download_install_docker.clicked.connect(self.downloadInstallDocker) @@ -145,6 +146,13 @@ def __init__(self): def __del__(self): self.cleanUp() + # This is a special version that only returns the base output path. + # The version in CfdTools.py returns the full output path, with the filename appended + def getDefaultOutputPath(self): + prefs = CfdTools.getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + return output_path + def cleanUp(self): if self.thread and self.thread.isRunning(): FreeCAD.Console.PrintError("Terminating a pending install task\n") @@ -179,7 +187,8 @@ def loadSettings(self): self.initial_gmsh_path = str(self.gmsh_path) self.form.le_gmsh_path.setText(self.gmsh_path) - self.output_dir = CfdTools.getDefaultOutputPath() + #self.output_dir = CfdTools.getDefaultOutputPath() + self.output_dir = self.getDefaultOutputPath() self.form.le_output_dir.setText(self.output_dir) if FreeCAD.ParamGet(prefs).GetBool("UseDocker", 0): @@ -192,6 +201,13 @@ def loadSettings(self): self.setDownloadURLs() + # disable add_filename_to_output for now. + # self.form.cb_add_filename_to_output.setEnabled(False) + if FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput",0): + self.form.cb_add_filename_to_output.setChecked(True) + else: + self.form.cb_add_filename_to_output.setChecked(False) + def consoleMessage(self, message="", colour_type=None): message = escape(message) message = message.replace('\n', '
') @@ -202,6 +218,10 @@ def consoleMessage(self, message="", colour_type=None): self.form.textEdit_Output.setText(self.console_message) self.form.textEdit_Output.moveCursor(QtGui.QTextCursor.End) + def addFilenameChanged(self): + prefs = CfdTools.getPreferencesLocation() + FreeCAD.ParamGet(prefs).SetBool("AddFilenameToOutput", self.form.cb_add_filename_to_output.isChecked()) + def foamDirChanged(self, text): self.foam_dir = text if self.foam_dir and os.access(self.foam_dir, os.R_OK): diff --git a/CfdOF/CfdRemotePreferencePage.py b/CfdOF/CfdRemotePreferencePage.py new file mode 100644 index 00000000..8b2af3f4 --- /dev/null +++ b/CfdOF/CfdRemotePreferencePage.py @@ -0,0 +1,1653 @@ + + +# *************************************************************************** +# * * +# * Copyright (c) 2017-2018 Oliver Oxtoby (CSIR) * +# * Copyright (c) 2017 Johan Heyns (CSIR) * +# * Copyright (c) 2017 Alfred Bogaers (CSIR) * +# * Copyright (c) 2019-2022 Oliver Oxtoby * +# * * +# * This program is free software: you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 3 of the * +# * License, or (at your option) any later version. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with this program. If not, * +# * see . * +# * * +# *************************************************************************** +# +# LinuxGuy123@gmail.com's notes. +# +# This file started out as a simple clone of the CfdPreferencesPage to handle a remote host instead +# of the local host. But with the changes needed to handle multiple host profiles, it changed a lot. +# +# The first iteration of this code was to change it to use a single remote host. +# +# The current iteration of this code now handles multiple remote hosts via host profiles. This resulted in +# the working parts getting almost complelely rewritten. This code presently only works with Linux remote hosts +# or at least it has only been tested with Linux remote hosts. I left in many pieces of the old code in case they are +# needed to add Docker, Windows, etc. on the remote host. +# +# How this page works... +# If Use Remote Processing is enabled, the controls are enabled. If not, they aren't. +# The page then loads the profile names from the parameters file. +# +# Then it loads the parameters for the first profile listed in the profile drop down box. +# With these parameters it initializes the controls and the local vars. +# When the users changes a parameter in the gui it is immediately written to the parameter file and +# the local var is updated. +# When the profile is changed the new parameters for the profile are loaded and the local vars +# are updated as are the controls. +# +# This page ignores the OK, Apply and Cancel buttons. There is a routine to save the current +# profile (saveProfile) on OK, change profile and Apply, but are those buttons are ignored in CfdOF. +# As they were in the previous version. Instead, every field is saved to the local var and the parameter file +# when it is entered. +# +# The remotePreferences GUI file (CfdRemotePreferencePage.ui) file still has many controls that are not used. See +# enableControls() for a list of what is or isn't used. +# They are set to disabled and not visible so they don't appear in the running GUI. They were left in in case +# they would be needed in the future. But generally the disabled controls don't make sense on a remote host - +# Paraview, for example. At some point, once the page has been deemed to operate in an acceptable manner, they +# should be removed entirely. +# +# In the initial code I wrote the variables that were used with remote operation had _remote_ in their name. I removed +# them because the var names were pretty long. Now all the vars in this code are assumed to be referring to a remote host. +# The only names that have remote in them are procedures that replaced non remote procedures. For example runRemoteDependencyCheck. +# I left the code for runDependencyCheck in the file because parts of it may be used if/when Windows or Docker is added for remote hosts. +# +# The reason I'm telling you this is to justify how messy the code is right now. It would be very easy to remove all the unused +# code and clean things up. But right now the unused code is present in case they are needed in the near future. +# +# This application doesn't install OpenFOAM on the remote host. It also doesn't install gmsh. It does install cfmesh +# if OpenFOAM (and OpenFOAM-devel) is installed. +# +# I did not use worker threads to install cfMesh. I just ran the processes from the ssh command line. +# +# TODO: + +# - fix downloadInstallCFMesh Its broken due to using profiles. Doesn't stop installing if a step fails. +# Also requires the working directory folder to be present. Fails if it isn't. +# - get About Remote Processing document window working - put User's Guide in it +# - put the remote host computer fields in a container like Docker, OpenFOAM, etc. +# - get tooltips working +# - OpenFOAM doesn't need the number of threads and processes like the meshers do. Remove one. + +# Done: get cfMesh URL control working +# Done: implement host profiles so that multiple remote hosts can be used +# Done: store and load the use remote processing boolean +# Done: enable and disable all the controls with the cb_use_remote_processing checkbox result +# Done: store and load the host name and username fields +# Done: get ping host working +# Done: get test ssh working +# Nope: move ping and ssh to CfdOFTools ? +# Done: get remote foam dir storing and loading +# Done: remote gmsh path storing and loading +# Done: remote work dir storing and loading +# Done: get download and install cfMesh working + +#******************************************************************************** + +import os +import os.path +import platform +import sys +if sys.version_info >= (3,): # Python 3 + import urllib.request as urlrequest + import urllib.parse as urlparse +else: + import urllib as urlrequest + import urlparse + + #import traceback #This as used during debugging + +import ssl +import ctypes +import FreeCAD +from CfdOF import CfdTools +import tempfile +from contextlib import closing +from xml.sax.saxutils import escape + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore + from PySide import QtGui + from PySide.QtCore import Qt, QObject, QThread + from PySide.QtGui import QApplication + + +# Constants +OPENFOAM_URL = \ + "https://sourceforge.net/projects/openfoam/files/v2206/OpenFOAM-v2206-windows-mingw.exe/download" +OPENFOAM_FILE_EXT = ".exe" +PARAVIEW_URL = \ + "https://www.paraview.org/paraview-downloads/download.php?submit=Download&version=v5.10&type=binary&os=Windows&downloadFile=ParaView-5.10.1-Windows-Python3.9-msvc2017-AMD64.exe" +PARAVIEW_FILE_EXT = ".exe" +CFMESH_URL = \ + "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof.zip/download" +CFMESH_URL_MINGW = \ + "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof-binaries-{}.zip/download" +CFMESH_FILE_BASE = "cfmesh-cfdof" +CFMESH_FILE_EXT = ".zip" +HISA_URL = \ + "https://sourceforge.net/projects/hisa/files/hisa-master.zip/download" +HISA_URL_MINGW = \ + "https://sourceforge.net/projects/hisa/files/hisa-master-binaries-{}.zip/download" +HISA_FILE_BASE = "hisa-master" +HISA_FILE_EXT = ".zip" +DOCKER_URL = \ + "docker.io/mmcker/cfdof-openfoam" + +OPENFOAM_DIR = "/usr/lib/openfoam/openfoam2206" + + +# Tasks for the worker thread +# Not used anymore +DOWNLOAD_OPENFOAM = 1 +DOWNLOAD_PARAVIEW = 2 +DOWNLOAD_CFMESH = 3 +DOWNLOAD_HISA = 4 +DOWNLOAD_DOCKER = 5 + + +class CloseDetector(QObject): + def __init__(self, obj, callback): + super().__init__(obj) + self.callback = callback + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.ChildRemoved: + self.callback() + return False + + +#************************************************************************* + +class CfdRemotePreferencePage: + def __init__(self): + + # load the page + ui_path = os.path.join(CfdTools.getModulePath(), 'Gui', "CfdRemotePreferencePage.ui") + self.form = FreeCADGui.PySideUic.loadUi(ui_path) + + # set the preferences parameters location + self.prefs_location = CfdTools.getPreferencesLocation() + + # get the use remote processing parameter and set the use RP control + self.use_remote_processing = FreeCAD.ParamGet(self.prefs_location).GetBool("UseRemoteProcessing") + self.form.cb_use_remote_processing.setChecked(self.use_remote_processing) + + #enable controls appropriately + self.enableControls(self.use_remote_processing) + + # connect the handler for remote_processing.clicked + self.form.cb_use_remote_processing.clicked.connect(self.useRemoteProcessingChanged) + + #TODOself.form.cb_use_remote_processing.setToolTip("Remote processing is + # the use of a remote computer to run meshing and OpenFOAM operations") + + # set up the profiles combo box + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) + + # connect the hostname and username controls + self.form.le_hostname.textChanged.connect(self.hostnameChanged) + self.form.le_username.textChanged.connect(self.usernameChanged) + + #connect the ping and ssh test push buttons + self.form.pb_ping_host.clicked.connect(self.pingHost) + self.form.pb_test_ssh.clicked.connect(self.testSSH) + + #connect the mesh and foam processes and threads controls + self.form.le_mesh_threads.textChanged.connect(self.meshThreadsChanged) + self.form.le_mesh_processes.textChanged.connect(self.meshProcessesChanged) + self.form.le_foam_threads.textChanged.connect(self.foamThreadsChanged) + self.form.le_foam_processes.textChanged.connect(self.foamProcessesChanged) + + # connect the foam_dir changed control + #self.form.tb_choose_remote_foam_dir.clicked.connect(self.chooseFoamDir) + self.form.le_foam_dir.textChanged.connect(self.foamDirChanged) + + #connect the add filename to output control + self.form.cb_add_filename_to_output.clicked.connect(self.addFilenameToOutputChanged) + + self.form.cb_copy_back.clicked.connect(self.copyBackChanged) + self.form.cb_delete_remote_results.clicked.connect(self.deleteRemoteResultsChanged) + + + + # connect handlers for various control events + # note: some of these aren't actually used. Code left in tact anyway + self.form.tb_choose_paraview_path.clicked.connect(self.chooseParaviewPath) + self.form.le_paraview_path.textChanged.connect(self.paraviewPathChanged) + + self.form.tb_choose_remote_gmsh_path.clicked.connect(self.chooseGmshPath) + self.form.le_gmsh_path.textChanged.connect(self.gmshPathChanged) + + self.form.pb_run_dependency_checker.clicked.connect(self.runRemoteDependencyChecker) + self.form.pb_download_install_openfoam.clicked.connect(self.downloadInstallOpenFoam) + self.form.tb_pick_openfoam_file.clicked.connect(self.pickOpenFoamFile) + self.form.pb_download_install_paraview.clicked.connect(self.downloadInstallParaview) + self.form.tb_pick_paraview_file.clicked.connect(self.pickParaviewFile) + + self.form.pb_download_install_cfMesh.clicked.connect(self.remoteDownloadInstallCfMesh) + self.form.tb_pick_cfmesh_file.clicked.connect(self.pickCfMeshFile) + self.form.pb_download_install_hisa.clicked.connect(self.downloadInstallHisa) + self.form.tb_pick_hisa_file.clicked.connect(self.pickHisaFile) + + self.form.pb_add_profile.clicked.connect(self.addProfile) + self.form.pb_delete_profile.clicked.connect(self.deleteProfile) + + # initialize general things with defaults + # this gets done from loadProfile() now. + # self.form.le_openfoam_url.setText(OPENFOAM_URL) + # self.form.le_paraview_url.setText(PARAVIEW_URL) + + self.form.tb_choose_remote_output_dir.clicked.connect(self.chooseOutputDir) + self.form.le_output_path.textChanged.connect(self.outputPathChanged) + + self.form.cb_docker_sel.clicked.connect(self.dockerCheckboxClicked) + self.form.pb_download_install_docker.clicked.connect(self.downloadInstallDocker) + + self.dockerCheckboxClicked() + + self.ev_filter = CloseDetector(self.form, self.cleanUp) + self.form.installEventFilter(self.ev_filter) + + # set global vars + self.thread = None + self.install_process = None + + self.console_message = "" + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + + self.profile_name = "" + self.host_prefs_location = "" + self.hostname = "" + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.gmsh_path = "" + self.add_filename_to_output = False + self.delete_remote_results = False + self.copy_back = False + + # TODO:fix these references + # if they are still used. Most are not. + #self.initial_remote_foam_dir = "" + #self.paraview_path = "" + #self.initial_paraview_path = "" + #self.initial_remote_gmsh_path = "" + #self.remote_output_dir = "" + + # not sure what this is for + self.form.gb_openfoam.setVisible(platform.system() == 'Windows') + self.form.gb_paraview.setVisible(platform.system() == 'Windows') + + # now load the profiles into the combo box + self.loadProfileNames() + + #now load the top profile + self.loadProfile(self.form.cb_profile.currentText()) + + + # enable/disable the controls on this page + def enableControls(self,value): + + self.form.cb_profile.setEnabled(value) + self.form.pb_add_profile.setEnabled(value) + self.form.pb_delete_profile.setEnabled(value) + + self.form.le_hostname.setEnabled(value) + self.form.le_username.setEnabled(value) + self.form.pb_ping_host.setEnabled(value) + self.form.pb_test_ssh.setEnabled(value) + + self.form.le_mesh_processes.setEnabled(value) + self.form.le_mesh_threads.setEnabled(value) + self.form.le_foam_processes.setEnabled(value) + self.form.le_foam_threads.setEnabled(value) + + self.form.le_foam_dir.setEnabled(value) + self.form.le_output_path.setEnabled(value) + self.form.cb_add_filename_to_output.setEnabled(value) + + self.form.pb_run_dependency_checker.setEnabled(value) + + self.form.pb_download_install_cfMesh.setEnabled(value) + self.form.le_cfmesh_url.setEnabled(value) + self.form.le_cfmesh_url.setText(CFMESH_URL) + + self.form.cb_copy_back.setEnabled(value) + self.form.cb_delete_remote_results.setEnabled(value) + self.form.cb_add_filename_to_output.setEnabled(value) + + + # Controls below here are not yet operational regardless of if remote processing + # is enabled or not. So they are disabled. Change this as new functionality + # is added to the page + value = False + + + self.form.pb_about_remote_processing.setEnabled(value) + + # disable the foam path chooser + self.form.tb_choose_remote_foam_dir.setEnabled(value) + self.form.tb_choose_remote_foam_dir.setVisible(value) + + # disable paraview stuff and make it invisible because we + # don't run paraview on the remote computer + # This makes it disappear from the page but allows us to + # leave the controls and code in place + self.form.le_paraview_path.setEnabled(value) + self.form.le_paraview_path.setVisible(False) + self.form.lbl_remote_paraview_path.setVisible(False) + self.form.tb_choose_paraview_path.setEnabled(value) + self.form.tb_choose_paraview_path.setVisible(False) + + # disable the gmsh path + # we don't need to know gmsh's path in Linux. Either it is installed or not + # we just call it from the shell, This might change if the remote host is running + # a different OS. + self.form.lbl_gmsh_path.setVisible(False) + self.form.le_gmsh_path.setEnabled(value) + self.form.le_gmsh_path.setVisible(False) + self.form.tb_choose_remote_gmsh_path.setEnabled(value) + self.form.tb_choose_remote_gmsh_path.setVisible(value) + + #disable the output path chooser + self.form.tb_choose_remote_output_dir.setEnabled(value) + self.form.tb_choose_remote_output_dir.setVisible(False) + + + # disable docker stuff. Setting it to invisible does not + # make it disappear. + self.form.gb_docker.setEnabled(False) + self.form.gb_docker.setVisible(False) + + self.form.pb_download_install_openfoam.setEnabled(value) + self.form.tb_pick_openfoam_file.setEnabled(value) + + self.form.pb_download_install_paraview.setEnabled(value) + self.form.tb_pick_paraview_file.setEnabled(value) + + self.form.cb_docker_sel.setEnabled(value) + self.form.le_docker_url.setEnabled(value) + self.form.pb_download_install_docker.setEnabled(value) + + self.form.tb_pick_cfmesh_file.setEnabled(value) + #self.form.pb_download_install_cfMesh.setEnabled(value) + + self.form.pb_download_install_hisa.setEnabled(value) + self.form.tb_pick_hisa_file.setEnabled(value) + self.form.le_hisa_url.setEnabled(value) + self.form.pb_download_install_hisa.setEnabled(value) + + self.form.le_openfoam_url.setEnabled(value) + + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + self.username = "" + self.hostname = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.add_filename_to_output = False + self.copy_back = False + self.delete_remote_results = False + + else: + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") + self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") + + + #now set the UI controls + self.form.le_hostname.setText(self.hostname) + self.form.le_username.setText(self.username) + self.form.le_mesh_processes.setText(str(self.mesh_processes)) + self.form.le_mesh_threads.setText(str(self.mesh_threads)) + self.form.le_foam_processes.setText(str(self.foam_processes)) + self.form.le_foam_threads.setText(str(self.foam_threads)) + self.form.le_foam_dir.setText(self.foam_dir) + self.form.le_output_path.setText(self.output_path) + self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + self.form.cb_copy_back.setChecked(self.copy_back) + self.form.cb_delete_remote_results.setChecked(self.delete_remote_results) + + + # create a new profile and add it to the cb_profile control + # and create its entry in the CfdOF parameters + def addProfile(self): + # get the new profile name + #https://wiki.freecad.org/PySide_Beginner_Examples/en + profile_name, ok = QtGui.QInputDialog.getText(None,"Profile Name", "Enter the name of the profile you'd like to add.\n It cannot contain spaces or slashes") + if ok and profile_name: + print("Adding: " + profile_name) + #TODO: test for spaces or slashes here. + + # add the profile name to profile combo list + self.form.cb_profile.addItem(profile_name) + # move to the item just added + # TODO: check if text isn't found, ie index = -1 + index = self.form.cb_profile.findText(profile_name) + self.form.cb_profile.setCurrentIndex(index) + + # update the global profile_name var + self.profile_name = profile_name + + # update the global host_prefs_location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + # add the default setting for a profile into parameters + hostPrefs = self.host_prefs_location + FreeCAD.ParamGet(hostPrefs).SetString("Hostname", "") + FreeCAD.ParamGet(hostPrefs).SetString("Username", "") + FreeCAD.ParamGet(hostPrefs).SetInt("MeshProcesses", 0) + FreeCAD.ParamGet(hostPrefs).SetInt("MeshThreads", 0) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamProcesses", 0) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamThreads", 0) + FreeCAD.ParamGet(hostPrefs).SetString("FoamDir", OPENFOAM_DIR) + FreeCAD.ParamGet(hostPrefs).SetString("OutputPath","/tmp") + FreeCAD.ParamGet(hostPrefs).SetBool("AddFilenameToOutput", False) + FreeCAD.ParamGet(hostPrefs).SetBool("CopyBack", False) + FreeCAD.ParamGet(hostPrefs).SetBool("DeleteRemoteResults", False) + + # now load the controls and local vars from the profile parameters + self.loadProfile(profile_name) + + # delete the current profile in the cb_profile and in parameters + # this will trigger a profile change which will update the controls and + # vars with the values from the new profile + def deleteProfile(self): + #check if there is a profile in the profile combo box to delete + if self.form.cb_profile.currentText() == "": + return + + # ask the user if they really want to delete the profile + #https://wiki.freecad.org/PySide_Beginner_Examples/en + deleteProfile = self.form.cb_profile.currentText() + deleteIndex = self.form.cb_profile.currentIndex() + deleteQuestion = "Delete profile " + deleteProfile + "?\n" + "This cannot be undone." + + reply = QtGui.QMessageBox.question(None, "Delete profile ?", deleteQuestion, + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + + if reply == QtGui.QMessageBox.Yes: + #delete it from the control + self.form.cb_profile.removeItem(deleteIndex) + # delete it from the parameters list + profileDir = self.prefs_location + "/Hosts" + FreeCAD.ParamGet(profileDir).RemGroup(deleteProfile) + + if reply == QtGui.QMessageBox.No: + # this is where the code relevant to a 'No' answer goes + pass + + # this gets called when the user changes the profile + # Note: doesn't save the last profile before changing to the new profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + + + # save the current profile by writing its the parameters + # not currently used. + def saveProfile(self): + print("saveProfile has fired") + if self.profile_name != "": + hostPrefs = self.host_prefs_location + FreeCAD.ParamGet(hostPrefs).SetString("Hostname", self.hostname) + FreeCAD.ParamGet(hostPrefs).SetString("Username", self.username) + FreeCAD.ParamGet(hostPrefs).SetInt("MeshProcesses", self.mesh_processes) + FreeCAD.ParamGet(hostPrefs).SetInt("MeshThreads", self.mesh_threads) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamProcesses",self.foam_processes) + FreeCAD.ParamGet(hostPrefs).SetInt("FoamThreads",self.foam_threads) + FreeCAD.ParamGet(hostPrefs).SetString("FoamDir", self.foam_dir) + FreeCAD.ParamGet(hostPrefs).SetString("OutputPath",self.output_path) + FreeCAD.ParamGet(hostPrefs).SetBool("AddFilenameToOutput", self.add_filename_to_output) + FreeCAD.ParamGet(hostPrefs).SetBool("DeleteRemoteResults", self.delete_remote_results) + FreeCAD.ParamGet(hostPrefs).SetBool("CopyBack", self.copy_back) + self.profileChanged = False + + def meshThreadsChanged(self): + if self.profile_name == "": + return + if self.form.le_mesh_threads.text() == "": + return + #else + #update the global var + self.mesh_threads = int(self.form.le_mesh_threads.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("MeshThreads", self.mesh_threads) + + def meshProcessesChanged(self): + if self.profile_name == "": + return + if self.form.le_mesh_processes.text() == "": + return + #else + #update the global var + self.mesh_processes = int(self.form.le_mesh_processes.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("MeshProcesses", self.mesh_processes) + + def foamThreadsChanged(self): + if self.profile_name == "": + return + if self.form.le_foam_threads.text() == "": + return + #else + #update the global var + self.foam_threads = int(self.form.le_foam_threads.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("FoamThreads", self.foam_threads) + + def foamProcessesChanged(self): + #print("foamProcessesChanged has fired") + #traceback.print_stack() + if self.profile_name == "": + return + if self.form.le_foam_processes.text() == "": + return + #else + #update the global var + self.foam_processes = int(self.form.le_foam_processes.text()) + #write the new ValueError to the host profile + FreeCAD.ParamGet(self.host_prefs_location).SetInt("FoamProcesses", self.foam_processes) + + def useRemoteProcessingChanged(self): + if (self.form.cb_use_remote_processing.isChecked() == True): + self.enableControls(True) + FreeCAD.ParamGet(self.prefs_location).SetBool("UseRemoteProcessing", True) + else: + self.enableControls(False) + FreeCAD.ParamGet(self.prefs_location).SetBool("UseRemoteProcessing", False) + + def hostnameChanged(self): + if self.profile_name == "": + return + # update the global variables + self.hostname = self.form.le_hostname.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("Hostname", self.hostname ) + self.profile_changed = True + + def usernameChanged(self): + if self.profile_name == "": + return + # update the global variables + self.username = self.form.le_username.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("Username", self.username ) + self.profile_changed = True + + def foamDirChanged(self): + #TODO: check that the path doesn't end with "/" + if self.profile_name == "": + return + # update the global variables + self.foam_dir = self.form.le_foam_dir.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("FoamDir", self.foam_dir ) + self.profile_changed = True + """ + self.foam_dir = text + if self.foam_dir and os.access(self.foam_dir, os.R_OK): + #TODO: change this? + #self.setDownloadURLs() + """ + def gmshPathChanged(self): + #TODO: check that the path doesn't end in "/" + if self.profile_name == "": + return + self.gmsh_path = self.form.le_gmsh_path.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("GmshDir", self.foam_dir ) + self.profile_changed = True + + def outputPathChanged(self, text): + #TODO: check that the path doesn't end in "/" + if self.profile_name == "": + return + self.output_path = self.form.le_output_path.text() + FreeCAD.ParamGet(self.host_prefs_location).SetString("OutputPath", self.output_path ) + self.profile_changed = True + + def addFilenameToOutputChanged(self): + #TODO: check that the filename doesn't have a space in it + if self.profile_name == "": + return + self.add_filename_to_output = self.form.cb_add_filename_to_output.isChecked() + FreeCAD.ParamGet(self.host_prefs_location).SetBool("AddFilenameToOutput", self.add_filename_to_output) + self.profile_changed = True + + def copyBackChanged(self): + if self.profile_name == "": + return + self.copy_back = self.form.cb_copy_back.isChecked() + FreeCAD.ParamGet(self.host_prefs_location).SetBool("CopyBack", self.copy_back) + self.profile_changed = True + + def deleteRemoteResultsChanged(self): + if self.profile_name == "": + return + self.delete_remote_results = self.form.cb_delete_remote_results.isChecked() + FreeCAD.ParamGet(self.host_prefs_location).SetBool("DeleteRemoteResults", self.delete_remote_results) + self.profile_changed = True + + #TODO: Move this to CfdTools + def pingHost(self): + self.consoleMessage("Performing ping test...") + if self.hostname == "": + self.consoleMessage("Error: missing hostname or IP address.") + return + result = CfdTools.runCommand("ping",["-c8", self.hostname]) + #result = runCommand("ssh", ["me@192.168.2.159", "ls", "-al"]) + #result = runCommand("ssh", ["me@192.168.2.159", "ping", "-c8","www.google.com"]) + #result = runCommand("ssh", ["me@192.168.2.159", "uname", "-a"]) + if result == 0: + self.consoleMessage("Successfully pinged " + self.hostname + ".") #+ remote_hostname +".") + else: + self.consoleMessage("Could not ping the remote host. Check network configure and test manually.") + + #TODO: Move this to CfdTools + def testSSH(self): + self.consoleMessage("Performing ssh test...") + if self.hostname == "": + self.consoleMessage("Error: missing hostname or IP address.") + return + if self.username =="": + self.consoleMessage("Error: missing username.") + return + + connectString = self.form.le_username.text() + "@" + self.hostname + #print("ssh connectString:", connectString); + result = CfdTools.runCommand("ssh",[connectString,"ls -al"]) + if result == 0: + self.consoleMessage("Successfully ran an ssh session on " + self.hostname + ".") + else: + self.consoleMessage("Failed to establish an ssh session with the remote host. Manually verify ssh operation with the remote host.") + + + def __del__(self): + self.cleanUp() + + + def cleanUp(self): + if self.thread and self.thread.isRunning(): + FreeCAD.Console.PrintError("Terminating a pending install task\n") + self.thread.quit = True + if self.install_process and self.install_process.state() == QtCore.QProcess.Running: + FreeCAD.Console.PrintError("Terminating a pending install task\n") + self.install_process.terminate() + QApplication.restoreOverrideCursor() + + # This gets called from outside the page + def saveSettings(self): + pass + #self.saveProfile() <- this is called in _init_ Doesn't need to be called here. + """ + print("Error: saveSettings has been depreciated.") + CfdTools.setRemoteFoamDir(self.foam_dir) + CfdTools.setParaviewPath(self.paraview_path) + prefs = self.prefs_location + FreeCAD.ParamGet(prefs).SetString("RemoteOutputPath", self.remote_output_dir) + FreeCAD.ParamGet(prefs).SetBool("UseDocker",self.form.cb_docker_sel.isChecked()) + FreeCAD.ParamGet(prefs).SetString("DockerURL",self.form.le_docker_url.text()) + """ + + # This gets called form outside the page + def loadSettings(self): + pass + #self.loadProfile(self.profile_name) + """ + print("Error: loadSettings has been depreciated.") + # Don't set the autodetected location, since the user might want to allow that to vary according + # to WM_PROJECT_DIR setting + prefs = self.prefs_location + self.foam_dir = FreeCAD.ParamGet(prefs).GetString("RemoteInstallationPath", "") + self.initial_remote_foam_dir = str(self.foam_dir) + self.form.le_foam_dir.setText(self.foam_dir) + + # Added by me + self.remote_output_dir = FreeCAD.ParamGet(prefs).GetString("RemoteOutputPath", "") + self.initial_remote_output_dir = str(self.remote_output_dir) + self.form.le_output_path.setText(self.remote_output_dir) + + self.paraview_path = CfdTools.getParaviewPath() + self.initial_paraview_path = str(self.paraview_path) + self.form.le_paraview_path.setText(self.paraview_path) + + self.gmsh_path = CfdTools.getRemoteGmshPath() + self.initial_remote_gmsh_path = str(self.gmsh_path) + self.form.le_gmsh_path.setText(self.gmsh_path) + + self.output_dir = CfdTools.getDefaultRemoteOutputPath() + self.form.le_output_path.setText(self.remote_output_dir) + + # Load the use_remote_processing parameter + if FreeCAD.ParamGet(prefs).GetBool("UseRemoteProcessing", 0): + self.form.cb_use_remote_processing.setCheckState(Qt.Checked) + self.enableControls(True) + else: + self.enableControls(False) + + #load the hostname + self.form.le_hostname.setText(FreeCAD.ParamGet(prefs).GetString("RemoteHostname","")) + self.form.le_username.setText(FreeCAD.ParamGet(prefs).GetString("RemoteUsername","")) + + if FreeCAD.ParamGet(prefs).GetBool("UseDocker", 0): + self.form.cb_docker_sel.setCheckState(Qt.Checked) + + # Set usedocker and enable/disable download buttons + self.dockerCheckboxClicked() + + self.form.le_docker_url.setText(FreeCAD.ParamGet(prefs).GetString("DockerURL", DOCKER_URL)) + + self.setDownloadURLs() + """ + + + def consoleMessage(self, message="", colour_type=None): + message = escape(message) + message = message.replace('\n', '
') + if colour_type: + self.console_message += '{1}
'.format(CfdTools.getColour(colour_type), message) + else: + self.console_message += message+'
' + self.form.textEdit_Output.setText(self.console_message) + self.form.textEdit_Output.moveCursor(QtGui.QTextCursor.End) + + + # Not used anymore + def testGetRuntime(self, disable_exception=True): + print("Error:testGetRuntime has been depreciated.") + """ Set the foam dir temporarily and see if we can detect the runtime """ + CfdTools.setFoamDir(self.foam_dir) + try: + runtime = CfdTools.getFoamRuntime() + except IOError as e: + runtime = None + if not disable_exception: + raise + CfdTools.setFoamDir(self.initial_remote_foam_dir) + return runtime + + + def setDownloadURLs(self): + print("Error:setDownloadURLs has been depreciated.") + if self.testGetRuntime() == "MinGW": + # Temporarily apply the foam dir selection + CfdTools.setFoamDir(self.foam_dir) + foam_ver = os.path.split(CfdTools.getFoamDir())[-1] + self.form.le_cfmesh_url.setText(CFMESH_URL_MINGW.format(foam_ver)) + self.form.le_hisa_url.setText(HISA_URL_MINGW.format(foam_ver)) + CfdTools.setFoamDir(self.initial_remote_foam_dir) + else: + self.form.le_cfmesh_url.setText(CFMESH_URL) + self.form.le_hisa_url.setText(HISA_URL) + + def paraviewPathChanged(self, text): + print("Error:paraviewPathChanged has been depreciated.") + self.paraview_path = text + + def chooseFoamDir(self): + d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose OpenFOAM directory', self.foam_dir) + if d and os.access(d, os.R_OK): + self.foam_dir = d + self.form.le_foam_dir.setText(self.foam_dir) + + def chooseParaviewPath(self): + p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView executable', self.paraview_path, + filter="*.exe" if platform.system() == 'Windows' else None) + if p and os.access(p, os.R_OK): + self.paraview_path = p + self.form.le_paraview_path.setText(self.paraview_path) + + def chooseGmshPath(self): + p, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose gmsh executable', self.gmsh_path, + filter="*.exe" if platform.system() == 'Windows' else None) + if p and os.access(p, os.R_OK): + self.gmsh_path = p + self.form.le_gmsh_path.setText(self.gmsh_path) + + def chooseOutputDir(self): + d = QtGui.QFileDialog().getExistingDirectory(None, 'Choose output directory', self.output_dir) + if d and os.access(d, os.W_OK): + self.output_dir = os.path.abspath(d) + self.form.le_output_path.setText(self.output_dir) + + + #******************************************************************************************* + # This function assumes the remote computer is running Linux or a derivative and a vanilla + # OpenFOAM installation. It does not handle a Windows remote computer or Docker at this time. + # It also does simple checks of the provided paths. It does not attempt to find executables the + # way the non remote dependency checker does + # It does not attempt to find ParaView on the remote computer because ParaView is not used as a server in + # FreeCAD. Though it could be, some day. + + def checkRemoteCfdDependencies(self): + + CF_MAJOR_VER_REQUIRED = 1 + CF_MINOR_VER_REQUIRED = 16 + + HISA_MAJOR_VER_REQUIRED = 1 + HISA_MINOR_VER_REQUIRED = 6 + HISA_PATCH_VER_REQUIRED = 4 + + return_message = "" + FreeCAD.Console.PrintMessage("Checking remote host CFD dependencies on " + self.hostname + "...\n") + + remote_user = self.username + remote_hostname = self.hostname + ssh_prefix = "ssh " + remote_user + "@" + remote_hostname + " " + + # check that the remote host is running Linux + # right now Linux is the only OS supported on the remote host + print("Checking the operating system on the remote host...\n") + + try: + os_string = CfdTools.runFoamCommand(ssh_prefix + "uname -o")[0] + except: + return_message += "Could not determine the OS on " + self.hostname + ".\n" + else: + if os_string.find("Linux") != 0: + return_message += "Remote host OS is " + os_string[:-1] + ".\n" + + else: + return_message += "The remote host OS is not Linux.\n" + return_message += "CfdOF requires that the remote host be running Linux.\n" + + # check that the open foam directory exists on the remote host + print("Checking the OpenFOAM directory...") + remote_foam_dir = self.foam_dir + foam_ls_command = ssh_prefix + " ls " + remote_foam_dir + #print("OpenFOAM dir testCommand:", foam_ls_command) + + try: + CfdTools.runFoamCommand(foam_ls_command) + except: + return_message += "Failed to find the OpenFOAM directory: "+ remote_foam_dir + " on " + self.hostname +".\n" + # print the message on the console + else: + return_message += "Found OpenFOAM directory " + remote_foam_dir + " on " + self.hostname + ".\n" + + + """ + try: + foam_dir = getRemoteFoamDir() + sys_msg = "System: {}\nRuntime: {}\nOpenFOAM directory: {}".format( + platform.system(), getFoamRuntime(), foam_dir if len(foam_dir) else "(system installation)") + print(sys_msg) + message += sys_msg + '\n' + except IOError as e: + ofmsg = "Could not find OpenFOAM installation: " + str(e) + print(ofmsg) + message += ofmsg + '\n' + else: + if foam_dir is None: + ofmsg = "OpenFOAM installation path not set and OpenFOAM environment neither pre-loaded before " + \ + "running FreeCAD nor detected in standard locations" + print(ofmsg) + message += ofmsg + '\n' + else: + if getFoamRuntime() == "PosixDocker": + startDocker() + try: + if getFoamRuntime() == "MinGW": + foam_ver = runFoamCommand("echo $FOAM_API")[0] + else: + foam_ver = runFoamCommand("echo $WM_PROJECT_VERSION")[0] + """ + + # Check if OpenFOAM will run and what its version is + FreeCAD.Console.PrintMessage("Checking OpenFOAM operation and version... ") + + try: + # TODO: figure out why foamVersion won't run properly in an ssh statement + # need sleep in the command so that the OpenFOAM source script gets run in the shell before foamVersion does + # help_string = runFoamCommand(ssh_prefix +"sleep 4s ; foamVersion")[0] + + help_string = CfdTools.runFoamCommand(ssh_prefix + "simpleFoam -help")[0] + except Exception as e: + runmsg = "Unable to run OpenFOAM: " + str(e) + return_message += runmsg + '\n' + print(runmsg) + #raise + else: + # TODO: this should be put in a try to go with the except below in case + # there is an issue with parsing out the OF version + # The command $foamVersion will give the version as well + version_start = help_string.find("OpenFOAM-") + 9 + version_string = help_string[version_start:version_start+4] + print(version_string) + return_message += "Successfully ran OpenFOAM version " + version_string + " on " + self.hostname + ".\n" + foam_version = int(version_string) + + """ + foam_ver = foam_ver.rstrip() + if foam_ver: + foam_ver = foam_ver.split()[-1] + if foam_ver and foam_ver != 'dev' and foam_ver != 'plus': + try: + # Isolate major version number + foam_ver = foam_ver.lstrip('v') + foam_ver = int(foam_ver.split('.')[0]) + if getFoamRuntime() == "MinGW": + if foam_ver < 2012 or foam_ver > 2206: + vermsg = "OpenFOAM version " + str(foam_ver) + \ + " is not currently supported with MinGW installation" + message += vermsg + "\n" + print(vermsg) + """ + if foam_version >= 1000: # Plus version + if foam_version < 1706: + vermsg = "OpenFOAM version " + str(foam_version) + " is outdated:\n" + \ + "Minimum version 1706 or 5 required" + return_message += vermsg + "\n" + print(vermsg) + if foam_version > 2206: + vermsg = "OpenFOAM version " + str(foam_version) + " is not yet supported:\n" + \ + "Last tested version is 2206" + return_message += vermsg + "\n" + print(vermsg) + """ + This code does not support foundation version numbers at this time. + TODO: handle foundation version numbers + else: # Foundation version + if foam_ver < 5: + vermsg = "OpenFOAM version " + str(foam_ver) + " is outdated:\n" + \ + "Minimum version 5 or 1706 required" + message += vermsg + "\n" + print(vermsg) + if foam_ver > 9: + vermsg = "OpenFOAM version " + str(foam_ver) + " is not yet supported:\n" + \ + "Last tested version is 9" + message += vermsg + "\n" + print(vermsg) + except ValueError: + vermsg = "Error parsing OpenFOAM version string " + foam_ver + message += vermsg + "\n" + print(vermsg) + + # Check for wmake + # if getFoamRuntime() != "MinGW" and getFoamRuntime() != "PosixDocker": + """ + + try: + # changed this from -help to -version + wmake_reply = CfdTools.runFoamCommand(ssh_prefix + "wmake -version")[0] + + except Exception as e: + #print(e) + wmakemsg = "Cannot run 'wmake' on remote host. \n" + wmakemsg += "Installation of cfMesh and HiSA is not be possible without 'wmake'.\n" + wmakemsg += "'wmake' is installed with most OpenFOAM development packages.\n" + wmakemsg += "You may want to install the OpenFOAM development package on the remote host to provide 'wmake'.\n" + return_message += wmakemsg + print(wmakemsg) + + else: + print(wmake_reply) + return_message += "'wmake' is installed on " + self.hostname + ".\n" + + # Check for cfMesh + try: + cfmesh_ver = CfdTools.runFoamCommand(ssh_prefix + "cartesianMesh -version")[0] + version_line = cfmesh_ver.splitlines()[-1] + vermsg = version_line[:-1] + " found on " + self.hostname + "." + cfmesh_ver = cfmesh_ver.rstrip().split()[-1] + cfmesh_ver = cfmesh_ver.split('.') + if (not cfmesh_ver or len(cfmesh_ver) != 2 or + int(cfmesh_ver[0]) < CF_MAJOR_VER_REQUIRED or + (int(cfmesh_ver[0]) == CF_MAJOR_VER_REQUIRED and + int(cfmesh_ver[1]) < CF_MINOR_VER_REQUIRED)): + vermsg += "cfMesh-CfdOF version {}.{} required".format(CF_MAJOR_VER_REQUIRED, CF_MINOR_VER_REQUIRED) + + except Exception as e: #subprocess.CalledProcessError: + vermsg = "cfMesh (CfdOF version) not found on " + self.hostname + "." + return_message += vermsg + '\n' + print(vermsg) + + + # Check for HiSA + try: + hisa_ver = CfdTools.runFoamCommand(ssh_prefix + "hisa -version")[0] + hisa_ver = hisa_ver.rstrip().split()[-1] + hisa_ver = hisa_ver.split('.') + if (not hisa_ver or len(hisa_ver) != 3 or + int(hisa_ver[0]) < HISA_MAJOR_VER_REQUIRED or + (int(hisa_ver[0]) == HISA_MAJOR_VER_REQUIRED and + (int(hisa_ver[1]) < HISA_MINOR_VER_REQUIRED or + (int(hisa_ver[1]) == HISA_MINOR_VER_REQUIRED and + int(hisa_ver[2]) < HISA_PATCH_VER_REQUIRED)))): + vermsg = "HiSA version {}.{}.{} required".format(HISA_MAJOR_VER_REQUIRED, + HISA_MINOR_VER_REQUIRED, + HISA_PATCH_VER_REQUIRED) + return_message += vermsg + "\n" + print(vermsg) + except Exception as e: #subprocess.CalledProcessError: + hisa_msg = "HiSA not found on " + self.hostname + "." + return_message += hisa_msg + '\n' + print(hisa_msg) + + + print("Checking for gmsh:") + # check that gmsh version 2.13 or greater is installed + # Note: ssh runs things in a shell. As long as the environment vars for that shell + # are set up properly on the remote computer, one doesn't need to know the gmsh path. + # You just call it and the shell $PATH takes care of the rest. + + try: + gmsh_reply = CfdTools.runFoamCommand(ssh_prefix + "gmsh -version")[0] + + except Exception as e: + #print(e) + gmsh_msg = "Cannot run 'gmsh' on " + self.hostname + ". \n" + gmsh_msg += "Please install gmsh on the remote host.\n" + return_message += gmsh_msg + print(gmsh_msg) + + else: + # print(gmsh_reply) + # print(type(gmsh_reply)) + return_message += "gmsh version " + gmsh_reply[:-1] + " is installed on " + self.hostname + ".\n" + versionlist = gmsh_reply.split(".") + if int(versionlist[0]) < 2 or (int(versionlist[0]) == 2 and int(versionlist[1]) < 13): + gmsh_ver_msg = "The installed gmsh version is older than minimum required (2.13).\n" + gmsh_ver_msg += "Please update gmsh on the remote host." + return_message += gmsh_ver_msg + '\n' + print(gmsh_ver_msg) + + """ + gmshversion = "" + gmsh_exe = getRemoteGmshExecutable() + if gmsh_exe is None: + gmsh_msg = "gmsh not found (optional)" + return_message += gmsh_msg + '\n' + print(gmsh_msg) + else: + gmsh_msg = "gmsh executable: " + gmsh_exe + return_message += gmsh_msg + '\n' + print(gmsh_msg) + try: + # Needs to be runnable from OpenFOAM environment + gmshversion = runFoamCommand("'" + gmsh_exe + "'" + " -version")[2] + except (OSError, subprocess.CalledProcessError): + gmsh_msg = "gmsh could not be run from OpenFOAM environment" + return_message += gmsh_msg + '\n' + print(gmsh_msg) + if len(gmshversion) > 1: + # Only the last line contains gmsh version number + gmshversion = gmshversion.rstrip().split() + gmshversion = gmshversion[-1] + versionlist = gmshversion.split(".") + if int(versionlist[0]) < 2 or (int(versionlist[0]) == 2 and int(versionlist[1]) < 13): + gmsh_ver_msg = "gmsh version is older than minimum required (2.13)" + return_message += gmsh_ver_msg + '\n' + print(gmsh_ver_msg) + """ + + print("Completed CFD remote dependency check.") + return_message += "Completed remote host dependency check on " + self.hostname + ".\n" + return return_message + + + # This dependency checker runs on the remote host + def runRemoteDependencyChecker(self): + + if self.profile_name == "": + self.consoleMessage("Error: empty profile") + return + + if self.hostname == "": + self.consoleMessage("Error: missing hostname or IP address") + return + + if self.username == "": + self.consoleMessage("Error: missing username") + return + + remote_hostname = self.hostname + + # Temporarily apply the foam dir selection and paraview path selection + #CfdTools.setRemoteFoamDir(self.foam_dir) + #CfdTools.setParaviewPath(self.paraview_path) + #CfdTools.setRemoteGmshPath(self.gmsh_path) + #QApplication.setOverrideCursor(Qt.WaitCursor) + + self.consoleMessage("Checking dependencies on " + remote_hostname + "...") + msg = self.checkRemoteCfdDependencies() + self.consoleMessage(msg) + #CfdTools.setRemoteFoamDir(self.initial_remote_foam_dir) + #CfdTools.setParaviewPath(self.initial_paraview_path) + #CfdTools.setRemoteGmshPath(self.initial_remote_gmsh_path) + QApplication.restoreOverrideCursor() + + + # Not used anymore. Kept for reference sake + # Some of the vars may have been changed to their remote versions + """ + def runDependencyChecker(self): + # Temporarily apply the foam dir selection and paraview path selection + CfdTools.setFoamDir(self.foam_dir) + CfdTools.setParaviewPath(self.paraview_path) + CfdTools.setGmshPath(self.gmsh_path) + QApplication.setOverrideCursor(Qt.WaitCursor) + self.consoleMessage("Checking dependencies...") + msg = CfdTools.checkCfdDependencies() + self.consoleMessage(msg) + CfdTools.setFoamDir(self.initial_foam_dir) + CfdTools.setParaviewPath(self.initial_paraview_path) + CfdTools.setGmshPath(self.initial_gmsh_path) + QApplication.restoreOverrideCursor() + """ + + + def showAdministratorWarningMessage(self): + if platform.system() == "Windows": + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + if not is_admin: + button = QtGui.QMessageBox.question( + None, + "CfdOF Workbench", + "Before installing this software, it is advised to run FreeCAD in administrator mode (hold down " + " the 'Shift' key, right-click on the FreeCAD launcher, and choose 'Run as administrator').\n\n" + "If this is not possible, please make sure OpenFOAM is installed in a location to which you have " + "full read/write access rights.\n\n" + "You are not currently running as administrator - do you wish to continue anyway?") + return button == QtGui.QMessageBox.StandardButton.Yes + return True + + def downloadInstallOpenFoam(self): + if not self.showAdministratorWarningMessage(): + return + if self.createThread(): + self.thread.task = DOWNLOAD_OPENFOAM + self.thread.openfoam_url = self.form.le_openfoam_url.text() + self.thread.start() + + def pickOpenFoamFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose OpenFOAM install file', filter="*.exe") + if f and os.access(f, os.R_OK): + self.form.le_openfoam_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallParaview(self): + if self.createThread(): + self.thread.task = DOWNLOAD_PARAVIEW + self.thread.paraview_url = self.form.le_paraview_url.text() + self.thread.start() + + def pickParaviewFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose ParaView install file', filter="*.exe") + if f and os.access(f, os.R_OK): + self.form.le_paraview_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + + #********************************************************************************************** + # TODO: Update to use profiles + # TODO: Check if cfmesh is already installed before installing in case the user + # accidentally pushes the button + def remoteDownloadInstallCfMesh(self): + + #TODO: this routine doesn't clean up after itself if it fails + # Nor does it check for a full or partial build + # If the user reruns the build after fully or partially building previously + # this routine will fail because the cfmesh directory is already present. + + # TODO this routine assumes the output dir exists. Will fail if it doesn't. Fix this. + + # TODO: if the remote host is missingzlib-dev, this routine fails silently and then the user will get + # an error /usr/bin/ld: checkSurfaceMesh.C:(.text.startup+0xabc): undefined reference to `Foam::triSurf::~triSurf()' + # when the solver is run See here: https://forum.freecad.org/viewtopic.php?style=10&t=60010 + # This appears to be a problem with the local install process as well. + + # Get the username and hostname for the remote host + remote_user = self.username + remote_hostname = self.hostname + ssh_prefix = "ssh -tt " + remote_user + "@" + remote_hostname + " " + working_dir = self.output_path + + self.consoleMessage("Installing cfMesh on " + self.hostname + "...") + + # create a dir to download the CfMesh source files to + # cfMesh is installed in the user's home directory, not the output directory + + command = "EOT\n" + command += "cd " + working_dir + "\n" + command += "mkdir cfMesh" + "\n" + command += "exit \n" + command += "EOT" + + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + "<< " + command)[0] + working_dir += "/cfMesh" + + print("Created cfMesh install dir: " + working_dir) + self.consoleMessage("Created cfMesh install directory " + working_dir) + except: + self.consoleMessage("Could not create an install directory on " + self.hostname + ".") + return + + # download the cfMesh source files + command = "EOT\n" + command += "cd " + working_dir + "\n" + command += "wget " + self.form.le_cfmesh_url.text() + "\n" + command += "exit \n" + command += "EOT" + + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + " << " + command)[0] + self.consoleMessage("Downloaded the cfMesh source files") + except: + self.consoleMessage("Could not download the cfMesh source files.") + return + + # unzip the downloaded file + # TODO: this assumes the downloaded archive is called "download". Which might not always be the case. + # Get name of the downloaded file from the URL and use that filename in this command + command = "EOT\n" + command += "cd " + working_dir + "\n" + command += "unzip download \n" + command += "exit \n" + command += "EOT" + + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + " << " + command)[0] + self.consoleMessage("Unzipped cfMesh source files") + except: + self.consoleMessage("Could not unzip cfMesh source files.") + return + + # run the wmake process + # TODO: add the number of processes to use on the build with export WM_NCOMPPROCS=`nproc`; + # TODO: add logging wiht ./Allwmake -> log.Allwmake + # TODO: delete the previous log before starting the new build + # Executing: { rm -f log.Allwmake; export WM_NCOMPPROCS=`nproc`; ./Allwmake 1> >(tee -a log.Allwmake) 2> >(tee -a log.Allwmake >&2); } in $WM_PROJECT_USER_DIR/cfmesh-cfdof + + command = "EOT\n" + command += "cd " + working_dir + "/cfmesh-cfdof \n" + command += "./Allwmake \n" + command += "exit \n" + command += "EOT" + + self.consoleMessage("Starting cfMesh build on " + self.hostname + ".") + self.consoleMessage("This takes a bit of time. Please wait.") + try: + command_result = CfdTools.runFoamCommand(ssh_prefix + " << " + command)[0] + self.consoleMessage("Successfully built and installed cfMesh.") + except: + self.consoleMessage("Could not build cfMesh: " + command_result) + return + + + + + + # old version, not used anymore. Doesn't handle remote install + def downloadInstallCfMesh(self): + print("Error:downloadInstallCFMesh has been depreciated.") + return + + if not self.showAdministratorWarningMessage(): + return + + runtime = self.testGetRuntime(False) + if runtime == "MinGW" and self.form.le_cfmesh_url.text() == CFMESH_URL: + # Openfoam might have just been installed and the URL would not have had a chance to update + self.setDownloadURLs() + + if self.createThread(): + self.thread.task = DOWNLOAD_CFMESH + # We are forced to apply the foam dir selection - reset when the task finishes + CfdTools.setFoamDir(self.foam_dir) + self.thread.cfmesh_url = self.form.le_cfmesh_url.text() + self.thread.start() + + def pickCfMeshFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose cfMesh archive', filter="*.zip") + if f and os.access(f, os.R_OK): + self.form.le_cfmesh_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + def downloadInstallHisa(self): + if not self.showAdministratorWarningMessage(): + return + + runtime = self.testGetRuntime(False) + if runtime == "MinGW" and self.form.le_hisa_url.text() == HISA_URL: + # Openfoam might have just been installed and the URL would not have had a chance to update + self.setDownloadURLs() + + if self.createThread(): + self.thread.task = DOWNLOAD_HISA + # We are forced to apply the foam dir selection - reset when the task finishes + CfdTools.setFoamDir(self.foam_dir) + self.thread.hisa_url = self.form.le_hisa_url.text() + self.thread.start() + + def pickHisaFile(self): + f, filter = QtGui.QFileDialog().getOpenFileName(None, 'Choose HiSA archive', filter="*.zip") + if f and os.access(f, os.R_OK): + self.form.le_hisa_url.setText(urlparse.urljoin('file:', urlrequest.pathname2url(f))) + + + def createThread(self): + if self.thread and self.thread.isRunning(): + self.consoleMessage("Busy - please wait...", 'Error') + return False + else: + self.thread = CfdRemotePreferencePageThread() + self.thread.signals.error.connect(self.threadError) + self.thread.signals.finished.connect(self.threadFinished) + self.thread.signals.status.connect(self.threadStatus) + self.thread.signals.downloadProgress.connect(self.downloadProgress) + return True + + def threadStatus(self, msg): + self.consoleMessage(msg) + + def threadError(self, msg): + self.consoleMessage(msg, 'Error') + self.consoleMessage("Download unsuccessful") + + def threadFinished(self, status): + if self.thread.task == DOWNLOAD_CFMESH: + if status: + if CfdTools.getFoamRuntime() != "MinGW": + self.consoleMessage("Download completed") + user_dir = self.thread.user_dir + self.consoleMessage("Building cfMesh. Lengthy process - please wait...") + self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, CFMESH_FILE_BASE)) + if CfdTools.getFoamRuntime() == 'WindowsDocker': + # There seem to be issues when using multi processors to build in docker + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=1; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=`nproc`; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+CFMESH_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.consoleMessage("Install completed") + # Reset foam dir for now in case the user presses 'Cancel' + CfdTools.setFoamDir(self.initial_remote_foam_dir) + elif self.thread.task == DOWNLOAD_HISA: + if status: + if CfdTools.getFoamRuntime() != "MinGW": + self.consoleMessage("Download completed") + user_dir = self.thread.user_dir + self.consoleMessage("Building HiSA. Please wait...") + self.consoleMessage("Log file: {}/{}/log.Allwmake".format(user_dir, HISA_FILE_BASE)) + if CfdTools.getFoamRuntime() == 'WindowsDocker': + # There seem to be issues when using multi processors to build in docker + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=1; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.install_process = CfdTools.startFoamApplication( + "export WM_NCOMPPROCS=`nproc`; ./Allwmake", + "$WM_PROJECT_USER_DIR/"+HISA_FILE_BASE, + 'log.Allwmake', self.installFinished, stderr_hook=self.stderrFilter) + else: + self.consoleMessage("Install completed") + # Reset foam dir for now in case the user presses 'Cancel' + CfdTools.setFoamDir(self.initial_remote_foam_dir) + elif self.thread.task == DOWNLOAD_DOCKER: + if status: + self.consoleMessage("Download completed") + else: + self.consoleMessage("Download unsuccessful") + self.thread = None + + def installFinished(self, exit_code): + if exit_code: + self.consoleMessage("Install finished with error {}".format(exit_code)) + else: + self.consoleMessage("Install completed") + + def downloadProgress(self, bytes_done, bytes_total): + mb_done = float(bytes_done)/(1024*1024) + msg = "Downloaded {:.2f} MB".format(mb_done) + if bytes_total > 0: + msg += " of {:.2f} MB".format(float(bytes_total)/(1024*1024)) + self.form.labelDownloadProgress.setText(msg) + + def stderrFilter(self, text): + # Print to stdout rather than stderr so as not to alarm the user + # with the spurious wmkdep errors on stderr + print(text, end='') + return '' + + def dockerCheckboxClicked(self): + if CfdTools.docker_container==None: + CfdTools.docker_container = CfdTools.DockerContainer() + CfdTools.docker_container.usedocker = self.form.cb_docker_sel.isChecked() + self.form.pb_download_install_docker.setEnabled(CfdTools.docker_container.usedocker) + self.form.pb_download_install_openfoam.setEnabled(not CfdTools.docker_container.usedocker) + #self.form.pb_download_install_hisa.setEnabled((not CfdTools.docker_container.usedocker )) + #self.form.pb_download_install_cfMesh.setEnabled((not CfdTools.docker_container.usedocker)) + self.form.gb_docker.setVisible(CfdTools.docker_container.docker_cmd!=None or CfdTools.docker_container.usedocker) + + def downloadInstallDocker(self): + # Set foam dir and output dir in preparation for using docker + CfdTools.setFoamDir(self.form.le_foam_dir.text()) + self.saveSettings() + if self.createThread(): + self.thread.task = DOWNLOAD_DOCKER + self.thread.docker_url = self.form.le_docker_url.text() + self.thread.start() + +class CfdRemotePreferencePageSignals(QObject): + error = QtCore.Signal(str) # Signal in PySide, pyqtSignal in PyQt + finished = QtCore.Signal(bool) + status = QtCore.Signal(str) + downloadProgress = QtCore.Signal(int, int) + + +class CfdRemotePreferencePageThread(QThread): + """ Worker thread to complete tasks in preference page """ + def __init__(self): + super(CfdRemotePreferencePageThread, self).__init__() + self.signals = CfdRemotePreferencePageSignals() + self.quit = False + self.user_dir = None + self.task = None + self.openfoam_url = None + self.paraview_url = None + self.cfmesh_url = None + self.hisa_url = None + self.docker_url = None + + def run(self): + self.quit = False + + try: + if self.task == DOWNLOAD_OPENFOAM: + self.downloadOpenFoam() + elif self.task == DOWNLOAD_PARAVIEW: + self.downloadParaview() + elif self.task == DOWNLOAD_CFMESH: + self.downloadCfMesh() + elif self.task == DOWNLOAD_HISA: + self.downloadHisa() + elif self.task == DOWNLOAD_DOCKER: + self.downloadDocker() + except Exception as e: + if self.quit: + self.signals.finished.emit(False) # Exit quietly since UI already destroyed + return + else: + self.signals.error.emit(str(e)) + self.signals.finished.emit(False) + return + self.signals.finished.emit(True) + + def downloadFile(self, url, **kwargs): + block_size = kwargs.get('block_size', 10*1024) + context = kwargs.get('context', None) + reporthook = kwargs.get('reporthook', None) + suffix = kwargs.get('suffix', '') + with closing(urlrequest.urlopen(url, context=context)) as response: # For Python < 3.3 backward compatibility + download_len = int(response.info().get('Content-Length', 0)) + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp_file: + i = 0 + while True: + data = response.read(block_size) + if not data: + break + if self.quit: + raise RuntimeError("Premature termination received") + tmp_file.write(data) + i += 1 + if reporthook: + reporthook(i, block_size, download_len) + filename = tmp_file.name + return filename, response.info() + + def download(self, url, suffix, name): + self.signals.status.emit("Downloading {}, please wait...".format(name)) + try: + if hasattr(ssl, 'create_default_context'): + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + else: + context = None + # Download + (filename, header) = self.downloadFile( + url, suffix=suffix, reporthook=self.downloadStatus, context=context) + except Exception as ex: + raise Exception("Error downloading {}: {}".format(name, str(ex))) + + self.signals.status.emit("{} downloaded to {}".format(name, filename)) + return filename + + def downloadOpenFoam(self): + filename = self.download(self.openfoam_url, OPENFOAM_FILE_EXT, "OpenFOAM") + if QtCore.QProcess().startDetached(filename): + self.signals.status.emit("OpenFOAM installer launched - please complete the installation") + else: + raise Exception("Failed to launch OpenFOAM installer") + + def downloadParaview(self): + filename = self.download(self.paraview_url, PARAVIEW_FILE_EXT, "ParaView") + if QtCore.QProcess().startDetached(filename): + self.signals.status.emit("ParaView installer launched - please complete the installation") + else: + raise Exception("Failed to launch ParaView installer") + + def downloadCfMesh(self): + filename = self.download(self.cfmesh_url, CFMESH_FILE_EXT, "cfMesh") + + if CfdTools.getFoamRuntime() == "MinGW": + self.user_dir = None + self.signals.status.emit("Installing cfMesh...") + CfdTools.runFoamCommand( + '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. + format(CfdTools.translatePath(filename))) + else: + self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] + # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. + if CfdTools.getFoamRuntime() != 'WindowsDocker': + self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) + + self.signals.status.emit("Extracting cfMesh...") + if CfdTools.getFoamRuntime() == 'WindowsDocker': + from zipfile import ZipFile + with ZipFile(filename, 'r') as zip: + with tempfile.TemporaryDirectory() as tempdir: + zip.extractall(path=tempdir) + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' + .format(CfdTools.translatePath(os.path.join(tempdir, CFMESH_FILE_BASE)))) + else: + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. + format(CFMESH_FILE_BASE, CfdTools.translatePath(filename))) + + def downloadHisa(self): + filename = self.download(self.hisa_url, HISA_FILE_EXT, "HiSA") + + if CfdTools.getFoamRuntime() == "MinGW": + self.user_dir = None + self.signals.status.emit("Installing HiSA...") + CfdTools.runFoamCommand( + '{{ mkdir -p "$FOAM_APPBIN" && cd "$FOAM_APPBIN" && unzip -o "{}"; }}'. + format(CfdTools.translatePath(filename))) + else: + self.user_dir = CfdTools.runFoamCommand("echo $WM_PROJECT_USER_DIR")[0].rstrip().split('\n')[-1] + # We can't reverse-translate the path for docker since it sits inside the container. Just report it as such. + if CfdTools.getFoamRuntime() != 'WindowsDocker': + self.user_dir = CfdTools.reverseTranslatePath(self.user_dir) + + self.signals.status.emit("Extracting HiSA...") + if CfdTools.getFoamRuntime() == 'WindowsDocker': + from zipfile import ZipFile + with ZipFile(filename, 'r') as zip: + with tempfile.TemporaryDirectory() as tempdir: + zip.extractall(path=tempdir) + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cp -r "{}" "$WM_PROJECT_USER_DIR/"; }}' + .format(CfdTools.translatePath(os.path.join(tempdir, HISA_FILE_BASE)))) + else: + CfdTools.runFoamCommand( + '{{ mkdir -p "$WM_PROJECT_USER_DIR" && cd "$WM_PROJECT_USER_DIR" && ( rm -r {}; unzip -o "{}"; ); }}'. + format(HISA_FILE_BASE, CfdTools.translatePath(filename))) + + def downloadDocker(self): + self.signals.status.emit("Downloading Docker image, please wait until 'Download completed' message shown below") + if CfdTools.docker_container.container_id!=None: + CfdTools.docker_container.stop_container() + cmd = '{} pull {}'.format(CfdTools.docker_container.docker_cmd, self.docker_url) + if 'docker'in CfdTools.docker_container.docker_cmd: + cmd = cmd.replace('docker.io/','') + + CfdTools.runFoamCommand(cmd) + + def downloadStatus(self, blocks, block_size, total_size): + self.signals.downloadProgress.emit(blocks*block_size, total_size) diff --git a/CfdOF/CfdTools.py b/CfdOF/CfdTools.py index 36e2e1fe..8f498796 100644 --- a/CfdOF/CfdTools.py +++ b/CfdOF/CfdTools.py @@ -40,12 +40,18 @@ from datetime import timedelta import FreeCAD from FreeCAD import Units +#import App import Part import BOPTools from BOPTools import SplitFeatures from CfdOF.CfdConsoleProcess import CfdConsoleProcess from CfdOF.CfdConsoleProcess import removeAppimageEnvironment from PySide import QtCore + +# added for runCommand +from PySide.QtCore import QProcess # I added +from PySide.QtGui import QApplication # I added + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtGui @@ -83,15 +89,75 @@ docker_container = None -def getDefaultOutputPath(): + +# Modified by LinuxGuy to handle add_filename_to_path +# This is called default because it is used in the mesh and solver objects +# but can be over ridden by the user +# WARNING: this routine returns the path with the filename add if enabled in Preferences +def getDefaultOutputPath(profile = 'local'): + + # host is local + if profile == 'local': + prefs = getPreferencesLocation() + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + if not output_path: + output_path = tempfile.gettempdir() + print("Root output path:" + output_path) + + add_filename = FreeCAD.ParamGet(prefs).GetBool("AddFilenameToOutput") + #print("AddFilenameToOutput:" + str(add_filename)) + + if add_filename: + #TODO: might want to warn the user if they haven't saved the filename yet + App = FreeCAD + filename = App.ActiveDocument.Name + if filename: + output_path += "/" + filename + + output_path = os.path.normpath(output_path) + print("Output path:" + output_path) + return output_path + + #remote case + else: + prefs = getPreferencesLocation() + hostprefs = prefs +"/Hosts/" + profile + output_path = FreeCAD.ParamGet(hostprefs).GetString("OutputPath", "") + + if not output_path: + #TODO: might want to warn the user if they haven't saved the filename yet + output_path = tempfile.gettempdir() + + if FreeCAD.ParamGet(hostprefs).GetBool("AddFilenameToOutput"): + App = FreeCAD + filename = App.ActiveDocument.Name + if filename: + output_path += "/" + filename + # the local computer might be Windows and the remote one Linux + # so don't normalize the path to the local OS + #output_path = os.path.normpath(output_path) + print("Output path:" + output_path) + return output_path + + +# TODO: Is this used anymore ? +def getDefaultRemoteOutputPath(): + print("Error: getDefaultRemoteOutputPath is depreciated.") prefs = getPreferencesLocation() - output_path = FreeCAD.ParamGet(prefs).GetString("DefaultOutputPath", "") + output_path = FreeCAD.ParamGet(prefs).GetString("DefaultRemoteOutputPath", "") if not output_path: - output_path = tempfile.gettempdir() - output_path = os.path.normpath(output_path) + #TODO: fix this so it will run on the server + #should be /home/username from the reference page + # output_path = tempfile.gettempdir() + output_path = "" + # assume the remote computer is Linux and + # don't adjust the output path + # output_path = os.path.normpath(output_path) return output_path +# This used to be called by CfdCaseWriterFoam, but isn't anymore. +# Not sure what uses it def getOutputPath(analysis): if analysis and 'OutputPath' in analysis.PropertiesList: output_path = analysis.OutputPath @@ -108,6 +174,20 @@ def getOutputPath(analysis): return output_path +# TODO Is this used anymore ? +def getRemoteOutputPath(analysis): + if analysis and 'RemoteOutputPath' in analysis.PropertiesList: + output_path = analysis.RemoteOutputPath + else: + output_path = "" + if not output_path: + output_path = getDefaultRemoteOutputPath() + # Assume the remote computer is Linux and don't + # adjust the path + # output_path = os.path.normpath(output_path) + return output_path + + # Get functions if FreeCAD.GuiUp: def getResultObject(): @@ -478,6 +558,20 @@ def setFoamDir(installation_path): # Set OpenFOAM install path in parameters FreeCAD.ParamGet(prefs).SetString("InstallationPath", installation_path) +#not used anymore +def setRemoteFoamDir(remote_installation_path): + print("Error: setRemoteFoamDir is depreciated.") + prefs = getPreferencesLocation() + # Set OpenFOAM remote install path in parameters + FreeCAD.ParamGet(prefs).SetString("RemoteInstallationPath", remote_installation_path) + +# not used anymore +def getRemoteFoamDir(): + print("Error: getRemoteFoamDir is depreciated.") + prefs = getPreferencesLocation() + # Set OpenFOAM remote install path in parameters + return FreeCAD.ParamGet(prefs).GetString("RemoteInstallationPath", "") + def startDocker(): global docker_container if docker_container==None: @@ -640,7 +734,6 @@ def setParaviewPath(paraview_path): # Set Paraview install path in parameters FreeCAD.ParamGet(prefs).SetString("ParaviewPath", paraview_path) - def getParaviewPath(): prefs = getPreferencesLocation() # Get path from parameters @@ -655,6 +748,12 @@ def setGmshPath(gmsh_path): # Set Paraview install path in parameters FreeCAD.ParamGet(prefs).SetString("GmshPath", gmsh_path) +# not used anymore +def setRemoteGmshPath(gmsh_path): + print("Error: setRemoteGmshPath is depreciated.") + prefs = getPreferencesLocation() + # Set Paraview install path in parameters + FreeCAD.ParamGet(prefs).SetString("RemoteGmshPath", gmsh_path) def getGmshPath(): prefs = getPreferencesLocation() @@ -664,6 +763,18 @@ def getGmshPath(): setGmshPath(gmsh_path) return gmsh_path +# not used anymore +def getRemoteGmshPath(): + print("Error: getRemoteGmshPath is depreciated.") + prefs = getPreferencesLocation() + # Get path from parameters + gmsh_path = FreeCAD.ParamGet(prefs).GetString("RemoteGmshPath", "") + # Ensure parameters exist for future editing + setGmshPath(gmsh_path) + return gmsh_path + + + def translatePath(p): """ @@ -796,7 +907,13 @@ def getRunEnvironment(): else: return {} - +# This routine is used by both the remote meshing process and the remote solver process +# to convert the remote command into a command that will run with CfDConsole process +# However, it adds a bash call that it doesn't need to and it sources the OpenFOAM init file +# on the local computer, which it doesn't have to. When a remote host is used, it is assumed +# that OpenFOAM will run from the command line. This means its init file is run in bashrc file. +# This is probably a dangerous assumption and this routine should be changed to work with remote +# hosts and source the OpenFOAM init file. def makeRunCommand(cmd, dir, source_env=True): """ Generate native command to run the specified Linux command in the relevant environment, @@ -913,7 +1030,6 @@ def makeRunCommand(cmd, dir, source_env=True): cmdline = ['bash', '-c', source + cd + cmd] return cmdline - def runFoamCommand(cmdline, case=None): """ Run a command in the OpenFOAM environment and wait until finished. Return output as (stdout, stderr, combined) @@ -930,6 +1046,7 @@ def runFoamCommand(cmdline, case=None): return proc.output, proc.outputErr, proc.outputAll + def startFoamApplication(cmd, case, log_name='', finished_hook=None, stdout_hook=None, stderr_hook=None): """ Run command cmd in OpenFOAM environment, sending output to log file. @@ -1230,6 +1347,71 @@ def checkCfdDependencies(msgFn): msgFn("Completed CFD dependency check") +#****************************************************************************** +# Done: TODO: right now this routine returns the error code of the outermost process, ie the one called with cmd parameter. +# So if you call ssh, for example, and it runs successfully, it will return no error, no matter what happens to the command +# that was used in ssh. This should be changed. If one gets an error message via stderr, this routine should +# return an error code, regardless of how ssh itself ran. +# +# TODO: add a timer so that the process times out if it doesn't create any output or do anything. +# If ssh credentials aren't set up properly on the remote host, it will ask for a password and not echo that to stdout or stderr. +# Thus the process will be stuck. +# +# This routine is not needed. runFoamCommand does the same thing but is integrated into CfdTools already. +# The only routines that use runCommand are ping and ssh test in RemotePreferences.py. Those routines should be +# rewritten with runFoamCommand and put in CfdTools. +# +# When that happens, this routine can be deleted. + +def runCommand(cmd, args=[]): + + process = QProcess() + returnValue = 0 + + def handleStdout(): + #print("In Stdout handler") + data = process.readAllStandardOutput() + message = bytes(data).decode("utf8") + print(message) + + def handleStderr(): + nonlocal returnValue + #print("In Stderr handler") + data = process.readAllStandardError() + message = bytes(data).decode("utf8") + print("Error:" + message) + returnValue = -1 + + def handleFinished(self, exitCode): + nonlocal returnValue + print("Command done.") + if ((exitCode == QProcess.NormalExit) and (returnValue == 0)): + returnValue = 0 + else: + returnValue = -1 + + # connect the events to handlers + process.readyReadStandardOutput.connect(handleStdout) + process.readyReadStandardError.connect(handleStderr) + process.finished.connect(handleFinished) + + # debug + message = "Remotely executing: " + cmd + for arg in args: + message = message + " " + arg + print(message) + + # start the command process + process.start(cmd, args) + + # wait for the process to get started + process.waitForStarted() + + # process application events while waiting for it to finish + while(process.state() == QProcess.Running): + QApplication.processEvents() + return(returnValue) + def getParaviewExecutable(): # If path of paraview executable specified, use that @@ -1264,6 +1446,22 @@ def getGmshExecutable(): gmsh_cmd='gmsh' return gmsh_cmd +def getRemoteGmshExecutable(): + # If path of gmsh executable specified, use that + gmsh_cmd = getGmshPath() + if not gmsh_cmd: + # On Windows, use gmsh supplied + if platform.system() == "Windows": + # Use forward slashes to avoid escaping problems + gmsh_cmd = '/'.join([FreeCAD.getHomePath().rstrip('/'), 'bin', 'gmsh.exe']) + if not gmsh_cmd: + # Otherwise, see if the command 'gmsh' is in the path. + gmsh_cmd = shutil.which("gmsh") + if getFoamRuntime() == "PosixDocker": + gmsh_cmd='gmsh' + return gmsh_cmd + + def startParaview(case_path, script_name, console_message_fn): proc = QtCore.QProcess() @@ -1730,6 +1928,11 @@ def run(self, cmdline, case=None): if startDocker(): return 1 print("Running ", cmdline) + + mycmdline = makeRunCommand(cmdline,case) + print("cmdline in CfdSynFoamProcess:") + print(mycmdline) + self.process.start(makeRunCommand(cmdline, case), env_vars=getRunEnvironment()) if not self.process.waitForFinished(): raise Exception("Unable to run command " + cmdline) @@ -1743,6 +1946,8 @@ def readError(self, output): self.outputErr += output self.outputAll += output + + # Only one container is needed. Start one for the CfdOF workbench as required class DockerContainer: container_id = None diff --git a/CfdOF/Mesh/CfdMesh.py b/CfdOF/Mesh/CfdMesh.py index 253da724..335f448d 100644 --- a/CfdOF/Mesh/CfdMesh.py +++ b/CfdOF/Mesh/CfdMesh.py @@ -102,12 +102,24 @@ def initProperties(self, obj): "Mesh elements per 360 degrees for surface triangulation with GMSH") addObjectProperty(obj, 'NumberOfProcesses', 1, "App::PropertyInteger", "", - "Number of parallel processes (only applicable to cfMesh and snappyHexMesh)") + "Number of parallel processes on local computer (only applicable to cfMesh and snappyHexMesh)") addObjectProperty(obj, 'NumberOfThreads', 0, "App::PropertyInteger", "", - "Number of parallel threads per process (only applicable to cfMesh and gmsh). " + "Number of parallel threads per process on local computer (only applicable to cfMesh and gmsh). " "0 means use all available (if NumberOfProcesses = 1) or use 1 (if NumberOfProcesses > 1)") + + # I added the remote processes and threads + """ + addObjectProperty(obj, 'NumberOfRemoteProcesses', 1, "App::PropertyInteger", "", + "Number of parallel processes on remote computer (only applicable to cfMesh and snappyHexMesh)") + + addObjectProperty(obj, 'NumberOfRemoteThreadsPerProcess', 0, "App::PropertyInteger", "", + "Number of parallel threads per process on remote computer (only applicable to cfMesh and gmsh). " + "0 means use all available (if NumberOfProcesses = 1) or use 1 (if NumberOfProcesses > 1)") + """ + + addObjectProperty(obj, "Part", None, "App::PropertyLinkGlobal", "Mesh Parameters", "Part object to mesh") if addObjectProperty(obj, "MeshUtility", MESHERS, "App::PropertyEnumeration", diff --git a/CfdOF/Mesh/CfdMeshTools.py b/CfdOF/Mesh/CfdMeshTools.py index 2a1a56b7..9bf987b7 100644 --- a/CfdOF/Mesh/CfdMeshTools.py +++ b/CfdOF/Mesh/CfdMeshTools.py @@ -70,7 +70,7 @@ def __init__(self, cart_mesh_obj): self.progressCallback = None - def writeMesh(self): + def writeMesh(self, profile_name): self.setupMeshCaseDir() CfdTools.cfdMessage("Exporting mesh refinement data ...\n") if self.progressCallback: @@ -81,10 +81,11 @@ def writeMesh(self): if self.progressCallback: self.progressCallback("Exporting the part surfaces ...") self.writePartFile() - self.writeMeshCase() + self.writeMeshCase(profile_name) CfdTools.cfdMessage("Wrote mesh case to {}\n".format(self.meshCaseDir)) if self.progressCallback: - self.progressCallback("Mesh case written successfully") + self.progressCallback("Mesh case written successfully") + def processExtrusions(self): """ Find and process any extrusion objects """ @@ -586,7 +587,7 @@ def loadSurfMesh(self): else: print('No mesh was created.') - def writeMeshCase(self): + def writeMeshCase(self, host_profile): """ Collect case settings, and finally build a runnable case. """ CfdTools.cfdMessage("Populating mesh dictionaries in folder {}\n".format(self.meshCaseDir)) @@ -711,14 +712,20 @@ def writeMeshCase(self): if CfdTools.getFoamRuntime() != 'WindowsDocker': self.settings['TranslatedFoamPath'] = CfdTools.translatePath(CfdTools.getFoamDir()) + # set the number of threads and processes + # these were set appropriately when the host was selected if self.mesh_obj.NumberOfProcesses <= 1: - self.settings['ParallelMesh'] = False - self.settings['NumberOfProcesses'] = 1 + self.settings['ParallelMesh'] = False + self.settings['NumberOfProcesses'] = 1 else: - self.settings['ParallelMesh'] = True - self.settings['NumberOfProcesses'] = self.mesh_obj.NumberOfProcesses + self.settings['ParallelMesh'] = True + self.settings['NumberOfProcesses'] = self.mesh_obj.NumberOfProcesses + self.settings['NumberOfThreads'] = self.mesh_obj.NumberOfThreads + if self.progressCallback: + self.progressCallback("Mesh case will use " + str(self.settings['NumberOfProcesses']) + " processes and " + str(self.settings['NumberOfThreads']) + " threads per process.") + self.progressCallback("0 threads per process means use all available (if NumberOfProcesses = 1) or use 1 per process (if NumberOfProcesses > 1)") TemplateBuilder(self.meshCaseDir, self.template_path, self.settings) # Update Allmesh permission - will fail silently on Windows @@ -728,7 +735,41 @@ def writeMeshCase(self): os.chmod(fname, s.st_mode | stat.S_IEXEC) self.analysis.NeedsMeshRewrite = False - CfdTools.cfdMessage("Successfully wrote meshCase to folder {}\n".format(self.meshCaseDir)) + CfdTools.cfdMessage("Wrote meshCase to local folder {}\n".format(self.meshCaseDir)) + if self.progressCallback: + self.progressCallback("Wrote meshCase to local folder {}\n".format(self.meshCaseDir)) + + #if this is a remote mesh, copy the mesh case folder from the local mesh case dir + # to the remote host's directory + if host_profile != "local": + #this code is also used in reverse in TaskPanelCfdMesh.py for copyback after the meshing is done + # TODO: make it a function ? + profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + host_profile + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(host_profile) + + # rsync the meshCase directory to the remote host's output directory + # Typical useage: rsync -r --remove-source-files --delete /tmp/meshCase me@david:/tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + # not using --remove-source-files because the workstation uses it to determine what actions are + # appropriate to do on the remote host, ie has a meshCase been written. + try: + CfdTools.runFoamCommand("rsync -r --delete --remove-source-files " + self.meshCaseDir + " " + remote_user + "@" + remote_hostname + \ + ":" + remote_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not move mesh case to remote host: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not move mesh case to remote host: " + str(e)) + else: + CfdTools.cfdMessage("Moved mesh case to " + remote_hostname + ":" + remote_output_path + "\n" ) + if self.progressCallback: + self.progressCallback("Moved mesh case to " + remote_hostname + ":" + remote_output_path + "\n") + if self.progressCallback: + self.progressCallback("Mesh case write process is complete.") + def writeSurfaceMeshFromShape(shape, path, name, mesh_obj): prefs = CfdTools.getPreferencesLocation() diff --git a/CfdOF/Mesh/TaskPanelCfdMesh.py b/CfdOF/Mesh/TaskPanelCfdMesh.py index 4acfde29..9717b224 100644 --- a/CfdOF/Mesh/TaskPanelCfdMesh.py +++ b/CfdOF/Mesh/TaskPanelCfdMesh.py @@ -21,6 +21,49 @@ # * see . * # * * # *************************************************************************** +# +# LinuxGuy123@gmail.com's notes: +# +# TODOs, in addition to TODOs in the code itself +# +# -This code uses (dangerous) global vars to access things like profile_name, hostname, output dir, etc. +# The profile_name, hostname, output dir should be attached to the mesh object and used from it. +# +# -the ParaView button gets enabled as soon as the mesh case is written before the results are +# available. This should be fixed. See updateUI. +# +# -the Clear Surface mesh button is enabled even when the surface isn't loaded. This should be fixed in +# updateUI. +# +# -the UI doesn't know where a meshCase is available to run. You can write a mesh case to the local, then +# change to a host and Run Mesh will be available to use. But there is no meshCase on that host yet. Ideally +# the mesh object would retain the host that the mesh case is on so that it could properly enable various actions. Luckily +# run mesh fails gracefully if no mesh case is found. +# +# -delete_remote_results removes the meshCase after a successful solve. Actually, I disabled this. Thus you (would) have to rewrite +# the mesh case and rerun it on the server after every successful solve, if delete_remote_results is selected. +# +# -edit mesh should copy the edited mesh to the server after the edit is done. Right now it doesn't. +# +# -remote meshing has not been tested in macros. +# +# -cfMesh runs only 1 mesh process on the remote machine if 1,0 is selected for processes, threads. If you run 8 threads (1,8) for example, it +# doesn't ever finish the meshing process stack on the remote host is this. Looks legit, but never finishes. +# +# $ ps aux | grep Mesh +#me 134162 0.0 0.0 174292 17336 ? Sl 21:58 0:00 mpiexec -np 8 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134163 0.0 0.0 221328 944 ? S 21:58 0:00 tee -a log.cartesianMesh +#me 134164 0.0 0.0 221328 892 ? S 21:58 0:00 tee -a log.cartesianMesh +#me 134168 102 0.3 4034732 237604 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134169 102 0.3 4038380 231280 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134170 102 0.3 4029584 221532 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134171 102 0.3 4063928 258864 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134172 102 0.3 4035376 234968 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134173 102 0.3 4032120 233276 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134174 102 0.3 4030184 221800 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel +#me 134175 102 0.3 4066644 257484 ? Rl 21:58 5:22 /home/me/OpenFOAM/me-2206/platforms/linux64GccDPInt32Opt/bin/cartesianMesh -parallel + + from __future__ import print_function import FreeCAD @@ -36,7 +79,6 @@ if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore - from PySide import QtCore from PySide import QtGui from PySide.QtCore import Qt from PySide.QtGui import QApplication @@ -58,20 +100,65 @@ def __init__(self, obj): stdout_hook=self.gotOutputLines, stderr_hook=self.gotErrorLines) + #set the prefs and host prefs locations + self.prefs_location = CfdTools.getPreferencesLocation() + self.host_prefs_location = self.prefs_location + "/Hosts" + self.useRemoteProcessing = FreeCAD.ParamGet(self.prefs_location).GetBool('UseRemoteProcessing', 0) + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + + self.profile_name = "" + self.hostname = "" + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + self.gmsh_path = "" + self.add_filename_to_output = False + self.copy_back = False + self.delete_remote_results = False + + #add a local host to cb_profile + self.form.cb_profile.addItem("local") + + # if using remote processing, add the host profiles as well + if self.useRemoteProcessing: + self.loadProfileNames() + else: + #disable cb_profile so that users aren't trying to change the host + self.form.cb_profile.setEnabled(False) + + # load the local profile + self.loadProfile("local") + self.Timer = QtCore.QTimer() self.Timer.setInterval(1000) self.Timer.timeout.connect(self.update_timer_text) + # set up the profiles combo box connection + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) + self.form.cb_utility.activated.connect(self.choose_utility) + self.form.pb_write_mesh.clicked.connect(self.writeMesh) + self.form.pb_edit_mesh.clicked.connect(self.editMesh) + self.form.pb_run_mesh.clicked.connect(self.runMesh) + self.form.pb_stop_mesh.clicked.connect(self.killMeshProcess) self.form.pb_paraview.clicked.connect(self.openParaview) self.form.pb_load_mesh.clicked.connect(self.pbLoadMeshClicked) self.form.pb_clear_mesh.clicked.connect(self.pbClearMeshClicked) self.form.pb_searchPointInMesh.clicked.connect(self.searchPointInMesh) self.form.pb_check_mesh.clicked.connect(self.checkMeshClicked) + self.form.pb_copy_to_host.clicked.connect(self.copyMeshcaseToHost) + self.form.pb_delete_mesh.clicked.connect(self.deleteMeshcase) self.radioGroup = QtGui.QButtonGroup() self.radioGroup.addButton(self.form.radio_explicit_edge_detection) @@ -97,6 +184,152 @@ def __init__(self, obj): self.Start = time.time() self.Timer.start() + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + print("Error: no host profile selected") + return + + # set the vars to the local parameters + if profile_name == "local": + self.hostname = "local" + # enable controls for the local host + self.form.pb_edit_mesh.setEnabled(True) + self.form.pb_paraview.setEnabled(True) + self.form.pb_check_mesh.setEnabled(True) + self.form.pb_load_mesh.setEnabled(True) + self.form.pb_clear_mesh.setEnabled(True) + + # the local code doesn't use these vars, setting them to be safe + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + #self.foam_dir = FreeCAD.ParamGet(Prefs).GetString("FoamDir", "") + #self.output_path = FreeCAD.ParamGet(Prefs).GetString("OutputPath","") + self.output_path = "" + self.add_filename_to_output = False + # Not sure if this causes a side effect or not. + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir + self.form.pb_delete_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + + else: + # set the vars to the remote host parameters + # most of these aren't used, at least not in this page + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + + # This is loading defaults. The user may change this number + self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + + self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + + #self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = CfdTools.getDefaultOutputPath(self.profile_name) + self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") + self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") + + #now set the control values + #read these from the parameters in case they've been edited + self.mesh_obj.NumberOfProcesses = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + self.mesh_obj.NumberOfThreads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + + + + # disable if using a remote host + self.form.pb_edit_mesh.setEnabled(False) + self.form.pb_paraview.setEnabled(False) + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + self.form.pb_clear_mesh.setEnabled(False) + + # enable if using a remote host + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir + self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + # TODO should check if there is a mesh case on the remote host and set accordingly + self.form.pb_delete_mesh.setEnabled(True) + + #TODO: fix these, if we need to. + #self.form.le_mesh_processes.setText(str(self.mesh_processes)) + #self.form.le_mesh_threads.setText(str(self.mesh_threads)) + + #self.form.le_hostname.setText(self.hostname) + #self.form.le_username.setText(self.username) + + #self.form.le_foam_processes.setText(str(self.foam_processes)) + #self.form.le_foam_threads.setText(str(self.foam_threads)) + #self.form.le_foam_dir.setText(self.foam_dir) + #self.form.le_output_path.setText(self.output_path) + #self.form.cb_add_filename_to_output.setChecked(self.add_filename_to_output) + + + # this gets called when the user changes the profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + + # enable and disable the appropriate controls + self.updateUI + + # test routine to run a mesh without a proxy + # The real routine is runMesh down below + def runRemoteMesh(self): + # run remote meshing directly, without a proxy + #profile_prefs = CfdTools.getPreferencesLocation() + '/Hosts/' + self.profile_name + remote_user = self.username + remote_hostname = self.hostname + + # create the ssh connection command + # was ssh -tt + ssh_prefix = 'ssh -t ' + remote_user + '@' + remote_hostname + ' ' + + # Get the working directory for the mesh + working_dir = self.output_path + #TODO: add filename to the path if selected + + # create the command to do the actual work + #command = 'EOT \n' + command = 'cd ' + working_dir + '/meshCase \n' + command += './Allmesh \n' + command += 'exit \n' + command += 'EOT \n' + command = ssh_prefix + ' << ' + command + + self.consoleMessage("Starting remote meshing...") + try: + CfdTools.runFoamCommand(command) + print("Remote meshing is complete.") + self.consoleMessage("Remote meshing is complete.") + except Exception as error: + self.consoleMessage("Error meshing on remote host: " + str(error)) + def getStandardButtons(self): return int(QtGui.QDialogButtonBox.Close) # def reject() is called on close button @@ -131,20 +364,53 @@ def load(self): (self.mesh_obj.MeshUtility, self.mesh_obj.ElementDimension, self.mesh_obj.ConvertToDualMesh), 0) self.form.cb_utility.setCurrentIndex(index_utility) + #TODO: this should be changed to reflect if the host is local or remote def updateUI(self): + case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir - self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) - self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) - self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) - self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) - self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) - utility = CfdMesh.MESHERS[self.form.cb_utility.currentIndex()] if utility == "snappyHexMesh": self.form.snappySpecificProperties.setVisible(True) else: self.form.snappySpecificProperties.setVisible(False) + # enable the appropriate controls + # this is always an appropriate action + self.form.pb_write_mesh.setEnabled(True) + + #enable these if the mesh is available locally + if self.profile_name == 'local' or self.copy_back : + self.form.pb_copy_to_host.setEnabled(False) + self.form.pb_delete_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + self.form.pb_edit_mesh.setEnabled(os.path.exists(case_path)) + # TODO have to enable this for the case that there is no local meshcase but + # we have written one to a remote host We should be checking if the case is available on + # the remote host, not the local host + #self.form.pb_run_mesh.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + self.form.pb_run_mesh.setEnabled(True) + + # TODO This enables as soon as the mesh case is written. It shouldn't. + self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(case_path, "pv.foam"))) + self.form.pb_load_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + self.form.pb_check_mesh.setEnabled(os.path.exists(os.path.join(case_path, "mesh_outside.stl"))) + # TODO Should check that the mesh is loaded before enabling this. Not working right the way it is + self.form.pb_clear_mesh.setEnabled(True) + + # remote host is being used without copy_back + # no local results to work on so disable these controls + else: + #self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(case_path, "Allmesh"))) + local_dir = CfdTools.getDefaultOutputPath('local') + self.form.pb_copy_to_host.setEnabled(os.path.exists(os.path.join(local_dir, "meshCase"))) + #TODO should check if the mesh is present on the remote host + self.form.pb_delete_mesh.setEnabled(True) + # remote hosts don't support these functions yet + self.form.pb_run_mesh.setEnabled(True) + self.form.pb_paraview.setEnabled(False) + self.form.pb_check_mesh.setEnabled(False) + self.form.pb_load_mesh.setEnabled(False) + self.form.pb_clear_mesh.setEnabled(False) + def store(self): mesher_idx = self.form.cb_utility.currentIndex() storeIfChanged(self.mesh_obj, 'CharacteristicLengthMax', getQuantity(self.form.if_max)) @@ -199,6 +465,10 @@ def writeMesh(self): # Re-initialise CfdMeshTools with new parameters self.store() + #get the host name we are writing the mesh case for + host_profile = self.profile_name + print ("Writing mesh for host profile " + host_profile + ".") + FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") FreeCADGui.doCommand("from CfdOF import CfdTools") FreeCADGui.doCommand("cart_mesh = " @@ -208,7 +478,10 @@ def writeMesh(self): cart_mesh.progressCallback = self.progressCallback # Start writing the mesh files - self.consoleMessage("Preparing meshing ...") + if host_profile == "local": + self.consoleMessage("Preparing local mesh ...") + else: + self.consoleMessage("Preparing remote mesh for " + host_profile + "...") try: QApplication.setOverrideCursor(Qt.WaitCursor) setQuantity(self.form.if_max, str(cart_mesh.getClmax())) @@ -219,7 +492,12 @@ def writeMesh(self): + cart_mesh.part_obj.Label + ', ShapeType: ' + cart_mesh.part_obj.Shape.ShapeType) print(' CharacteristicLengthMax: ' + str(cart_mesh.clmax)) - FreeCADGui.doCommand("cart_mesh.writeMesh()") + + if host_profile == "local": + FreeCADGui.doCommand("cart_mesh.writeMesh('local')") + else: + FreeCADGui.doCommand("cart_mesh.writeMesh('"+ host_profile +"')") + except Exception as ex: self.consoleMessage("Error " + type(ex).__name__ + ": " + str(ex), 'Error') raise @@ -231,6 +509,7 @@ def writeMesh(self): # Update the UI self.updateUI() + def progressCallback(self, message): self.consoleMessage(message) @@ -261,6 +540,8 @@ def checkMeshClicked(self): " proxy.check_mesh_process.waitForFinished()\n" + "else:\n" + " proxy.check_mesh_process.start(cmd, env_vars=env_vars)") + + if self.mesh_obj.Proxy.check_mesh_process.waitForStarted(): self.consoleMessage("Mesh check started ...") else: @@ -278,12 +559,17 @@ def checkMeshClicked(self): finally: QApplication.restoreOverrideCursor() + def editMesh(self): case_path = self.mesh_obj.Proxy.cart_mesh.meshCaseDir self.consoleMessage("Please edit the case input files externally at: {}\n".format(case_path)) CfdTools.openFileManager(case_path) def runMesh(self): + + #disable the run button so a second instance isn't started + self.form.pb_run_mesh.setEnabled(False) + if CfdTools.getFoamRuntime() == "PosixDocker": CfdTools.startDocker() @@ -306,32 +592,97 @@ def runMesh(self): try: QApplication.setOverrideCursor(Qt.WaitCursor) + self.consoleMessage("Initializing {} ...".format(self.mesh_obj.MeshUtility)) FreeCADGui.doCommand("from CfdOF.Mesh import CfdMeshTools") FreeCADGui.doCommand("from CfdOF import CfdTools") FreeCADGui.doCommand("from CfdOF import CfdConsoleProcess") - FreeCADGui.doCommand("cart_mesh = " - " CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") + + FreeCADGui.doCommand("cart_mesh = " + + "CfdMeshTools.CfdMeshTools(FreeCAD.ActiveDocument." + self.mesh_obj.Name + ")") + FreeCADGui.doCommand("proxy = FreeCAD.ActiveDocument." + self.mesh_obj.Name + ".Proxy") FreeCADGui.doCommand("proxy.cart_mesh = cart_mesh") FreeCADGui.doCommand("cart_mesh.error = False") - FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False)") - FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") - FreeCADGui.doCommand("proxy.running_from_macro = True") - self.mesh_obj.Proxy.running_from_macro = False - FreeCADGui.doCommand("if proxy.running_from_macro:\n" + - " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + - " mesh_process.start(cmd, env_vars=env_vars)\n" + - " mesh_process.waitForFinished()\n" + - "else:\n" + - " proxy.mesh_process.start(cmd, env_vars=env_vars)") + + # run locally + if self.profile_name == "local": + FreeCADGui.doCommand("cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False)") + FreeCADGui.doCommand("env_vars = CfdTools.getRunEnvironment()") + + FreeCADGui.doCommand("print('cmd:')") + FreeCADGui.doCommand("print(cmd)") + #FreeCADGui.doCommand("print('env_vars:' + env_vars)") + + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(cmd, env_vars=env_vars)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(cmd, env_vars=env_vars)") + + + # run on remote host + else: + #self.runRemoteMesh() #For testing the non proxy function above + + # Get the username and hostname for the remote host + prefsCmd = "profile_prefs = CfdTools.getPreferencesLocation() + " + '"/Hosts/' + self.profile_name + '"' + #print("prefsCmd:" + prefsCmd) + FreeCADGui.doCommand(prefsCmd) + FreeCADGui.doCommand("print('profile_prefs:' + profile_prefs)") + + FreeCADGui.doCommand("remote_user = FreeCAD.ParamGet(profile_prefs).GetString('Username', '')") + FreeCADGui.doCommand("remote_hostname =FreeCAD.ParamGet(profile_prefs).GetString('Hostname', '')") + + #FreeCADGui.doCommand("print('username:' + remote_user)") + #FreeCADGui.doCommand("print('hostname:' + remote_hostname)") + + # create the ssh connection command + # was ssh -tt + FreeCADGui.doCommand("ssh_prefix = 'ssh -t ' + remote_user + '@' + remote_hostname + ' '") + + # Get the working directory for the mesh + #FreeCADGui.doCommand("working_dir = FreeCAD.ParamGet(profile_prefs).GetString('OutputPath', '')") + FreeCADGui.doCommand("working_dir = CfdTools.getDefaultOutputPath('" + self.profile_name + "' )") + #FreeCADGui.doCommand("print('working directory:' + working_dir)") + + # create the command to do the actual work + FreeCADGui.doCommand("command = 'EOT \\n' \n" + + "command += 'cd ' + working_dir + '/meshCase \\n' \n" + + "command += './Allmesh \\n' \n" + + "command += 'exit \\n' \n" + + "command += 'EOT' \n") + FreeCADGui.doCommand("command = ssh_prefix + ' << ' + command + '\\n'") + + #FreeCADGui.doCommand("print(command)") + + FreeCADGui.doCommand("runCommand = CfdTools.makeRunCommand(command, None)") + + FreeCADGui.doCommand("proxy.running_from_macro = True") + self.mesh_obj.Proxy.running_from_macro = False + + FreeCADGui.doCommand("if proxy.running_from_macro:\n" + + " mesh_process = CfdConsoleProcess.CfdConsoleProcess()\n" + + " mesh_process.start(runCommand)\n" + + " mesh_process.waitForFinished()\n" + + "else:\n" + + " proxy.mesh_process.start(runCommand)") + + if self.mesh_obj.Proxy.mesh_process.waitForStarted(): - self.form.pb_run_mesh.setEnabled(False) # Prevent user running a second instance + # enable/disable the correct buttons self.form.pb_stop_mesh.setEnabled(True) + self.form.pb_run_mesh.setEnabled(False) self.form.pb_write_mesh.setEnabled(False) + self.form.pb_edit_mesh.setEnabled(False) self.form.pb_check_mesh.setEnabled(False) self.form.pb_paraview.setEnabled(False) self.form.pb_load_mesh.setEnabled(False) + self.form.pb_clear_mesh.setEnabled(False) self.consoleMessage("Mesher started ...") else: self.consoleMessage("Error starting meshing process", 'Error') @@ -342,6 +693,7 @@ def runMesh(self): finally: QApplication.restoreOverrideCursor() + def killMeshProcess(self): self.consoleMessage("Meshing manually stopped") self.error_message = 'Meshing interrupted' @@ -359,28 +711,178 @@ def gotErrorLines(self, lines): self.consoleMessage(print_err, 'Error') def meshFinished(self, exit_code): + if self.form.cb_notify.isChecked(): + #print("Beeping now") + QApplication.beep() if exit_code == 0: self.consoleMessage('Meshing completed') self.analysis_obj.NeedsMeshRerun = False - self.form.pb_run_mesh.setEnabled(True) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(True) - self.form.pb_write_mesh.setEnabled(True) - self.form.pb_check_mesh.setEnabled(True) - self.form.pb_load_mesh.setEnabled(True) + + #check if there is work to do on the remote host + if self.profile_name != 'local': + # copy the meshcase back to the workstation? + # this code is also used in reverse in CfdMeshTools.py for copying the mesh case to the server + if self.copy_back: + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + local_output_path = CfdTools.getDefaultOutputPath('local') + + #case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) + # If this ^ is used as the destination dir, it will put the remote meshCase dir in the + # local meshCase directory, which is wrong (/tmp/CfdOF/meshCase/meshCase, for example + + # if we are deleting the mesh case on the server + # don't delete the mesh case. In most cases we need it for the solver to run later + # it will be deleted after the solve if we elect to remove results + + """ + if self.delete_remote_results: + deleteStr = "--remove-source-files " + else: + deleteStr = "" + """ + deleteStr = "" + + # rsync the meshCase result on the server to the workstation's output directory + # Typical useage: rsync -r --delete --remove-source-files me@david/tmp/meshCase /tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + + try: + CfdTools.runFoamCommand("rsync -r --delete " + deleteStr + remote_user + "@" + remote_hostname + ":" + remote_output_path + "/meshCase " + \ + local_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy mesh to local computer: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not copy mesh to local computer: " + str(e)) + else: + CfdTools.cfdMessage("Copied mesh case to " + local_output_path + " on local computer\n" ) + if self.progressCallback: + self.progressCallback("Copied mesh case to " + local_output_path + "on local computer\n") + if self.progressCallback: + self.progressCallback("Mesh case copy back process is complete.") + + # delete the result on the server ? + # if this got deleted then we'd have to remesh every run + # or copy a mesh from the local machine to the server + # not implemented for this reason + if self.delete_remote_results: + pass + else: self.consoleMessage("Meshing exited with error", 'Error') - self.form.pb_run_mesh.setEnabled(True) - self.form.pb_stop_mesh.setEnabled(False) - self.form.pb_write_mesh.setEnabled(True) - self.form.pb_check_mesh.setEnabled(False) - self.form.pb_paraview.setEnabled(False) + # update the controls self.error_message = '' # Get rid of any existing loaded mesh self.pbClearMeshClicked() self.updateUI() + + # Delete the mesh case on the current host, including the local host if it is the current host + # TODO: Add a warning for the user so they can cancel if desired + def deleteMeshcase(self): + # local host + if self.hostname == 'local': + #TODO Check if the meshCase exists + local_output_path = CfdTools.getDefaultOutputPath('local') + print("Local output path:" + local_output_path) + + # create the command to do the actual work + # TODO This won't work on a Windows or Mac machine Fix it + command = "rm -rf " + local_output_path + "/meshCase" + + try: + CfdTools.runFoamCommand(command, "./") + + except Exception as e: + CfdTools.cfdMessage("Could not delete the local mesh case:" + str(e)) + self.consoleMessage("Could not delete the local mesh case:" + str(e)) + else: + CfdTools.cfdMessage("Deleted the mesh case in " + local_output_path + "\n" ) + self.consoleMessage("Deleted the mesh case in " + local_output_path + "\n" ) + # now update the UI + self.updateUI + + # remote host + # TODO uses a combination of looked up parameters and local vars ! Very dangerous. Fix this + # TODO check if mesh case exists + else: + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + #remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + + # create the command to do the actual work + command = 'ssh -tt ' + self.username + '@' + self.hostname + " " # was -tt + command += ' << EOT \n' + command += 'cd ' + remote_output_path + '\n' + command += 'rm -rf meshCase ' + '\n' + command += 'exit \n ' + command += 'EOT \n' + #print("Code command:" + command) + + try: + CfdTools.runFoamCommand(command) + + except Exception as e: + CfdTools.cfdMessage("Could not delete mesh case on remote host:" + str(e)) + self.consoleMessage("Could not delete mesh case on remote host:" + str(e)) + else: + CfdTools.cfdMessage("Deleted mesh case in " + remote_hostname + ":" + remote_output_path + "\n" ) + self.consoleMessage("Deleted mesh case in " + remote_hostname + ":" + remote_output_path + "\n" ) + # now update the UI + self.updateUI + + + + def copyMeshcaseToHost(self): + #check that a host is selected + if self.profile_name == 'local': + self.consoleMessage("Select a host to copy the mesh case to.") + else: + # copy the meshcase back to the workstation + # this code is also used in reverse in CfdMeshTools.py for copying the mesh case to the server + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #local_output_path = FreeCAD.ParamGet(local_prefs).GetString("DefaultOutputPath","") + + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + local_output_path = CfdTools.getDefaultOutputPath('local') + + + #case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) + # If this ^ is used as the destination dir, it will put the remote meshCase dir in the + # local meshCase directory, which is wrong (/tmp/CfdOF/meshCase/meshCase, for example + + # rsync the meshCase result on the server to the workstation's output directory + # Typical useage: rsync -r --delete --remove-source-files me@david/tmp/meshCase /tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + + try: + CfdTools.runFoamCommand("rsync -r --delete " + local_output_path + "/meshCase " + remote_user + "@" + remote_hostname + ":" \ + + remote_output_path) + + except Exception as e: + CfdTools.cfdMessage("Could not copy mesh case to host: " + str(e)) + self.consoleMessage("Could not copy mesh case to host: " + str(e)) + else: + CfdTools.cfdMessage("Copied mesh case to " + remote_hostname + ":" + remote_output_path + "\n" ) + self.consoleMessage("Copied mesh case to " + remote_hostname + ":" + remote_output_path + "\n" ) + + def openParaview(self): QApplication.setOverrideCursor(Qt.WaitCursor) case_path = os.path.abspath(self.mesh_obj.Proxy.cart_mesh.meshCaseDir) diff --git a/CfdOF/Solve/CfdCaseWriterFoam.py b/CfdOF/Solve/CfdCaseWriterFoam.py index 1d1668cf..b70051d8 100644 --- a/CfdOF/Solve/CfdCaseWriterFoam.py +++ b/CfdOF/Solve/CfdCaseWriterFoam.py @@ -24,7 +24,8 @@ import os import os.path -from FreeCAD import Units +#import FreeCAD +from FreeCAD import Units, ParamGet from CfdOF import CfdTools from CfdOF.TemplateBuilder import TemplateBuilder from CfdOF.CfdTools import cfdMessage @@ -51,12 +52,16 @@ def __init__(self, analysis_obj): self.zone_objs = CfdTools.getZoneObjects(analysis_obj) self.dynamic_mesh_refinement_obj = CfdTools.getDynamicMeshAdaptation(self.mesh_obj) self.mesh_generated = False + + # LG123 changed this for testing self.working_dir = CfdTools.getOutputPath(self.analysis_obj) + #self.working_dir = CfdTools.getDefaultOutputPath('local') + self.progressCallback = None self.settings = None - def writeCase(self): + def writeCase(self, profile_name): """ writeCase() will collect case settings, and finally build a runnable case. """ cfdMessage("Writing case to folder {}\n".format(self.working_dir)) if not os.path.exists(self.working_dir): @@ -153,8 +158,44 @@ def writeCase(self): cfdMessage("Successfully wrote case to folder {}\n".format(self.working_dir)) if self.progressCallback: - self.progressCallback("Case written successfully") - + self.progressCallback("Case written locally") + + # if using a remote host, copy the case folder from the local case dir + # to the remote host's directory + if profile_name != "local": + + profile_prefs = CfdTools.getPreferencesLocation() +"/Hosts/" + profile_name + remote_user = ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = ParamGet(profile_prefs).GetString("Hostname", "") + + #remote_output_path = ParamGet(profile_prefs).GetString("OutputPath","") + remote_output_path = CfdTools.getDefaultOutputPath(profile_name) + + #print("remote_user:" + remote_user) + #print("remote_hostname:" + remote_hostname) + #print("remote_output_path:" + remote_output_path) + #print("self.case_folder:" + self.case_folder) + #print("self.working_dir:" + self.working_dir) + + # rsync the meshCase directory to the remote host's output directory + # Typical useage: rsync -r --delete /tmp/ me@david:/tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + # + + try: + CfdTools.runFoamCommand("rsync -r --delete --remove-source-files " + self.case_folder + " " + remote_user + "@" + remote_hostname + \ + ":" + remote_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not move case to remote host: " + str(e)) + if self.progressCallback: + self.progressCallback("Could not move case to remote host: " + str(e)) + return False + else: + CfdTools.cfdMessage("Moved solver case to " + remote_hostname + ":" + remote_output_path + "\n" ) + if self.progressCallback: + self.progressCallback("Moved solver case to " + remote_hostname + ":" + remote_output_path + "\n") + return True def getSolverName(self): diff --git a/CfdOF/Solve/TaskPanelCfdSolverControl.py b/CfdOF/Solve/TaskPanelCfdSolverControl.py index ef0ae544..54760474 100644 --- a/CfdOF/Solve/TaskPanelCfdSolverControl.py +++ b/CfdOF/Solve/TaskPanelCfdSolverControl.py @@ -35,6 +35,28 @@ from PySide.QtCore import Qt from PySide.QtGui import QApplication +# ******************************************************************************************************** +# LinuxGuy123's Notes +# +# TODOs (there are some in the code as well) +# +# - the solver +# +# -makeRunCommand is using the local OF bash command to build the remote run command. It works but it isn't correct. +# The remote run is not calling the source command to set up OF usage. It is relying on the bash shell on the remote host +# to do that, through bashrc and OF working directly from the command line. +# +# -enable and disable buttons appropriately when running and when done +# +# global vars are used for stuff that should be passed via the solver object. +# +# -edit case doesn't copy the case back to the server after the edit is done. +# +# -remote solving has not been tested in macros +# +#- presently does not set the number of threads that the solver uses properly. Must be done manually. Not sure where +# the default is set. It runs with the number of cores in the editor, but that should be set to the preferences value +# when the host is changed. It doesn't. The field name is "ParallelCores" as far as I can tell. class TaskPanelCfdSolverControl: def __init__(self, solver_runner_obj): @@ -61,9 +83,44 @@ def __init__(self, solver_runner_obj): self.working_dir = CfdTools.getOutputPath(self.analysis_object) + #set the prefs and host prefs locations + self.prefs_location = CfdTools.getPreferencesLocation() + self.host_prefs_location = self.prefs_location + "/Hosts" + self.useRemoteProcessing = FreeCAD.ParamGet(self.prefs_location).GetBool('UseRemoteProcessing', 0) + + #setting these here so they get created as globals + #they also get initiated in loadProfile() + # self.use_remote_processing = False <- this is set above before the control is loaded + self.profile_name = "" + self.hostname = "" + self.username = "" + #self.mesh_processes = 0 + #self.mesh_threads = 0 + #self.foam_processes = 0 + #self.foam_threads = 0 + self.foam_dir = "" + self.output_path = "" + #self.gmsh_path = "" + self.add_filename_to_output = False + + #add a local host to cb_profile + self.form.cb_profile.addItem("local") + + # if using remote processing, add the host profiles as well + if self.useRemoteProcessing: + self.loadProfileNames() + else: + #disable cb_profile so that users aren't trying to change the host + self.form.cb_profile.setEnabled(False) + + # load the local profile + self.loadProfile("local") + self.updateUI() # Connect Signals and Slots + # set up the profiles combo box connection + self.form.cb_profile.currentIndexChanged.connect(self.profileChanged) self.form.pb_write_inp.clicked.connect(self.write_input_file_handler) self.form.pb_edit_inp.clicked.connect(self.editSolverInputFile) self.form.pb_run_solver.clicked.connect(self.runSolverProcess) @@ -72,9 +129,102 @@ def __init__(self, solver_runner_obj): self.Start = time.time() self.Timer.start() + # loads the profiles names into the profile combo box + def loadProfileNames(self): + profileDir = self.prefs_location + "/Hosts" + profiles = FreeCAD.ParamGet(profileDir) + profileList = profiles.GetGroups() + for item in profileList: + self.form.cb_profile.addItem(item) + + # load profile parameters into the controls and local vars + def loadProfile(self, profile_name): + + #set the global profile name + self.profile_name = profile_name + + #set the global host prefs location + self.host_prefs_location = self.prefs_location + "/Hosts/" + profile_name + + #set the other global vars + if profile_name == "": + print("Error: no host profile selected") + return + + # set the vars to the local parameters + if profile_name == "local": + self.hostname = "local" + self.output_path = CfdTools.getDefaultOutputPath('local') + # the local code doesn't use these vars, so don't set them + # dangerous. + """ + self.username = "" + self.mesh_processes = 0 + self.mesh_threads = 0 + self.foam_processes = 0 + self.foam_threads = 0 + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = "" + self.add_filename_to_output = False + """ + else: + # set the vars to the remote host parameters + # most of these aren't used, at least not in this page + hostPrefs = self.host_prefs_location + self.hostname = FreeCAD.ParamGet(hostPrefs).GetString("Hostname", "") + self.username = FreeCAD.ParamGet(hostPrefs).GetString("Username", "") + + + #self.mesh_processes = FreeCAD.ParamGet(hostPrefs).GetInt("MeshProcesses") + #self.mesh_threads = FreeCAD.ParamGet(hostPrefs).GetInt("MeshThreads") + #self.foam_processes = FreeCAD.ParamGet(hostPrefs).GetInt("FoamProcesses") + #self.foam_threads = FreeCAD.ParamGet(hostPrefs).GetInt("FoamThreads") + self.foam_dir = FreeCAD.ParamGet(hostPrefs).GetString("FoamDir", "") + + #self.output_path = FreeCAD.ParamGet(hostPrefs).GetString("OutputPath","") + self.output_path = CfdTools.getDefaultOutputPath(self.profile_name) + + # these are used + #self.add_filename_to_output = FreeCAD.ParamGet(hostPrefs).GetBool("AddFilenameToOutput") + self.copy_back = FreeCAD.ParamGet(hostPrefs).GetBool("CopyBack") + self.delete_remote_results = FreeCAD.ParamGet(hostPrefs).GetBool("DeleteRemoteResults") + + + # now set the control values + # leaving these in in case we pass parameters like these to the solver + # object some day. + + #self.mesh_obj.NumberOfProcesses = self.mesh_processes + #self.mesh_obj.NumberOfThreads = self.mesh_threads + + # this gets called when the user changes the profile + def profileChanged(self): + print("The profile was changed") + # change the global profile name + self.profile_name = self.form.cb_profile.currentText() + #load the values for the new profile + print ("New profile is ", self.profile_name) + self.loadProfile(self.profile_name) + # TODO enable and disable the appropriate controls here + # Remote hosts can't edit the case nor Paraview, check mesh, etc. + # Nor load surface mesh nor clear surface mesh. + if self.profile_name == 'local': + self.form.pb_write_inp.enabled = True + else: + pass + def updateUI(self): solverDirectory = os.path.join(self.working_dir, self.solver_object.InputCaseName) - self.form.pb_edit_inp.setEnabled(os.path.exists(solverDirectory)) + + if self.profile_name == 'local': + self.form.pb_edit_inp.setEnabled(os.path.exists(solverDirectory)) + + # TODO: enable local editing of the solver case + else: + self.form.pb_edit_inp.setEnabled(False) + + # TODO: Paraview is enabled even though the solver hasn't been run yet. Fix this ? self.form.pb_paraview.setEnabled(os.path.exists(os.path.join(solverDirectory, "pv.foam"))) self.form.pb_run_solver.setEnabled(os.path.exists(os.path.join(solverDirectory, "Allrun"))) @@ -114,31 +264,35 @@ def write_input_file_handler(self): self.consoleMessage("Case writer called") self.form.pb_paraview.setEnabled(False) self.form.pb_edit_inp.setEnabled(False) - self.form.pb_run_solver.setEnabled(False) + self.form.pb_run_solver.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) try: - FreeCADGui.doCommand("FreeCAD.ActiveDocument." + self.solver_object.Name + ".Proxy.case_writer = " + FreeCADGui.doCommand("FreeCAD.ActiveDocument." + self.solver_object.Name + ".Proxy.case_writer = " "CfdCaseWriterFoam.CfdCaseWriterFoam(FreeCAD.ActiveDocument." + self.solver_runner.analysis.Name + ")") - FreeCADGui.doCommand("writer = FreeCAD.ActiveDocument." + + FreeCADGui.doCommand("writer = FreeCAD.ActiveDocument." + self.solver_object.Name + ".Proxy.case_writer") - writer = self.solver_object.Proxy.case_writer - writer.progressCallback = self.consoleMessage - FreeCADGui.doCommand("writer.writeCase()") + writer = self.solver_object.Proxy.case_writer + writer.progressCallback = self.consoleMessage + FreeCADGui.doCommand("writer.writeCase('" + self.profile_name +"')") + except Exception as e: - self.consoleMessage("Error writing case:", 'Error') - self.consoleMessage(type(e).__name__ + ": " + str(e), 'Error') - self.consoleMessage("Write case setup file failed", 'Error') - raise + self.consoleMessage("Error writing case:", 'Error') + self.consoleMessage(type(e).__name__ + ": " + str(e), 'Error') + self.consoleMessage("Write case setup file failed", 'Error') + raise else: - self.analysis_object.NeedsCaseRewrite = False + self.analysis_object.NeedsCaseRewrite = False finally: - QApplication.restoreOverrideCursor() + QApplication.restoreOverrideCursor() self.updateUI() self.form.pb_run_solver.setEnabled(True) + else: self.consoleMessage("Case check failed", 'Error') + + def check_prerequisites_helper(self): self.consoleMessage("Checking dependencies...") @@ -153,10 +307,11 @@ def editSolverInputFile(self): self.consoleMessage("Please edit the case input files externally at: {}\n".format(case_path)) CfdTools.openFileManager(case_path) - def runSolverProcess(self): + def runSolverProcess(self, profileName): self.Start = time.time() # Check for changes that require remesh + # TODO: This will not run the mesher on a remote host. Fix this ? if FreeCAD.GuiUp and ( self.analysis_object.NeedsMeshRewrite or self.analysis_object.NeedsCaseRewrite or @@ -180,7 +335,7 @@ def runSolverProcess(self): if self.analysis_object.NeedsMeshRewrite or self.analysis_object.NeedsMeshRerun: from CfdOF.Mesh import CfdMeshTools - mesh_obj = CfdTools.getMeshObject(self.analysis_object) + mesh_obj = CfdTools.getMeshObject(self.analysis_object) #TODO: This won't work with remote hosts. Need to pass the host name into the mesh object cart_mesh = CfdMeshTools.CfdMeshTools(mesh_obj) cart_mesh.progressCallback = self.consoleMessage if self.analysis_object.NeedsMeshRewrite: @@ -202,15 +357,49 @@ def runSolverProcess(self): cart_mesh.error = False cmd = CfdTools.makeRunCommand('./Allmesh', cart_mesh.meshCaseDir, source_env=False) env_vars = CfdTools.getRunEnvironment() - self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) - if self.solver_object.Proxy.solver_process.waitForStarted(): - # Setting solve button to inactive to ensure that two instances of the same simulation aren't started - # simultaneously - self.form.pb_write_inp.setEnabled(False) - self.form.pb_run_solver.setEnabled(False) - self.form.terminateSolver.setEnabled(True) - self.consoleMessage("Mesher started ...") + + # run locally + if profileName == 'local': + self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + if self.solver_object.Proxy.solver_process.waitForStarted(): + # Setting solve button to inactive to ensure that two instances of the same simulation aren't started + # simultaneously + self.form.pb_write_inp.setEnabled(False) + self.form.pb_run_solver.setEnabled(False) + self.form.terminateSolver.setEnabled(True) + self.consoleMessage("Mesher started ...") + return + + # run remotely + # not implemented + else: + self.consoleMessage("Meshing from within the solver is not implemented for remote hosts.") + self.consoleMessage("Generate the mesh from the mesh object instead.") return + """ + remote_user = self.username + remote_hostname = self.hostname + + # create the ssh connection command + # use ssh -t, not ssh -tt + ssh_prefix = 'ssh -t' + remote_user + '@' + remote_hostname + ' ' + + # Get the working directory for the mesh + working_dir = self.output_path + #TODO: add filename to the path if selected + + # create the command to do the actual work + command = 'EOT \n' + command += 'cd ' + working_dir + '/meshCase \n' + command += './Allrun \n' + command += 'exit \n' + command += 'EOT \n' + command = ssh_prefix + ' << ' + command + + cmd = CfdTools.makeRunCommand(command,None) + """ + + #Mesh is ready to solve, so run it. else: self.Start = time.time() @@ -222,29 +411,120 @@ def runSolverProcess(self): # This is a workaround to emit code into macro without actually running it FreeCADGui.doCommand("proxy.running_from_macro = True") self.solver_object.Proxy.running_from_macro = False - FreeCADGui.doCommand( - "if proxy.running_from_macro:\n" + - " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + - " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + - " working_dir = CfdTools.getOutputPath(analysis_object)\n" + - " case_name = solver_object.InputCaseName\n" + - " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + - " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + - " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + - " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + - " env_vars = solver_runner.getRunEnvironment()\n" + - " solver_process = CfdConsoleProcess.CfdConsoleProcess(stdout_hook=solver_runner.process_output)\n" + - " solver_process.start(cmd, env_vars=env_vars)\n" + - " solver_process.waitForFinished()\n") - working_dir = CfdTools.getOutputPath(self.analysis_object) - case_name = self.solver_object.InputCaseName - solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) - cmd = self.solver_runner.get_solver_cmd(solver_directory) - env_vars = self.solver_runner.getRunEnvironment() - self.solver_object.Proxy.solver_process = CfdConsoleProcess(finished_hook=self.solverFinished, - stdout_hook=self.gotOutputLines, - stderr_hook=self.gotErrorLines) - self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + + # if running on local host + if self.profile_name == "local": + # This must be kept in one doCommand because of the if statement + # TODO: check that the doCommand code is the same as the inline code + FreeCADGui.doCommand( + "if proxy.running_from_macro:\n" + + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + + " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + + + " working_dir = CfdTools.getOutputPath(analysis_object)\n" + + " case_name = solver_object.InputCaseName\n" + + " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + + " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + + " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + + " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + " env_vars = solver_runner.getRunEnvironment()\n" + + " solver_process = CfdConsoleProcess.CfdConsoleProcess(stdout_hook=solver_runner.process_output)\n" + + " solver_process.start(cmd,env_vars= env_vars)\n" + + " solver_process.waitForFinished()") + + working_dir = CfdTools.getOutputPath(self.analysis_object) + case_name = self.solver_object.InputCaseName + solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) + cmd = self.solver_runner.get_solver_cmd(solver_directory) + env_vars = self.solver_runner.getRunEnvironment() + self.solver_object.Proxy.solver_process = CfdConsoleProcess(finished_hook=self.solverFinished, + stdout_hook=self.gotOutputLines, + stderr_hook=self.gotErrorLines) + self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + + # running remotely + else: + + # This must be kept in one doCommand because of the if statement + # The only difference between this command and the local command is " cmd = CfdTools.makeRunCommand('" + command + "',None)" + # TODO: Test the macro code. + FreeCADGui.doCommand( + "if proxy.running_from_macro:\n" + + " analysis_object = FreeCAD.ActiveDocument." + self.analysis_object.Name + "\n" + + " solver_object = FreeCAD.ActiveDocument." + self.solver_object.Name + "\n" + + + # TODO: this might be wrong + " working_dir = CfdTools.getOutputPath(analysis_object)\n" + + " case_name = solver_object.InputCaseName\n" + + " solver_directory = os.path.abspath(os.path.join(working_dir, case_name))\n" + + " from CfdOF.Solve.CfdRunnableFoam import CfdRunnableFoam\n" + + " solver_runner = CfdRunnableFoam.CfdRunnableFoam(analysis_object, solver_object)\n" + + + # create the command to do the actual work + # was ssh -tt but then the shell wouldn't exit + " ssh_prefix = 'ssh -t ' + '" + self.username + "'+ '@' +'" + self.hostname + "'\n" + + " #command = 'EOT \\n' \n" + + " command = 'cd ' + '" + self.working_dir + "' + '/case \\n' \n" + + " command += './Allrun \\n' \n" + + " command += 'exit \\n' \n" + + " command += 'EOT' \n" + + " command = ssh_prefix + ' << ' + command + '\\n' \n" + + " print('doCommand command:' + command) \n" + + " cmd = CfdTools.makeRunCommand(command,None)\n" + # was " cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + " print('doCommand cmd:')\n" + + " print(cmd)\n" + + " env_vars = solver_runner.getRunEnvironment()\n" + + " solver_process = CfdConsoleProcess.CfdConsoleProcess(stdout_hook=solver_runner.process_output)\n" + + " solver_process.start(cmd,env_vars= env_vars)\n" + + " solver_process.waitForFinished()") + + + """ + # This was used for testing + # create the command to do the actual work + ssh_prefix = 'ssh -tt ' + self.username + '@' + self.hostname + command = 'EOT \n' + command += 'cd ' + self.working_dir + '/case \n' + command += './Allrun \n' + command += 'exit \n ' + command += 'EOT \n' + command = ssh_prefix + ' << ' + command + ' \n' + print("Code command:" + command) + cmd = CfdTools.makeRunCommand(command,None) # was cmd = solver_runner.get_solver_cmd(solver_directory)\n" + + print("Code cmd:") + print(cmd) + """ + + # create the command to do the actual work + remote_working_dir = CfdTools.getDefaultOutputPath(self.profile_name) + + command = 'ssh -t ' + self.username + '@' + self.hostname # was -tt + command += '<< EOT \n' + command += ' cd ' + remote_working_dir + '/case \n' + command += './Allrun \n' + command += 'exit \n ' + command += 'EOT \n' + print("Code command:" + command) + + #cmd = ['bash', '-c', command] + #print("Code cmd:") + #print(cmd) + + cmd = CfdTools.makeRunCommand(command,None) + + # TODO not sure this is correct anymore + working_dir = CfdTools.getOutputPath(self.analysis_object) + case_name = self.solver_object.InputCaseName + solver_directory = os.path.abspath(os.path.join(working_dir, case_name)) + + + #cmd = self.solver_runner.get_solver_cmd(solver_directory) + env_vars = self.solver_runner.getRunEnvironment() + self.solver_object.Proxy.solver_process = CfdConsoleProcess(finished_hook=self.solverFinished, + stdout_hook=self.gotOutputLines, + stderr_hook=self.gotErrorLines) + self.solver_object.Proxy.solver_process.start(cmd, env_vars=env_vars) + if self.solver_object.Proxy.solver_process.waitForStarted(): # Setting solve button to inactive to ensure that two instances of the same simulation aren't started # simultaneously @@ -257,14 +537,66 @@ def runSolverProcess(self): self.consoleMessage("Error starting solver", 'Error') QApplication.restoreOverrideCursor() + def killSolverProcess(self): self.consoleMessage("Solver manually stopped") self.solver_object.Proxy.solver_process.terminate() + # TODO: kill the process on the server if we are running locally # Note: solverFinished will still be called def solverFinished(self, exit_code): + if self.form.cb_notify.isChecked(): + #print("Beeping now") + QApplication.beep() if exit_code == 0: - self.consoleMessage("Simulation finished successfully") + self.consoleMessage("Simulation finished") + + #check if there is work to do on the remote host + if self.profile_name != 'local': + # copy the solver case back to the workstation? + # this code is also used in TaskPanelCfdMesh.py for copying the mesh case to the workstation + if self.copy_back: + local_prefs = CfdTools.getPreferencesLocation() + profile_prefs = local_prefs +"/Hosts/" + self.profile_name + + remote_user = FreeCAD.ParamGet(profile_prefs).GetString("Username", "") + remote_hostname = FreeCAD.ParamGet(profile_prefs).GetString("Hostname", "") + #remote_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + #local_output_path = FreeCAD.ParamGet(profile_prefs).GetString("OutputPath","") + + remote_output_path = CfdTools.getDefaultOutputPath(self.profile_name) + local_output_path = CfdTools.getDefaultOutputPath('local') + + # if we are deleting the solver and mesh case on the server + # if we delete the mesh case we'll need to remesh before running the solver + + if self.delete_remote_results: + deleteStr = "--remove-source-files " + else: + deleteStr = "" + + # rsync the solver case result on the server to the workstation's output directory + # Typical useage: rsync -r --delete --remove-source-files me@david/tmp/case /tmp + # --remove-source-files removes the files that get transfered + # --delete removes files from the destination that didn't get transfered + + try: + CfdTools.runFoamCommand("rsync -r --delete " + deleteStr + remote_user + "@" + remote_hostname + ":" + remote_output_path + "/case " + \ + local_output_path) + except Exception as e: + CfdTools.cfdMessage("Could not copy solver case back to local computer: " + str(e)) + self.consoleMessage("Could not copy solver case back to local computer: " + str(e)) + + else: + CfdTools.cfdMessage("Copied solver case to " + local_output_path + "\n" ) + self.consoleMessage("Copied solver case to " + local_output_path + "\n" ) + + # the mesh case is still on the server + # delete the mesh case result on the server ? + # for now we'll leave it there + if self.delete_remote_results: + pass + else: self.consoleMessage("Simulation exited with error", 'Error') self.solver_runner.solverFinished() diff --git a/Gui/CfdPreferencePage.ui b/Gui/CfdPreferencePage.ui index 3ecc267e..a62c6ee7 100644 --- a/Gui/CfdPreferencePage.ui +++ b/Gui/CfdPreferencePage.ui @@ -7,7 +7,7 @@ 0 0 560 - 909 + 1158 @@ -34,6 +34,19 @@ 6 + + + + true + + + The directory to which case folders are written. Used unless overridden on a per-analysis basis. + + + false + + + @@ -47,21 +60,21 @@ - - - - true - - - The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + + + 75 + true + - - false + + ParaView executable path - - + + true @@ -70,8 +83,8 @@ - - + + 75 @@ -79,12 +92,22 @@ - Default output directory + gmsh executable path - - + + + + true + + + ... + + + + + 75 @@ -92,7 +115,7 @@ - ParaView executable path + Default output directory @@ -109,11 +132,8 @@ - - - - true - + + ... @@ -129,39 +149,26 @@ - - + + true - The directory to which case folders are written. Used unless overridden on a per-analysis basis. + The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. false - - - - - 75 - true - - - - gmsh executable path - - - - - + + - ... + Add filename to output path ? @@ -417,14 +424,14 @@ Docker Container - + Use docker: - - - + + + @@ -446,7 +453,7 @@ - + diff --git a/Gui/CfdRemotePreferencePage.ui b/Gui/CfdRemotePreferencePage.ui new file mode 100644 index 00000000..f3aa1f28 --- /dev/null +++ b/Gui/CfdRemotePreferencePage.ui @@ -0,0 +1,870 @@ + + + CfdRemotePreferencePage + + + + 0 + 0 + 488 + 1531 + + + + Remote Processing + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 12 + 75 + true + + + + + + + 1 + + + Use Remote Processing + + + + + + + + + + 1 + + + About Remote Processing + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + false + + + + + + + + 0 + 0 + + + + Add Profile + + + + + + + + 0 + 0 + + + + Delete Profile + + + + + + + + 75 + true + + + + Host profile: + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + + 75 + true + + + + Username on remote host: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Remote hostname or IP address: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Ping Remote Host + + + + + + + Test SSH + + + + + + + + + 8 + + + 8 + + + 8 + + + 0 + + + + + + 75 + true + + + + Meshing + + + Qt::AlignCenter + + + + + + + + 75 + true + + + + OpenFOAM + + + Qt::AlignCenter + + + + + + + + + 8 + + + 0 + + + 8 + + + 8 + + + + + Processes + + + + + + + Threads + + + + + + + + 0 + 0 + + + + + + + + Processes + + + + + + + Threads + + + + + + + + + + + + + + + + + + 8 + + + 8 + + + 8 + + + 8 + + + 14 + + + 6 + + + + + ... + + + + + + + Add filename to output path ? + + + + + + + true + + + The directory to which case folders are written. Used unless overridden on a per-analysis basis. + + + false + + + + + + + + 75 + true + + + + Remote gmsh executable path + + + + + + + true + + + ... + + + + + + + + 75 + true + + + + Remote ParaView executable path + + + + + + + true + + + The full path of the ParaView executable. Leave blank to use search path. + + + false + + + + + + + Copy mesh and solver results back to workstation ? + + + + + + + true + + + ... + + + + + + + true + + + The OpenFOAM install folder (e.g. 'OpenFOAM-xxx'). Leave blank to use $WM_PROJECT_DIR environment setting or search standard locations. + + + false + + + + + + + true + + + ... + + + + + + + + + + + 75 + true + + + + Remote OpenFOAM executable directory + + + + + + + + 75 + true + + + + Remote output path + + + + + + + Delete mesh and server results on remote host ? + + + + + + + + + 8 + + + 8 + + + 8 + + + 6 + + + 14 + + + 8 + + + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Remote Software dependencies + + + + + + + true + + + Run remote host dependency checker + + + + + + + + + + OpenFOAM + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + Install OpenFOAM + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + + + + + + ParaView + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + Install ParaView + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + + + + + + cfMesh + + + + + + + 0 + 0 + + + + URL: + + + + + + + Install cfMesh on remote host + + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + + + + + + + HiSA + + + + + + Install HiSA + + + + + + + + 0 + 0 + + + + URL: + + + + + + + + 0 + 0 + + + + Choose existing file ... + + + + + + + + + + + + + Docker Container + + + + + + Use docker: + + + + + + + + + + Download from URL: + + + + + + + + + + Install Docker Container + + + + + + + + + + + + 8 + + + 5 + + + 8 + + + 8 + + + + + + 75 + true + + + + Output + + + + + + + + 0 + 0 + + + + + 0 + 120 + + + + QTextEdit::NoWrap + + + + + + + + + le_foam_dir + tb_choose_remote_foam_dir + le_paraview_path + tb_choose_paraview_path + le_gmsh_path + tb_choose_remote_gmsh_path + le_output_path + tb_choose_remote_output_dir + pb_run_dependency_checker + pb_download_install_openfoam + le_openfoam_url + tb_pick_openfoam_file + le_paraview_url + pb_download_install_paraview + tb_pick_paraview_file + le_cfmesh_url + pb_download_install_cfMesh + tb_pick_cfmesh_file + le_hisa_url + pb_download_install_hisa + tb_pick_hisa_file + + + + diff --git a/Gui/Icons/preferences-cfdof.svg b/Gui/Icons/preferences-cfdof.svg index 1f471823..b0298dda 100644 --- a/Gui/Icons/preferences-cfdof.svg +++ b/Gui/Icons/preferences-cfdof.svg @@ -1,62 +1,23 @@ - - - - - - image/svg+xml - - - - - - - - + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/Gui/TaskPanelCfdMesh.ui b/Gui/TaskPanelCfdMesh.ui index 3a45b05c..c0eecaef 100644 --- a/Gui/TaskPanelCfdMesh.ui +++ b/Gui/TaskPanelCfdMesh.ui @@ -6,8 +6,8 @@ 0 0 - 510 - 856 + 512 + 1127 @@ -47,26 +47,6 @@ - - - - - 75 - true - - - - Mesh Parameters - - - - - - - Mesh utility: - - - @@ -112,6 +92,26 @@ + + + + Mesh utility: + + + + + + + + 75 + true + + + + Mesh Parameters + + + @@ -132,6 +132,42 @@ 0 + + + + Search + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 50 + false + + + + Point in mesh + + + @@ -156,37 +192,6 @@ 0 - - - - Edge detection - - - - - - - Implicit - - - - - - - Explicit - - - - - - - 1 - - - 100 - - - @@ -217,45 +222,40 @@ + + + + Edge detection + + + + + + + 1 + + + 100 + + + + + + + Implicit + + + + + + + Explicit + + + - - - - - 50 - false - - - - Point in mesh - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 5 - - - - - - - - Search - - - @@ -380,7 +380,52 @@ - + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + 0 + 0 + + + + + 75 + true + + + + Host + + + + + + + + 0 + 0 + + + + + + + + 8 @@ -396,44 +441,52 @@ 8 - - + + - Stop + Delete mesh case on host - - + + - Run mesher + Copy local mesh case to host - - - - - 75 - true - - + + - Meshing + Run mesh case - + Write mesh case - + - Edit + Edit mesh case + + + + + + + Stop + + + + + + + Notify when Run mesh is complete @@ -462,7 +515,7 @@ - Clear + Clear surface mesh diff --git a/Gui/TaskPanelCfdSolverControl.ui b/Gui/TaskPanelCfdSolverControl.ui index 1f003858..889a357b 100644 --- a/Gui/TaskPanelCfdSolverControl.ui +++ b/Gui/TaskPanelCfdSolverControl.ui @@ -6,8 +6,8 @@ 0 0 - 366 - 563 + 612 + 1067 @@ -15,29 +15,20 @@ - - - 8 - - - 8 - - - 8 + + + + 75 + true + - - 8 - - - 14 - - - 6 + + OpenFOAM Solver - + - + 8 @@ -48,33 +39,22 @@ 8 - 6 + 8 14 - 8 + 6 - - - - Write - - - - - - - false - - - Edit - - - - + + + + 0 + 0 + + 75 @@ -82,14 +62,33 @@ - Case setup + Host + + + + + + + + 0 + 0 + + + + false + + + + + + -1 - + 8 @@ -108,94 +107,47 @@ 8 - - - - false - + + - Stop + Write OF case - - + + - false + true - Run - - - - - - - - 75 - true - - - - Solver + Stop - - - - - - 8 - - - 8 - - - 8 - - - 6 - - - 14 - - - 8 - - - + + - false + true - Paraview + Edit - - - - - 75 - true - + + + + true - + Run OF case - - - - - 75 - true - - + + - Results + Notify when Run OF case is done @@ -215,6 +167,64 @@ 8 + + + + 8 + + + 8 + + + 8 + + + 6 + + + 14 + + + 8 + + + + + + 75 + true + + + + Results + + + + + + + + 75 + true + + + + + + + + + + + true + + + Paraview + + + + + diff --git a/InitGui.py b/InitGui.py index e5a69d63..28f97b29 100644 --- a/InitGui.py +++ b/InitGui.py @@ -31,6 +31,7 @@ def __init__(self): from CfdOF import CfdTools from PySide import QtCore from CfdOF.CfdPreferencePage import CfdPreferencePage + from CfdOF.CfdRemotePreferencePage import CfdRemotePreferencePage icon_path = os.path.join(CfdTools.getModulePath(), "Gui", "Icons", "cfd.svg") self.__class__.Icon = icon_path @@ -40,6 +41,7 @@ def __init__(self): icons_path = os.path.join(CfdTools.getModulePath(), "Gui", "Icons") QtCore.QDir.addSearchPath("icons", icons_path) FreeCADGui.addPreferencePage(CfdPreferencePage, "CfdOF") + FreeCADGui.addPreferencePage(CfdRemotePreferencePage, "CfdOF") def Initialize(self): # Must import QtCore in this function, not at the beginning of this file for translation support diff --git a/TestCfdOF.py b/TestCfdOF.py old mode 100644 new mode 100755