概要‎ > ‎

Friendlyの基本

https://www.nuget.org/packages/Codeer.Friendly.Windows/



Friendlyは対象プロセスを別のプロセスから操作するためのライブラリです。
C++のfriendクラスが名前の由来です。他プロセスの内部のメソッドなどをFriendlyに呼び出せます。
このサンプルを使用するにはFriendly.Windowsが必要となります。
Nugetからの取得方法はこちらを参照お願いします。

ここでは、Codeer.Friendly.Dynamicを使った基本操作を説明します。
もし、テスト自体も.NetFramework4.0より前のバージョンを使って書かなければならない場合は、



以下のサンプルはこちらからダウンロードできます。

シンプルな例を用意しました。
次のコードは操作対象です。SampleFormクラスを表示するだけのWindowsFormアプリです。
//プロダクトプロセス。(テスト対象)
using System.Windows.Forms;

namespace
ProductProcess
{
   
public partial class SampleForm : Form
    {
       
int testValue;

       
private void SetTestValue(int value)
        {
            Text = value.ToString();
            testValue = value;
        }

    }
}

このプロセスを起動し、SampleFormのSetTestValue呼び出し、フィールドtestValueの値とFromのTextプロパティーを評価するテストです。
//テストプロセス。当然プロダクトプロセスとは別のプロセス。
using System;
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Dynamic;
using System.Windows.Forms;

namespace TestProcess
{
    [
TestClass]
   
public class BasicSample
    {
       
WindowsAppFriend _app;
       
Process _process;

        [
TestInitialize]
       
public void TestInitialize()
        {
           
//プロダクトプロセスを起動し、接続する。
            _app =
new WindowsAppFriend(Process.Start("ProductProcess.exe"));
            _process =
Process.GetProcessById(_app.ProcessId);
        }

        [
TestCleanup]
       
public void TestCleanup()
        {
            _app.Dispose();
            _process.CloseMainWindow();
        }

        [
TestMethod]
       
public void TestSetValue()
        {
           
//プロダクトプロセスを、プログラムレベルで操作する。
           
dynamic sampleForm = _app.Type<Control>().FromHandle(_process.MainWindowHandle);
            sampleForm.SetTestValue(5);
           
int value = sampleForm.testValue;
           
string text = sampleForm.Text;

           
Assert.AreEqual(5, value);
           
Assert.AreEqual("5", text);
        }
    }
}

TestSetValueを見てください。
まるで、自分のプロセスに存在するstaticメソッドやオブジェクトを操作しているように見えます。
しかし、このコードで、本当にプロダクトプロセスを操作しているのです!

これが、Friendlyの基本です。たったこれだけです。
しかし、これが可能ということは、ほぼ全ての操作が可能ということになります!
Codeer.Friendly.Dynamicの目指すところは、マニュアルレスで使えるというものです・・・。

とは言えやはり、実際はプロセス間通信が実行されているわけで、その辺を抑えていなければ、理解しづらいところもあります。
また、上記サンプルに書いていない部分もあるので、以下で詳細に説明します。



初期設定

プラットフォームターゲットを合わせる。
x86かx64かです。
プロダクトプロセスと合わせてください。
(VisualStudioのテストプロジェクトを使用する場合、実行環境はデフォルトではx86になっています。これはビルドの設定ではなく、テストホストの設定を変更する必要があります。詳細はこちらを参照お願いします。)



権限を合わせる。
テストプロセスの権限をプロダクトプロセスと同等以上にしてください。
プロダクトプロセスを管理者権限にしている場合はテストプロセスも管理者権限にしてください。
(通常モードで起動しているアプリに対してはこれは必要ありません。)



using
Dynamic用の拡張メソッドがあり、それを有効にするにはCodeer.Friendly.Dynamicの名前空間のusingが必要です。
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;



対象プロセスと接続する。

[TestInitialize]
public void TestInitialize()
{
   
//プロダクトプロセスを起動し、接続する。
    _app =
new WindowsAppFriend(Process.Start(TargetPath.ProductProcessPath));
    _process =
Process.GetProcessById(_app.ProcessId);
}

WindowsAppFriend操作対象のプロセスと接続するためのクラスです。対象プロセスへの操作の起点となります。
接続はWindowsAppFriendを生成することで確立できます。
コンストラクタはいくつかバージョンがありますので、こちらでご確認ください。
ここでは以下二つ説明します。

public WindowsAppFriend(IntPtr executeContextWindowHandle);
public WindowsAppFriend(Process process);

