Repository: xonsh/xonsh

Issue:
Xonsh crash when config.json is incorrect
It is very problematic if xonsh is your default shell, You can't display any terminal, and you'd better not have xonsh as the default shell of your root otherwise you'rer even more screwed.



Discussion:
This is definitely a bug. Fix incoming.


Resolving patch:
diff --git a/xonsh/environ.py b/xonsh/environ.py
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -14,7 +14,8 @@
 from pprint import pformat
 from functools import wraps
 from contextlib import contextmanager
-from collections import MutableMapping, MutableSequence, MutableSet, namedtuple
+from collections import (Mapping, MutableMapping, MutableSequence,
+    MutableSet, namedtuple)
 
 from xonsh import __version__ as XONSH_VERSION
 from xonsh.tools import (
@@ -25,7 +26,7 @@
     is_completions_display_value, to_completions_display_value, is_string_set,
     csv_to_set, set_to_csv, get_sep, is_int, is_bool_seq, csv_to_bool_seq,
     bool_seq_to_csv, DefaultNotGiven, setup_win_unicode_console,
-    intensify_colors_on_win_setter
+    intensify_colors_on_win_setter, print_exception
 )
 from xonsh.codecache import run_script_with_cache
 from xonsh.dirstack import _get_cwd
@@ -1189,8 +1190,22 @@ def load_static_config(ctx, config=None):
                                 DEFAULT_VALUES.get('XONSH_ENCODING_ERRORS',
                                                    'surrogateescape'))
         with open(config, 'r', encoding=encoding, errors=errors) as f:
-            conf = json.load(f)
-        ctx['LOADED_CONFIG'] = True
+            try:
+                conf = json.load(f)
+                assert isinstance(conf, Mapping)
+                ctx['LOADED_CONFIG'] = True
+            except Exception as e:
+                conf = {}
+                ctx['LOADED_CONFIG'] = False
+                print_exception()
+                # JSONDecodeError was added in Python v3.5
+                jerr = json.JSONDecodeError \
+                       if hasattr(json, 'JSONDecodeError') else ValueError
+                if isinstance(e, jerr):
+                    msg = 'Xonsh config file is not valid JSON.'
+                else:
+                    msg = 'Could not load xonsh config.'
+                print(msg, file=sys.stderr)
     else:
         conf = {}
         ctx['LOADED_CONFIG'] = False
diff --git a/xonsh/foreign_shells.py b/xonsh/foreign_shells.py
--- a/xonsh/foreign_shells.py
+++ b/xonsh/foreign_shells.py
@@ -465,17 +465,13 @@ def _get_shells(shells=None, config=None, issue_warning=True):
     elif shells is not None:
         pass
     else:
-        if config is None:
-            config = builtins.__xonsh_env__.get('XONSHCONFIG')
-        if os.path.isfile(config):
-            with open(config, 'r') as f:
-                conf = json.load(f)
-            shells = conf.get('foreign_shells', DEFAULT_SHELLS)
+        env = getattr(builtins, '__xonsh_env__', os.environ)
+        if env.get('LOADED_CONFIG', False):
+            conf = builtins.__xonsh_config__
         else:
-            if issue_warning:
-                msg = 'could not find xonsh config file ($XONSHCONFIG) at {0!r}'
-                warn(msg.format(config), RuntimeWarning)
-            shells = DEFAULT_SHELLS
+            from xonsh.environ import load_static_config
+            conf = load_static_config(env, config)
+        shells = conf.get('foreign_shells', DEFAULT_SHELLS)
     return shells
 
 
diff --git a/xonsh/tools.py b/xonsh/tools.py
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -388,10 +388,11 @@ def suggest_commands(cmd, env, aliases):
 
 def print_exception():
     """Print exceptions with/without traceback."""
-    if 'XONSH_SHOW_TRACEBACK' not in builtins.__xonsh_env__:
+    env = getattr(builtins, '__xonsh_env__', os.environ)
+    if 'XONSH_SHOW_TRACEBACK' not in env:
         sys.stderr.write('xonsh: For full traceback set: '
                          '$XONSH_SHOW_TRACEBACK = True\n')
-    if builtins.__xonsh_env__.get('XONSH_SHOW_TRACEBACK'):
+    if env.get('XONSH_SHOW_TRACEBACK', False):
         traceback.print_exc()
     else:
         exc_type, exc_value, exc_traceback = sys.exc_info()


Test patch:
diff --git a/tests/test_environ.py b/tests/test_environ.py
--- a/tests/test_environ.py
+++ b/tests/test_environ.py
@@ -2,12 +2,16 @@
 """Tests the xonsh environment."""
 from __future__ import unicode_literals, print_function
 import os
+import tempfile
+import builtins
 
 import nose
 from nose.tools import (assert_equal, assert_true, assert_not_in,
-                        assert_is_instance, assert_in)
+    assert_is_instance, assert_in, assert_raises)
 
-from xonsh.environ import Env, format_prompt
+from xonsh.environ import Env, format_prompt, load_static_config
+
+from tests.tools import mock_xonsh_env
 
 def test_env_normal():
     env = Env(VAR='wakka')
@@ -73,7 +77,6 @@ def test_HISTCONTROL():
     assert_true('ignoredups' in env['HISTCONTROL'])
 
 def test_swap():
-
     env = Env(VAR='wakka')
     assert_equal(env['VAR'], 'wakka')
 
@@ -101,6 +104,27 @@ def test_swap():
     assert 'VAR2' not in env
     assert 'VAR3' not in env
 
+def check_load_static_config(s, exp, loaded):
+    env = {'XONSH_SHOW_TRACEBACK': False}
+    with tempfile.NamedTemporaryFile() as f, mock_xonsh_env(env):
+        f.write(s)
+        f.flush()
+        conf = load_static_config(env, f.name)
+    assert_equal(exp, conf)
+    assert_equal(env['LOADED_CONFIG'], loaded)
+
+def test_load_static_config_works():
+    s = b'{"best": "awash"}'
+    check_load_static_config(s, {'best': 'awash'}, True)
+
+def test_load_static_config_type_fail():
+    s = b'["best", "awash"]'
+    check_load_static_config(s, {}, False)
+
+def test_load_static_config_json_fail():
+    s = b'{"best": "awash"'
+    check_load_static_config(s, {}, False)
+
 
 if __name__ == '__main__':
     nose.runmodule()