#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