diff --git a/doc/source/getting_started/cli.rst b/doc/source/getting_started/cli.rst new file mode 100644 index 0000000000..d3b1a5208e --- /dev/null +++ b/doc/source/getting_started/cli.rst @@ -0,0 +1,276 @@ + +.. _ref_cli: + +============================== +PyMAPDL command line interface +============================== + +For your convenience, PyMAPDL package includes a command line interface +which allows you to launch, stop and list local MAPDL instances. + + +Launch MAPDL instances +====================== + +To start MAPDL, just type on your activated virtual environment: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl + Success: Launched an MAPDL instance (PID=23644) at 127.0.0.1:50052 + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl + Success: Launched an MAPDL instance (PID=23644) at 127.0.0.1:50052 + +If you want to specify an argument, for instance the port, then you need to call +`launch_mapdl start`: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl start --port 50054 + Success: Launched an MAPDL instance (PID=18238) at 127.0.0.1:50054 + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl start --port 50054 + Success: Launched an MAPDL instance (PID=18238) at 127.0.0.1:50054 + + +This command `launch_mapdl start` aims to replicate the function +:func:`ansys.mapdl.core.launcher.launch_mapdl`, hence you can use +some of the arguments which this function allows. +For instance, you could specify the working directory: + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl start --run_location C:\Users\user\temp\ + Success: Launched an MAPDL instance (PID=32612) at 127.0.0.1:50052 + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl start --run_location /home/user/tmp + Success: Launched an MAPDL instance (PID=32612) at 127.0.0.1:50052 + + +For more information see :func:`ansys.mapdl.core.launcher.launch_mapdl`, +and :func:`ansys.mapdl.core.cli.launch_mapdl` + + +Stop MAPDL instances +==================== +MAPDL instances can be stopped by using `launch_mapdl stop` command in the following +way: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl stop + Success: Ansys instances running on port 50052 have been stopped. + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl stop + Success: Ansys instances running on port 50052 have been stopped. + + +By default, the instance running on the port `50052` is stopped. + +You can specify the instance running on a different port using `--port` argument: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl stop --port 50053 + Success: Ansys instances running on port 50053 have been stopped. + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl stop --port 50053 + Success: Ansys instances running on port 50053 have been stopped. + + +Or an instance with a given process id (PID): + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl stop --pid 40952 + Success: The process with PID 40952 and its children have been stopped. + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl stop --pid 40952 + Success: The process with PID 40952 and its children have been stopped. + + +Alternatively, you can stop all the running instances by using: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl stop --all + Success: Ansys instances have been stopped. + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl stop --all + Success: Ansys instances have been stopped. + + +List MAPDL instances and processes +================================== + +You can also list MAPDL instances and processes. +If you want to list MAPDL process, just use the following command: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl list + Name Is Instance Status gRPC port PID + ------------ ------------- -------- ----------- ----- + ANSYS.exe False running 50052 35360 + ANSYS.exe False running 50052 37116 + ANSYS222.exe True running 50052 41644 + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl list + Name Is Instance Status gRPC port PID + ------------ ------------- -------- ----------- ----- + ANSYS.exe False running 50052 35360 + ANSYS.exe False running 50052 37116 + ANSYS222.exe True running 50052 41644 + + +If you want, to just list the instances (avoiding listing children MAPDL +processes), just type: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl list -i + Name Status gRPC port PID + ------------ -------- ----------- ----- + ANSYS222.exe running 50052 41644 + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl list -i + Name Status gRPC port PID + ------------ -------- ----------- ----- + ANSYS222.exe running 50052 41644 + + +You can also print other fields like the working directory (using `--cwd`) +or the command line (using `-c`). +Additionally, you can also print all the available information by using the +argument `--long` or `-l`: + + +.. tab-set:: + + .. tab-item:: Windows + :sync: key1 + + .. code:: pwsh-session + + (.venv) PS C:\Users\user\pymapdl> launch_mapdl list -l + Name Is Instance Status gRPC port PID Command line Working directory + ------------ ------------- -------- ----------- ----- -------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------- + ANSYS.exe False running 50052 35360 C:\Program Files\ANSYS Inc\v222\ANSYS\bin\winx64\ANSYS.EXE -j file -b -i .__tmp__.inp -o .__tmp__.out -port 50052 -grpc C:\Users\User\AppData\Local\Temp\ansys_ahmfaliakp + ANSYS.exe False running 50052 37116 C:\Program Files\ANSYS Inc\v222\ANSYS\bin\winx64\ANSYS.EXE -j file -b -i .__tmp__.inp -o .__tmp__.out -port 50052 -grpc C:\Users\User\AppData\Local\Temp\ansys_ahmfaliakp + ANSYS222.exe True running 50052 41644 C:\Program Files\ANSYS Inc\v222\ansys\bin\winx64\ansys222.exe -j file -np 2 -b -i .__tmp__.inp -o .__tmp__.out -port 50052 -grpc C:\Users\User\AppData\Local\Temp\ansys_ahmfaliakp + + .. tab-item:: Linux + :sync: key1 + + .. code:: console + + (.venv) user@machine:~$ launch_mapdl list -l + Name Is Instance Status gRPC port PID Command line Working directory + ------------ ------------- -------- ----------- ----- ------------------------------------------------------------------------- -------------------------------- + ANSYS False running 50052 35360 /ansys_inc/v222/ansys/bin/linx64/ansys -j file -port 50052 -grpc /home/user/temp/ansys_ahmfaliakp + ANSYS False running 50052 37116 /ansys_inc/v222/ansys/bin/linx64/ansys -j file -port 50052 -grpc /home/user/temp/ansys_ahmfaliakp + ANSYS222 True running 50052 41644 /ansys_inc/v222/ansys/bin/linx64/ansys222 -j file -np 2 -port 50052 -grpc /home/user/temp/ansys_ahmfaliakp + + +The converter module has its own command line interface to convert +MAPDL files to PyMAPDL. For more information, see +:ref:`ref_cli_converter`. \ No newline at end of file diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst index 852964fa69..211bd3bf83 100644 --- a/doc/source/getting_started/index.rst +++ b/doc/source/getting_started/index.rst @@ -12,6 +12,7 @@ codespaces faq versioning + cli docker macos wsl diff --git a/doc/source/getting_started/launcher.rst b/doc/source/getting_started/launcher.rst index df173b2433..0d8ae78403 100644 --- a/doc/source/getting_started/launcher.rst +++ b/doc/source/getting_started/launcher.rst @@ -90,6 +90,9 @@ port 50005 with this command: /usr/ansys_inc/v241/ansys/bin/ansys211 -port 50005 -grpc +From version v0.68, you can use a command line interface to launch, stop and list +local MAPDL instances. +For more information, see :ref:`ref_cli`. .. _connect_grpc_madpl_session: diff --git a/doc/source/user_guide/convert.rst b/doc/source/user_guide/convert.rst index d7d34a050e..b0155e2924 100644 --- a/doc/source/user_guide/convert.rst +++ b/doc/source/user_guide/convert.rst @@ -8,6 +8,8 @@ would take place within Python because APDL commands are less transparent and more difficult to debug. +.. _ref_cli_converter: + Command-line interface ---------------------- diff --git a/pyproject.toml b/pyproject.toml index 20505324cd..15fa2e3037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "ansys-platform-instancemanagement~=1.0", "platformdirs>=3.6.0", "click>=8.1.3", # for CLI interface + "tabulate>=0.8.0", # for cli plotting "grpcio>=1.30.0", # tested up to grpcio==1.35 "importlib-metadata>=4.0", "matplotlib>=3.0.0", # for colormaps for pyvista @@ -112,7 +113,8 @@ name = "ansys.mapdl.core" Source = "https://github.com/ansys/pymapdl" [project.scripts] -pymapdl_convert_script = "ansys.mapdl.core.convert:cli" +pymapdl_convert_script = "ansys.mapdl.core.cli:convert" +launch_mapdl = "ansys.mapdl.core.cli:launch_mapdl" [tool.pytest.ini_options] junit_family = "legacy" diff --git a/src/ansys/mapdl/core/cli.py b/src/ansys/mapdl/core/cli.py new file mode 100644 index 0000000000..04ae369e25 --- /dev/null +++ b/src/ansys/mapdl/core/cli.py @@ -0,0 +1,691 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +import psutil + +try: + import click + from tabulate import tabulate + + _HAS_CLICK = True +except ModuleNotFoundError: + _HAS_CLICK = False + +if _HAS_CLICK: + ################################### + # Convert CLI + + @click.command() + @click.argument("filename_in") + @click.option("-o", default=None, help="Name of the output Python script.") + @click.option( + "--filename_out", default=None, help="Name of the output Python script." + ) + @click.option( + "--loglevel", + default="WARNING", + help="Logging level of the ansys object within the script.", + ) + @click.option( + "--auto_exit", + default=True, + help="Adds a line to the end of the script to exit MAPDL. Default ``True``", + ) + @click.option( + "--line_ending", default=None, help="When None, automatically is ``\n.``" + ) + @click.option( + "--exec_file", + default=None, + help="Specify the location of the ANSYS executable and include it in the converter output ``launch_mapdl`` call.", + ) + @click.option( + "--macros_as_functions", + default=True, + help="Attempt to convert MAPDL macros to python functions.", + ) + @click.option( + "--use_function_names", + default=True, + help="Convert MAPDL functions to ansys.mapdl.core.Mapdl class methods. When ``True``, the MAPDL command ``K`` will be converted to ``mapdl.k``. When ``False``, it will be converted to ``mapdl.run('k')``.", + ) + @click.option( + "--show_log", + default=False, + help="Print the converted commands using a logger (from ``logging`` Python module).", + ) + @click.option( + "--add_imports", + default=True, + help='If ``True``, add the lines ``from ansys.mapdl.core import launch_mapdl`` and ``mapdl = launch_mapdl(loglevel="WARNING")`` to the beginning of the output file. This option is useful if you are planning to use the output script from another mapdl session. See examples section. This option overrides ``auto_exit``.', + ) + @click.option( + "--comment_solve", + default=False, + help='If ``True``, it will pythonically comment the lines that contain ``"SOLVE"`` or ``"/EOF"``.', + ) + @click.option( + "--cleanup_output", + default=True, + help="If ``True`` the output is formatted using ``autopep8`` before writing the file or returning the string. This requires ``autopep8`` to be installed.", + ) + @click.option( + "--header", + default=True, + help="If ``True``, the default header is written in the first line of the output. If a string is provided, this string will be used as header.", + ) + @click.option( + "--print_com", + default=True, + help="Print command ``/COM`` arguments to python console. Defaults to ``True``.", + ) + def convert( + filename_in, + o, + filename_out, + loglevel, + auto_exit, + line_ending, + exec_file, + macros_as_functions, + use_function_names, + show_log, + add_imports, + comment_solve, + cleanup_output, + header, + print_com, + ): + """PyMAPDL CLI tool for converting MAPDL scripts to PyMAPDL scripts. + + USAGE: + + This example demonstrates the main use of this tool: + + $ pymapdl_convert_script mapdl.dat -o python.py + + File mapdl.dat successfully converted to python.py. + + The output argument is optional, in which case the "py" extension is used: + + $ pymapdl_convert_script mapdl.dat + + File mapdl.dat successfully converted to mapdl.py. + + You can use any option from ``ansys.mapdl.core.convert.convert_script`` function: + + $ pymapdl_convert_script mapdl.dat --auto-exit False + + File mapdl.dat successfully converted to mapdl.py. + + $ pymapdl_convert_script.exe mapdl.dat --filename_out mapdl.out --add_imports False + + File mapdl.dat successfully converted to mapdl.out. + + + """ + from ansys.mapdl.core.convert import convert_script + + if o: + filename_out = o + + convert_script( + filename_in, + filename_out, + loglevel, + auto_exit, + line_ending, + exec_file, + macros_as_functions, + use_function_names, + show_log, + add_imports, + comment_solve, + cleanup_output, + header, + print_com, + ) + + if filename_out: + print(f"File {filename_in} successfully converted to {filename_out}.") + else: + print( + f"File {filename_in} successfully converted to {os.path.splitext(filename_in)[0] + '.py'}." + ) + + def is_ansys_process(proc): + return ( + "ansys" in proc.name().lower() or "mapdl" in proc.name().lower() + ) and "-grpc" in proc.cmdline() + + class MyGroup(click.Group): + def invoke(self, ctx): + ctx.obj = tuple(ctx.args) + super(MyGroup, self).invoke(ctx) + + @click.group(invoke_without_command=True, cls=MyGroup) + @click.pass_context + def launch_mapdl(ctx): + args = ctx.obj + if ctx.invoked_subcommand is None: + from ansys.mapdl.core.cli import start + + start(args) + + @launch_mapdl.command( + short_help="Launch MAPDL instances.", + help="""This command aims to replicate the behavior of :func:`ansys.mapdl.core.launcher.launch_mapdl` + +For more information see :func:`ansys.mapdl.core.launcher.launch_mapdl`.""", + ) + @click.option( + "--exec_file", + default=None, + type=str, + help="The location of the MAPDL executable. Will use the cached location when left at the default ``None`` and no environment variable is set. The executable path can be also set through the environment variable ``PYMAPDL_MAPDL_EXEC``.", + ) + @click.option( + "--run_location", + default=None, + type=str, + help="MAPDL working directory. Defaults to a temporary working directory. If directory doesn't exist, one is created.", + ) + @click.option( + "--jobname", + default="file", + type=str, + help="MAPDL jobname. Defaults to ``'file'``.", + ) + @click.option( + "--nproc", type=int, default=2, help="Number of processors. Defaults to 2." + ) + @click.option( + "--ram", + default=None, + type=int, + help="Fixed amount of memory to request for MAPDL. If ``None``, then MAPDL will use as much as available on the host machine.", + ) + @click.option( + "--mode", + type=str, + default=None, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--override", + default=False, + type=bool, + help="Attempts to delete the lock file at the ``run_location``. Useful when a prior MAPDL session has exited prematurely and the lock file has not been deleted.", + ) + @click.option( + "--loglevel", + default="", + type=str, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--additional_switches", + default="", + type=str, + help="Additional switches for MAPDL, for example ``'aa_r'``, the academic research license. Avoid adding switches like ``-i``, ``-o`` or ``-b`` as these are already included to start up the MAPDL server.", + ) + @click.option( + "--start_timeout", + default=45, + type=int, + help="Maximum allowable time to connect to the MAPDL server.", + ) + @click.option( + "--port", + default=None, + type=int, + help="Port to launch MAPDL gRPC on. Final port will be the first port available after (or including) this port. Defaults to 50052. You can also override the port default with the environment variable ``PYMAPDL_PORT=`` This argument has priority over the environment variable.", + ) + @click.option( + "--cleanup_on_exit", + default=False, + type=bool, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--start_instance", + default=None, + type=bool, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--ip", + default=None, + type=str, + help="Argument not allowed in CLI. It will be ignored", + ) + @click.option( + "--clear_on_connect", + default=False, + type=bool, + help="Argument not allowed in CLI. It will be ignored", + ) + @click.option( + "--log_apdl", + default=None, + type=str, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--remove_temp_files", + default=None, + type=str, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--remove_temp_dir_on_exit", + default=False, + type=bool, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--verbose_mapdl", + default=None, + type=str, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--license_server_check", + default=False, + type=bool, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--license_type", + default=None, + type=str, + help="Enable license type selection. You can input a string for its license name (for example ``'meba'`` or ``'ansys'``) or its description ('enterprise solver' or 'enterprise' respectively). You can also use legacy licenses (for example ``'aa_t_a'``) but it will also raise a warning. If it is not used (``None``), no specific license will be requested, being up to the license server to provide a specific license type. Default is ``None``.", + ) + @click.option( + "--print_com", + default=False, + type=bool, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--add_env_vars", + default=None, + type=str, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--replace_env_vars", + default=None, + type=str, + help="Argument not allowed in CLI. It will be ignored.", + ) + @click.option( + "--version", + default=None, + type=str, + help="Version of MAPDL to launch. If ``None``, the latest version is used. Versions can be provided as integers (i.e. ``version=222``) or floats (i.e. ``version=22.2``). To retrieve the available installed versions, use the function :meth:`ansys.tools.path.path.get_available_ansys_installations`.", + ) + def start( + exec_file, + run_location, + jobname, + nproc, + ram, + mode, # ignored + override, + loglevel, # ignored + additional_switches, + start_timeout, + port, + cleanup_on_exit, # ignored + start_instance, # ignored + ip, + clear_on_connect, # ignored + log_apdl, # ignored + remove_temp_files, # ignored + remove_temp_dir_on_exit, # ignored + verbose_mapdl, # ignored + license_server_check, # ignored + license_type, + print_com, # ignored + add_env_vars, # ignored + replace_env_vars, # ignored + version, + ): + from ansys.mapdl.core.launcher import launch_mapdl + + if mode: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'mode'.\nIgnoring argument." + ) + + if loglevel: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'loglevel'.\nIgnoring argument." + ) + + if cleanup_on_exit: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'cleanup_on_exit'.\nIgnoring argument." + ) + + if start_instance: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'start_instance'.\nIgnoring argument." + ) + + if ip: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'ip'.\nIgnoring argument." + ) + + if clear_on_connect: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'clear_on_connect'.\nIgnoring argument." + ) + + if log_apdl: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'log_apdl'.\nIgnoring argument." + ) + + if remove_temp_files: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'remove_temp_files'.\nIgnoring argument." + ) + + if remove_temp_dir_on_exit: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'remove_temp_dir_on_exit'.\nIgnoring argument." + ) + + if verbose_mapdl: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'verbose_mapdl'.\nIgnoring argument." + ) + + if print_com: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'print_com'.\nIgnoring argument." + ) + + if add_env_vars: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'add_env_vars'.\nIgnoring argument." + ) + + if replace_env_vars: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'replace_env_vars'.\nIgnoring argument." + ) + + if license_server_check: + click.echo( + click.style("Warn:", fg="yellow") + + " The following argument is not allowed in CLI: 'license_server_check'.\nIgnoring argument." + ) + + out = launch_mapdl( + exec_file=exec_file, + just_launch=True, + run_location=run_location, + jobname=jobname, + nproc=nproc, + ram=ram, + override=override, + additional_switches=additional_switches, + start_timeout=start_timeout, + port=port, + license_server_check=license_server_check, + license_type=license_type, + version=version, + ) + + if len(out) == 3: + header = f"Launched an MAPDL instance (PID={out[2]}) at " + else: + header = "Launched an MAPDL instance at " + + click.echo(click.style("Success: ", fg="green") + header + f"{out[0]}:{out[1]}") + + @launch_mapdl.command( + short_help="Stop MAPDL instances.", + help="""This command stop MAPDL instances running on a given port or with a given process id (PID). + +By default, it stops instances running on the port 50052.""", + ) + @click.option( + "--port", + default=None, + type=int, + help="Port where the MAPDL instance is running.", + ) + @click.option( + "--pid", + default=None, + type=int, + help="Process PID where the MAPDL instance is running.", + ) + @click.option( + "--all", + is_flag=True, + flag_value=True, + type=bool, + default=False, + help="Kill all MAPDL instances", + ) + def stop(port, pid, all): + if not pid and not port: + port = 50052 + + if port or all: + killed_ = False + for proc in psutil.process_iter(): + if psutil.pid_exists(proc.pid) and is_ansys_process(proc): + # Killing "all" + if all: + try: + proc.kill() + killed_ = True + except psutil.NoSuchProcess: + pass + + else: + # Killing by ports + if str(port) in proc.cmdline(): + try: + proc.kill() + killed_ = True + except psutil.NoSuchProcess: + pass + + if all: + str_ = "" + else: + str_ = f" running on port {port}" + + if not killed_: + click.echo( + click.style("ERROR: ", fg="red") + + "No Ansys instances" + + str_ + + " have been found.\n" + + "If you are sure there are MAPDL " + ) + else: + click.echo( + click.style("Success: ", fg="green") + + "Ansys instances" + + str_ + + " have been stopped." + ) + return + + if pid: + try: + pid = int(pid) + except ValueError: + click.echo( + click.style("ERROR: ", fg="red") + + "PID provided could not be converted to int." + ) + + p = psutil.Process(pid) + for child in p.children(recursive=True): + child.kill() + p.kill() + + if p.status == "running": + click.echo( + click.style("ERROR: ", fg="red") + + f"The process with PID {pid} and its children could not be killed." + ) + else: + click.echo( + click.style("Success: ", fg="green") + + f"The process with PID {pid} and its children have been stopped." + ) + return + + @launch_mapdl.command( + short_help="List MAPDL instances.", + help="""This command list MAPDL instances""", + ) + @click.option( + "--instances", + "-i", + is_flag=True, + flag_value=True, + type=bool, + default=False, + help="Print only instances", + ) + @click.option( + "--long", + "-l", + is_flag=True, + flag_value=True, + type=bool, + default=False, + help="Print all info.", + ) + @click.option( + "--cmd", + "-c", + is_flag=True, + flag_value=True, + type=bool, + default=False, + help="Print cmd", + ) + @click.option( + "--location", + "-cwd", + is_flag=True, + flag_value=True, + type=bool, + default=False, + help="Print running location info.", + ) + def list(instances, long, cmd, location): + # Assuming all ansys processes have -grpc flag + mapdl_instances = [] + for proc in psutil.process_iter(): + if ( + "ansys" in proc.name().lower() or "mapdl" in proc.name().lower() + ) and "-grpc" in proc.cmdline(): + if len(proc.children(recursive=True)) < 2: + proc.ansys_instance = False + else: + proc.ansys_instance = True + mapdl_instances.append(proc) + + # printing + table = [] + + if long: + cmd = True + location = True + + if instances: + headers = ["Name", "Status", "gRPC port", "PID"] + else: + headers = ["Name", "Is Instance", "Status", "gRPC port", "PID"] + + if cmd: + headers.append("Command line") + if location: + headers.append("Working directory") + + def get_port(proc): + cmdline = proc.cmdline() + ind_grpc = cmdline.index("-port") + return cmdline[ind_grpc + 1] + + table = [] + for each_p in mapdl_instances: + if instances and not each_p.ansys_instance: + continue + + proc_line = [] + proc_line.append(each_p.name()) + + if not instances: + proc_line.append(each_p.ansys_instance) + + proc_line.extend([each_p.status(), get_port(each_p), each_p.pid]) + + if cmd: + proc_line.append(" ".join(each_p.cmdline())) + + if location: + proc_line.append(each_p.cwd()) + + table.append(proc_line) + + print(tabulate(table, headers)) + +else: + + def convert(): + print("PyMAPDL CLI requires 'click' python package to be installed.") + + def launch_mapdl(): + print("PyMAPDL CLI requires 'click' python package to be installed.") + + def stop_mapdl(): + print("PyMAPDL CLI requires 'click' python package to be installed.") diff --git a/src/ansys/mapdl/core/convert.py b/src/ansys/mapdl/core/convert.py index fc69a8ea2f..99c276d595 100644 --- a/src/ansys/mapdl/core/convert.py +++ b/src/ansys/mapdl/core/convert.py @@ -1223,155 +1223,3 @@ def find_match(self, cmd): for each in pymethods: if each.startswith(cmd): return each - - -try: - import click - - _HAS_CLICK = True -except ModuleNotFoundError: - _HAS_CLICK = False - -if _HAS_CLICK: - # Loading CLI - - @click.command() - @click.argument("filename_in") - @click.option("-o", default=None, help="Name of the output Python script.") - @click.option( - "--filename_out", default=None, help="Name of the output Python script." - ) - @click.option( - "--loglevel", - default="WARNING", - help="Logging level of the ansys object within the script.", - ) - @click.option( - "--auto_exit", - default=True, - help="Adds a line to the end of the script to exit MAPDL. Default ``True``", - ) - @click.option( - "--line_ending", default=None, help="When None, automatically is ``\n.``" - ) - @click.option( - "--exec_file", - default=None, - help="Specify the location of the ANSYS executable and include it in the converter output ``launch_mapdl`` call.", - ) - @click.option( - "--macros_as_functions", - default=True, - help="Attempt to convert MAPDL macros to python functions.", - ) - @click.option( - "--use_function_names", - default=True, - help="Convert MAPDL functions to ansys.mapdl.core.Mapdl class methods. When ``True``, the MAPDL command ``K`` will be converted to ``mapdl.k``. When ``False``, it will be converted to ``mapdl.run('k')``.", - ) - @click.option( - "--show_log", - default=False, - help="Print the converted commands using a logger (from ``logging`` Python module).", - ) - @click.option( - "--add_imports", - default=True, - help='If ``True``, add the lines ``from ansys.mapdl.core import launch_mapdl`` and ``mapdl = launch_mapdl(loglevel="WARNING")`` to the beginning of the output file. This option is useful if you are planning to use the output script from another mapdl session. See examples section. This option overrides ``auto_exit``.', - ) - @click.option( - "--comment_solve", - default=False, - help='If ``True``, it will pythonically comment the lines that contain ``"SOLVE"`` or ``"/EOF"``.', - ) - @click.option( - "--cleanup_output", - default=True, - help="If ``True`` the output is formatted using ``autopep8`` before writing the file or returning the string. This requires ``autopep8`` to be installed.", - ) - @click.option( - "--header", - default=True, - help="If ``True``, the default header is written in the first line of the output. If a string is provided, this string will be used as header.", - ) - @click.option( - "--print_com", - default=True, - help="Print command ``/COM`` arguments to python console. Defaults to ``True``.", - ) - def cli( - filename_in, - o, - filename_out, - loglevel, - auto_exit, - line_ending, - exec_file, - macros_as_functions, - use_function_names, - show_log, - add_imports, - comment_solve, - cleanup_output, - header, - print_com, - ): - """PyMAPDL CLI tool for converting MAPDL scripts to PyMAPDL scripts. - - USAGE: - - This example demonstrates the main use of this tool: - - $ pymapdl_convert_script mapdl.dat -o python.py - - File mapdl.dat successfully converted to python.py. - - The output argument is optional, in which case the "py" extension is used: - - $ pymapdl_convert_script mapdl.dat - - File mapdl.dat successfully converted to mapdl.py. - - You can use any option from ``ansys.mapdl.core.convert.convert_script`` function: - - $ pymapdl_convert_script mapdl.dat --auto-exit False - - File mapdl.dat successfully converted to mapdl.py. - - $ pymapdl_convert_script.exe mapdl.dat --filename_out mapdl.out --add_imports False - - File mapdl.dat successfully converted to mapdl.out. - - - """ - if o: - filename_out = o - - convert_script( - filename_in, - filename_out, - loglevel, - auto_exit, - line_ending, - exec_file, - macros_as_functions, - use_function_names, - show_log, - add_imports, - comment_solve, - cleanup_output, - header, - print_com, - ) - - if filename_out: - print(f"File {filename_in} successfully converted to {filename_out}.") - else: - print( - f"File {filename_in} successfully converted to {os.path.splitext(filename_in)[0] + '.py'}." - ) - -else: - - def cli(): - print("PyMAPDL CLI requires click to be installed.") diff --git a/src/ansys/mapdl/core/launcher.py b/src/ansys/mapdl/core/launcher.py index 484a45fa5c..4203c5bfd2 100644 --- a/src/ansys/mapdl/core/launcher.py +++ b/src/ansys/mapdl/core/launcher.py @@ -327,9 +327,6 @@ def launch_grpc( these are already included to start up the MAPDL server. See the notes section for additional details. - custom_bin : str, optional - Path to the MAPDL custom executable. - override : bool, optional Attempts to delete the lock file at the run_location. Useful when a prior MAPDL session has exited prematurely and @@ -1142,11 +1139,6 @@ def launch_mapdl( environment variable ``PYMAPDL_PORT=`` This argument has priority over the environment variable. - custom_bin : str, optional - Path to the MAPDL custom executable. On release 2020R2 on - Linux, if ``None``, will check to see if you have - ``ansys.mapdl_bin`` installed and use that executable. - cleanup_on_exit : bool, optional Exit MAPDL when python exits or the mapdl Python instance is garbage collected. @@ -1454,6 +1446,7 @@ def launch_mapdl( "Hence this argument is not valid." ) use_vtk = kwargs.pop("use_vtk", None) + just_launch = kwargs.pop("just_launch", None) # Transferring MAPDL arguments to start_parameters: start_parm = {} @@ -1594,6 +1587,10 @@ def launch_mapdl( if not start_instance: LOG.debug("Connecting to an existing instance of MAPDL at %s:%s", ip, port) + if just_launch: + print(f"There is an existing MAPDL instance at: {ip}:{port}") + return + mapdl = MapdlGrpc( ip=ip, port=port, @@ -1734,6 +1731,13 @@ def launch_mapdl( verbose=verbose_mapdl, **start_parm, ) + + if just_launch: + out = [ip, port] + if hasattr(process, "pid"): + out += [process.pid] + return out + mapdl = MapdlGrpc( ip=ip, port=port, diff --git a/tests/conftest.py b/tests/conftest.py index e5adb1d7e7..f567de7214 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,6 @@ # SOFTWARE. from collections import namedtuple -from importlib import import_module import os from pathlib import Path from sys import platform @@ -112,6 +111,14 @@ ) +def import_module(requirement): + from importlib import import_module + + if os.name == "nt": + requirement = requirement.replace("-", ".") + return import_module(requirement) + + def has_dependency(requirement): try: if os.name == "nt": diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000000..a60508e259 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,161 @@ +# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re + +import psutil +import pytest + +from conftest import requires + + +@pytest.fixture +@requires("click") +@requires("nostudent") +def run_cli(): + def do_run(arguments=""): + from click.testing import CliRunner + + from ansys.mapdl.core.cli import launch_mapdl + + if arguments: + args = list(arguments.split(" ")) + else: + args = [] + + runner = CliRunner() + result = runner.invoke(launch_mapdl, args) + + assert result.exit_code == 0 + return result.output + + return do_run + + +@requires("click") +@requires("local") +@requires("nostudent") +def test_launch_mapdl_cli(run_cli): + output = run_cli() + + # In local + assert "Success: Launched an MAPDL instance " in output + + # grab ips and port + pid = int(re.search(r"\(PID=(\d+)\)", output).groups()[0]) + + output = run_cli(f"stop --pid {pid}") + + try: + p = psutil.Process(pid) + assert not p.status() + except: + # An exception means the process is dead? + pass + + +@requires("click") +@requires("local") +@requires("nostudent") +def test_launch_mapdl_cli_config(run_cli): + cmds_ = ["start", "--port 50090", "--jobname myjob"] + cmd_warnings = [ + "ip", + "license_server_check", + "mode", + "loglevel", + "cleanup_on_exit", + "start_instance", + "clear_on_connect", + "log_apdl", + "remove_temp_files", + "remove_temp_dir_on_exit", + "verbose_mapdl", + "print_com", + "add_env_vars", + "replace_env_vars", + ] + + cmd = " ".join(cmds_) + cmd_warnings_ = ["--" + each + " True" for each in cmd_warnings] + + cmd = cmd + " " + " ".join(cmd_warnings_) + + output = run_cli(cmd) + + assert "Launched an MAPDL instance" in output + assert "50090" in output + + # assert warnings + for each in cmd_warnings: + assert ( + f"The following argument is not allowed in CLI: '{each}'" in output + ), f"Warning about '{each}' not printed" + + # grab ips and port + pid = int(re.search(r"\(PID=(\d+)\)", output).groups()[0]) + p = psutil.Process(pid) + cmdline = " ".join(p.cmdline()) + + assert "50090" in cmdline + assert "myjob" in cmdline + + run_cli(f"stop --pid {pid}") + + +@requires("click") +@requires("local") +@requires("nostudent") +def test_launch_mapdl_cli_list(run_cli): + output = run_cli("list") + assert "running" in output + assert "Is Instance" in output + assert len(output.splitlines()) > 2 + assert "ansys" in output.lower() or "mapdl" in output.lower() + + output = run_cli("list -i") + assert "running" in output + assert "Is Instance" not in output + assert len(output.splitlines()) > 2 + assert "ansys" in output.lower() or "mapdl" in output.lower() + + output = run_cli("list -c") + assert "running" in output + assert "Command line" in output + assert "Is Instance" in output + assert len(output.splitlines()) > 2 + assert "ansys" in output.lower() or "mapdl" in output.lower() + + output = run_cli("list -cwd") + assert "running" in output + assert "Command line" not in output + assert "Working directory" in output + assert "Is Instance" in output + assert len(output.splitlines()) > 2 + assert "ansys" in output.lower() or "mapdl" in output.lower() + + output = run_cli("list -l") + assert "running" in output + assert "Is Instance" in output + assert "Command line" in output + assert len(output.splitlines()) > 2 + assert "ansys" in output.lower() or "mapdl" in output.lower()