PowershellでAsyncCallbackを使うにはどうしたらよいか

PowershellUdp双方向通信を行うスクリプトを書こうとしてハマった

前提

メインスレッド Udp送信担当
サブスレッド Udp受信担当
受信はメインスレッド内でUdpClient.BeginReceiveを呼び出して非同期処理にする。

コールバック関数を定義し、 Receiveが値を受け取った時に関数呼び出し(受信データを基にメインスレッドのフォーム更新や結果のUdp送信など)を実行して、再びUdpClient.BeginReceiveを呼ぶ

いわゆるAPM(Asynchronous Programming Model)ってやつです

スクリプト

受信開始とコールバックのところのみ

(略)
# UDP受信待ち受け
function udpreceiver_start {
    param(
        $ipaddress = "127.0.0.1", #バインドするアドレス
        $port = 4444  #バインドするポート
    )

    # ローカルエンドポイント定義
    $localEP = new-object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($ipaddress),$port)
    # リモートエンドポイント定義
    #$remoteEP = new-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0);
    # UDPサーバーを生成
    $udprecv = new-Object System.Net.Sockets.UdpClient($localEP)

    # 非同期的なデータ受信を開始する
    Write-Host "BeginReceive!"
    $handle = $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv);
}

# UDP受信callback
function udpreceiver_callback {
    param(
        $ar
    )

    Write-host "Callback!!"

    $udprecv = [System.Net.Sockets.UdpClient]{$ar.AsyncState};

    # 非同期受信を終了する
    [System.Net.IPEndPoint]$remoteEP = $null;
    try{
        $buf = $udprecv.EndReceive($ar,[ref]$remoteEP);
    }
    catch [System.Net.Sockets.SocketException] {
        Write-Host ("受信エラー({0}/{1})" -f $_.Message, $_.ErrorCode)
        return;
    }
    catch [ObjectDisposedException] {
        //すでに閉じている時は終了
        Write-Host "Socketは閉じられています。"
        return;
    }

    $str = [System.TexT.Encoding]::UTF8.GetString($buf)

    Write-host "データを受信しました"
    Write-Host ("受信したデータ:{0}" -f $str)
    Write-Host ("送信元アドレス:{0}/ポート番号:{1}" -f $remoteEP.Address, $remoteEP.Port)

    //再びデータ受信を開始する
    $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv);
}
(略)
udpreceiver_start
(略)

問題点

UDPでデータを受信すると・・・

f:id:hightoro:20170816234635p:plain

_人人 人人_
> 突然の死 <
 ̄YYYY

原因分析1

コールバックとして普通の関数を[System.AsyncCallback]でキャストしたものを渡したのが原因だろうかと思って、そこをC#で書いて渡してみる。

(略)
# UDP受信待ち受け
function udpreceiver_start {
    param(
        $ipaddress = "127.0.0.1", #バインドするアドレス
        $port = 4444  #バインドするポート
    )

    # ローカルエンドポイント定義
    $localEP = new-object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($ipaddress),$port)
    # リモートエンドポイント定義(空:後で使う)
    $remoteEP = new-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0);
    # UDPサーバーを生成
    $udprecv = new-Object System.Net.Sockets.UdpClient($localEP)

    # 非同期的なデータ受信を開始する #無理だった
    Write-Host "BeginReceive!"
    #$handle = $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv);
    $handle = $udprecv.BeginReceive([Udp]::udpreceiver_callback,$udprecv);
}


Add-type @"
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public static class Udp
    {
        public static void udpreceiver_callback(IAsyncResult ar=null)
        {
            Console.WriteLine("Callback!");

            var udprecv = (System.Net.Sockets.UdpClient)ar.AsyncState;
            byte[] buf;

            // 非同期受信を終了する
            var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0);
            try
            {
                buf = udprecv.EndReceive(ar, ref remoteEP);
            }
            catch(System.Net.Sockets.SocketException ex)
            {
                Console.WriteLine("受信エラー({0}/{1})",ex.Message, ex.ErrorCode);
                return;
            }
            catch(ObjectDisposedException)
            {
                //すでに閉じている時は終了
                Console.WriteLine("Socketは閉じられています。");
                return;
            }

            var str = System.Text.Encoding.UTF8.GetString(buf);

            Console.WriteLine("データを受信しました");
            Console.WriteLine("受信したデータ:{0}", str);
            Console.WriteLine("送信元アドレス:{0}/ポート番号:{1}", remoteEP.Address, remoteEP.Port);

            //再びデータ受信を開始する
            Console.WriteLine("BeginReceive!");
            udprecv.BeginReceive(udpreceiver_callback, udprecv);
        }
    }
"@
(略)
udpreceiver_start
(略)

f:id:hightoro:20170816234635p:plain

_人人 人人_
> 突然の死 <
 ̄YYYY

ダメでした

対応策

全部C#で書く(爆)

(略)
Add-type @"
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public static class Udp
    {
        public static void udpreceiver_start(
            //AsyncCallback callback,
            string ipaddress = "127.0.0.1", //バインドするアドレス
            int port = 4445  //バインドするポート
        )
        {
            //ローカルエンドポイント定義
            var localEP = new System.Net.IPEndPoint(System.Net.IPAddress.Parse(ipaddress), port);
            //リモートエンドポイント定義(空:後で使う)
            var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0);
            //UDPサーバーを生成
            var udpClient = new System.Net.Sockets.UdpClient(localEP);

            //非同期的なデータ受信を開始する
            Console.WriteLine("BeginReceive!");
            //BeginReceive2(callback, udpClient);
            udpClient.BeginReceive(udpreceiver_callback, udpClient);
        }

        public static void udpreceiver_callback(IAsyncResult ar=null)
        {
            Console.WriteLine("Callback!");

            var udprecv = (System.Net.Sockets.UdpClient)ar.AsyncState;
            byte[] buf;

            // 非同期受信を終了する
            var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0);
            try
            {
                buf = udprecv.EndReceive(ar, ref remoteEP);
            }
            catch(System.Net.Sockets.SocketException ex)
            {
                Console.WriteLine("受信エラー({0}/{1})",ex.Message, ex.ErrorCode);
                return;
            }
            catch(ObjectDisposedException)
            {
                //すでに閉じている時は終了
                Console.WriteLine("Socketは閉じられています。");
                return;
            }

            var str = System.Text.Encoding.UTF8.GetString(buf);

            Console.WriteLine("データを受信しました");
            Console.WriteLine("受信したデータ:{0}", str);
            Console.WriteLine("送信元アドレス:{0}/ポート番号:{1}", remoteEP.Address, remoteEP.Port);

            //再びデータ受信を開始する
            udprecv.BeginReceive(udpreceiver_callback, udprecv);
        }

    }
"@
(略)
[Udp]::udpreceiver_start()
(略)

これだとうまくいきました。

改良版

PowershellのUdpClientではBeginReceive()メソッドを呼び出さず、C#で記述したstaticメソッド[Udp]::BeginReceiveの中で呼び出すようにすると、 問題点1を解決しつつ、startのメソッドをpowershellにできます。

(略)
# UDP受信待ち受け
function udpreceiver_start {
    param(
        $ipaddress = "127.0.0.1", #バインドするアドレス
        $port = 4445  #バインドするポート
    )

    # ローカルエンドポイント定義
    $localEP = new-object System.Net.IPEndPoint([System.Net.IPAddress]::Parse($ipaddress),$port)
    # リモートエンドポイント定義(空:後で使う)
    $remoteEP = new-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0);
    # UDPサーバーを生成
    $udprecv = new-Object System.Net.Sockets.UdpClient($localEP)

    # 非同期的なデータ受信を開始する
    Write-Host "BeginReceive!"
    #$handle = $udprecv.BeginReceive([System.AsyncCallback]${function:udpreceiver_callback},$udprecv);
    $handle = [UDP]::BeginReceive($null,$udprecv)
}

Add-type @"
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public static class Udp
    {
        public static void udpreceiver_callback(IAsyncResult ar)
        {
            Console.WriteLine("Callback!");

            var udprecv = (System.Net.Sockets.UdpClient)ar.AsyncState;
            byte[] buf;

            // 非同期受信を終了する
            var remoteEP = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0);
            try
            {
                buf = udprecv.EndReceive(ar, ref remoteEP);
            }
            catch(System.Net.Sockets.SocketException ex)
            {
                Console.WriteLine("受信エラー({0}/{1})",ex.Message, ex.ErrorCode);
                return;
            }
            catch(ObjectDisposedException)
            {
                //すでに閉じている時は終了
                Console.WriteLine("Socketは閉じられています。");
                return;
            }

            var str = System.Text.Encoding.UTF8.GetString(buf);

            Console.WriteLine("データを受信しました");
            Console.WriteLine("受信したデータ:{0}", str);
            Console.WriteLine("送信元アドレス:{0}/ポート番号:{1}", remoteEP.Address, remoteEP.Port);

            //再びデータ受信を開始する
            udprecv.BeginReceive(udpreceiver_callback, udprecv);
        }
        // UdpClient.BeginReceive(callback)を呼び出す関数
        public static System.IAsyncResult BeginReceive(
            Action<IAsyncResult> callback,
            System.Net.Sockets.UdpClient udpClient
        )
        {
            return udpClient.BeginReceive( _ => {udpreceiver_callback(_);}, udpClient);
        }
    }
"@

udpreceiver_start

ちなみにpowershellで書いた関数をコールバック内で呼ぶだけで死ぬので、コールバックはC#で書くしかなかった。どうしようもなかった。

参考にしたリンク先

UDPによりデータの送受信を行う: .NET Tips: C#, VB.NET