2006年03月11日

COM クライアント実装の道程 for TaskScheduler その5

ID3タグってーのは調べれば調べるほど混沌としてきますね。あと v2.4 が出てもう5年以上立ってるけど結局 v2.3 の方が使われているような。WMP も v2.3 みたいだし。

さて、今回は前回の資産を生かして ITaskScheduler を .NET に相応しいクラスにラッピングします。まずは改めて ITaskScheduler のメンバを一覧しましょう。

メソッド 機能
SetTargetComputerこのインスタンスが操作の対象とするコンピュータの名前を設定する。
GetTargetComputerこのインスタンスが操作の対象とするコンピュータの名前を取得する。
Enum 現在のタスクフォルダに入っているタスクを列挙する。
Activate 操作するタスクを取得する。
Delete タスクを削除する。
NewWorkItem 新しいタスクアイテムをメモリに作成する。
AddWorkItem 新しいタスクアイテムのファイルを作成する。
IsOfType 指定したタスクアイテムが特定のインターフェイスをサポートしているかどうかを確認する。

ITaskScheduler を再確認していて、AddWorkItem を大いに誤解していたことが判明。こりゃあ ITask を受け取るんじゃなくて渡すんだッ。更にそれに伴って ITask が CLSID 持ちだったこと、つまり単独でインスタンスを作成できることの意味がようやく分かりました。となると TaskSchedulerClass / ITaskScheduler と同じ関係を ITask も持てるわけだけど……。

まずやはり .NET/C# の特徴と言えばプロパティ。SetTargetComputer & GetTargetComputer はいかにもプロパティにするのに向いているようです。これは TargetComputer として読み書き両用のプロパティにしましょう。あとはプロパティにできるのはなさそうですね。

残りはメソッドとして実装しますが、まず IsOfType は除外します。ITask インターフェイスは必ずサポートするはずですから確認する意味がありません。Enum はそのまま実装するのも不格好です。ここは一つ列挙子を返すのではなくもう列挙済みの配列を返すようにするのがそれっぽいかと思います。Delete はそのまま実装するしかない感じです。Activate は名前が宜しくないので GetTask という名前にしましょう。悩ましいのは AddWorkItem と NewWorkItem です。結局やることは同じなんですが、そのフローが違います。

AddWorkItem の場合
  1. ユーザが ITask を作成。これに各種データを設定する。
  2. AddWorkItem を呼び出し、作成した ITask をタスクスケジューラに登録。自動的に保存される。
NewWorkItem の場合
  1. NewWorkItem の呼び出しによって名前付き ITask を取得する。この段階ではメモリ上にしか存在しない。
  2. ユーザはその ITask に対してデータを設定。
  3. 設定が終了した時点で ITask を( IPersistFile を経由して)Save 。

結局同じことをやるのだから処理はどちらか一方で良いかと思うのですが、ではどちらにするか。処理がすっきりするのは、AddWorkItem の方だと思います。作って追加。直感的です。しかし、ここはあえて NewWorkItem の方を選びたいと思います。理由は、ITask のラップクラスも ComInterfaceWrapper からの派生クラスにするつもりだからです。ComInterfaceWrapper は IDisposable 。つまり使い終わったら Dispose を呼ぶべきクラスですが、AddWorkItem を使った場合、「 ITask 作成 > ITaskScheduler に Add > ITask を Dispose 」と言うことになります。このとき、Add した後で Dispose を呼び出す、と言うことに(私は)抵抗を覚えます。Add というと他のコレクションか何かに追加するイメージなのに、その中のアイテムを勝手に壊すわけで、(実際にはそう言う挙動じゃないし問題はないんですが)心安らかにコーディングできません。しかし NewWorkItem の場合、ITaskScheduler はファクトリとして機能するイメージになります。工場から出荷されたものなら、壊そうがどうしようがユーザの勝手ですよね?

それでは実装していきましょう。その前に骨組みを作っておきましょうね。

// C#
using System;
using System.Collection;
#if !V10 && !V11
using System.Collection.Generic;
#endif
using System.IO;
using HongliangSoft.Utilities.Unmanaged;
using HonglinagSoft.Utilities.Unmanaged.ComHelpers;
using HonglinagSoft.Utilities.TaskSchedulers.Inerop;
namespace HongliangSoft.Utilities.TaskSchedulers {
    public sealed class TaskScheduler : ComInterfaceWrapper {
        public TaskScheduler() : base(new TaskSchedulerClass()) {
        }
        private ITaskScheduler ITaskScheduler {
            get { return (ITaskScheduler)base.Instance; }
        }
    }
}

