418 I'm a teapot

元ニートのIT新参がなんかする

CLR20r3と格闘

CLR20r3があらわれた!

先日配布を始めたアプリに寄せられたバグ報告として、起動すらせずに落ちるというものがあった
自分の環境ではもちろん動くし、バグ報告をしてきた一人以外は問題なく動いてるっぽい
エラー表示は何かでないのかと聞いたところ以下のようなものらしい

問題のイベント名:CLR20r3
問題の署名01:○○○.exe
~中略~
問題の署名07:f
問題の署名08:182
問題の署名09:N3CTRYE2KN3C34SGL4ZQYRBFTE4M13NB

CLR20r3なんて初めてみたがな……

結論

結論から言うと、コンストラクタ内でNull Reference Exceptionが発生していたのが原因だった
解決方法は

  1. 例外をなんとかして補足してe.Exception.GetType();する
  2. 例外の種類に合わせてスタックトレースやらなんやら情報を吐き出させる
  3. がんばる

以下6時間の格闘の軌跡
自分の手元で再現できない(と思っていた)せいもあって時間が超かかった

.NETのバージョン?編

.NET 3.5のアプリを.NET 2.0と.NET 4.0環境で動かすと落ちる – majishini
Tech TIPS:.NET Frameworkのバージョンを整理する (1/2) - @IT
CLR20r3でググると.NETのバージョンや修正パッチの未適用に起因する場合があるということでインストールし直してみてもらう
→効果なし

ildasmを使う編

引き続きググると、ildasmを用いて逆アセンブルして原因を突き止めましょうというような記述が多いのでやってみる
VisualStudio→ツール→外部ツール→追加からメニューにildasmを追加してメニューから起動できるようにする
タイトル:今回はildasm
コマンド:ildasmのパスを入力 自分の環境ではC:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools\ildasm.exeを入力した
引数:$(TargetPath)と入力
@IT:.NET TIPS Visual Studio .NETからILDASMを起動するには? - C# VS.NET
メニューに追加されたのでildasmを使って問題の署名を読み解いていく
DD開発ROOM: .NETプログラム例外落ちイベント(CLR20r3)を読み取る
ツール→ildasm起動→表示→メタ情報→表示!を選択

問題の署名07

問題の署名07が欠陥アセンブリの型とメソッド(16進数)を表しているとのこと
自分はfと表示がでていたのでこの場合は0600000fで検索をかけるもよう

Method #8 (0600000f) 
-------------------------------------------------------
	MethodName: .ctor (0600000F)
	Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor]  (00001886)
	RVA       : 0x000022a8
	ImplFlags : [IL] [Managed]  (00000000)
	CallCnvntn: [DEFAULT]
	hasThis 
	ReturnType: Void
	No arguments.

なるほどわからん
.ctorで調べてみるとコンストラクタを表すとのこと。
当時の自分はスルーしちゃったけど今思えばここにちょっとヒントがあったらしい

問題の署名08

問題の署名08が欠陥メソッドのIL命令(16進数)とやらを表しているらしいのでみてみる。
参考サイトの例に倣って「IL_0182」で検索するがヒットしない……どころか「IL_」すら一件もヒットなし

問題の署名09

問題の署名09がスローされた例外の種類(32文字制限付き)を表すらしい ただし長過ぎるとハッシュ化
一応N3CTRYE2KN3C34SGL4ZQYRBFTE4M13NBをググってみるも有力な情報を得られず

エラーの詳細に迫る編

イベントハンドラを利用したい

vb.netアプリケーションで発生した例外エラー - Visual Basic | 【OKWAVE】のベストアンサーの参考URLにある
捕捉されなかった例外がスローされたことを知る: .NET Tips: C#, VB.NETを参考に、AppDomain.UnhandledExceptionイベントハンドラを利用することにしてみる

static void Main(string[] args)
{
    //UnhandledExceptionイベントハンドラを追加する
    System.AppDomain.CurrentDomain.UnhandledException +=
        new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

    //例外をスローする
    throw new Exception("エラーのテストです。");
}

なるほどMainメソッドの中でイベントハンドラを追加するんだな
WPFのメインメソッドってあったっけ

WPFのMainメソッド

WPF と Main メソッド - しばやん雑記
つまりApp.xamlのビルドアクションをApplicationDefinitionからPageに変えたうえでApp.xaml.csにMainメソッドを書いてやればいいらしい

public partial class App : System.Windows.Application
{
    [STAThread]
    public static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

        App app = new App();
        app.InitializeComponent();
        app.Run();
    }

    private static void CurrentDomain_UnhandledException(object sender,UnhandledExceptionEventArgs e)
    {
    	/* 中略 エラーの種類をMessageBoxで表示させる */
    }
}

動かしてもらったところエラーは補足されなかったとのこと
以前と全く同じエラーの出方だしメッセージボックスもでないらしい

DispatcherUnhandledExceptionイベント

他に有効そうなものがないかグーグル先生にお尋ねする
WPFサンプル:未処理例外に対応する:Gushwell's C# Dev Notes
App.xamlにDispatcherUnhandledExceptionイベントを追加してApp.xaml.csに以下を記述

public partial class App : System.Windows.Application
{
    private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
    {
        string message = string.Format("{0} {1}", e.Exception.GetType(), e.Exception.Message);
        MessageBox.Show(message);
  }
}

実行してもらったところメッセージボックスが出た!

System.Reflection.TargetInvocationException 呼び出しのターゲットが例外をスローしました。

TargetInvocationException

とりあえずググる
InnerExceptionは誰が設定するのか(例外が再スローされるときにいつでも設定される訳ではない) - tekkの日記 C#,VB.NET
つまりTargetInvocationExceptionのInnerExceptionには本来の例外が設定されてる
じゃあそれを踏まえて表示させていけば解決の手がかりになりそう
TargetInvocationExceptionのプロパティで役に立ちそうなものは全部出力してみる

private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
    var ex = (TargetInvocationException)e.Exception;
    MessageBox.Show("Data:" + ex.Data.ToString() +
                    "\nInnerException:" + ex.InnerException.ToString() +
                    "\nMessage:" + ex.Message +
                    "\nSource:" + ex.Source +
                    "\nStackTrace:" + ex.StackTrace +
                    "\nTargetSite:" + ex.TargetSite.ToString() +
                    "\nException:" + ex.ToString());
}

String.Formatメソッドで書いたら何かそこで例外が発生してめんどくさかったから思考停止文字列連結
結果は

~中略~
InnerException:System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
場所 (アセンブリ名).MainWindow..ctor() 場所 D:\Users\(中略 ファイルのフルパス)\MainWindow.xaml.cs:行 114
~中略~

ぬるりだと……
114行目を見てみるとそこにはレジストリキーを使ってあるソフトのインストールパスを取得して、それを変数に代入してる処理
これで解決できたからいいけど今思えばInnerExceptionのほうの色々を出力したほうが良かったかも

余談だけど、いままでの試行錯誤は全てバグが出ると言ってる人にexeを渡して実行してもらって結果を受け取ってる
なのにファイルのフルパスに表示されてたものが完全に筆者の環境のものですごく焦った(ユーザ名も含まれている)
なんなんだろうこれ心臓に悪すぎた

レジストリキー取得の罠

nullなら入れてあげれば解決じゃんということで原因調査
本アプリのユーザなら間違いなくインストールしているはずのソフトのレジストリキーが取得できてないという謎の事態
レジストリを参照できないからインストールフォルダを特定できずにぬるりが発生している
とりあえずインストールしてるPCで動かしているかどうか聞いてみるけどしているとの返答

一旦フォルダ選択ダイアログでユーザ自身でインストールフォルダを指定するようにしたものを渡して動かしてもらう→動いたとのこと

調べるとどうやら32bitと64bitでレジストリキーの場所が違うらしい
x64 Windows でのレジストリの扱い - Life like a clown
確認したところ、確かにバグの出るユーザのみ32bitだったから原因はこれで間違いなさそう
上記のページを踏まえてWow6432Nodeありで取得してnullだったらWow6432Nodeなしで取得を試みるといいんだなと思い実践
Before

var regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\SoftwareName", false);
this.folderPath = (string)regkey.GetValue("InstallLocation");

After

var regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\SoftwareName", false);
if (regkey == null)
{
    regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SoftwareName", false);
}
this.folderPath = (string)regkey.GetValue("InstallLocation");

……取得できない!
そんなに単純なことじゃなかったのかなと思いつつ更に調べる
[C#, dotNet4.0] レジストリのリダイレクトを回避 | 学習B5デスノート
どうやらWow6432Nodeにリダイレクトされているらしい
ということで[.NET, C#]レジストリの値が取得できない原因を参考にしながらコードを変更する

var regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\SoftwareName", false);
if (regkey == null)
{
  var reg32 = Microsoft.Win32.RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64);
    regkey = reg32.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SoftwareName", false);
}
this.folderPath = (string)regkey.GetValue("InstallLocation");

取得できた!
なんでRegistryView.Registry64と書くことで32bitのレジストリが参照できるのかはわからないけど取得できた!
長い戦いだった