・第一引数で実行スレッドを決定する。
Friendlyではテストプロセスで使った操作がテストプロセスで実行されます。
しかし、どのスレッドで実行されるのでしょうか?

それは、このコンストラクタで指定します。
executeContextWindowHandleで指定したウィンドウの属するスレッドで実行されます。

では、processを渡した場合はと言うと、processのメインウィンドウの属するスレッドで実行されるようになります。
また、processを渡した場合、メインウィンドウが存在しなければ、メインウィンドウが作成されるまで待ちます。

この例では、プロセスを開始して、直後にコンストラクタに渡しているので、多くの場合はまだメインウィンドウが作成されていません。
そのため、接続が終了した後、Process.GetProcessById(_app.ProcessId)にて再度プロセスを取得しています。
接続後であれば、確実にメインウィンドウが存在するからです。

Friendly関連のライブラリでは、このようにタイミングを合わせる処理が、いくつか提供されています。
それは、Sleepに頼らず、タイミング依存のトラブルを排除し、確実に処理を進めるためです。

それから、実行スレッドは途中で変更することも可能です。
WindowsAppFriendChangeContextメソッドを使えば、変更できます。
めったに使う必要はありませんが、必要な場合はAPIリファレンスを参照お願いします。



基本は同期で操作される。

Friendlyの操作は基本的には、すべて同期で操作されます。
テストプロセスで処理を呼び出すと、対象プロセスの指定のスレッドでの処理が完了するまで、待ち合わせます。
これは、テストシナリオを確実に進めるために重要なことです。
しかし、場合によっては非同期実行も必要な場合があります。それは、後述の「非同期実行」で説明します。
その指定がなければ、処理はすべて同期で実行されるので、以下のドキュメントはそのように読んでください。



static操作の呼び出し。

dynamic sampleForm = _app.Type<Control>().FromHandle(_process.MainWindowHandle);

staticなメソッド、プロパティー、フィールドの呼び出しです。
_app.Type()から続けて書くと、それは、対象プロセスにつながるのです。

_app.Typeは拡張メソッドです。
Codeer.Friendly.Dynamicネームスペースをusingすることにより、AppFriendExtensionsの拡張メソッドが使用できるようになります。
このページではDynamicを使うことを前提に話ますので、拡張メソッド込みで説明します。
また、旧の操作に関してはここでは説明を省きます。
興味のある方はFriendlyの基本(.NetFramework4.0が使えない場合)を参照お願いします。

_app.Type<Control>()の戻り値はDynamicAppVarいうクラスです。(それがdynamic型で返ります。)
このDynamicAppTypeに対して、任意の操作を実行することでstaticメソッドが呼び出せます。

この例ではDynamicAppVarControlのを指定しているので、System.Windows.Controlクラスのすべてのstatic操作を実行させることができます。privateな操作もすべて、呼び出すことが可能です。

この例では、FromHandleメソッドを呼び出しています。
これは、次のように書くことも可能です。

dynamic sampleForm = _app.Type().System.Windows.Forms.Control.FromHandle(_process.MainWindowHandle);

この書き方は、ネームスペースから、フルネームで指定しています。
そのため、テストプロセスで知らない型(テストプロセスでは参照していないアセンブリに定義されている型)でも使うことができます。

しかし、長いので、可能ならプロダクトのアセンブリを参照して、その型を指定する方が良いと思います。
internalな型でも、テストプロジェクトをフレンドアセンブリにすれば、参照することが可能です。

また、次のように書いても、Formのオブジェクトを取得できます。
System.Windows.Forms.ApplicationクラスのOpenFormsプロパティーを使っています。

dynamic sampleForm = _app.Type<Application>().OpenForms[0];



Friendlyの操作に渡せる型と戻り値。

static操作に限らず、Friendlyでの操作は次のようなルールになっています。

メソッドの引数、プロパティー、フィールドへのsetterに渡せるもの。
・シリアライズ可能なオブジェクト。
DynamicAppVar
AppVar

メソッドの戻り値、プロパティー、フィールドのgetter。
DynamicAppVar

DynamicAppVarという型名称は実装上はめったに表れません。
なぜなら、dynamic型で受けて使うか、もしくは期待の型にキャストして使うからです。



オブジェクトの操作(DynamicAppVar)。

前述したように、この型はコード上にはめったに表れません。
しかし、Friendlyの操作では常に使う重要なクラスなのです。

