それ自体が落ちた、またはコロボックが調査をリードする







そのため、アプリケーションをデプロイし、適切で経験豊富な開発者として、クラッシュレポーターを挿入することを忘れませんでした。 最初のレポートを取得し、スタックを開き、周囲を見て、再現を試み、中断し、「これを何と言いましたか?」という質問をします。 しかし、それはどうして起こったのでしょうか?」アプリケーションがクラッシュするために、ユーザーは何をしましたか?



そして、ここでアプリケーションのデバッグログ、または少なくとも開いた最後のユーザーアクションのログ、突かれた場所など。 さらに、WinFormsアプリケーションでユーザーアクションのログを記録し、このログをクラッシュレポートに含めることに焦点を当てます。



最初に思い浮かぶのは、単純なアプローチです。各ユーザーアクションのハンドラーを入力し、ログにエントリを追加します。 しかし、これは膨大な作業と馬鹿げたコードです。 さて、内部のすべての人に必要なイベントをサブスクライブする基本的なフォーム/コントロールを実行できますか? ああ、コントロールは追加と削除ができるようになり、リークリークアプリケーションとこれらすべてがコードのために一緒に遅くなります。 しかし、基本的なフォームなしでのみ同じことを書き、フォームの変更を追跡し、多数のイベントをサブスクライブし、それでもスローダウンしませんか?



フックの前にすべてがすでに発明されていることがわかりました! しかし、すみません、これはどのようなWinFormsなのですか、それは同じWin32 APIなのですか?! そして、 あなたはを生きたいです-あなたは腕を使い果たしたり、行きませんか?



そのため、ユーザーがアプリケーションをあふれさせるために行ったことに興味があります。 つまり、マウスを使ってどこでどのように突っ込んだか、どのキーを押したのか、何を入力したのか、どのウィンドウを開いたのか、フォーカスがどのように移動したのかなどです。 開始するには十分です。



マウスフックは、 通常のマウスと低レベルのマウスを担当します 。 キーボードの場合、それぞれキーボード、 通常および低レベル 。 1つのアプリケーションのみで入力を監視する必要があるため、おそらく、低レベルのグローバルフックは多すぎます。



叙情的な余談
しかし、低レベルのフックは、コンソールアプリケーションにマウスコントロールを追加するなど、特定のタスクに最も適しています。これは、聴覚でも精神でもないマウスに関するものです。 さらに、コンソールだけでなく、DOSでも使用できます。 むかしむかし、著者は、 fidoshnikのとき、低レベルのマウスフックを使用してゴールデッドマウス入力を教えました 。 後で判明したように、他の多くのdosプログラムで機能しました。



そして、通常のフックで十分なので、すぐにメッセージフックに掛けることができます。1つのボトルにマウスとキーボードがあります。 ウィンドウのアクティブ化とフォーカスの移動は、 CBTフックで簡単に捕捉できます。



掘る場所は明確です



行こう!
相互運用性について説明します。



delegate int HookProc(int ncode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] static extern int GetCurrentThreadId (); [DllImport("USER32.dll", CharSet = CharSet.Auto)] static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId); [DllImport("USER32.dll", CharSet = CharSet.Auto)] static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("USER32.dll", CharSet = CharSet.Auto)] static extern bool UnhookWindowsHookEx(IntPtr hhk);
      
      





購読する:



 const int WH_GETMESSAGE = 3; const int WH_CBT = 5; IntPtr getMsgHookHandle = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, IntPtr.Zero, threadId); IntPtr cbtHookHandle = SetWindowsHookEx(WH_CBT, CbtProc, IntPtr.Zero, threadId) static int GetMsgProc(int nCode, IntPtr wParam, IntPtr lParam) { return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); } static int CbtProc(int nCode, IntPtr wParam, IntPtr lParam) { return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); }
      
      





スレッドごとにフックを設定する必要があります(threadIdを参照)



 static readonly Dictionary<int, HookInfo> hooks = new Dictionary<int, HookInfo>();
      
      





どこで



 class HookInfo { internal IntPtr GetMsgHookHandle; internal IntPtr CbtHookHandle; internal bool InHook; }
      
      





ディクショナリをサブスクライブするときはエントリを追加し、サブスクライブを解除するときは削除し、順序どおりにすべてを修復します。



