VB のたまご

作成日: 2016/12/14, 更新日: 2016/12/14


Friendlyを使ったライブデバッグを作れなかった話

  •  この記事は、Visual Basic Advent Calendar 2016 の14日目のエントリーです。

  •  数年前の話になりますが、プログラム開発をしている最中に何度かこう思うことがありました。
  • 「実行中のプログラムを Visual Studio 無しでデバッグできれば、開発環境以外のパソコンでも調査が楽にできるのになー。」
  • これは問い合わせ応対中に思ってたことでした。今ではもう思っていませんが。

  •  これを実現させるためには、実行中のプログラムに介入して監視する必要があります(exe ファイルの監視ではなく、exe 内で動作中の【プログラム自体という粒度】の監視)。 また、実行中のプログラム自体にはデバッグ系プログラムは含めたくありません。

  •  闇の契約を結ばないと実現できそうにないな、ダメだこりゃ。と思って諦めていました。 しかし、今期 Friendly を勉強しまして、Friendly の Dll インジェクションを利用することで相手プロセスに介入して、 デバッグ表示画面を埋め込めるじゃん!Dll インジェクションする側も exe ファイルとして動かすので、デバッガ(Visual Studio)使わなくてもイケるじゃん! って思って以前のネタを思い出したのでした。

  •  で、結果できませんでした。よって、このネタはボツネタとなりました。 どこらへんが実現不可だったかというと、デバッグ表示画面は、自動変数やウォッチ画面みたいな画面を考えていたのですが、 VSがデバッグ中にやってる、実行と一時停止と再開を制御しつつ関連変数の状態変化を取得するとか、それ具体的にはどうやんのよ?というところで無理っぽいなと判断しました。 意外と簡単にできるんじゃね?っていう考えは甘かったです。そもそもデバッガ技術のデの字も知らないレベルなのにデバッグ機能を作ろうだなんて無理がありましたね。

  •  それと一般的に、Dll インジェクションは、あまりよろしくないイメージ、悪いことに使われる技術というイメージがあるかと思います(よね?そうでもない?)。 今回のように、自アプリに対して使い、問題が生じてしまった場合の責任を持つという心意気で利用する分には有意義ではないかと思っていまして、 他のソフトウェアに対して、特に逆アセンブル止めてねって言ってる系、相手が嫌がる・困らせるような使い方はダメ絶対と理解・認識しています。

  •  ボツネタだけど公開したのは、PictureBox を複数使用することで多層レイヤーを作れることが分かったことと、高 DPI のことを意識するきっかけとなったので、情報共有ということで。

んじゃ、妥協してUI周りの情報出すやつ作る

  •  (私の技術不足により)振る舞いを自由自在に制御できないのは分かった。それじゃあせめてメンバー情報だけでも表示させよう。それならできそうだ。 と考えましたが、それ、Codeer 様の TestAssistant。orz、すでにありました(´;ω;`)、すごいやつ。 これすごいです。コントロールを選択すると、選択コントロールが強調されるんです。画面デザイン時に出ていたデザインプロパティまで表示されてて、すごいしか言えない。

  •  今回作ったのは、もろにこのツールのパクリと言えるでしょうorz。それなのに機能が無い廉価版。ただし、パクったのは見た目だけで、内部実装はググって調べながら作りましたよ。

画面構成

  •  プログラムは、Windows フォームアプリケーション形式で、NuGet から、Codeer.Friendly, Codeer.Friendly.Windows をインストールしています。 System.Drawing も参照追加しています。担当は、介入したいプロセスを選択・決定する人と、介入先プロセス内にデバッグ画面を埋め込んで表示する人の連携構成となっています。

  • 介入したいプロセスを選択・決定する人
  • イメージ

  • 介入先プロセス内にデバッグ画面を埋め込んで表示する人
  • イメージ
    イメージ

介入したいプロセスを選択・決定する人

  •  ここでは、実行中のプロセス一覧の取得と表示、選択したプログラムへの Dll インジェクションを担当します。 Dll インジェクションできたら、本アプリは終了させます。

  • Option Strict Off
    
    Imports Codeer.Friendly.Windows
    Imports Codeer.Friendly.Dynamic
    
    Public Class InjectorForm
    
        ' 画面の表示
        Private Sub InjectorForm_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
            Me.UpdateProcessList()
        End Sub
    
        ' プロセス一覧/更新ラベルのクリック
        Private Sub LinkLabel1_Click(sender As Object, e As EventArgs) Handles LinkLabel1.Click
            Me.UpdateProcessList()
        End Sub
    
        Private Sub UpdateProcessList()
    
            Dim ps = Process.GetProcesses()
            Me.ListBox1.Items.Clear()
    
            For Each p In ps
                If p.MainWindowTitle <> String.Empty AndAlso Not p.ProcessName.Contains("ProcessInjector") Then
                    Me.ListBox1.Items.Add(p)
                End If
            Next
    
            Me.ListBox1.DisplayMember = "MainWindowTitle"
    
        End Sub
    
        ' プロセス一覧/選択欄の選択切り替え
        Private Sub ListBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ListBox1.SelectedIndexChanged
    
            Dim p = CType(Me.ListBox1.SelectedItem, Process)
            Dim app = New WindowsAppFriend(p)
    
            ' 相手プロセス内に入って、こちら側の画面インスタンスを生成して置いてきて、こちら側のアプリケーションを終了する(ということになる?)
            WindowsAppExpander.LoadAssembly(app, Me.GetType().Assembly)
            app.Type(Of InjectorForm)().Inject()
    
            AddHandler Me.FormClosing, Sub(s2, e2) app.Dispose()
            Me.Close()
    
        End Sub
    
        Shared Sub Inject()
    
            ' このメソッドは、相手プロセス内で実行される
            Dim frm = New DisplayForm
            frm.Show()
            frm.Activate()
            frm.BringToFront()
    
        End Sub
    
    End Class
    

介入先プロセス内にデバッグ画面を埋め込んで表示する人

  •  ここでは、2層レイヤー作りと表示、見たいコントロールの選択・強調表示・プロパティ一覧の表示を担当しています。 今回、ハマりポイントが2点出ました。 見たい画面があった場合、画面キャプチャして2層レイヤーに表示させているのですが、キャプチャ画像がずれる現象に遭遇しました。 ちゃんと見たい画面の表示位置・サイズを指定している(つもり)のですが、キャプチャ画像を表示してみると表示位置が余計に左上になってしまい、さらにはなぜか拡大してしまっています。

  •  SplitContainer にも影響が出るみたいで、画面デザイン時は問題無いのですが実行するとサイズ調整がダメみたいでした。

  • 介入先となるターゲットアプリ
  • イメージ

  • 実行するとなんじゃこりゃ!みたいな
  • イメージ

  • 高 DPI 時はスケーリングの面倒は見なくていいよという設定をすると、良い感じになる。 ただし、今度は文字が細くひょろひょろになってしまう(フォントが変わってる?)。
  • イメージ
    イメージ

  •  という風に、先に実行結果を見せてしまいましたが、この問題を解決するにあたり、 いえひのプログラミング部屋様の【C#】レイヤー機能を作ると、 Web ディレクターズ ハンドブック様のフリーソフト「WinShot」が画面拡大されてキャプチャできない時の解消法 の記事を参考に実装したり解決させていただきました。

  • Imports System.Drawing.Imaging
    
    
    ' いえひのプログラミング部屋
    ' 【C#】レイヤー機能を作る
    ' http://skylinker.blog.fc2.com/blog-entry-57.html
    
    
    
    ' 本プログラムは、デバッグ実行するとキャプチャ画像がずれる現象が発生します
    ' また、SplitContainer を使う事でも、実行時のコントロール位置がずれる現象が発生します
    ' これを回避するために、本exeファイルと相手プロセス(の元となるexeファイル)の2つに対して、
    ' 以下の設定をおこなった後で、exe ファイルを直接実行して確認してください
    ' --------------------------------------------------
    ' Web ディレクターズ ハンドブック
    ' フリーソフト「WinShot」が画面拡大されてキャプチャできない時の解消法
    ' http://anicle.jp/web-d-handbook/freesoft-winshot-bugfix/
    ' 
    ' 高dpi 設定の場合、位置ずれが発生します。
    ' ビルド後、デバッグ実行はせずに、生成された exe に対して、
    ' 右クリック→プロパティ→互換性タブ→設定→
    ' 高 DPI 設定ではスケーリングを無効にする、にチェックを付けてください。
    ' その後、exe を直接起動して確認してください。
    ' --------------------------------------------------
    ' Windows 8.1 上では上記対策でOKだったが、Windows 10 上では上記対策を行ってもNGだった、なぜorz
    
    
    
    
    Public Class DisplayForm
    
        ' 画面の表示
        Private Sub DisplayForm_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
    
            ' PictureBox を2つ重ねて、2層レイヤー表示できるように準備
            Me.PictureBox1.BackColor = Color.Transparent
            Me.PictureBox2.BackColor = Color.Transparent
    
            Me.PictureBox1.Size = Me.Panel1.Size
            Me.PictureBox2.Parent = Me.PictureBox1
            Me.PictureBox2.Size = Me.PictureBox1.Size
            Me.PictureBox2.Location = New Point(0, 0)
    
            Me.UpdateFormList()
    
        End Sub
    
        ' 画面一覧パネルの表示・非表示のチェック切り替え
        Private Sub ToolStripButton1_CheckedChanged(sender As Object, e As EventArgs) Handles ToolStripButton1.CheckedChanged
    
            ' 画面内のコントロール情報を見るスペースが狭いため、不要な情報は隠すおもてなし処置
            If Me.ToolStripButton1.Checked Then
                Me.SplitContainer1.Panel1Collapsed = False
            Else
                Me.SplitContainer1.Panel1Collapsed = True
            End If
    
        End Sub
    
        ' 画面一覧/更新ラベルのクリック
        Private Sub LinkLabel1_Click(sender As Object, e As EventArgs) Handles LinkLabel1.Click
            Me.UpdateFormList()
        End Sub
    
        Private Sub UpdateFormList()
    
            Me.ListBox1.Items.Clear()
            For Each frm As Form In Application.OpenForms
    
                If frm.GetType().FullName.Contains("ProcessInjector.") Then
                    Continue For
                End If
                Me.ListBox1.Items.Add(frm)
    
            Next
            Me.ListBox1.DisplayMember = "Text"
    
        End Sub
    
        ' 画面一覧/選択欄の選択切り替え
        Private Sub ListBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ListBox1.SelectedIndexChanged
    
            Dim target = CType(Me.ListBox1.SelectedItem, Form)
            Dim item = String.Format("{0} : {1}", target.Name, target.Text)
            Me.TreeView1.Nodes.Clear()
            Me.TreeView1.Nodes.Add(item)
            Me.TreeView1.Nodes(0).Tag = target
    
            If target.HasChildren Then
    
                ' ※逆順ソートしないと、コントロールを貼った順番にならないみたい
                Dim children = target.Controls.Cast(Of Control)().Reverse()
                For Each child As Control In children
                    Me.AddControlNode(child, Me.TreeView1.Nodes(0))
                Next
    
            End If
    
            Me.TreeView1.ExpandAll()
    
            ' 画面イメージを取得
            target.Activate()
            target.BringToFront()
    
            Dim rc As Rectangle = Rectangle.Empty
            rc = target.Bounds
    
            Dim bmp As New Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb)
    
            Using g As Graphics = Graphics.FromImage(bmp)
                g.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy)
            End Using
    
            Me.PictureBox1.Image = bmp
            Me.Activate()
            Me.BringToFront()
    
        End Sub
    
        Private Sub AddControlNode(target As Control, node As TreeNode)
    
            ' 再帰的に全コントロールを取得&セット
            Dim item = String.Format("{0} : {1}", target.Name, target.Text)
            node.Nodes.Add(item)
            node.Nodes(node.Nodes.Count - 1).Tag = target
    
            If target.HasChildren Then
    
                Dim children = target.Controls.Cast(Of Control)().Reverse()
                For Each child As Control In children
                    Me.AddControlNode(child, node.Nodes(node.Nodes.Count - 1))
                Next
    
            End If
    
        End Sub
    
        ' 見たいコントロールの選択切り替え
        Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
    
            If e.Node.Tag Is Nothing Then
                Exit Sub
            End If
    
            Dim form = CType(Me.TreeView1.Nodes(0).Tag, Form)
            Dim ctrl = CType(e.Node.Tag, Control)
    
            ' レイヤー2、コントロール枠を赤線で囲む
    
            ' 非クライアント領域(タイトルバー、左右、下のウィンドウ枠)の幅や高さを求めて、
            ' クライアント領域の位置、サイズに足して、位置調整する
    
            ' 全体からクライアント領域を引いて、非クライアント領域を取得
    
            ' タイトルバー(ウィンドウ上幅)
            ' ウィンドウ幅(左右)
            Dim titleBarHeight = form.Bounds.Height - form.ClientRectangle.Height
            Dim windowBandWidth = form.Bounds.Width - form.ClientRectangle.Width
    
            ' ウィンドウ幅は左と右の両方が含まれているので、左側だけ取得
            ' タイトルバーの高さのみ取得したい、ウィンドウ幅(下)は、【多分】左右の幅と同じはず
            windowBandWidth = CType(windowBandWidth \ 2, Integer)
            titleBarHeight = titleBarHeight - windowBandWidth
    
            Me.PictureBox2.Size = Me.PictureBox1.Size
            Me.PictureBox2.Image = Nothing
    
            Dim img As New Bitmap(Me.PictureBox2.Width, Me.PictureBox2.Height)
            Using g As Graphics = Graphics.FromImage(img)
                Dim p As New Pen(Color.Red, 5)
                Dim bounds = ctrl.Bounds
                If form.Name = ctrl.Name Then
                    bounds = New Rectangle(0, 0, form.Width, form.Height)
                Else
                    bounds.X += windowBandWidth
                    bounds.Y += titleBarHeight
                End If
                g.DrawRectangle(p, bounds)
            End Using
    
            img.MakeTransparent(Color.Transparent)
            Me.PictureBox2.Image = img
    
            ' コントロール名&種類をラベルに表示
            Dim ctrlName = ctrl.Name
            Dim ctrlNamespace = ctrl.GetType().FullName
            Me.Label1.Text = ctrlName & vbNewLine & ctrlNamespace
    
            ' コントロールをプロパティグリッドにバインド
            Me.PropertyGrid1.SelectedObject = ctrl
    
    
        End Sub
    
    End Class
    

まとめ

  •  ツリー上のコントロールを選択すると、そのコントロールに対して赤枠が付き、そのコントロールのデザインプロパティ一覧を表示できるようになりました。 用途としては、実行時のコントロールの位置が分かる!重ねすぎてzオーダーがよく分からなくなっても見て把握できる!というくらいかなと思います。 作ってみて分かる、TestAssistantを使おうかなと! 最後までこの記事を読んでいただき、ありがとうございました。