対象プロセス内のオブジェクトの参照を格納します。
参照なので、実体は対象プロセス内部にあります。
そのため、このクラスから呼び出す処理はすべて、対象プロセスで実行されます。
(static同様privateな操作もすべて呼び出すことができます。)

以下のコードでは、メソッド、プロパティー、フィールドを呼び出しています。
dynamic sampleForm = _app.Type<Control>().FromHandle(_process.MainWindowHandle);
sampleForm.SetTestValue(5);
int value = sampleForm.testValue;
string text = sampleForm.Text;

実は上記のコードには、もうひとつ重要な処理が隠れています。
それはキャストです。
string text = sampleForm.Text;
この処理を分かりやすく書くと次のようになります。

//dynamicの正体はDynamicAppVar
//
この時点では、対象アプリケーション内のオブジェクトを持っているだけ。
dynamic dyamicAppVar = sampleForm.Text;

//キャストした時点で、シリアライズされ実体のコピーが対象アプリケーションから転送されてくる。
string text = (string)dyamicAppVar;

コード中のコメントで説明してしまいましたが、つまりは、そういうことなのです。
処理の戻り値は、dynamic(中にDynamicAppVarが入っている)で返ってきます。
この時点では、実体はまだ対象プロセス内にあります。
そのため、それに対する処理はすべて対象プロセス内で実行されます。
FromHandleの戻り値のsampleFormはDynamicAppVarであるため、その処理はすべて対象プロセスで実施することができます。

そして、そのオブジェクトがシリアライズ可能なものであるなら、キャストによって、それを取得することができます。
キャストなのですが、実際には、

「対象プロセスでシリアライズ」→「テストプロセスへ転送」→「テストプロセスでデシリアライズ」

という流れになるので、コピーが取得されます。

DynamicAppVarのキャストでは注意すべき点があります。
(というが、DynamicObjectを継承したクラスの型変換の注意点です。)
以下のようになります。
//○もちろんキャストされ、値が取得できる。
string cast = (string)dyamicAppVar;

//○
代入もOK。キャストが実行される。
string substitution = dyamicAppVar;

//×is演算ではキャストされない。falseになる。
bool isString = dyamicAppVar is string;

//×as演算でもキャストされない。nullになる。
string textAs = dyamicAppVar as string;

//×関数の引数に渡すときには明示的にキャストされない。実行時例外が発生する。
string.IsNullOrEmpty(dyamicAppVar);
//  ↓○明示的にキャストして渡してください。
string.IsNullOrEmpty((string)dyamicAppVar);



DynamicAppVarの特殊なキャストとその用法。

DynamicAppVarのキャストは通常は前述したように中身のコピーを取得することができるのですが、
実は特殊なキャストが4種類存在します。

AppVarDynamicAppVar
AppVarからの変換です。(これだけDynamicAppVarへのキャストです。)
AppVarDynamicAppVarとほとんど同等のものです。(.NetFramework4.0が使えない場合などはこれを使います。)
対象アプリの操作の呼び出し方法が違うだけです。
そのため、implicit operatorを定義して暗黙で変換できるようにしています。
この変換は、関数の引数渡しでも実行されます。

DynamicAppVarAppVar
AppVarへのキャストです。
これもimplicit operatorで定義されていますので暗黙に変換されます。
関数の引き渡しでも実行されます。
AppVarを引き渡すインターフェイスは、いくつかありますので、その際に便利です。

DynamicAppVarIDisposable
めったに使わないのですが、AppVarの指すオブジェクトが大量のメモリを抱えている場合などは、これを使ってIDisposableにキャストしてから、Diposeを呼ぶことによりFriendlyの管理から外すことができます。普通にDiposeを呼ぶとそれは、対象アプリ内のオブジェクトに対して実行されるだけです。using構文に入れることもできます。
普通はこれを使わなくても、正常に動作します。

DynamicAppVarIEnumerable
対象のオブジェクトがIEnumerableを実装している場合はキャスト可能です。
これは、こちらに対象を転送せずにループを回すことができます。
例として、次のようなコードが書けます。
//回す方法のみプロダクトプロセスから転送されてきて、
//その情報を元にforeachを回す。
//formDynamicAppVarなので、プロダクトプロセス内のオブジェクトを操作できる。
foreach (dynamic form in _app.Type<Application>().OpenForms)
{
    form.BackColor =
Color.Pink;
}

ただ、この方法は便利なのですが、Friendlyでの操作はすべてプロセス間通信が発生しますので、非常に低速です。
(一回に1ミリ秒ほどかかります)
開いているフォームを回すくらいなら問題ありませんが、大量データなのどは、一度データ転送してテストプロセスで処理してください。



