(Tera Term をダミー接続するための) Windows 名前付きパイプ サーバー

# 動機

Tera Term のマクロ言語 (Tera Term Language) の通信コマンドのほとんどはなにがしかのホストに接続していないと実行できません。職場の Windows 10 デスクトップ環境には Tera Term が入っていて TTL の通信コマンドの動作を実行させて確かめたいのですが、開発系でないので気安く接続できるようなホストはありません。

Tera Term は名前付きパイプに接続でき、 Windows に .NET が入っていればローカルに名前付きパイプ サーバーを立てられるそうなので、試みてみました。

# 参考文献

1. [ Powershellで名前付きパイプを扱う - じゅんじゅんのきまぐれ](https://junjun777.hatenablog.com/entry/20130804/powershell_named_pipe " Powershellで名前付きパイプを扱う - じゅんじゅんのきまぐれ")
2. [NamedPipeServerStream クラス (System.IO.Pipes) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.pipes.namedpipeserverstream?view=netcore-3.1 "NamedPipeServerStream クラス (System.IO.Pipes) | Microsoft Docs")
3. [エコーバックせずに文字列を入力する(Console.ReadKey) - Programming/.NET Framework/Tips - 総武ソフトウェア推進所](https://smdn.jp/programming/netfx/tips/console_input_without_echo_back/ "エコーバックせずに文字列を入力する(Console.ReadKey) - Programming/.NET Framework/Tips - 総武ソフトウェア推進所")
4. [[C#]処理ループ内でキー入力を受け付ける – エンジニ屋](https://sevenb.jp/wordpress/ura/2018/01/22/c%E5%87%A6%E7%90%86%E3%83%AB%E3%83%BC%E3%83%97%E5%86%85%E3%81%A7%E3%82%AD%E3%83%BC%E5%85%A5%E5%8A%9B%E3%82%92%E5%8F%97%E3%81%91%E4%BB%98%E3%81%91%E3%82%8B/ "[C#]処理ループ内でキー入力を受け付ける – エンジニ屋")
5. [非同期名前付きパイプの基礎 / クライアントSide - Neareal](http://neareal.net/index.php?Programming%2F.NetFramework%2FNetworkAndStream%2FNamedPipe%2FAsyncNamedPipe%2FClientSide "非同期名前付きパイプの基礎 / クライアントSide - Neareal"))
6. [.NETで名前付きパイプを試す(4) - 複数のクライアントに対応したサーバにする - いちろぐ](https://ichiroku11.hatenablog.jp/entry/2016/08/03/225606 ".NETで名前付きパイプを試す(4) - 複数のクライアントに対応したサーバにする - いちろぐ")

# 実装

C# および .NET の学習が不十分なので、これで問題ないのか心もとないですが、動作はしているようです。

基本的に文献 1. の非同期バージョンを C# に書き直しただけですが、クライアントからの入力をエコーするのではなく、双方向から入力できるようにしてみました。その際に参考になったのが文献 3. と 4. です。

なお、文献 1. の同期バージョンの PowerShell スクリプト[^1]を実行すると Ctrl + C も Ctrl + Break もきかなくなることもあり、不慣れな C# にチャレンジしてみました[^2]。

[^1]: Tera Term では `Unicode` を `UTF8` に置換する必要あり。また、 `exit` と `quit` は Tera Term からだとインプット メソッド (IME で半角英数に変換) をかまさないと効果がありません。

[^2]: 当該の同期バージョンのスクリプトを C# に書き直してみると Ctrl + C がききます。

```csharp:PipeServer.cs
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class PipeServer {
  private NamedPipeServerStream pipe;
  private byte[] buffer;
  
  PipeServer(string name) {
    this.pipe = new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
    this.buffer = new byte[512];
  }
  
  private void ConnectCallback(IAsyncResult result) {
    Console.WriteLine("Connected.");
    // this.pipe.EndWaitForConnection(result);
    ((NamedPipeServerStream)result.AsyncState).EndWaitForConnection(result);
  }
  
  private void ReadCallback(IAsyncResult result) {
    int len = ((NamedPipeServerStream)result.AsyncState).EndRead(result);
    if (len > 0)
      Console.Write(Encoding.UTF8.GetString(this.buffer, 0, len));
  }
  
  async private void WriteCallback(IAsyncResult result) {
    ((NamedPipeServerStream)result.AsyncState).EndWrite(result);
    while (!result.IsCompleted)
      await Task.Delay(10);
  }
  
  async private void ReadingThread() {
    while (this.pipe.IsConnected) {
      IAsyncResult ar = this.pipe.BeginRead(this.buffer, 0, this.buffer.Length, this.ReadCallback, this.pipe);
      while (!ar.IsCompleted)
        await Task.Delay(10);
    }
  }
  
  static void Main(string[] args) {
    PipeServer server = new PipeServer(args[0]);
    try {
      IAsyncResult ar = server.pipe.BeginWaitForConnection(server.ConnectCallback, server.pipe);
      Console.WriteLine("Waiting for client connection at \\\\.\\pipe\\" + args[0] + " ...");
      while (!ar.IsCompleted)
        Thread.Sleep(100);
      Thread t = new Thread(server.ReadingThread);
      t.Start();
      byte[] buf;
      ConsoleKeyInfo cki;
      while (server.pipe.IsConnected) {
        if (Console.KeyAvailable) {
          cki = Console.ReadKey(true);
          buf = Encoding.UTF8.GetBytes(new char[1] { (char)cki.KeyChar });
          if (buf.Length > 0)
            ar = server.pipe.BeginWrite(buf, 0, buf.Length, server.WriteCallback, server.pipe);
          while (!ar.IsCompleted)
            Thread.Sleep(10);
        }
      }
    } finally {
      server.pipe.Close();
    }
  }
}
```

これらのコールバック メソッドはそのコールバック メソッドを指定したメソッドの最後の引数のオブジェクトを `result.AsyncState` で受け取れるらしいのですが、当該の引数のオブジェクトの型にキャストしてやらなければメンバーにアクセスできないようです。 (文献 5.)

## 使い方

```console
C>C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe PipeServer.cs

C>PipeServer.exe test
```

`PipeServer.exe` の引数にパイプの名前 (例 `test`) を指定します。 Tera Term の **新しい接続** の **ホスト** に例として `\\.\pipe\test` と入力して接続します。

接続後、 Tera Term 側でキー入力すると対応する文字が `PipeServer.exe` 側のコンソールに出ます。その逆も同様です。

なお、 Enter キーを押しても入力されるのは `CR` (Ctrl + M) のみのようです。「改行」するには Ctrl + M, Ctrl + J と入力しなければなりません。 

Tera Term で **接続断** を選択するかコンソールで Ctrl + C を入力して接続を切ります。

# 改良版

名前付きパイプはパイプ ストリームを複数もつことができ、それぞれにクライアントを接続できます。よって、名前付きパイプを介して二つの Tera Term のインスタンスで通信できれば所期の目的は達成できることに気が付きました。また、文献 2. をもうすこし丁寧に見直したら、うってつけのメソッドがありました。

```csharp:PipeServer2.cs
using System;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;

class PipeServer {
  private static void ConnectCallback(IAsyncResult result) {
    ((NamedPipeServerStream)result.AsyncState).EndWaitForConnection(result);
  }

  static void Main(string[] args) {
    string name = args[0];
    Console.WriteLine("Waiting for client connection at \\\\.\\pipe\\" + name + " ...");
    using (NamedPipeServerStream stream1 = new NamedPipeServerStream(name, PipeDirection.InOut, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous),
           stream2 = new NamedPipeServerStream(name, PipeDirection.InOut, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)) {
      IAsyncResult ar = stream1.BeginWaitForConnection(ConnectCallback, stream1);
      while (!ar.IsCompleted)
        Thread.Sleep(100);
      ar = stream2.BeginWaitForConnection(ConnectCallback, stream2);
      while (!ar.IsCompleted)
        Thread.Sleep(100);
      Console.WriteLine("Connected.");
      while (stream1.IsConnected && stream2.IsConnected) {
        Task.WaitAll(new [] {
          stream1.CopyToAsync(stream2),
          stream2.CopyToAsync(stream1),
        });
      }
    }
  }
}
```

## 使い方

```console
C>C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe PipeServer2.cs

C>PipeServer2.exe test
```

`PipeServer2.exe` の引数にパイプの名前 (例 `test`) を指定します。

 Tera Term を二つ起動してそれぞれの **新しい接続** の **ホスト** に例としては `\\.\pipe\test` と入力して接続します。パイプ名は同一ですが、それぞれが別々のパイプ ストリームにつながります。

接続後、一方の Tera Term でキー入力すると対応する文字が他方の Tera Term に出ます。

こちらの方法では XMODEM などのファイル転送も可能です[^3] [^4]。

[^3]: XMODEM だと転送先のファイルの末尾に `^Z` が付加されますが、これはコメントでご指摘をいただきましたとおり XMODEM の仕様とのことです。
[^4]: [B-Plus](https://ttssh2.osdn.jp/manual/4/ja/usage/tips/b-plus.html "B-Plus のヒント") を除きます。

Tera Term で **接続断** を選択するかコンソールで Ctrl + C を入力して接続を切ります。

# 改良版 2

```csharp:PipeServer3.cs
using System;
using System.IO.Pipes;
using System.Threading.Tasks;

class PipeServer {
  static void Main(string[] args) {
    string name = args[0];
    NamedPipeServerStream stream1 = new NamedPipeServerStream(name, PipeDirection.InOut, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous),
                          stream2 = new NamedPipeServerStream(name, PipeDirection.InOut, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
    try {
      Console.WriteLine("Waiting for client connection at \\\\.\\pipe\\" + name + " ...");
      stream1.WaitForConnection();
      stream2.WaitForConnection();
      Console.WriteLine("Connected.");
      while (true) {
        Task.WaitAny(new [] {
          stream1.CopyToAsync(stream2),
          stream2.CopyToAsync(stream1),
        });
        if (!stream1.IsConnected) {
          stream1.Close();
          stream1 = new NamedPipeServerStream(name, PipeDirection.InOut, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
          stream1.WaitForConnection();
        } else if (!stream2.IsConnected) {
          stream2.Close();
          stream2 = new NamedPipeServerStream(name, PipeDirection.InOut, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
          stream2.WaitForConnection();
        }
      }
    } finally {
      stream1.Dispose();
      stream2.Dispose();
    }
  }
}
```

これで片方の接続が切れても再接続させることができます。ただし、両方の接続を切ってもパイプ サーバーは終了しません。パイプ サーバー起動中のコンソールで Ctrl + C を入力するしかないです。