読者です 読者をやめる 読者になる 読者になる

ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

世界が驚いた!Friendlyマジック - 種明かし編

Friendly(Win32, WinForms, WPF)

これは「Friendly Advent Calendar 2014 - Qiita」の記事です。
昨日は森理麟さんの自動化の話をしようじゃないか - 森理 麟(moririring)のプログラマブログでした。

今日は最終日ですねー。
なので、お楽しみ的なことをやってみようかなーと。

世界が驚いたVisualStudioへの介入デモの種明かしです。
まずは一回動画みてください。

このデモアプリのコードはこちらからダウンロードできます。
Codeer-Software/Demo_FriendlyAttachVisualStudio · GitHub

ボタンが移動した!

はい。まあ、これは手品ですw。
Friendlyはプロセスの壁を突破できるのですが、さすがにヒープ領域繋げれるわけではないので、直接インスタンスのやり取りはできません。

では、どうやっているかというと、
①相手プロセスに新しくインスタンスを生成してビジュアルツリーに追加する。
②自分のプロセスのボタンを隠す。

そうすると、インスタンスが別プロセスに移動したようにみえますよねw。
(さすがに、それくらいはわかってましたか?)

WindowControl mainWindow;
using (var app = GetVS(out mainWindow))
{
    WindowsAppExpander.LoadAssembly(app, GetType().Assembly);
    WindowsAppExpander.LoadAssembly(app, typeof(DynamicAppVar).Assembly);
    WindowsAppExpander.LoadAssembly(app, typeof(WPFMenuBase).Assembly);
    var meue = mainWindow.IdentifyFromTypeFullName("Microsoft.VisualStudio.PlatformUI.VsMenu");
    var ctrl = app.Type<InsertControl>()(new WindowInteropHelper(this).Handle, meue);
    mainWindow.GetFromTypeFullName("System.Windows.Controls.Grid")[0].Dynamic().Children.Add(ctrl);
}

最初にDLLインジェクションしてますねー。
いつもより大目に差し込んでます。
これは相手プロセスでもFriendlyを使うためですね。
で作成しているのは、自分のアセンブリに定義したクラスInsertControlですね。
これを見ると結構ガッツリ書かれています。
何やっているかと言うと、VisualStudioでボタンを押されたときに実行する動作ですね。

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using RM.Friendly.WPFStandardControls.Inside;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;

namespace AttachVS
{
    public partial class InsertControl : UserControl
    {
        Menu _menu;
        IntPtr _targetHandle;
        static List<MenuItem> _executeItems;

        public InsertControl(IntPtr targetHandle, Menu menu)
        {
            _targetHandle = targetHandle;
            _menu = menu;
            InitializeComponent();
        }

        void ButtonClick(object sender, RoutedEventArgs e)
        {
            try
            {
                MenuItems items = new MenuItems();
                _executeItems = new List<MenuItem>();
                GetAllItems(_menu, items, _executeItems);
                using (var app = new WindowsAppFriend(_targetHandle))
                {
                    WindowsAppExpander.LoadAssembly(app, _menu.GetType().Assembly);
                    var targetWindow = new WindowControl(app, _targetHandle);
                    targetWindow.Dynamic().AddMenu(items);
                }
            }
            catch
            {
                return;
            }
            finally
            {
                ((Panel)this.Parent).Children.Remove(this);
            }
            _menu.Visibility = Visibility.Hidden;
        }

        static void Execute(int index)
        {
            IInvokeProvider invoker = new MenuItemAutomationPeer(_executeItems[index]);
            invoker.Invoke();
        }

        static void GetAllItems(Visual visual, MenuItems parent, List<MenuItem> executeItems)
        {
            foreach (var element in VisualTreeUtility.GetChildren(visual))
            {
                Visual o = element;
                var item = o as MenuItem;

                MenuItems nextParent = parent;
                if (item != null && item.Visibility == Visibility.Visible)
                {
                    var nextItem = new MenuItems()
                    {
                        ExecuteIndex = executeItems.Count,
                        Text = (item.Header != null) ? item.Header.ToString() : string.Empty,
                        IsEnabled = item.IsEnabled
                    };
                    if (string.IsNullOrEmpty(nextItem.Text))
                    {
                        continue;
                    }
                    parent.Items.Add(nextItem);
                    executeItems.Add(item);
                    nextParent = nextItem;
                    if (0 < item.Items.Count)
                    {
                        IInvokeProvider invoker = new MenuItemAutomationPeer(item);
                        invoker.Invoke();
                        DoEvents();
                    }
                }

                var next = element;
                var popup = next as Popup;
                if (popup != null)
                {
                    next = popup.Child as Visual;
                }
                GetAllItems(next, nextParent, executeItems);
            }
        }

        static void DoEvents()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrames), frame);
            Dispatcher.PushFrame(frame);
        }

        static object ExitFrames(object f)
        {
            ((DispatcherFrame)f).Continue = false;
            return null;
        }
    }
}

メニューが移動した!

これも同じですね。
元プロセスにメニュー項目を送り込んで、それを使ってにメニュー作成して、VisualStudioのは隠しているのでした。
上のコードのButtonClickの処理です。
これ、新しいですねw
他の怖い人の作品でも、送り込んだプロセスから、さらにFriendlyを使うのはなかったでしょ?
さすが作者w
ここでは一つポイントがありますね。
それは、メニューを開いてリストの中に格納しているのです。
これはなぜでしょうか?
WPFではメニューは一回開かないと生成されないんですね。
だから、隠してしまうともう操作できません。
なので、隠す前に一回開いて、それを保持しておくわけです。
そうすると、このアイテムをクリックすることで処理が実行されるのですね。
だから、最初のバーッとメニューが開いているのは演出ではなく、必然性に迫られてだったんですねー。

別プロセスからメニューが使える!

はい。で元プロセスでメニュー作成のために呼び出されるコードはコレ。
MainWindowに定義されていたのでした。

void AddMenu(MenuItems root)
{
    var menu = new Menu();
    Height = _originalHeight;
    _buttonInsert.Visibility = Visibility.Visible;
    menu.Width = _panel.Width - 1;
    menu.Height = _panel.Height - 1;
            
    Set(menu.Items, root.Items);

    //return menu
    MenuItem item = new MenuItem();
    item.Header = "return";
    item.Click += delegate { ReturnList(); };
    menu.Items.Add(item);

    _panel.Children.Add(menu);
}

void Set(ItemCollection parent, List<MenuItems> src)
{
    foreach (var e in src)
    {
        MenuItem item = new MenuItem();
        item.Header = e.Text;
        item.IsEnabled = e.IsEnabled;
        int index = e.ExecuteIndex;
        item.Click += delegate { Execute(index); };
        parent.Add(item);
        Set(item.Items, e.Items);
    }
}

void Execute(int index)
{
    try
    {
        WindowControl mainWindow;
        using (var app = GetVS(out mainWindow))
        {
            app.Type<InsertControl>().Execute(index);
        }
    }
    catch { }
}

void ReturnList()
{
    if (_panel.Children.Count == 0)
    {
        return;
    }
    try
    {
        _panel.Children.Clear();
        Height = _firstHeight;
        WindowControl mainWindow;
        using (var app = GetVS(out mainWindow))
        {
            mainWindow.IdentifyFromTypeFullName("Microsoft.VisualStudio.PlatformUI.VsMenu").Dynamic().Visibility = Visibility.Visible;
        }
    }
    catch { }
}

メニューが押されると、相手プロセスに送り込んだInsertControlのExecuteが呼び出され、先ほど格納したメニューの処理が実行されるのでしたー。

種明かし終了

タネがわかっても意味わかりませんか?
はい、この辺は慣れなんですね。
複数のプロセスを同時に操作して、あっちへ行ったり、こっちへ行ったりする感じ。
慣れたら難しくないんですよー。ホントに。
それにヤミツキになります。
楽しいんですって!
皆さんも是非面白アプリも作ってみてくださいねー。
(でも、当然テストにもつかってください。)

アドベントカレンダー完走

いやー、完走しました!
書いていただいた皆様本当にありがとうございました。
去年と比べると格段にFriendlyも広まりましたが、本当に周りの方々に助けていただいたおかげです。ひたすら感謝ですねー。
アドベントカレンダーは今日でひと段落ですが、Friendlyはまだまだ続きます。(当然)
来年こそはキャズムを越える!<言いたかったw

何はともあれ、メリークリスマス(*⌒ー⌒)ο∠☆:
皆さん、本当にありがとうございました!


で、最終回と思わせといて・・・
しばらくすると、MS-MVP界のマドンナ@hr_saoさんの番外編がアップされます!
こうご期待!