Windowsコンソールに付随するstdout/stderrの不可解な動き

# 遭遇した事象

以下のようなバッチファイルとVBScriptファイルがある。

```parentBatch.cmd
@echo off
cd /d %~dp0
set LOGFILE=example.log
echo =開始= %DATE% %TIME% > %LOGFILE% 2>&1
cscript.exe //nologo childScript.vbs >> %LOGFILE% 2>&1
echo =終了= %DATE% %TIME% >> %LOGFILE% 2>&1
```

```childScript.vbs
WScript.Echo " -呼出先- " & Now
WScript.Sleep 10000
```

出力されるログファイル `example.log` の内容は、以下のようになる、はず。

```example.log (expected)
=開始= 2023/09/22  0:49:16.34 
 -呼出先- 2023/09/22 0:49:16
=終了= 2023/09/22  0:49:26.44 
```

ところが、なぜかVBScriptの処理部分だけ取りこぼす現象が発生した。

```example.log (actual)
=開始= 2023/09/22  0:49:16.34 
=終了= 2023/09/22  0:49:26.44 
```

なんで、こんなことが発生するんだ？という話。


# 起動元パラメータの影響を受ける

実は起動元は、とあるミドルウエア製品である。
製品のコードは提示できないので、代わりに再現のためのPowerShellコードを以下に提示する。

```caller.ps1
Start-ProcessEx -CommandLine 'cmd.exe /c parentBatch.cmd' `
   -CreationFlags DetachedProcess `
   -StartupInfo (New-StartupInfo -Flags UseStdHandles )
```

[Start-ProcessEx](https://github.com/jborean93/ProcessEx)
サンプルは本コマンドレットを使ってますのでご紹介。
Win32APIの `CreateProcessA` を呼べます。

[gistにもあります](https://gist.github.com/jborean93/a7c181b1ea0a4c0c8b815af9a8cbe508)

CreateProcessAの起動時パラメータで明示的に`DETACH_PROCESS`しておらず、STARTUPINFOのフラグ設定`STARTF_USESTDHANDLES`がない場合は再現しない。
たとえば以下のようにWSH経由で起動された場合は、期待した通りの出力がされる。

```callerWsh.vbs
Dim wsh: Set wsh = CreateObject("WScript.Shell")
rv = wsh.Run("cmd.exe /c parentBatch.cmd", 0, True)
```

`caller.ps1` を実行すると、cscriptの起動時に、新たなコンソールウィンドウが開いてしまうことが分かる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50759/8eabe094-179b-fb5e-e80c-5c0b5916f629.png)

これはcscript以外のプログラム、たとえばpowershellで処理させた場合でも、同様にコンソールウィンドウが開いてしまう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50759/0837f02a-2b50-5496-aa35-036b75cf6d3f.png)

プロセスツリーを見ると、 `cscript.exe` の配下に `conhost.exe` がいることが分かる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50759/c4a26b41-37b6-82f1-1d42-d16f44d1e35a.png)

WSHからの起動では、こうはならずに `cmd.exe` の配下に `conhost.exe` がいる。 `cscript.exe` とは並列になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50759/f7ac4ed3-fbcd-f576-7045-624414113e07.png)

stdin/stdout/stderrについて、自プロセスがどこから起動されたかによってふるまいが変わるというのは、直感的に受け入れ難い。。。
にわかに信じがたいが、目の前で起こっていることをみると受け入れざるを得ない。。。


# 回避策

現象は理解できたので、回避策。

呼出元を改修すると、すべての起動パターンが変わってしまうのでリスクが高く、できれば回避したい。
あくまで呼び出された側、本プログラムだけの改修で済ませたい。

標準出力の制御を取り戻すために、とりあえず自前で保持するコンソールを持てればいいんじゃないか。
ということで、以下のようなVBScriptを追加作成。

```wshRun.vbs
Dim wsh: Set wsh = CreateObject("WScript.Shell")
rv = wsh.Run(WScript.Arguments(0), 0, True)
```

元の `parentBatch.cmd` を以下のように改修する。
```parentBatch(modified).cmd
@echo off
cd /d %~dp0
set LOGFILE=example.log
echo =開始= %DATE% %TIME% > %LOGFILE% 2>&1
cscript.exe //nologo wshRun.vbs "cmd.exe /c cscript.exe //nologo childScript.vbs >> %LOGFILE% 2>&1"
echo =終了= %DATE% %TIME% >> %LOGFILE% 2>&1
```

サンプルだと「別窓が開く」という残念な状況は変わらないが、別窓の中でexample.logに出力するという処理結果を得られる。

ミドルウエアはデスクトップセッションを持たないので、別窓の心配は無用。


# Windows「コンソールの作成」

ところで、そもそも、なんでこんな事象が起きるのか。

[Microsoft Learn] 内に掲載されている「[Windowsコンソール]」の文書を読むと、少しは推し量る余地があるかもしれない。

[Microsoft Learn]:<https://learn.microsoft.com/ja-jp/>
[Windowsコンソール]:<https://learn.microsoft.com/ja-jp/windows/console/>

その記述を見れば、少しは推し量る余地があるかもしれない。

- キャラクター モード アプリケーションについて

https://learn.microsoft.com/ja-jp/windows/console/about-character-mode-applications

- コンソールの作成

https://learn.microsoft.com/ja-jp/windows/console/creation-of-a-console

要は、Windowsにおいてはstdin/stdout/stderrは単なるファイルハンドルではなくって、コンソールに紐づいている入出力なのだということらしい。
これは知らないと分からないな。。

なにかの参考になれば幸いです。