Try .NET Core

.NET Coreを動かした、試した記録を書き残します。

音楽サーバ"Mopidy"のフロントエンドを作る:14 Linuxでハマりやすいところ

音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第14回です。

今回は、Asp.Net CoreアプリをLinuxで動かす際にハマるポイントを追っていきます。

ソケットは自動で破棄されない

C#の動作環境である.Net VMガベージコレクションが付いてます。
なので、インスタンスの破棄を意識しないで書いていても、それなりに動いてくれます。

しかし、LinuxのSocket周りには注意が必要です。

Windows機の場合は、参照されなくなったSocketはガベコレが回収してくれます。
ところがLinuxでは、意図して破棄しないと、Socketをすぐに使い切ってしまいます。

昨今のC#では、TCP/UDPのSocketを生成する場面は少ないかもしれません。
しかし、例えばHttpClient
外部のWebサービスを使う際に便利な実装ですが、内部的にはSocketが生成されます。

これをDisposeしないまま放置しておくと、LinuxではやがてSocketを使い切ってしまい、接続出来なくなってしまいます。

例えば、こんな検証コードを書いてみました。

public class Program
{
    // Set your Mopidy Address
    private const string MopidyRpcUrl = "http://192.168.254.251:6680/mopidy/rpc";

    private static int _count = 0;

    public static void Main(string[] args)
    {
        var withUsing = false;
        if (args.Contains("--withoutusing"))
            withUsing = false;
        else if (args.Contains("--withusing"))
            withUsing = true;

        Console.WriteLine("Start Socket Overflow Test: " + ((withUsing) ? "with Using" : "without Using"));

        while (true)
        {
            try
            {
                Program._count++;

                var sendJson = $"{{\"jsonrpc\": \"2.0\", \"method\":\"core.playback.get_state\", \"id\": {Program._count}}}";
                var content = new StringContent(sendJson, Encoding.UTF8, "application/json");
                content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                Console.WriteLine("");
                Console.WriteLine("");
                Console.WriteLine($"Try {((withUsing) ? "with Using" : "without Using")} Count: " + Program._count.ToString());
                Console.WriteLine("Send Message: " + sendJson);

                var result = (withUsing)
                    ? Program.GetResultWithUsing(content)
                    : Program.GetResultWithoutUsing(content);

                Console.WriteLine("Result: " + result);

                Task.Delay(200).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception!");
                Program.DumpException(ex);

                Task.Delay(1000).GetAwaiter().GetResult();
            }
        }
    }

    private static string GetResultWithUsing(StringContent content)
    {
        using (var client = Program.GetHttpClient())
        {
            var message = client.PostAsync(Program.MopidyRpcUrl, content).GetAwaiter().GetResult();
            var result = message.Content.ReadAsStringAsync().GetAwaiter().GetResult();

            return result;
        }
    }

    private static string GetResultWithoutUsing(StringContent content)
    {
        var client = Program.GetHttpClient();

        var message = client.PostAsync(Program.MopidyRpcUrl, content).GetAwaiter().GetResult();
        var result = message.Content.ReadAsStringAsync().GetAwaiter().GetResult();

        return result;
    }

    private static HttpClient GetHttpClient()
    {
        var result = new HttpClient();
        result.DefaultRequestHeaders.Accept.Clear();
        result.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json")
        );
        result.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
        result.Timeout = TimeSpan.FromMilliseconds(1000);

        return result;
    }

    private static void DumpException(Exception ex)
    {
        Console.WriteLine("----------------------------------------");
        Console.WriteLine($"Message: {ex.Message}");
        Console.WriteLine($"StackTrace: {ex.StackTrace}");
        if (ex.InnerException != null)
            Program.DumpException(ex.InnerException);
    }
}

MopidyのJSON-RPCに、単純なクエリを繰り返すコードです。
引数を"--withusing"とすると、HttpClientをusingでラップして破棄するように。
"--withouusing"とすると、破棄しないで放置するようにしています。

このコードで、linux用バイナリを生成します。

# dotnet publish -c Release -r linux-x64

これをUbuntu18.0.4LTSで実行してみます。
ミニマルインストール後に、SSHと、実行ファイルコピー用のSambaを入れました。

まず、"--withoutusing"スイッチを付けて実行してみると...
f:id:try_dot_net_core:20190815164054p:plain

接続回数1004回目で、落ちてしまいます。
f:id:try_dot_net_core:20190815164204p:plain

CentOS7.6でも試してみます。
f:id:try_dot_net_core:20190815170330p:plain やはり、1004回目で落ちますね。
f:id:try_dot_net_core:20190815164501p:plain

そして、"--withusing"スイッチを付けて実行してみると。
Ubuntu18では...
f:id:try_dot_net_core:20190815165008p:plain 問題なし、です。

CentOS7.6でも、問題ありません。
f:id:try_dot_net_core:20190815164810p:plain

Windowsでも実行してみます。
まずば実行バイナリの生成。

# dotnet publish -c Release -r win-x64

そして、Linuxでは落ちてしまう"--withoutusing"スイッチを付けて実行すると... f:id:try_dot_net_core:20190815165157p:plain

こちらは、問題なく動き続けてしまいます。
f:id:try_dot_net_core:20190815165233p:plain

このように、Windowsでは動作に問題ないコードであっても、Linuxでは動かなくなることがあるんですね。
C#で通信している箇所は要注意、と見ておいたほうが良いでしょう。

一部NuGetパッケージのバージョン暗黙解釈に失敗する

下記は、Asp.NetCore2.2-MVCプロジェクト生成直後のcsprojファイルです。

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
  </ItemGroup>
</Project>

Microsoft.AspNetCore.Appに、バージョンが書かれていないんですね。
しかし、NuGetパッケージマネージャで見てみると。
f:id:try_dot_net_core:20190815172408p:plain
ここではきちんと、v2.2.0と表記があります。

どうやら、バージョンを暗黙的に解釈しているようです。

しかしこのままコードを作り進み、Linuxに持って行ってpublishしてみると...

[umuser@ume01srv tmp]$ dotnet publish -c Release -r linux-x64
Microsoft (R) Build Engine version 16.1.76+g14b0a930a7 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version.  [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605:  MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore (>= 2.2.6 && < 2.3.0)  [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605:  MopidyFinder -> Microsoft.EntityFrameworkCore (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore.Design from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version.  [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605:  MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore.Design (>= 2.2.6 && < 2.3.0)  [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605:  MopidyFinder -> Microsoft.EntityFrameworkCore.Design (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore.Tools from 2.2.6 to 2.2.4. Reference the package directly from the project to select a different version.  [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605:  MopidyFinder -> Microsoft.AspNetCore.App 2.2.6 -> Microsoft.EntityFrameworkCore.Tools (>= 2.2.6 && < 2.3.0)  [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj : error NU1605:  MopidyFinder -> Microsoft.EntityFrameworkCore.Tools (>= 2.2.4) [/mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.sln]
  Restore failed in 1.13 sec for /mnt/sdb/private/Work/DoBes/tmp/MopidyFinder.csproj.

こんなふうに、NuGetパッケージの整合性が取れずにエラーになることがあります。

この場合は、csprojファイル上のMicrosoft.AspNetCore.Appパッケージに、バージョンを書き加えます。

<PackageReference Include="Microsoft.AspNetCore.App" />

 ↓

<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />

これでpublishが通るようになりました。

今のところ、Microsoft.AspNetCore.App以外で、バージョン表記が無いという現象に行き当たったことはありません。
が、時々はcsprojファイルの中身もチェックしといた方が良さそうです。

パス文字列の大文字/小文字

これは言語に関わらず、出てくる問題ですね。
Webプログラミングをやっていれば必ず直面する問題のため、みなさま恐らく普段から気をつけていらっしゃると思います。

私も気を付けてはいたのですが...。
Mopidy.Finderでも、一度直面しました。こちらのコミットです。

キャメルケースのフォルダ名を「Sidebars」→「SideBars」と変更した際の現象です。
Windows上では「SideBars」に変わっていたのですが、WindowsのGitクライアントがその違いを認識できておらず。
Gitリポジトリ上ではずっと、「Sidebars」として保持されていました。

TypeScriptのコンパイルLinux上で実行することは滅多に無かったため、リリース直前まで気が付きませんでした。

いやはや、お恥ずかしい。

RaspberryPi(Raspbian)は32bitOS

最近のラズパイのCPUは既に64bit化されているのですが、OSはまだ32bit版なんですね。

CentOSUbuntuで動かしたバイナリをそのままコピペしてしまい、起動しない原因が分からずにしばらく悩みました。

Raspbianで動かすためのバイナリを作るには、publishの引数を下記のようにします。

# dotnet publish -c Release -r linux-arm

でも、乗り越えてしまえば

こまごまと問題に直面することも、あるとはいえ。
それらを乗り切ってしまえば、.Net Coreアプリは快調に動いてくれます。

最近のpublishの処理は、大変優秀です!
ミニマルインストールしたLinuxにバイナリをコピペするだけで、すんなりと動きます。

C#erとしては、まったく良い時代になったもんだ、としみじみ思います。

以上、Linuxのハマりポイントのおはなし、でした!

これで、予定していた記事を全て書き終えました。
またC#ネタが出てきたら、適当に書き連ねようと思います。
長々と駄文にお付き合いいただき、ありがとうございました!