Windows版のpythonで、cmd.exeの内容を完全に取得する事は「出来ませんでした」

npmをインストールしている時のコンソールの出力を**全て**残そうと思ったけど出来なかった。
理由は、`isTTY`で出し分けをしており、pythonのttyモジュールはwindowsでは使えないから。

# やりたかった事

↓コマンド実行中に表示されるプログレスバー。コマンド完了時には消えてる。
![2020-08-09-00-52-47.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5825/76e42910-0d98-72c9-e495-d104ea8a1366.gif)

これを**全て**テキストファイルに保存したかった。

# した事

以下のコードで`npm install typescript`コマンドを実行した。

```python
commands=["npm","install","typescript"]
def gl():
    cp = subprocess.Popen(commands,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    with open("./stdout.txt","bw") as fh1,open("./stderr.txt","bw") as fh2:
        while True:
            if cp.stdout is None:
                print("stdout is none")
                break
            if cp.stderr is None:
                print("stderr is none")
                break
            line = cp.stdout.readline()
            if line:
                fh1.write(line)
                fh1.flush()
                yield line
            if not line and cp.poll() is not None:
                break
            line = cp.stderr.readline()
            if line:
                fh2.write(line)
                fh2.flush()
                yield line
            if not line and cp.poll() is not None:
                break

for line in gt():
    sys.stdout.write(line.decode("utf-8").strip()+"\n")
```

実行結果のファイルは以下の通り

```plaintext:stdout.txt
+ typescript@3.9.7
updated 1 package and audited 1 package in 7.016s
found 0 vulnerabilities
```

```plaintext:stderr.txt
npm WARN saveError ENOENT: no such file or directory, open 'D:\tmp_20200809Sun004215 \package.json'
npm WARN enoent ENOENT: no such file or directory, open 'D:\tmp_20200809Sun004215 \package.json'
npm WARN tmp_20200809Sun004215  No description
npm WARN tmp_20200809Sun004215  No repository field.
npm WARN tmp_20200809Sun004215  No README data
npm WARN tmp_20200809Sun004215 No license field.
```

# 原因

## プログレスバーが表示されない

コンソールで出すには標準出力か、標準エラーのどちらか経由しか無いはず。という事は、npm側で出し分けをしている？
と思ってコードを追いかけてみたら、以下の箇所でした。
https://github.com/npm/cli/blob/ad8fe71a4f8c6d717375a6c699aaa00f11521adf/lib/npm.js#L349

```js

if (config.get('progress') && process.stderr.isTTY && process.env['TERM'] !== 'dumb') {
  log.enableProgress()
} else {
  log.disableProgress()
}
```

python経由で実行した時は`process.stderr.isTTY`の値が`undefined`になっていた。cmd.exe経由では`true`。

ちなみに、無理やり`process.stderr.isTTY=true`としたり、ifの判定を飛ばして実行してみたらターミナルが無反応になったので注意。ctrl+c すら吸い込まれた。おそらく、どこかの処理で別の事をしているのだろう。

## isTTY をtrueにするには

pythonならやり方あるだろう。と思って調べたら以下の記事を発見
[Filter out command that needs a terminal in Python subprocess module \- Stack Overflow](https://stackoverflow.com/questions/30139401/filter-out-command-that-needs-a-terminal-in-python-subprocess-module)

回答の中に、疑似tty？ を作る方法が書かれていた
https://stackoverflow.com/a/43012216/4314961 より転載

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import select
import termios
import tty
import pty
from subprocess import Popen

command = 'vim'

# save original tty setting then set it to raw mode
old_tty = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())

# open pseudo-terminal to interact with subprocess
master_fd, slave_fd = pty.openpty()

# use os.setsid() process the leader of a new session, or bash job control will not be enabled
p = Popen(command,
          preexec_fn=os.setsid,
          stdin=slave_fd,
          stdout=slave_fd,
          stderr=slave_fd,
          universal_newlines=True)

while p.poll() is None:
    r, w, e = select.select([sys.stdin, master_fd], [], [])
    if sys.stdin in r:
        d = os.read(sys.stdin.fileno(), 10240)
        os.write(master_fd, d)
    elif master_fd in r:
        o = os.read(master_fd, 10240)
        if o:
            os.write(sys.stdout.fileno(), o)

# restore tty settings back
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
```

`tty.setraw`とか、いかにもこれだよこれ。と思って実行してみたら、ダメ。
調べると、依存しているttyモジュールがunix限定。

`Because it requires the termios module, it will work only on Unix.`
https://docs.python.org/3/library/tty.html

まさかここでwindowsの2級市民っぷりを再認識する羽目になるとは。

# どうすれば？

npmにプルリク出して、isTTYをオーバーライドする設定を追加してもらうとか。

pythonではどうしようもない感じなので、nodejsを使う。pty.js というライブラリがあるが
[chjj/pty\.js: Bindings to forkpty\(3\) for node\.js\.](https://github.com/chjj/pty.js)

最終更新が2016年の上、win10で動かないというissueも上がっていて微妙。
[\`npm install pty\.js\` failed on Windows 10 · Issue \#151 · chjj/pty\.js](https://github.com/chjj/pty.js/issues/151)