Repository: xonsh/xonsh

Issue:
Make xonsh tolerant to inaccessible xonsh data paths
<!--- Provide a general summary of the issue in the Title above -->
<!--- If you have a question along the lines of "How do I do this Bash command in xonsh"
please first look over the Bash to Xonsh translation guide: https://xon.sh/bash_to_xsh.html
If you don't find an answer there, please do open an issue! -->

## xonfig

<details>

My distro is actually NixOS.

```
$ xonfig
+------------------+--------------------+
| xonsh            | 0.15.1             |
| Python           | 3.11.8             |
| PLY              | 3.11               |
| have readline    | True               |
| prompt toolkit   | 3.0.43             |
| shell type       | prompt_toolkit     |
| history backend  | json               |
| pygments         | 2.17.2             |
| on posix         | True               |
| on linux         | True               |
| distro           | unknown            |
| on wsl           | False              |
| on darwin        | False              |
| on windows       | False              |
| on cygwin        | False              |
| on msys2         | False              |
| is superuser     | False              |
| default encoding | utf-8              |
| xonsh encoding   | utf-8              |
| encoding errors  | surrogateescape    |
| xontrib          | []                 |
| RC file 1        | /etc/xonsh/xonshrc |
+------------------+--------------------+
```

</details>

## Expected Behavior
<!--- Tell us what should happen -->
Xonsh runs, though giving up logging command history to file.

People need a shell for data rescue if they mess up their filesystem and can only mount it read only.

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
Xonsh keeps throwing errors on an `open(...)` call and I cannot input anything except pressing \<c-C\> to exit the shell.

<!--- If part of your bug report is a traceback, please first enter debug mode before triggering the error
To enter debug mode, set the environment variable `XONSH_DEBUG=1` _before_ starting `xonsh`.
On Linux and OSX, an easy way to to do this is to run `env XONSH_DEBUG=1 xonsh` -->

### Traceback (if applicable)

<details>

I havn't yet tried readonly `~/.local/share/xonsh`, but I set `$XONSH_DATA_DIR` to `p"~/ro"` where I build up a readonly overlayfs, and got the following output (and now I understand why it is generating infinite logs: I set xonsh as my login shell and it keeps falling back to itself on error)

```
/etc/xonsh/xonshrc:16:16 -     source-bash "/nix/store/ml2wn9nd7p5912ynfxq27dwlkh77qx1m-set-environment"
/etc/xonsh/xonshrc:16:16 +     ![source-bash "/nix/store/ml2wn9nd7p5912ynfxq27dwlkh77qx1m-set-environment"]
Traceback (most recent call last):
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/main.py", line 469, in main
    args = premain(argv)
           ^^^^^^^^^^^^^
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/main.py", line 409, in premain
    env = start_services(shell_kwargs, args, pre_env=pre_env)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/main.py", line 355, in start_services
    _load_rc_files(shell_kwargs, args, env, execer, ctx)
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/main.py", line 310, in _load_rc_files
    XSH.rc_files = xonshrc_context(
                   ^^^^^^^^^^^^^^^^
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/environ.py", line 2492, in xonshrc_context
    status = xonsh_script_run_control(
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/environ.py", line 2553, in xonsh_script_run_control
    exc_info = run_script_with_cache(filename, execer, ctx)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/codecache.py", line 169, in run_script_with_cache
    update_cache(ccode, cachefname)
  File "/nix/store/3sgwcszkn0c5ab5nwm0k1iga4shj2ps0-python3-3.11.8-env/lib/python3.11/site-packages/xonsh/codecache.py", line 98, in update_cache
    with open(cache_file_name, "wb") as cfile:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 30] Read-only file system: 'ro/xonsh_script_cache/nix/store/hy8zfhlx9rjwws4w62bjdk5ix9x550q1-etc-xonsh-xonshrc.cpython-311'
Xonsh encountered an issue during launch
Failback to /run/current-system/sw/bin/xonsh
```

</details>

