SSH越しにバッチファイルを実行すると非同期処理が完了しない問題の対策

初投稿です。
タイトル付けるの難しい...

# やりたいこと
- sshで、Windows上のバッチファイル（親バッチファイル）をリモート実行
- 親バッチファイル内部で、startコマンドを使って非同期で子バッチファイルを呼び出す
- 子バッチファイルの終了を待たずに、親バッチファイルを終了させたい

# 実装NG例
```bat:一見動きそうな実装（test1.bat）
@echo off

echo "%time% test1.bat start." > C:\work\test.log

rem 10秒スリープ
ping -n 11 127.0.0.1 > nul

rem test2.batを非同期実行
start "" C:\work\test2.bat

rem 5秒スリープ
ping -n 6 127.0.0.1 > nul

echo "%time% test1.bat end." >> C:\work\test.log

exit /b 0
```
```bat:非同期実行するバッチ（test2.bat）
@echo off

echo "%time% test2.bat start." >> C:\work\test.log

rem 時間がかかる処理
ping -n 31 127.0.0.1 > nul

echo "%time% test2.bat end." >> C:\work\test.log

exit 0
```
  
test1.batを直接実行したときのログがこちら：

```bat:実行結果（test1.bat直接実行）
"13:12:55.37 test1.bat start." 
"13:13:05.69 test2.bat start." 
"13:13:10.70 test1.bat end." 
"13:13:36.12 test2.bat end." 
```

test1.batを起動して10秒後にtest2.batが非同期で起動して
その5秒後にtest1.batは終了、非同期起動の30秒後にtest2.batも終了した。

次に、test1.batをSSH越しに実行したときのログがこちら：

```bat:実行結果（test1.batをSSH越しに実行）
"13:17:21.53 test1.bat start." 
"13:17:31.78 test2.bat start." 
"13:17:36.81 test1.bat end." 
```

あれ？ test2.batの終了ログが出ていない？

# 原因
タスクマネージャーを見ると原因がわかる。
SSH越しにtest1.batを実行したときの、タスクマネージャーの表示を見てみる。

1. test1.batを起動した後  
SSH実行により、PID=3936のconhost.exeが起動。  
PID=13556のcmd.exeがtest1.batを実行している。  
conhost.exe、cmd.exeともに「ジョブオブジェクトID=1000」の中で実行している（画面最右列）。
![test1_起動直後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522478/0aac05c5-33da-0b8a-c823-36cd6191f38e.png)
2. test2.batが非同期実行された後  
PID＝12464のcmd.exeが立ち上がり、test2.batを実行している。  
このcmd.exeも、「ジョブオブジェクトID=1000」の中で実行している。
![test1_test2起動時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522478/7b8ce913-62dd-5567-5a8c-e2be80c97e2a.png)
3. sshコマンドの応答が返ってきた後  
ジョブオブジェクトID=1000のプロセスがすべて消滅した。
![test1_終了時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522478/344bde7a-d716-16be-2a46-cf15a574eae3.png)

※ちなみに、test1.batを直接実行したときはジョブオブジェクトIDでまとめられていなかった。

# 対策
明示的に「違うジョブオブジェクトIDで起動して」といえばよいのだが、`start`コマンドにはそのようなオプションがないみたい。
そのため、`Win32_Process`クラスの`Create`メソッドでtest2.batが動くプロセスを作るようにした。

```bat:呼び出し側処理の修正（test1-2.bat）
@echo off
echo "%time% test1-2.bat start." > C:\work\test.log

ping -n 11 127.0.0.1 > nul

rem test2.batを非同期実行
powershell -Command "Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='C:\work\test2.bat'}"

ping -n 6 127.0.0.1 > nul

echo "%time% test1-2.bat end." >> C:\work\test.log

exit /b 0
```

念のため、test1-2.batを直接実行したときのログがこちら：

```bat:実行結果（test1-2.bat直接実行）
"13:36:58.41 test1-2.bat start." 
"13:37:09.88 test2.bat start." 
"13:37:14.94 test1-2.bat end." 
"13:37:40.22 test2.bat end." 
```

そして、test1-2.batをSSH越しに実行したときのログがこちら：

```bat:実行結果（test1.batをSSH越しに実行）
"13:39:40.37 test1-2.bat start." 
"13:39:51.46 test2.bat start." 
"13:39:56.52 test1-2.bat end." 
"13:40:21.79 test2.bat end." 
```

ちゃんとtest2.batが完走した。

タスクマネージャを見てみよう。

1. test1.batを起動した後  
SSH実行により、PID=7948のconhost.exeが起動。  
PID=8268のcmd.exeがtest1-2.batを実行している。  
conhost.exe、cmd.exeともに「ジョブオブジェクトID=744」の中で実行している（画面最右列）。
![test1-2_起動直後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522478/b5cfe85b-f034-96f8-860b-c742e9609ea9.png)
2. test2.batが非同期実行された後  
PID＝12260のcmd.exeが立ち上がり、test2.batを実行している。  
このcmd.exeは、「ジョブオブジェクトID=744」に属していない。
![test1-2_test2起動時 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522478/5be5f15a-b1e3-a13c-2df2-38aede99cddd.png)
3. sshコマンドの応答が返ってきた後  
ジョブオブジェクトID=744のプロセスがすべて消滅したが、PID=12260のcmd.exeは動き続けている。
![test1-2_非同期実行中.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522478/c9dff09a-27e1-50ef-f335-2c0f393e8b84.png)

## 参考

https://docs.microsoft.com/ja-jp/windows/win32/procthread/job-objects
> ジョブオブジェクト を使用すると、プロセスのグループを1つの単位として管理できます。
> （中略）
> たとえば、作業セットのサイズやプロセスの優先順位などの制限の適用や、ジョブに関連付けられているすべてのプロセスの終了などがあります。

https://github.com/PowerShell/Win32-OpenSSH/issues/1032
> we currently achieve this through some P/Invoke calls in PoweShell and call the CreateProcess Win32 API with a specific flag.

OpenSSHがWindowsに移植されてから、いろんな人がこの問題に直面している模様。
この回答を見ると、`Win32_Process Create`するときに`CREATE_BREAKAWAY_FROM_JOB`を指定してあげた方がいいのかも。