Repository: xonsh/xonsh

Issue:
Xonsh crashes when interrupting running process
<!--- 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: http://xon.sh/bash_to_xsh.html
If you don't find an answer there, please do open an issue! -->

## xonfig
<!--- Please post the output of the `xonfig` command (run from inside xonsh) so we know more about your current setup -->
```
+------------------+----------------------+
| xonsh            | 0.7.8                |
| Git SHA          | ecca7a7c             |
| Commit Date      | Sep 11 13:48:32 2018 |
| Python           | 3.7.0                |
| PLY              | 3.11                 |
| have readline    | True                 |
| prompt toolkit   | 2.0.4                |
| shell type       | prompt_toolkit2      |
| pygments         | None                 |
| on posix         | True                 |
| on linux         | True                 |
| distro           | arch                 |
| 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      |
+------------------+----------------------+
```

## Expected Behavior
<!--- Tell us what should happen -->
When running a command that takes longer (or forever, like cat /dev/random) interrupting it (with ctrl-c) will cause the running process to terminate.
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
<!--- 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` -->
When running a command that takes longer (or forever, like cat /dev/random) interrupting it (with ctrl-c) will cause the xonsh shell to close.

## Steps to Reproduce
<!--- Please try to write out a minimal reproducible snippet to trigger the bug, it will help us fix it! -->
execute ```cat /dev/random```, then ctrl-c

Please note that I'm using xonsh in byoubu (screen) with mutliple xonsh instances open. When the bug occurs, the affected instance (tab) is closed.
I just tested it without screen, the shell window doesn't close but the process (cat) doesn't terminate either.


Discussion:
yeah, I can't kill process too with `ctrl+c`.
Thanks for reporting @Deviluc and @alex-bender.  I can't reproduce this locally.  With both prompt-toolkint v2 and readline, `Ctrl+C` will kill `cat /dev/random` and kill the process.  Do you have other examples?
Will check that tomorrow

@scopatz indeed, `xonsh` from docker works fine. 
Hmm that is weird
For me the shell doesn't crash, but becomes unresponsive. Also, `cat /dev/random > test` freezes the shell and the resulting `test` file is empty.
@laloch - that works for me:

```console
scopatz@artemis ~ $ cat /dev/random > test
scopatz@artemis ~ $ cat test
���'�e��Nx\���]�\
```
@Deviluc try to run `xonsh -c "cat /dev/random"` and kill that
Any progress on finding another example here?
@scopatz unfortunately not, nothing new.
This bug is no longer there on master. Should it be closed?
> This bug is no longer there on master. Should it be closed?

Can't confirm. `cat /dev/random` still freezes the shell for me with no output whatsoever and no way to SIGINT the subprocess. There's in fact no subprocess at all since `cat` is a builtin. `/bin/cat /dev/random` works just fine.
@laloch - what does `which -a cat` say?
It says that it is indeed the xonsh builtin:
```
Type:             function
String form:      <function cat at 0x7f001412b620>
File:             /home/laloch/code/xonsh/xonsh/xoreutils/cat.py
```
Hmm interesting, I wonder if this is an issue with xoreutils cat.
Yeah it seems that xoreutils cat has this problem.

Resolving patch:
diff --git a/xonsh/xoreutils/cat.py b/xonsh/xoreutils/cat.py
--- a/xonsh/xoreutils/cat.py
+++ b/xonsh/xoreutils/cat.py
@@ -1,13 +1,44 @@
 """Implements a cat command for xonsh."""
 import os
+import time
 import builtins
 
+import xonsh.proc as xproc
 from xonsh.xoreutils.util import arg_handler
 
 
+def _cat_line(
+    f, sep, last_was_blank, line_count, opts, out, enc, enc_errors, read_size
+):
+    _r = r = f.readline(size=80)
+    if isinstance(_r, str):
+        _r = r = _r.encode(enc, enc_errors)
+    if r == b"":
+        last_was_blank, line_count, read_size, True
+    if r.endswith(sep):
+        _r = _r[: -len(sep)]
+    this_one_blank = _r == b""
+    if last_was_blank and this_one_blank and opts["squeeze_blank"]:
+        last_was_blank, line_count, read_size, False
+    last_was_blank = this_one_blank
+    if opts["number_all"] or (opts["number_nonblank"] and not this_one_blank):
+        start = ("%6d " % line_count).encode(enc, enc_errors)
+        _r = start + _r
+        line_count += 1
+    if opts["show_ends"]:
+        _r = _r + b"$"
+    out.buffer.write(_r)
+    out.flush()
+    read_size += len(r)
+    return last_was_blank, line_count, read_size, False
+
+
 def _cat_single_file(opts, fname, stdin, out, err, line_count=1):
     env = builtins.__xonsh__.env
     enc = env.get("XONSH_ENCODING")
+    enc_errors = env.get("XONSH_ENCODING_ERRORS")
+    read_size = 0
+    file_size = fobj = None
     if fname == "-":
         f = stdin
     elif os.path.isdir(fname):
@@ -17,31 +48,38 @@ def _cat_single_file(opts, fname, stdin, out, err, line_count=1):
         print("cat: No such file or directory: {}".format(fname), file=err)
         return True, line_count
     else:
-        f = open(fname, "rb")
-    sep = os.linesep.encode()
+        file_size = os.stat(fname).st_size
+        if file_size == 0:
+            file_size = None
+        fobj = open(fname, "rb")
+        f = xproc.NonBlockingFDReader(fobj.fileno(), timeout=0.1)
+    sep = os.linesep.encode(enc, enc_errors)
     last_was_blank = False
-    while True:
-        _r = r = f.readline()
-        if isinstance(_r, str):
-            _r = r = _r.encode()
-        if r == b"":
-            break
-        if r.endswith(sep):
-            _r = _r[: -len(sep)]
-        this_one_blank = _r == b""
-        if last_was_blank and this_one_blank and opts["squeeze_blank"]:
-            continue
-        last_was_blank = this_one_blank
-        if opts["number_all"] or (opts["number_nonblank"] and not this_one_blank):
-            start = ("%6d " % line_count).encode()
-            _r = start + _r
-            line_count += 1
-        if opts["show_ends"]:
-            _r = _r + b"$"
+    while file_size is None or read_size < file_size:
         try:
-            print(_r.decode(enc), flush=True, file=out)
-        except:
+            last_was_blank, line_count, read_size, endnow = _cat_line(
+                f,
+                sep,
+                last_was_blank,
+                line_count,
+                opts,
+                out,
+                enc,
+                enc_errors,
+                read_size,
+            )
+            if endnow:
+                break
+            if last_was_blank:
+                time.sleep(1e-3)
+        except KeyboardInterrupt:
+            print("got except", flush=True, file=out)
+            break
+        except Exception as e:
+            print("xonsh:", e, flush=True, file=out)
             pass
+    if fobj is not None:
+        fobj.close()
     return False, line_count
 
 


Test patch: