Repository: mlflow/mlflow

Issue:
[BUG] MLFlow Pipelines Open fails on Linux
### Willingness to contribute

Yes. I would be willing to contribute a fix for this bug with guidance from the MLflow community.

### MLflow version

mlflow, version 1.27.0

### System information

- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**:
Red Hat Enterprise Linux release 8.6 (Ootpa)

- **Python version**:
4.8

- **yarn version, if running the dev UI**:


### Describe the problem

When running a pipeline on linux the pipeline fails when attempting to open the html file with the following error:

```
Couldn't get a file descriptor referring to the console
```

It appears the step command attempts to utilize the `open` command in the following file:

mlflow/pipelines/utils/step.py", line 81, in display_html

```python
subprocess.run(["open", html_file_path], check=True)
```

`open` is the correct command on MacOS to open a file from the command line but in Linux it should use `xdg-open`.  `open` is still a valid command in Linux but it serves a different purpose from `open` on MacOS and `xdg-open`.

### Tracking information

_No response_

### Code to reproduce issue

```sh
git clone https://github.com/mlflow/mlp-regression-template.git
cd mlp-regression-template
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
mlflow pipelines run --step split
```

### Other info / logs

```python-traceback
2022/07/25 15:25:48 INFO mlflow.pipelines.utils.step: Opening HTML file at: '/home/user/.mlflow/pipelines/5aec531bf0b8a3fdce03a58ff123085ef27fd6bdc47f9450eaf9b87078c8d0ff/steps/split/outputs/card.html'
Couldn't get a file descriptor referring to the console
Traceback (most recent call last):
  File "/home/user/code/mlp-regression-template/venv/bin/mlflow", line 8, in <module>
    sys.exit(cli())
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/cli.py", line 46, in run
    Pipeline(profile=profile).run(step)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/regression/v1/pipeline.py", line 337, in run
    return super().run(step=step)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/pipeline.py", line 82, in run
    self.inspect(last_executed_step.name)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/regression/v1/pipeline.py", line 588, in inspect
    super().inspect(step=step)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/pipeline.py", line 118, in inspect
    self._get_step(step).inspect(output_directory)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/step.py", line 150, in inspect
    display_html(html_data=card.to_html(), html_file_path=card_html_path)
  File "/home/user/code/mlp-regression-template/venv/lib64/python3.8/site-packages/mlflow/pipelines/utils/step.py", line 81, in display_html
    subprocess.run(["open", html_file_path], check=True)
  File "/usr/lib64/python3.8/subprocess.py", line 516, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['open', '/home/user/.mlflow/pipelines/5aec531bf0b8a3fdce03a58ff123085ef27fd6bdc47f9450eaf9b87078c8d0ff/steps/split/outputs/card.html']' returned non-zero exit status 1.
```

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [X] `area/pipelines`: Pipelines, Pipeline APIs, Pipeline configs, Pipeline Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [ ] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:

Resolving patch:
diff --git a/mlflow/pipelines/utils/step.py b/mlflow/pipelines/utils/step.py
--- a/mlflow/pipelines/utils/step.py
+++ b/mlflow/pipelines/utils/step.py
@@ -76,9 +76,23 @@ def display_html(html_data: str = None, html_file_path: str = None) -> None:
         import shutil
         import subprocess
 
-        if os.path.exists(html_file_path) and shutil.which("open") is not None:
+        # Use xdg-open in Linux environment
+        if shutil.which("xdg-open") is not None:
+            open_tool = shutil.which("xdg-open")
+        elif shutil.which("open") is not None:
+            open_tool = shutil.which("open")
+        else:
+            open_tool = None
+
+        if os.path.exists(html_file_path) and open_tool is not None:
             _logger.info(f"Opening HTML file at: '{html_file_path}'")
-            subprocess.run(["open", html_file_path], check=True)
+            try:
+                subprocess.run([open_tool, html_file_path], check=True)
+            except Exception as e:
+                _logger.warning(
+                    f"Encountered unexpected error opening the html page."
+                    f" The file may be manually accessed at {html_file_path}. Exception: {e}"
+                )
 
 
 def get_pandas_data_profile(data_frame, title: str):


Test patch: