Newer
Older
TIASShot / TIASshot / HwTest / HwTestRunner.Hang.cs
#if HWTEST
#if !SELFTEST
#error HWTEST は SELFTEST の部品(SelfTestReport)を使うため,SELFTEST と同時に定義すること
#endif
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace TIASshot {

    /// <summary>
    /// 実機テストのタイムアウト時に止まった場所を記録する(HWTEST 限定.本番・SELFTEST のみのビルドには含まれない).
    /// ウォッチドッグが項目・全体の上限を超えたと判定したときだけ呼ばれ,
    /// UI スレッド・保存スレッドのマネージドスタックを結果ファイルの詳細行に,自プロセスのミニダンプを作業フォルダに書く.
    /// どちらも上限時間を設けた別スレッドで行い,取得に失敗・時間切れしてもウォッチドッグの流れ(光源のポートを閉じる → 終了コード 3)は止めない.
    /// </summary>
    internal static partial class HwTestRunner {

        /// <summary>
        /// UI スレッドと保存スレッド(生きていれば)のマネージドスタックを取り,結果ファイルの詳細行("  - " の後ろ)にする行の並びを返す.
        /// 失敗・時間切れの場合も理由を行にして返す(例外は漏らさない).
        /// </summary>
        private static List<string> CaptureHangStacks(Session session) {
            var lines = new List<string>();
            AppendThreadStack(lines, "UI スレッド", session.UiThread);
            Thread saveThread = null;
            try {
                saveThread = session.Camera?.HwTestSaveThread;
            } catch (Exception ex) {
                lines.Add($"保存スレッド: 参照を取れません: {ex.GetType().Name}: {ex.Message}");
                Debug.WriteLine($"[HWTEST] 保存スレッドの参照を取れません: {ex}");
            }
            if (saveThread == null) {
                lines.Add("保存スレッド: なし(まだ起動していない)");
            } else if (!saveThread.IsAlive) {
                lines.Add("保存スレッド: 終了済み");
            } else {
                AppendThreadStack(lines, "保存スレッド", saveThread);
            }
            return lines;
        }

        /// <summary>
        /// 1 スレッドのマネージドスタックを取る.取得は別スレッドで行い,HwTestThresholds.HangStackTimeout で打ち切る
        /// (Suspend が安全点に届かない等でウォッチドッグ自体が止まらないため).
        /// </summary>
        private static void AppendThreadStack(List<string> lines, string label, Thread target) {
            if (target == null) {
                lines.Add($"{label}: スレッドが記録されていません");
                return;
            }
            var name = $"{label}(ManagedThreadId {target.ManagedThreadId}{(string.IsNullOrEmpty(target.Name) ? "" : $",{target.Name}")})";
            if (!target.IsAlive) {
                lines.Add($"{name}: 終了済みのためスタックを取れません");
                return;
            }
            string stack = null;
            string error = null;
            var helper = new Thread(() => {
                try {
                    stack = CaptureStack(target);
                } catch (Exception ex) {
                    error = $"{ex.GetType().Name}: {ex.Message}";
                    Debug.WriteLine($"[HWTEST] {label}のスタックを取れません: {ex}");
                }
            }) { IsBackground = true, Name = "HwTest.StackCapture" };
            helper.Start();
            if (!helper.Join(HwTestThresholds.HangStackTimeout)) {
                lines.Add($"{name}: スタックの取得が {HwTestThresholds.HangStackTimeout.TotalSeconds:0} 秒以内に終わりません(Suspend が戻らない可能性.ダンプで確認してください)");
                return;
            }
            if (error != null) {
                lines.Add($"{name}: スタックを取れません: {error}");
                return;
            }
            var frames = stack.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
            lines.Add($"{name}のマネージドスタック({frames.Length} 行.上が最も内側.ネイティブのフレームは含まない):");
            foreach (var f in frames) lines.Add($"    {f.Trim()}");
        }

        /// <summary>
        /// 対象スレッドを一時停止してマネージドスタックを取り,必ず再開する.
        /// .NET Framework 4.8 で他スレッドのスタックを取る唯一の手段のため,Obsolete の Thread.Suspend / Resume と
        /// StackTrace(Thread, bool) を使う(HWTEST 限定.タイムアウト後にプロセスを終了する直前だけ呼ぶ).
        /// </summary>
        private static string CaptureStack(Thread target) {
#pragma warning disable 618   // Thread.Suspend / Resume・StackTrace(Thread, bool) は Obsolete(他スレッドのスタックを取る目的に限って使う)
            target.Suspend();
            try {
                return new StackTrace(target, true).ToString();
            } finally {
                target.Resume();
            }
#pragma warning restore 618
        }

        /// <summary>
        /// 自プロセスのミニダンプを作業フォルダ(HwTestJudge.HangDumpPath)に書き,パスと大きさ(書けなければ WARN)を結果ファイルに出す.
        /// 種類(全メモリ/小さいダンプ/書かない)はプライベートメモリと空き容量から HwTestJudge.ChooseDumpKind で決める.
        /// 書き出しは別スレッドで行い,HwTestThresholds.HangDumpTimeout で打ち切る(例外は漏らさない).
        /// </summary>
        private static void WriteHangDump(Session session) {
            var report = session.Report;
            string path;
            HwTestDumpKind kind;
            long privateBytes = -1;
            long freeBytes = -1;
            try {
                path = HwTestJudge.HangDumpPath(session.WorkDir, DateTime.Now);
                Directory.CreateDirectory(session.WorkDir);
                using (var self = Process.GetCurrentProcess()) privateBytes = self.PrivateMemorySize64;
                var root = Path.GetPathRoot(Path.GetFullPath(path));
                freeBytes = new DriveInfo(root).AvailableFreeSpace;
                kind = HwTestJudge.ChooseDumpKind(privateBytes, freeBytes);
            } catch (Exception ex) {
                report.WriteLine($"  WARN: ミニダンプの準備に失敗したため書きません: {SelfTestReport.DescribeException(ex)}");
                return;
            }
            if (kind == HwTestDumpKind.None) {
                report.WriteLine($"  WARN: 空き容量が足りないためミニダンプを書きません(空き {FormatBytes(freeBytes)},必要 {FormatBytes(HwTestThresholds.SmallDumpMinFreeBytes)} 以上)");
                return;
            }

            string error = null;
            var sw = Stopwatch.StartNew();
            var helper = new Thread(() => {
                try {
                    WriteMiniDump(path, HwTestJudge.DumpTypeFlags(kind));
                } catch (Exception ex) {
                    error = SelfTestReport.DescribeException(ex);
                    Debug.WriteLine($"[HWTEST] ミニダンプを書けません: {path}: {ex}");
                }
            }) { IsBackground = true, Name = "HwTest.MiniDump" };
            helper.Start();
            var kindText = kind == HwTestDumpKind.Full
                ? "全メモリ"
                : $"小さいダンプ(全メモリにしなかった理由: プライベートメモリ {FormatBytes(privateBytes)},空き {FormatBytes(freeBytes)})";
            if (!helper.Join(HwTestThresholds.HangDumpTimeout)) {
                report.WriteLine($"  WARN: ミニダンプの書き出しが {HwTestThresholds.HangDumpTimeout.TotalSeconds:0} 秒以内に終わりません({kindText}.途中までのファイルが残る可能性: {path})");
                return;
            }
            if (error != null) {
                report.WriteLine($"  WARN: ミニダンプを書けませんでした({kindText}): {path}: {error}");
                return;
            }
            long size = -1;
            try {
                size = new FileInfo(path).Length;
            } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) {
                Debug.WriteLine($"[HWTEST] ミニダンプの大きさを取れません: {path}: {ex.Message}");
            }
            report.WriteLine($"  - ミニダンプ: {path}({kindText},{(size >= 0 ? FormatBytes(size) : "大きさ不明")},{sw.ElapsedMilliseconds} ms."
                + $"プライベートメモリ {FormatBytes(privateBytes)}.MINIDUMP_TYPE 0x{HwTestJudge.DumpTypeFlags(kind):X})");
        }

        /// <summary>dbghelp の MiniDumpWriteDump で自プロセスのダンプを書く(失敗は Win32Exception.途中まで書いたファイルは残す)</summary>
        private static void WriteMiniDump(string path, uint dumpType) {
            using (var self = Process.GetCurrentProcess())
            using (var stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) {
                if (!NativeMethods.MiniDumpWriteDump(self.Handle, (uint)self.Id, stream.SafeFileHandle, dumpType,
                        IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) {
                    // MiniDumpWriteDump は失敗の理由を HRESULT として GetLastError で返す
                    var err = Marshal.GetLastWin32Error();
                    throw new Win32Exception(err, $"MiniDumpWriteDump が失敗しました(0x{err:X8})");
                }
            }
        }

        private static string FormatBytes(long bytes) {
            if (bytes < 0) return "不明";
            return $"{bytes / (1024.0 * 1024.0):0.0} MB";
        }

        private static class NativeMethods {
            [DllImport("dbghelp.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeFileHandle hFile, uint dumpType,
                IntPtr exceptionParam, IntPtr userStreamParam, IntPtr callbackParam);
        }
    }
}
#endif