## Steps to Reproduce
<!--- Please try to write out a minimal reproducible snippet to trigger the bug, it will help us fix it! -->
1. make ~/.local read only (maybe through mounting an overlayfs)
2. start xonsh

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
We need to avoid stopping the work in this case and show the warning about setting [`$XONSH_DATA_DIR`](https://xon.sh/envvars.html#xonsh-data-dir) manually.
PR is welcome!

It seems whether to use cache is not checked before updating cache [on boot](https://github.com/xonsh/xonsh/blob/04cba39a53ccb7bb8e8a5a23939ed8d000687d6b/xonsh/codecache.py#L169), but is checked [in `BaseShell`](https://github.com/xonsh/xonsh/blob/main/xonsh/base_shell.py#L522). Maybe the former should also check [`use_cache`](https://github.com/xonsh/xonsh/blob/04cba39a53ccb7bb8e8a5a23939ed8d000687d6b/xonsh/codecache.py#L161) before updating cache?

The warning should be given directly from [the `update_cache` function](https://github.com/xonsh/xonsh/blob/04cba39a53ccb7bb8e8a5a23939ed8d000687d6b/xonsh/codecache.py#L91) directly, I think. Is it the right way calling [`is_writable_file`](https://github.com/xonsh/xonsh/blob/04cba39a53ccb7bb8e8a5a23939ed8d000687d6b/xonsh/tools.py#L1136) to see if the log file is writable, and calling [`print_warning`](https://github.com/xonsh/xonsh/blob/04cba39a53ccb7bb8e8a5a23939ed8d000687d6b/xonsh/tools.py#L975) to show the warning?

BTW, I found some bad practice [here](https://github.com/xonsh/xonsh/blob/04cba39a53ccb7bb8e8a5a23939ed8d000687d6b/xonsh/tools.py#L1149).

Resolving patch:
diff --git a/xonsh/codecache.py b/xonsh/codecache.py
--- a/xonsh/codecache.py
+++ b/xonsh/codecache.py
@@ -9,6 +9,7 @@
 from xonsh.built_ins import XSH
 from xonsh.lazyasd import lazyobject
 from xonsh.platform import PYTHON_VERSION_INFO_BYTES
+from xonsh.tools import is_writable_file, print_warning
 
 
 def _splitpath(path, sofar=()):
@@ -94,6 +95,13 @@ def update_cache(ccode, cache_file_name):
     represented by ``ccode``.
     """
     if cache_file_name is not None:
+        if not is_writable_file(cache_file_name):
+            if XSH.env.get("XONSH_DEBUG", "False"):
+                print_warning(
+                    f"update_cache: Cache file is not writable: {cache_file_name}\n"
+                    f"Set $XONSH_CACHE_SCRIPTS=0, $XONSH_CACHE_EVERYTHING=0 to disable cache."
+                )
+            return
         os.makedirs(os.path.dirname(cache_file_name), exist_ok=True)
         with open(cache_file_name, "wb") as cfile:
             cfile.write(XONSH_VERSION.encode() + b"\n")
@@ -166,7 +174,8 @@ def run_script_with_cache(filename, execer, glb=None, loc=None, mode="exec"):
         with open(filename, encoding="utf-8") as f:
             code = f.read()
         ccode = compile_code(filename, code, execer, glb, loc, mode)
-        update_cache(ccode, cachefname)
+        if use_cache:
+            update_cache(ccode, cachefname)
     return run_compiled_code(ccode, glb, loc, mode)
 
 
diff --git a/xonsh/history/main.py b/xonsh/history/main.py
--- a/xonsh/history/main.py
+++ b/xonsh/history/main.py
@@ -36,7 +36,16 @@ def construct_history(backend=None, **kwargs) -> "History":
             file=sys.stderr,
         )
         kls_history = JsonHistory
-    return kls_history(**kwargs)
+
+    try:
+        return kls_history(**kwargs)
+    except Exception as e:
+        xt.print_exception(
+            f"Error during load {kls_history}: {e}\n"
+            f"Set $XONSH_HISTORY_BACKEND='dummy' to disable history.\n"
+            f"History disabled."
+        )
+        return DummyHistory()
 
 
 def _xh_session_parser(hist=None, newest_first=False, **kwargs):


Test patch: