Repository: xonsh/xonsh

Issue:
Pipes hang on windows.
I get a hanging process if I pipe stuff on windows. Here are two examples:

``` bash
snail@sea ~\Documents\xonsh master $ echo @('hello') | clip
       [hangs here... Presssing ctrl-c gives this]
ValueError: Unsupported signal: 2
snail@sea ~\Documents\xonsh master $ cat CHANGELOG.rst | grep regression
       [hangs here... Pressing ctrl-c gives this]
ValueError: Unsupported signal: 2
snail@sea ~\Documents\xonsh master $ jobs
[1]+ running: cat CHANGELOG.rst | grep regression (19004)
```



Discussion:
Hi @melund, thanks for reporting. I won't be at my windows box until after work, but did #1870 help at all?  I fixed a couple on piping issues on windows that I noticed there.

I hoped it would as well, but sadly no.  #1870 didn't change anything for this issue. 

OK, I will take a look tonight.

I'm seeing this, too, actually, but without any error messages.

``` console
cat junkfile | wc -l
```

just hangs and Ctrl-C returns to prompt without output.

Oh yeah, weird.  Great though.  That means I can debug at least part of this on Linux :)


Resolving patch:
diff --git a/xonsh/proc.py b/xonsh/proc.py
--- a/xonsh/proc.py
+++ b/xonsh/proc.py
@@ -1484,6 +1484,7 @@ def iterraw(self):
             stderr = NonBlockingFDReader(stderr.fileno(), timeout=timeout)
         # read from process while it is running
         check_prev_done = len(self.procs) == 1
+        prev_end_time = None
         while proc.poll() is None:
             if getattr(proc, 'suspended', False):
                 return
@@ -1504,8 +1505,22 @@ def iterraw(self):
             yield from stdout_lines
             stderr_lines = safe_readlines(stderr, 1024)
             self.stream_stderr(stderr_lines)
-            if not check_prev_done and (stdout_lines or stderr_lines):
-                check_prev_done = True
+            if not check_prev_done:
+                # if we are piping...
+                if (stdout_lines or stderr_lines):
+                    # see if we have some output.
+                    check_prev_done = True
+                elif prev_end_time is None:
+                    # or see if we already know that the next-to-last
+                    # proc in teh pipeline has ended.
+                    if self.procs[-2].poll() is not None:
+                        # if it has, record the time
+                        prev_end_time = time.time()
+                elif time.time() - prev_end_time >= 0.1:
+                    # if we still don't have any output, even though the
+                    # next-to-last proc has finished, wait a bit to make
+                    # sure we have fully started up, etc.
+                    check_prev_done = True
             time.sleep(timeout)
         # read from process now that it is over
         yield from safe_readlines(stdout)


Test patch:
diff --git a/tests/bin/cat b/tests/bin/cat
new file mode 100755
--- /dev/null
+++ b/tests/bin/cat
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+import sys
+with open(sys.argv[-1]) as f:
+    for line in f:
+        print(line, end='')
diff --git a/tests/bin/cat.bat b/tests/bin/cat.bat
new file mode 100644
--- /dev/null
+++ b/tests/bin/cat.bat
@@ -0,0 +1,15 @@
+@echo on
+call :s_which py.exe
+rem note that %~dp0 is dir of this batch script
+if not "%_path%" == "" (
+  py -3 %~dp0cat %*
+) else (
+  python %~dp0cat %*
+)
+
+goto :eof
+
+:s_which
+  setlocal
+  endlocal & set _path=%~$PATH:1
+  goto :eof
diff --git a/tests/bin/wc b/tests/bin/wc
new file mode 100755
--- /dev/null
+++ b/tests/bin/wc
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+import sys
+
+if len(sys.argv) == 1:
+    f = sys.stdin.buffer
+else:
+    f = open(sys.argv[1], 'rb')
+
+doc = f.read()
+lines = len(doc.splitlines())
+words = len(doc.split())
+bytes = len(doc)
+
+print(' {0} {1:>2} {2} {3}'.format(lines, words, bytes, f.name))
\ No newline at end of file
diff --git a/tests/bin/wc.bat b/tests/bin/wc.bat
new file mode 100644
--- /dev/null
+++ b/tests/bin/wc.bat
@@ -0,0 +1,15 @@
+@echo on
+call :s_which py.exe
+rem note that %~dp0 is dir of this batch script
+if not "%_path%" == "" (
+  py -3 %~dp0wc %*
+) else (
+  python %~dp0wc %*
+)
+
+goto :eof
+
+:s_which
+  setlocal
+  endlocal & set _path=%~$PATH:1
+  goto :eof
\ No newline at end of file
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -132,6 +132,13 @@ def _g(args, stdin=None):
 aliases['g'] = _g
 g
 """, (("x"*100) + '\n') * 1000, 0),
+# test piping 'real' command
+("""
+with open('tttt', 'w') as fp:
+    fp.write("Wow mom!\\n")
+
+![cat tttt | wc]
+""", '      1       2      10\n' if ON_WINDOWS else " 1  2 9 <stdin>\n", 0),
 ]
 
 
@@ -145,7 +152,7 @@ def test_script(case):
 
 @skip_if_on_windows
 @pytest.mark.parametrize('cmd, fmt, exp', [
-    ('pwd', None, os.getcwd() + '\n'),
+    ('pwd', None, lambda: os.getcwd() + '\n'),
     ('echo WORKING', None, 'WORKING\n'),
     ('ls -f', lambda out: out.splitlines().sort(), os.listdir().sort()),
     ])
@@ -156,17 +163,21 @@ def test_single_command(cmd, fmt, exp):
     out, err, rtn = run_xonsh(cmd, stderr=sp.DEVNULL)
     if callable(fmt):
         out = fmt(out)
+    if callable(exp):
+        exp = exp()
     assert out == exp
     assert rtn == 0
 
 
 @skip_if_on_windows
 @pytest.mark.parametrize('cmd, exp', [
-    ('pwd', os.getcwd() + '\n'),
+    ('pwd', lambda: os.getcwd() + '\n'),
     ])
 def test_redirect_out_to_file(cmd, exp, tmpdir):
     outfile = tmpdir.mkdir('xonsh_test_dir').join('xonsh_test_file')
     command = '{} > {}'.format(cmd, outfile)
     out, _, _ = run_xonsh(command)
     content = outfile.read()
+    if callable(exp):
+        exp = exp()
     assert content == exp