' VB.NET
Imports System
Imports System.Collections
#If Not(V = 10 OrElse V = 11)
Imports System.Collections.Generic
#End If
Imports HongliangSoft.Utilities.Unmanaged
Imports HonglinagSoft.Utilities.Unmanaged.ComHelpers
Imports HonglinagSoft.Utilities.TaskSchedulers.Inerop
Namespace HongliangSoft.Utilities.TaskSchedulers
    Public NotInheritable Class TaskScheduler
        Inherits ComInterfaceWrapper
        Public Sub New()
            MyBase.New(New TaskSchedulerClass())
        End Sub
        Private ReadOnly Property ITaskScheduler() As ITaskScheduler
            Get
                Return DirectCast(MyBase.Instance, ITaskScheduler)
            End Get
        End Property
    End Class
End Namespace

まず using している名前空間の解説。簡略のため一番下の名前で。Unmanaged は CoTaskMem を扱うためです。他に UnmanagedMemory や SimpleMemory を定義しました。また ComHelpers には ComInerfaceWrapper の定義のほか、HResult を列挙体として納めることにしています。Interop は、今まで省略してきましたが COM オブジェクトやインターフェイスの定義、また相互運用に直接使用する構造体などを格納します。ちなみにこの Interop の中はほとんど internal/Friend です。

コンストラクタはそのまま。それから基底クラスの Instance プロパティは object を返すため扱いにくいので、プライベートなプロパティで直接インターフェイスを扱えるようにしておきましょう。

初めに TargetComputer を実装してみます。

// C#
public string TargetComputer {
    get {
        IntPtr ptr;
        base.CheckHR(this.ITaskScheduler.GetTargetComputer(out ptr));
        using (CoTaskMem mem = ptr) {
            return mem.ReadString(CharSet.Unicode);
        }
    }
    set {
        base.CheckHR(this.ITaskScheduler.SetTargetComputer(value));
    }
}

' VB.NET
Public Property TargetComputer() As String
    Get
        Dim ptr As IntPtr;
        MyBase.CheckHR(Me.ITaskScheduler.GetTargetComputer(ptr))
#If Not(V = 10 OrElse V = 11)
        Using mem As New CoTaskMem(ptr)
            Return mem.ReadString(CharSet.Unicode)
        End Using
#Else
      Try
            Return Marshal.PtrToStringUni(ptr)
        Finally
            Marshal.FreeCoTaskMem(ptr)
        End Try
#End If
    End Get
    Set
        MyBase.CheckHR(Me.ITaskScheduler.SetTargetComputer(value))
    End Set
End Property