Hrenak、hrenak、そして生産中! ええ、shchazz、これは.NET、ベイビーです。 CallNextHookEx呼び出しの直後に、フックが1〜2サブスクリプション/サブスクライブ解除された後、すぐではありません。 宙ぶらりんのポインターがあることは直感的に明らかですが、コードをざっと見てみるとわかりませんが、なぜですか? SetWindowsHookExを注意深く見てみましょう。 2番目のパラメーターは、ハンドラー関数へのポインターです。 つまり、ハンドラーのアドレスは、C#の観点ではIntPtrになります。 Soooo、HookProc lpfnをIntPtr lpfnに置き換えるとどうなりますか? 次に、 マーシャルを使用する必要があります GetFunctionPointerForDelegateを使用して、デリゲートからアドレスを取得します。 元のコードを見ます-デリゲートを明示的に作成しません。 ilspyの助けを借りて見ます:



 SetWindowsHookEx(3, new HookProc(Win32HookManagerBase.GetMsgProc), IntPtr.Zero, threadId);
      
      





トナカイです! 構文糖衣、ありがとう、私たちから重要なものを隠しました-アンマネージコードに正常に渡されるアドレスを含む中間デリゲートオブジェクトが作成されます。 その後、デリゲートオブジェクトはガベージコレクターによって正常に処理され、どこにも制御を移そうとするとアンマネージコードがクラッシュし始めます。



OK、デリゲートを明示的に保存します。



 static HookProc procGetMsg = Win32HookManager.GetMsgProc; static HookProc procCbt = Win32HookManager.CbtProc; SetWindowsHookEx(WH_GETMESSAGE, procGetMsg, IntPtr.Zero, threadId); SetWindowsHookEx(WH_CBT, procCbt, IntPtr.Zero, threadId);
      
      





コンパイル、チェック-動作します。



彼らは戦いの半分を行い、イベントの通知を受け取ることを学びました。 次に、イベントをわかりやすく人間が読める形式で表示できるようにする情報を収集して記録する必要があります。



例を挙げる



マウスメッセージ
 Win32.WindowsMessages message = (Win32.WindowsMessages)m.Msg; if (message == Win32.WindowsMessages.WM_LBUTTONUP) ProcessMouseMessage(ref m, MouseButtons.Left, true); else if (message == Win32.WindowsMessages.WM_RBUTTONUP) ProcessMouseMessage(ref m, MouseButtons.Right, true); else if (message == Win32.WindowsMessages.WM_MBUTTONUP) ProcessMouseMessage(ref m, MouseButtons.Middle, true); else if (message == Win32.WindowsMessages.WM_LBUTTONDOWN) ProcessMouseMessage(ref m, MouseButtons.Left, false); else if (message == Win32.WindowsMessages.WM_RBUTTONDOWN) ProcessMouseMessage(ref m, MouseButtons.Right, false); else if (message == Win32.WindowsMessages.WM_MBUTTONDOWN) ProcessMouseMessage(ref m, MouseButtons.Middle, false); void ProcessMouseMessage(ref Message m, MouseButtons button, bool isUp) { Dictionary<string, string> data = new Dictionary<string, string>(); data["mouseButton"] = button.ToString(); data["action"] = isUp ? "up" : "down"; Breadcrumb item = new Breadcrumb(); item.Event = isUp ? BreadcrumbEvent.MouseUp : BreadcrumbEvent.MouseDown; item.CustomData = data; AddBreadcrumb(item); }
      
      







次に、ユーザーアクションの記録をオンにして、ボタンのいずれかのハンドラーにアプリケーションをドロップする単純なアプリケーションを作成し、「再生の手順」を含むクラッシュレポートがシステムに送信されるようにします。







 static void Main() { LogifyAlert client = LogifyAlert.Instance; client.ApiKey = "<my-api-key>"; client.CollectBreadcrumbs = true; client.StartExceptionsHandling(); // etc } void showMessageButton_Click(object sender, EventArgs e) { MessageBox.Show(this, "text"); } void crashMeButton_ClickEvent(object sender, EventArgs e) { object o = null; o.ToString(); }
      
      





サーバー側では、収集された情報に基づいて、次のようなユーザーアクションの説明を生成します。



 string GenerateMouseEventMessage(Breadcrumb breadcrumb) { return GetBreadcrumbCustomField(breadcrumb, "mouseButton") + " mouse button" + GetBreadcrumbCustomField(breadcrumb, "action"); }
      
      





送られたメモを開いて確認します







何も明確ではないことは明らかです。 はい、ユーザーはマウスでつついていますが、正確にはどこですか? そして、記録も送信もしませんでした。 マウスメッセージについて興味深い点は何ですか? ああ、 hWnd 、必要なもの。



 [DllImport("user32.dll")] static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); string GetWindowText(ref Message m) { StringBuilder windowText = new StringBuilder(1024); GetWindowText(m.Hwnd, windowText, 1024); return windowText.ToString(); }
      
      





同時に、ユーザーが押したキーをログに入力します。



お客様
 if (message == Win32.WindowsMessages.WM_KEYDOWN) ProcessKeyMessage(ref m, false); else if (message == Win32.WindowsMessages.WM_KEYUP) ProcessKeyMessage(ref m, true); else if (message == Win32.WindowsMessages.WM_CHAR) ProcessKeyCharMessage(ref m); void ProcessKeyMessage(ref Message m, bool isUp) { Dictionary<string, string> data = new Dictionary<string, string>(); data["key"] = ((Keys)m.WParam).ToString(); data["action"] = isUp ? "up" : "down"; data["windowCaption"] = GetWindowText(ref m); Breadcrumb item = new Breadcrumb(); item.Event = isUp ? BreadcrumbEvent.KeyUp : BreadcrumbEvent.KeyDown; item.CustomData = data; AddBreadcrumb(item); } void ProcessKeyCharMessage(ref Message m) { Dictionary<string, string> data = new Dictionary<string, string>(); data["char"] = new string((char)m.WParam, 1); data["action"] = "press"; data["windowCaption"] = GetWindowText(ref m); Breadcrumb item = new Breadcrumb(); item.Event = BreadcrumbEvent.KeyPress; item.CustomData = data; AddBreadcrumb(item); }
      
      







サーバー
 string GenerateMouseEventMessage(Breadcrumb breadcrumb) { return GetBreadcrumbCustomField(breadcrumb, "mouseButton") + " mouse button" + GetBreadcrumbCustomField(breadcrumb, "action") + " over " + GetBreadcrumbCustomField(breadcrumb, "windowCaption"); } string GenerateKeyEventMessage(Breadcrumb breadcrumb) { return GetBreadcrumbCustomField(breadcrumb, "key") + GetBreadcrumbCustomField(breadcrumb, "action"); } string GenerateKeyCharEventMessage(Breadcrumb breadcrumb) { string character = GetBreadcrumbCustomField(breadcrumb, "char"); if (!String.IsNullOrEmpty(character)) return "Type " + character; else return GetBreadcrumbCustomField(breadcrumb, "key") + " press"; }
      
      







新しいクラッシュレポートを再構築、実行、生成、監視します。







もう悪くない。 そして、キーストロークは適切に思えたので、パスワードの収集を開始できます。 鳥といえば、パスワード。 ユーザーの明示的な同意なしに個人情報を送信することはおそらく良くありません。 たぶん



アスタリスクで置き換える
 bool ProcessKeyMessage(ref Message m, bool isUp) { string key = ((Keys)m.WParam).ToString(); bool maskKey = IsPasswordBox(ref m); if (maskKey) key = Keys.Multiply.ToString(); Dictionary<string, string> data = new Dictionary<string, string>(); data["key"] = key; // etc } bool ProcessKeyCharMessage(ref Message m) { char @char = (char)m.WParam; bool maskKey = IsPasswordBox(ref m); if (maskKey) @char = '*'; Dictionary<string, string> data = new Dictionary<string, string>(); data["char"] = new string(@char, 1); // etc }
      
      







ここで、IsPasswordBoxメソッドを実装する必要があります。これは非常に優れたものになります。 通常の入力フィールドとパスワード入力フィールドの違いは何ですか? もう一度、メモリの深さからWinAPIのメモリを思い出します。ここでそのような例を見つけます。 これから、ゼロ以外のパスワードcharを持つES_PASSWORDスタイルの編集コントロールが必要であることがわかり ます 。 私たちは実現します:



 [DllImport("USER32.dll")] static extern int GetWindowLong(IntPtr hwnd, int flags); [DllImport("USER32.dll")] static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); static bool IsPasswordBox(IntPtr hWnd) { const int ES_PASSWORD = 32; int style = GetWindowLong(hWnd, GWL_STYLE); if ((style & ES_PASSWORD) == 0) return false; int result = SendMessage(hWnd, EM_GETPASSWORDCHAR, IntPtr.Zero, IntPtr.Zero); return result != 0; }
      
      





取得するもの:







ボタンと入力フィールドを使用すると、すべてがうまく機能します。 そして、パズルを複雑にしたら? 複雑なコントロール、グリッド、リボンを備えたデモアプリケーションに統合します。







実行し、グリッド内のリボンのボタンをつついて、結果を確認します。







悲しい光景。 リボン全体にhWndが1つだけあり、グリッドに2つ、グリッド自体に1つ、アクティブなエディターに1つがあります。 そして、私たちはこのすべての善で何をすべきでしょうか? 特定のコントロールのギブレットを掘り下げることは選択肢ではありません。異なるベンダーからのそれらが多すぎるため、内部は非常に定期的に変化する可能性があります。 セクション508を思い出し、それが何であり、何と一緒に食べられるのかを調べ始めます。 その結果、 IAccessibleについて学び、しばらくしてAccessibleObjectFromWindowメソッドとAccessibleObjectFromPointメソッドについて学びます。 最初の方法はキーボードイベントに最適で、2番目の方法はマウスイベントに最適です。



別のコード
 [return: MarshalAs(UnmanagedType.Bool)] static extern bool ClientToScreen(IntPtr hwnd, ref POINT point); [DllImport("oleacc.dll")] static extern IntPtr AccessibleObjectFromPoint(POINT pt, [Out, MarshalAs(UnmanagedType.Interface)] out IAccessible accObj, [Out] out object ChildID); [DllImport("oleacc.dll")] static extern int AccessibleObjectFromWindow(IntPtr hwnd, int id, ref Guid iid, [In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object ppvObject); static IAccessible GetAccessibleObject(IntPtr hWnd) { Guid guid = new Guid("618736e0-3c3d-11cf-810c-00aa00389b71"); Object instance = null; const int OBJID_WINDOW = 0x0; int hResult = AccessibleObjectFromWindow(hWnd, OBJID_WINDOW, ref guid, ref instance); if (hResult != 0 || instance == null) return null; return instance as IAccessible; } static IAccessible GetAccessibleObject(IntPtr hWnd, Point point) { IAccessible accObj; object obj = new object(); POINT pt = new POINT(); pt.X = point.X; pt.Y = point.Y; ClientToScreen(hWnd, ref pt); if (AccessibleObjectFromPoint(pt, out accObj, out obj) != IntPtr.Zero) return null; if (accObj == null) return null; return accObj; }
      
      







それは技術の問題です。 ゼロ以外のIAccessibleを取得した場合、 AccessibleRoleを分析し、必要に応じコントロールとその親のaccNameを記述します。



そして再び歌詞
ユーザーがマウスで何を突いたかを調べるには、少なくとももう1つの方法があります。 より正確には、これも:ユーザーがどのテキストに突入したか。 しかし、この方法は型破りであるため、著者はそれを使用することを控え、直腸扁桃摘出術にふさわしく告発されないようにします。 この方法の本質は次のとおりです。 このようなWinAPIテキストレンダリング関数をインターセプトする方法を学びます。 さらに、脇の下のテキストを見つける必要がある場合は、インターセプトをオンにして、どのテキストとそれが描画されたかを別のノートブックで書き始めます。 次に、脇の下の窓を強制的に再描画し、遮断をオフにします。 その後、静かにノートを読み、腕の真下に描かれたテキストを特定します。 間接データ(間接データだけでなく)によると、これは、アビーリンボチームのメンバーのためのクイックルックアップ機能の仕組みです。



次に、サーバー部分を終了して、次の図を取得します。







それははるかに有益になり、各レコードがどのコントロール(またはその一部)に属するかは非常に明確です。 すでに動作することは可能ですが、今は同じユーザーアクションをもっとコンパクトな形式で見たいと思いました。 さらに、私たちは従事します。



PS:



→WinFormsクライアントのソース



興味のある方はプロジェクトのウェブサイトドキュメントを ご覧ください

こちらのHabréのLogifyに関する紹介記事もご覧ください。

他のプラットフォームでのユーザーアクティビティのコレクション: WPFJavaScript、ASP.NET



All Articles