オブジェクト生成。

対象プロセス内でオブジェクトを生成することもできます。
例えば、シリアライズできないオブジェクトや、テストプロセスで型がわからず、生成、転送できない場合などに有効です。
しかし、これも可能なら、そのオブジェクトにSerializableAttributeを付けたりテストプロセスでプロダクトプロセスを参照して、コピーを受け渡せるようにしてください。

この例では、どうしても受け渡すことのできないComboBoxを生成してフォームに設定してみます。
もちろん、実際のテストでこのような処理をすることはないと思いますが、サンプルなのでご了承ください。
//対象プロセス内にコンボボックスを生成。
dynamic comboBox = _app.Type<ComboBox>()();

//Friendlyの操作で対象アプリ内で設定処理を実行する。
comboBox.Items.Add("a");
comboBox.Items.Add(
"b");
comboBox.Items.Add(
"c");

//フォームを取得してきて、
dynamic mainForm = _app.Type<Application>().OpenForms[0];

//コンボボックスをコントロールに追加。
//comboBoxDynamicAppVarなので引数に渡せる。
//対象アプリ内でAddを実行するときにはSystem.Windows.Forms.ComboBoxが渡る。
mainForm.Controls.Add(comboBox);

DynamicApptypeを関数ポインタ(delegate)のように使うことで、生成することができます。
この例では
_app.Type<ComboBox>()
までで、DynamicApptypeが返され、そのオブジェクトに()呼び出しすることによって、生成しています。

もちろん、コンストラクタの引数を渡すこともできます。
List<int>を引数付きコンストラクタで生成するには、以下のようなコードになります。
dynamic list = _app.Type<List<int>>()(new int[] { 1, 2, 3 });

また、staticの場合と同様に、タイプを型ではなく、名前で指定することもできます。
dynamic comboBox = _app.Type().System.Windows.Forms.ComboBox();

それから、生成ではないのですが、テストプロセスのオブジェクトをシリアライズしてプロダクトプロセスへ転送したり、対象プロセスにnullの変数を確保したりできます。それぞれ、Copy()とNull()を使います。
Dictionary<int, string> dic = new Dictionary<int, string>();
dic.Add(1,
"1");

//シリアライズして転送。
dynamic dicInTarget = _app.Copy(dic);
           
//out引数に渡すときにはNullが便利です。
dynamic value = _app.Null();
dicInTarget.TryGetValue(1, value);
Assert.AreEqual("1", (string)value);



非同期実行。
Friendlyの操作は基本は同期実行です。
しかし、場合によっては非同期で実行したい場合もあります。

Friendlyの非同期制御は実に簡単です。
Asyncクラスを引数のどこかに渡してやるだけで実現できます。
また、そのAsyncクラスのオブジェクトを使って、非同期の実行情報を取得することができます。

//引数のどこかにAsyncオブジェクトを渡すと非同期実行となります。
Async async = new Async();
sampleForm.SetTestValue(async, 5);

//終了しているかをチェックできます。
if (async.IsCompleted)
{
   
//終了していたら・・・
}

//終了を待ち合わせることもできます。
async.WaitForCompletion();

プロパティー、フィールドは同名でメソッド形式にすることで非同期呼び出しができます。
//プロパティー、フィールドの場合
sampleForm.Text(new Async(), "abc");
sampleForm.testValue(
new Async(), 1);

//[]でのアクセスの場合は.NetFrameworkで定義されている別名を使ってください。
dynamic list = _app.Copy(new List<string>(new string[] { "0", "1", "2" }));
list.set_Item(
new Async(), 1, "10");

dynamic
array = _app.Copy(new string[] { "0", "1", "2" });
list.Set(
new Async(), 1, "10");

戻り値のある操作も実行可能です。
dyanmicには戻り値が入る予定のDynamicAppVarが返ります。
そのため、操作が完了すると、その値を取得することができるのです。
//getterを非同期実行
Async async = new Async();
dynamic text = sampleForm.Text(async);

//処理が完了するとtextstringオブジェクトが格納されるので、それを取得することができる。
async.WaitForCompletion();
string textValue = (string)text;



いかがだったでしょうか。
詳細に書くと結構なボリュームになってしまいました。
しかし、これで「Friendlyの操作呼び出し」に関しては全て紹介できました。

ただ、Friendlyの提供する基礎的な機能がもう一つあります。
「DLLインジェクション」です。
次項ではそれを説明します。