そう特異なところはないでしょう。わざわざ定義した CoTaskMem が微妙なところぐらいですか。特にUsing の使えない旧 VB.NET(そう言えば C# にあわせて V を 10、11、20 にしてるけど、これは 70、71、80 の方が正しいですね)は素直に書いた方が手っ取り早いので使わないという有様。base.CheckHR は前回のコードに書いていましたが解説していなかったので改めて。COM は返値で HRESULT 型(要するに Int32 ですが)のエラー情報を返します。この値を .NET の例外に変換する(そして投げる)のが Marshal.ThrowExceptionForHR メソッドです。どっちかってーと TranslateExceptionForHR とか言う名前で例外オブジェクト返してくれた方が親切な気がしますけど。当然のことながらマッピングできない値(例えばタスクスケジューラセキュリティサービスが無効であることを示す SCHED_E_NO_SECURITY_SERVICES とかのローカルなエラー)も存在するので、そう言うのはまとめて COMException に変換します。

そういえば null チェックしてない! とか思いますが、実は null も有効らしいです。逆に空文字列とかはアウトですが、その辺言い出すと「じゃあ何が有効な文字列なんだ?」ってことになってこっちでは判断できなくなるので、CheckHR に丸投げします。

次は列挙用のメソッド。ITaskScheduler.Enum はタスクアイテムそのものではなくそのアイテムのファイル名を返すという動作です。まあ確かに COM オブジェクト無分別に増やされても困りますし。それ以前に、実は ITask からアイテムの名前(ファイル名)を取得する手段がないという……それはどうだろう。

// C#
public string[] GetTaskNames() {
    IEnumWorkItems enumerator;
    base.CheckHR(this.ITaskScheduler.Enum(out enumerator));
    using (new ComInterfaceWrapper(enumerator)) {
#if !V10 && !V11
        List<string> tasks = new List<string>();
#else
        ArrayList tasks = new ArrayList();
#endif
        IntPtr item;
        HResult result;
        while ((result = enumerator.Next(1, out item, IntPtr.Zero)) == HResult.OK) {
            //返るのは文字列へのポインタの配列(要素数1)。
            //そのポインタ配列から先頭の要素をポインタとして読みとる。
            using (CoTaskMem array = item,
#if !V10 && !V11
                   mem = array.ReadStructure<IntPtr>()
#else
                   mem = (IntPtr)array.ReadStructure(typeof(IntPtr))
#endif
                   ) {
                //ファイル名を返すので、拡張子を排除
                string read = mem.ReadString(CharSet.Unicode);
                tasks.Add(Path.GetFileNameWithoutExtension(read));
            }
        }
        // 最後が HResult.False 以外の場合は何らかの致命的エラーの可能性が高い
        if (result != HResult.False) {
            base.CheckHR(result);
        }
#if !V10 && !V11
        return tasks.ToArray();
#else
        return (string[])tasks.ToArray(typeof(string));
#endif
    }
}

' VB.NET
Public Function GetTaskNames() As String()
    Dim enumerator As IEnumWorkItems = Nothing
    MyBase.CheckHR(Me.ITaskScheduler.[Enum](enumerator))
#If Not(V = 10 OrElse V = 11)
    Using New ComInterfaceWrapper(enumerator)
        Dim tasks As New List(Of String)
        Dim item As IntPtr
        Dim result As HResult
        Do
            result = enumerator.Next(1, item, IntPtr.Zero)
            If result = HResult.OK Then
                ' 返るのは文字列へのポインタの配列(要素数1)。
                Using array As New CoTaskMem(item)
                    ' そのポインタ配列から先頭の要素をポインタとして読みとる。
                    Using mem As CoTaskMem = array.ReadStructure(Of IntPtr)()
                        Dim read As String = mem.ReadString(CharSet.Unicode)
                        ' ファイル名を返すので、拡張子を排除
                        tasks.Add(Path.GetFileNameWithoutExtension(read))
                    End Using
                End Using
            Else
                ' 最後が HResult.False 以外の場合は何らかの致命的エラーの可能性が高い
                If Not(result = HResult) Then
                    MyBase.CheckHR(result)
                End If
                Exit Do
            End If
        Loop
        Return tasks.ToArray()
    End Using
#Else
    Try
        Dim tasks As New ArrayList
        Dim item As IntPtr
        Dim result As HResult = HResult.OK
        Do
            result = enumerator.Next(1, item, IntPtr.Zero)
            If result = HResult.OK Then
                ' 返るのは文字列へのポインタの配列(要素数1)。
                Try
                    Dim mem As IntPtr = Marshal.ReadIntPtr(item)
                    Try
                        ' そのポインタ配列から先頭の要素をポインタとして読みとる。
                        Dim read As String = Marshal.PtrToStringUni(mem)
                        ' ファイル名を返すので、拡張子を排除
                        tasks.Add(Path.GetFileNameWithoutExtension(read))
                    Finally
                        Marhsal.FreeCoTaskMem(mem)
                    End Try
                Finally
                    Marshal.FreeCoTaskMem(item)
                End Try
            Else
                ' 最後が HResult.False 以外の場合は何らかの致命的エラーの可能性が高い
                If Not(result = HResult) Then
                    MyBase.CheckHR(result)
                End If
                Exit Do
            End If
        Loop
        Return DirectCast(tasks.ToArray(GetType(String)), String)
    Finally
        Marshal.ReleaseComObject(enumerator)
    End Try
#End If
End Function

まず先に言い訳。CoTaskMem 周りはまだまだ流動的です。いや TaskScheduler 周りも記事書きながら間違い見つけたり実装を変えたりしてますが。で、UnmanagedMemory.ReadStructure に、.NET 2.0 以降の機能としてジェネリックメソッドを追加しました。型を事前にジェネリックで指定することで、object &キャストを使わずコンパイル時型チェックができるようになります。

VB の方ではバージョンの差がジェネリックにとどまらず Using にも影響するので、実装をほぼ完全に分けました。

IEnumTaskItems インターフェイスの列挙メソッドである Next は、ポインタの配列(へのポインタ)を返します。今回は一つずつ返すように指定しているので要素数は 1 で固定ですけど。そのため、直接 ReadString するんではなく、ReadStructure(typeof(IntPtr)) で読み出したポインタに対して ReadString を行うことになります。

残りはタスクアイテムに関するメソッドですね。

// C#
public bool Delete(string taskName) {
    if (taskName == null)
        throw new ArgumentNullException(
            "taskName", "taskName を null に指定することはできません。");
    HResult hr = this.ITaskScheduler.Delete(taskName);
    if (hr == HResult.CorFileNotFound)
        return false;
    base.CheckHR(hr);
    return true;
}
private static Guid ITaskIid = new Guid("148BD524-A2AB-11CE-B11F-00AA00530503");
private static Guid CTaskClsid = new Guid("148BD520-A2AB-11CE-B11F-00AA00530503");
public Task GetTask(string taskName) {
    if (taskName == null)
        throw new ArgumentNullException(
            "taskName", "タスクアイテムの名前を null にすることはできません。");
    ITask task;
    base.CheckHR(this.ITaskScheduler.Activate(
                     taskName, ref TaskScheduler.ITaskIid, out task));
    return new Task(task);
}
public Task CreateTask(string taskName) {
    if (taskName == null)
        throw new ArgumentNullException(
            "taskName", "タスクアイテムの名前を null にすることはできません。");
    ITask iTask;
    HResult result = this.ITaskScheduler.NewWorkItem(
                         taskName, ref TaskScheduler.CTaskClsid, 
                         ref TaskScheduler.ITaskIid, out iTask);
    if (result == HResult.OK) {
        Task task = new Task(iTask);
        task.Save();
        return task;
    }
    else if (result == HResult.FileExists)
        throw new IOException("同名のファイルが既に存在しています。",
                              (int)result);
    else
        base.CheckHR(result);
    // ここには来ない
    return null;
}

' VB.NET
Public Function Delete(ByVal taskName As String) As Boolean
    If taskName Is Nothing Then
        Throw New ArgumentNullException(
            "taskName", "taskName を null に指定することはできません。")
    End If
    Dim hr As HResult = Me.ITaskScheduler.Delete(taskName)
    If hr = HResult.CorFileNotFound Then
        Return False
    End If
    MyBase.CheckHR(hr)
    Return True
End Function
Private Shared ITaskIid As New Guid("148BD524-A2AB-11CE-B11F-00AA00530503")
Private Shared CTaskClsid As New Guid("148BD520-A2AB-11CE-B11F-00AA00530503")
Public Function GetTask(ByVal taskName As String) As Task
    If taskName Is Nothing Then
        Throw New ArgumentNullException(
            "taskName", "タスクアイテムの名前を null にすることはできません。")
    End If
    Dim task As ITask
    MyBase.CheckHR(Me.ITaskScheduler.Activate(
                       taskName, TaskScheduler.ITaskIid, task))
    Return New Task(task)
End Function
Public Function CreateTask(ByVal taskName As String) As Task
    If taskName Is Nothing Then
        Throw New ArgumentNullException(
            "taskName", "タスクアイテムの名前を null にすることはできません。")
    End If
    Dim iTask As ITask
    HResult result = Me.ITaskScheduler.NewWorkItem(
                         taskName, TaskScheduler.CTaskClsid, 
                         TaskScheduler.ITaskIid, iTask)
    If result = HResult.OK Then
        Task task = New Task(iTask)
        task.Save()
        Return task
    ElseIf result = HResult.FileExists Then
        Throw New IOException("同名のファイルが既に存在しています。",
                              result)
    Else
        MyBase.CheckHR(result)
    End If
    ' ここには来ない
    Return Nothing
End Function

一気に出してみました。

特に目新しいところと言うと、HResult 列挙体とそのメンバ CorFileNotFound / FileExists、それから今まで何も書いていない Task なる型、その Save メソッド辺りでしょうか。プログラムロジックについては特に問題ないかと思います。

HResult は単純にエラー番号が並んでるだけなので連載の最後に回します。Task に関しては、次回以降じっくりと。ここでは作成直後に Save している理由だけ書いておきましょう。

AddWorkItem は、必要なデータをそろえてから書き込む方式である、と解説しました。反対に NewWorkItem は、まず器を作り、そこにデータを書き込んで保存する方式です。つまり、NewWorkItem は取得から保存までタイムラグが存在します。このラグの間に同じ名前のファイルが作成されてしまった場合、保存するのが後になった方は保存できず破棄するしかありません(別名を使って AddWorkItem で、と言う手もありますが)。しかし、NewWorkItem はこのメソッドを呼び出すときに既にファイルが存在していた場合(あくまで「ファイルが存在している」が用件で、過去に同名で NewWorkItem を呼び出したかどうかは考慮されません)、ERROR_FILE_EXISTS を返す、と定義されています。つまりファイルさえそこにあれば、作ろうとした時点で成否が分かるため、作った後保存できず破棄ということはなくなるってことです。それで、NewWorkItem の直後に Save を実行して存在を確定させているという次第です。

最後に、一応完成形である DLL を ILDASM で開いたクラス一覧を載せておきます。

……まだ半分近く未紹介な気がしますよ? あ、HongliangSoft.Utilities.Unmanaged 以下のクラスは現実には別 DLL にしたほうがいいでしょうかね。

posted by Hongliang at 18:08| Comment(0) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/14641818

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。