Windows と C++
タスク スケジューラ 2.0
Kenny Kerr
コードのダウンロード : :
WindowsWithC++2007_10.exe
(156 KB)
Browse the Code Online

コンテンツ
Windows Vista® では、タスク スケジューラが大きく見直されました。類似点もいくつかありますが、Windows® 98 以来使用されてきた元のタスク スケジューラに比べて、新しいタスク スケジューラ (タスク スケジューラ 2.0) ははるかに強力です。単なるエンド ユーザー向けの簡単なツールから、複雑なバックグラウンド操作を設計および管理するための強力なプラットフォームへと進化を遂げました。このため、多くの場合に Windows サービスを開発する必要がなくなります。
たとえば、プロジェクトで更新を自動的に確認する必要があるものとします。この場合、バックグラウンドで稼働して数日ごとに更新を確認するような Windows サービスを記述することが考えられます。ただし、常に実行し続けるサービスの代わりに、数日ごとに実行されて更新を確認した後に停止するようなスケジュール タスクを設計することもできます。さらには、更新を実行するユーザーがいないときにリソースが浪費されないよう、ユーザーがログインしている間にのみ実行されるようにすることもできます。
これからは新しいタスク スケジューラの時代であるとマイクロソフトの開発者たちが考えていることは、Windows Vista のあちこちでバックグラウンド タスクの管理にこれが使用されている事実からも明らかです。副次的効果として、こうしたタスクすべてを処理するために新しく作成しなければならないサービスの数が減少します。また、ブラックボックス的な Windows サービスの目的を解明するよりは、タスクを調べてその動作を確認するほうがはるかに簡単なため、Windows の管理と診断も簡素化されます。Windows 自体の一部を成すタスクにとって、タスク スケジューラはタスク ホストとしても機能します。この場合、タスク固有の多数のプロセスが 1 つに統合されるため、それらの操作をホストするために必要なリソースがさらに減少します。
このすばらしい新サービスに読者が一刻も早く習熟して活用できるよう、このコラムではタスク スケジューラの主要概念と構成要素について説明します。
タスク サービスと記憶域
まず取り上げるのは、タスク スケジューラ サービス、つまりタスクを実際にスケジュールするサービスです。タスク スケジューラではファイル システムを使用してタスク情報を保存するため、このサービスを利用しなくてもタスクの列挙や準備が可能です。しかし、これを利用せずにできることには限りがあります。実際、タスク スケジューラの今後のバージョンでは保存の形式と場所が変更されると予想されるため、現在のそれに依存することはお勧めしません。現時点でも、ファイル システムに直接書き込まれたタスクはタスク スケジューラによって認識されない可能性があるため、また、改ざんされたタスクの実行が拒否される可能性が高いため、記憶域への直接アクセスに依存しないようにすることが重要です。タスク スケジューラは既に Windows Vista の不可欠な一部であり、管理者が単純にこれを無効化することはできなくなりました。つまり、バックグラウンド操作の実行とタスク記憶域の抽出に利用しても安全だということです。これは開発者にとって朗報です。
タスクは原則として、社名、製品名、およびコンポーネント名によってグループ化されます。たとえば、Windows のディスク デフラグ ツールでは、タスクが \Microsoft\Windows\Defrag サブフォルダに保存されます。各タスクは、タスク スケジューラ スキーマで定義された XML ドキュメントとして保存されます。このため、XmlLite などの XML パーサーを使用してタスクを直接作成したり (私の以前の MSDN
® Magazine 記事
msdn.microsoft.com/msdnmag/issues/07/04/Xml/ を参照)、タスク スケジューラ API が提供するさまざまな COM インターフェイスを使用したりできます。COM インターフェイスを使用すると、タスクの XML 定義を直接提供したり照会したりもできます。そうすることにより、実際の記憶域にアクセスすることなく、XML を使用してタスクの作成と照会を行うメリットが得られます。タスク スケジューラにはリモートからアクセスすることもできます。
タスク サービスを使用する
ITaskService インターフェイスは、タスク スケジューラ API へのゲートウェイです。必要な定義は、すべて taskschd.h ヘッダー ファイル内にあります。もちろん、Windows Vista に対応した最新の Windows SDK も必要です。まず、次のようにローカルのインプロセス インスタンスを作成します。
|
CComPtr<ITaskService> service;
HR(service.CoCreateInstance(__uuidof(TaskScheduler)));
|
(これらのサンプルでは、確認を必要とする HRESULT がメソッドから返される場所を明確に特定するために、HR マクロを使用しています。これを、例外をスローするか HRESULT を返すようなエラー処理に置き換えてもかまいません。)
次に、Connect メソッドを呼び出して、任意のコンピュータ上にあるタスク スケジューラへの接続を確立します。Connect メソッドは次のように定義されています。
|
HRESULT Connect(
VARIANT computer, VARIANT user,
VARIANT domain, VARIANT password);
|
VARIANT 型を使用しているのが残念ですが、便利な Active Template Library (ATL) の CComVariant 派生クラスを使用すれば処理は簡単です。ローカル コンピュータに接続するには、コンピュータ名を省略します。呼び出し元の ID または実効トークンを使用して接続するには、ユーザー名の指定を省略します。ローカル コンピュータの現在のドメインを使用するには、ドメイン名の指定を省略します。最後に、ユーザー名を指定した場合はパスワードだけを指定します。よって、ローカル コンピュータのタスク スケジューラ サービスに接続する場合、次のような最も簡単な形式で Connect メソッドを呼び出します。
|
HR(service->Connect(
CComVariant(), // local computer
CComVariant(), // current user
CComVariant(), // current domain
CComVariant())); // no password
|
これで、タスクの作成、既存のフォルダやタスクの列挙など、さまざまな操作を実行できるようになりました。タスクはフォルダに格納しなければ使用できないため、まず ITaskFolder インターフェイスについて説明します。ITaskService の GetFolder メソッドからは、指定したフォルダに対する ITaskFolder インターフェイス ポインタが返されます。次に示すように、バックスラッシュはルート タスク フォルダを表します。
|
CComPtr<ITaskFolder> folder;
HR(service->GetFolder(CComBSTR(L"\\"), &folder));
|
次に、アプリケーション用のフォルダを次のように作成できます。
|
CComPtr<ITaskFolder> newFolder;
HR(folder->CreateFolder(
CComBSTR(L"Company\\Product"),
CComVariant(), &newFolder));
|
CreateFolder に対する 2 つ目のオプション パラメータでは、新しく作成するファイル システム フォルダのセキュリティ記述子を指定します。省略した場合、フォルダには親のセキュリティ記述子が継承されます。セキュリティ記述子定義言語 (SDDL) を使用すると、既定のアクセス制御をオーバーライドできます。たとえば、ローカル システムと管理者とに完全な制御を許可するには、次のような SDDL を使用します。
|
HR(folder->CreateFolder(
CComBSTR(L"Company\\Product"),
CComVariant(L"D:(A;;FA;;;BA)(A;;FA;;;SY)"),
&newFolder));
|
簡単に言うと、D: に続くアクセス制御エントリ (ACE) は、随意アクセス制御リスト (DACL) の一部となります。最初のエントリは、すべてのファイル (FA) へのアクセス許可 (A) が組み込みの管理者 (BA) に対して付与されることを意味します。2 番目のエントリは、同様のアクセス許可がローカル システム (SY) に対して付与されることを意味します。このセキュリティ記述子定義が使用されるのは、Product フォルダの作成時のみです。親の Company フォルダは、継承されたセキュリティ記述子を使用して作成されます。なお、Local System アカウントに対してアクセスを拒否しないようにしてください。この ID はタスク スケジューラ サービスによって使用されます。スケジュール対象のタスクにタスク スケジューラがアクセスできないと、予期しない動作が発生します。
フォルダが存在するようになったら、ITaskService の GetFolder メソッドを使用して、ルート タスク フォルダから相対指定したフォルダを開くことができます。ITaskFolder の同じメソッドを次のように使用すると、インターフェイス インスタンスによって表されるフォルダから相対指定したフォルダを開くことができます。
|
CComPtr<ITaskFolder> productFolder;
HR(service->GetFolder(
CComBSTR(L"Company\\Product"),
&productFolder));
CComPtr<ITaskFolder> componentFolder;
HR(productFolder->GetFolder(
CComBSTR(L"Component"),
&componentFolder));
|
このコラムのダウンロードに含まれている、タスク スケジューラ エクスプローラのサンプル アプリケーションを、図 1 に示します。これによってタスクを概観でき、タスク スケジューラ API を使い始める際に役立ちます。
図 1 タスク スケジューラ エクスプローラ (画像を拡大するには、ここをクリックします)
タスク定義
タスクを作成するには、まず、タスク定義でタスクをモデル化する必要があります。タスク定義を構成するコンポーネントについて、図 2 で説明します。タスク定義は複雑であるうえ、タスク作成において必要とされるため、すぐにタスク作成に取りかかることはお勧めしません。基本的な構成要素について前もって理解しておくと役立ちます。アクションとトリガの定義の詳細を検討する前に、タスク定義の作成方法について簡単に説明します。

Figure 2 タスク定義のコンポーネント
| タスク |
定義 |
| アクション |
1 つまたは複数のアクションで、タスクが実行する作業を定義します。 |
| トリガ |
1 つまたは複数のトリガで、タスクをいつ開始するのかを指定します。 |
| プリンシパル |
承認と監査は、このセキュリティ コンテキストに基づきます。 |
| 設定 |
これらの設定で、タスクの実行時動作と制限を制御します。 |
| データ |
アクションで使用できるようにする文字列です。 |
| RegistrationInfo |
管理用のブックキーピング情報です。 |
最初に、タスク スケジューラ サービスを使用して、設定に使用する空のタスク定義を作成します。
|
CComPtr<ITaskDefinition> definition;
HR(service->NewTask(0, // reserved
&definition));
|
タスク定義を特定のフォルダに登録する前に、少なくともアクションを 1 つ設定する必要があります。オプションで、トリガやその他の情報を設定することもできます。準備ができたら、RegisterTaskDefinition メソッドを次のように使用して、フォルダに登録します。
|
CComPtr<IRegisteredTask> registeredTask;
HR(folder->RegisterTaskDefinition(
CComBSTR(L"Task"),
definition,
TASK_CREATE_OR_UPDATE,
CComVariant(), // user name
CComVariant(), // password
TASK_LOGON_INTERACTIVE_TOKEN,
CComVariant(), // sddl
®isteredTask));
|
新しく登録されたタスクを表す IRegisteredTask インターフェイス ポインタが返されます。このインターフェイスを使用して、タスクの開始や停止のほか、タスクに関するランタイム情報の取得も行います。
RegisterTaskDefinition の最初のパラメータでは、新しいタスクの名前を指定します。この値は、フォルダに保存されるファイルの名前としても使用されるため、ファイル システムの名前付け規則に準拠する必要があります。このパラメータを NULL に設定すると、メソッドによって GUID が生成され、名前として使用されます。しかしこれは、コンピュータの管理者にとってわかりやすいとは言えないため、お勧めしません。2 番目のパラメータでは、作成するタスクを記述するタスク定義を指定します。これについては、以下のセクションで詳しく説明します。3 番目のパラメータでは、TASK_CREATION 列挙からの値を指定します。よく使用される値は、TASK_CREATE および TASK_UPDATE です。TASK_CREATE_OR_UPDATE は、これら 2 つのビット単位の OR です。
続く 3 つのパラメータでは、登録資格情報を指定します。RegisterTaskDefinition を使用すると、後続のパラメータで指定する DACL で明示的に付与しなくても、登録するタスクの少なくとも読み取りアクセス権が、指定したユーザーに必ず付与されます。タスク スケジューラは、何種類ものログオン方法をサポートしています。上の例で使用している TASK_LOGON_INTERACTIVE_TOKEN は、指定したユーザーの対話型ログオン セッションでのみタスクが実行されることを示します。ユーザー名とパスワードを指定しない場合、呼び出し元の ID が使用されます。対話型ログオン セッションを必要とするため、パスワードは保存されません。
代わりに TASK_LOGON_PASSWORD を使用すると、タスク用のバッチ ログオン セッションを作成するようタスク スケジューラに指示できます。この場合、ユーザー名とパスワードの両方を指定し、アカウントには "バッチ ジョブとしてログオン" 権限を付与する必要があります。
さらに安全性の高いオプションとして、TASK_LOGON_S4U があります。S4U (service for user) ログオンを利用し、指定したユーザーに代わってタスクを実行しますが、パスワードの保存は不要です。ローカル システム アカウント内でタスク スケジューラが実行されるため、S4U ログオン セッションを作成し、ID として使用されるだけでなくローカル コンピュータにおける偽装にも使用されるトークンを受け取ることができます。通常、S4U トークンは ID としてのみ有効です。
タスクには、登録時に指定した資格情報に加え、セキュリティ コンテキストに対する制御が IPrincipal インターフェイスによってさらに提供されます。具体的に言うと、IPrincipal は、タスク スケジューラ エンジンをホストするプロセスの実行レベルを、タスクに合わせて制御するための便利な方法です。まず、関連付けられたプリンシパル オブジェクトのタスク定義を照会した後、RunLevel プロパティを必要に応じて設定します。これは、タスク定義の登録前に行う必要があります。次に例を示します。
|
CComPtr<IPrincipal> principal;
HR(definition->get_Principal(&principal));
HR(principal->put_RunLevel(TASK_RUNLEVEL_HIGHEST));
|
TASK_RUNLEVEL_HIGHEST は、そのユーザーにとって最も制約が緩いセキュリティ コンテキストでタスクを実行することを表します。ユーザーが管理者である場合、ユーザー アカウント制御 (UAC) の制約を受けないトークンがプロセスに与えられます。代わりに TASK_RUNLEVEL_LUA を使用すると、制限された (昇格されない) トークンが与えられます。
タスク アクション
基礎的な説明が済んだので、タスク スケジューラに用意されているさまざまなアクションの種類を見てみましょう。アクションの作成とタスク定義への追加には、タスク定義の Actions プロパティから返される IActionCollection インターフェイス ポインタを使用します。
|
CComPtr<ITaskDefinition> definition;
HR(service->NewTask(0, // reserved
&definition));
CComPtr<IActionCollection> actions;
HR(definition->get_Actions(&actions));
|
IActionCollection が提供する Create メソッドは、新しいアクション オブジェクトを作成してコレクションに追加します。アクション自体は、IAction から派生するインターフェイスを通じて公開されます。次に例を示します。
|
CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_EXEC, &action));
|
TASK_ACTION_EXEC は、コマンドライン操作を記述し、あらゆるプログラムの起動に使用できるため、最もよく使用される種類のアクションです。Windows シェルに登録されたドキュメントやその他の種類のファイルを起動することもできます。アクション固有のプロパティを設定するには、まず、その種類に固有のインターフェイスを照会する必要があります。CComQIPtr は、この動作だけをうまくやってのけます。
|
CComQIPtr<IExecAction> execAction(action);
|
IExecAction インターフェイス ポインタを取得したら、任意のコマンドを実行するアクションを構成できます。次の例では、仮想ハード ドライブ イメージを最適化する Sysinternals Contig ツールを起動します。
|
HR(execAction->put_Path(CComBSTR(L"g:\\Tools\\contig.exe")));
HR(execAction->put_Arguments(CComBSTR(L"-s g:\\VirtualMachines")));
|
コマンドの実行は、管理者にとっては便利ですが、開発者にとって最適な解決策であるとは限りません。コマンドは、タスク内でどのようなアクションを意味するのかを認識しないばかりか、スケジュールされたタスクの一部であることすら関知しないからです。さいわい、タスク スケジューラには、まさにこの問題に対処するために設計された、別の種類のアクションが用意されています。そのアクションの種類 TASK_ACTION_COM_HANDLER は、COM サーバーを作成し、実装が必要な ITaskHandler インターフェイスをこのサーバー上で照会します。このため、アクションとタスク スケジューラ エンジンの間に通信チャネルが確立され、アプリケーション固有の操作がより明快にタスク スケジューラに統合されます。
COM アクションの作成方法を次に示します。
|
CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_COM_HANDLER, &action));
CComQIPtr<IComHandlerAction> comAction(action);
HR(comAction->put_ClassId(CComBSTR(
L"{25C6DB11-4ADC-4e89-BA47-04576C7AA46A}")));
|
もちろん、指定したクラス識別子 (CLSID) で登録され ITaskHandler インターフェイスを実装する COM サーバーが必要です。図 3 に、COM クラスで ATL を利用して ITaskHandler を実装する例を示します。

Figure 3 COM アクション クラス
|
class DECLSPEC_UUID("25C6DB11-4ADC-4e89-BA47-04576C7AA46A")
DECLSPEC_NOVTABLE CoSampleTask :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CoSampleTask, &__uuidof(CoSampleTask)>,
public ITaskHandler
{
public:
DECLARE_REGISTRY_RESOURCEID(IDR_SAMPLETASK)
BEGIN_COM_MAP(CoSampleTask)
COM_INTERFACE_ENTRY(ITaskHandler)
END_COM_MAP()
private:
STDMETHODIMP Start(IUnknown* taskScheduler, BSTR data);
STDMETHODIMP Stop(HRESULT* actionResult);
STDMETHODIMP Pause();
STDMETHODIMP Resume();
};
|
もう 1 つ、追加の手順を実行する必要があります。これを執筆している時点ではどのドキュメントにも記載されていませんが、この手順を実行しないと COM アクションの読み込みに失敗します。タスク スケジューラ エンジンは、CoCreateInstance 関数に CLSID を指定して呼び出す際、CLSCTX_LOCAL_SERVER コンテキストを使用して COM サーバーを別個のプロセスに読み込もうとします (ただし、アクションが Windows 自体の一部である場合は、インプロセスで読み込まれることがあります)。これを成功させるには、代理プロセスでのアクティブ化を許可するよう COM サーバーを構成する必要があります。技術的には、従来のアウトプロセス コンポーネントを使用することもできますが、この方法は、多くの場合、一般的でも実際的でもありません。これを簡単に解決するには、登録のコードを更新します。ATL 登録を更新して DllSurrogate 値を含めます。次に例を示します。
|
HKCR
{
NoRemove AppID
{
'%APPID%' = s 'SampleTask'
{
val DllSurrogate = s ''
}
'SampleTask.DLL'
{
val AppID = s '%APPID%'
}
}
}
|
COM アクションは、いくつもの点で、他の種類のアクションよりも優れています。特に、正常かつ予測可能な方法でアクションを停止できるのは、COM アクションだけです。COM クラスのインスタンスを作成した後、タスク スケジューラ エンジンは Start メソッドを呼び出し、タスク スケジューラとの通信に使用できるインターフェイス ポインタを提供します。次に Start メソッドで ITaskHandlerStatus インターフェイスを照会できます。これにより、タスクの進捗状況と最終的な完了をタスク スケジューラに通知する簡単なメカニズムが実現します。アクションが終了する方法は 2 つあります。結果として COM クラスの Stop メソッドが呼び出されるようなさまざまなメソッドを使用して、ユーザーがタスクを終了する場合があります。また、ITaskHandlerStatus の TaskCompleted メソッドを呼び出すことによって、COM クラスが処理の完了を報告する場合もあります。
タスクから電子メール メッセージを送信するための、便利なアクションの種類も用意されています。これまでに説明したアクションの種類とは異なり、操作や管理タスクの実行にはそれほど役立ちませんが、通知メカニズムとしての使用に最適です。次のセクションでトリガについて解説しますが、今のところは、従来のカレンダーベースや時間ベースのトリガ以外に、イベントによってもタスクの開始が可能であるとだけ申し上げておきましょう。電子メール アクションの作成方法を次に示します。
|
CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_SEND_EMAIL, &action));
CComQIPtr<IEmailAction> emailAction(action);
HR(emailAction->put_From(CComBSTR(L"kenny@example.com")));
HR(emailAction->put_To(CComBSTR(L"karin@example.com")));
HR(emailAction->put_Subject(CComBSTR(L"subject")));
HR(emailAction->put_Body(CComBSTR(L"body")));
HR(emailAction->put_Server(CComBSTR(L"mail.example.com")));
|
タスク トリガ
タスク スケジューラには、ユーザー操作なしにタスクを開始できる、さまざまなトリガが用意されています。通常の時間ベースやカレンダーベースのトリガのほか、便利なイベントベースのトリガがいくつもあります。
タスクの作成とタスク定義への追加には、タスク定義の Triggers プロパティから返される ITriggerCollection インターフェイス ポインタを使用します。
|
CComPtr<ITriggerCollection> triggers;
HR(definition->get_Triggers(&triggers));
|
ITriggerCollection が提供する Create メソッドは、新しいトリガ オブジェクトを作成してコレクションに追加します。トリガ自体は、ITrigger から派生するインターフェイスを通じて公開されます。前のセクションの例で、最適化操作を開始するトリガの例を次に示します。
|
CComPtr<ITrigger> trigger;
HR(triggers->Create(TASK_TRIGGER_WEEKLY, &trigger));
CComQIPtr<IWeeklyTrigger> weeklyTrigger(trigger);
HR(weeklyTrigger->put_StartBoundary(CComBSTR(
L"2007-01-01T02:00:00-08:00")));
HR(weeklyTrigger->put_DaysOfWeek(0x01)) // Sunday
|
新しいタスク スケジューラの初期リリースでは、TASK_TRIGGER_WEEKLY を含む 11 種類のトリガを使用できます。使用可能なトリガの種類と、それぞれに関連付けられた COM インターフェイスを、図 4 にまとめて示します。StartBoundary プロパティは、タスクがトリガされる最も早い日付と、タスクが開始される時刻を表します。DaysOfWeek プロパティは、タスクを実行する曜日を表します。ビットマスクを使用して、任意の曜日を組み合わせることができます。上の例のトリガでは、2007 年の元日以降、毎週日曜日の午前 2 時 (太平洋標準時) にタスクが実行されます。

Figure 4 トリガの種類
| 種類 |
インターフェイス |
用途とオプション |
| TASK_TRIGGER_EVENT |
IEventTrigger |
Windows イベント ログで定義されたイベント。 |
| TASK_TRIGGER_TIME |
ITimeTrigger |
時刻、ランダム遅延 (オプション)。 |
| TASK_TRIGGER_DAILY |
IDailyTrigger |
n 日ごと、ランダム遅延 (オプション)。 |
| TASK_TRIGGER_WEEKLY |
IWeeklyTrigger |
指定した曜日、n 週ごと、ランダム遅延 (オプション)。 |
| TASK_TRIGGER_MONTHLY |
IMonthlyTrigger |
指定した日付、指定した月、月の最終日 (オプション)、ランダム遅延 (オプション)。 |
| TASK_TRIGGER_MONTHLYDOW |
IMonthlyDOWTrigger |
指定した曜日、月の第 n 週、指定した月、月の最終週 (オプション)、ランダム遅延 (オプション)。 |
| TASK_TRIGGER_IDLE |
IIdleTrigger |
コンピュータのアイドル時。 |
| TASK_TRIGGER_REGISTRATION |
IRegistrationTrigger |
タスクの作成時または更新時、遅延 (オプション)。 |
| TASK_TRIGGER_BOOT |
IBootTrigger |
コンピュータの起動時、遅延 (オプション)。 |
| TASK_TRIGGER_LOGON |
ILogonTrigger |
ユーザーのログオン時、遅延 (オプション)。 |
| TASK_TRIGGER_SESSION_STATE_CHANGE |
ISessionStateChangeTrigger |
さまざまなセッション イベント。 |
次のステップ
Windows Vista 以降、新しいタスク スケジューラには、使用せずにいられない理由が数多くあります。Windows の以前のバージョンでタスク スケジューラを使用していた読者は、間違いなくすべての新機能に満足することでしょう。実際、今日のアプリケーション用に記述された多くのカスタムビルドのスケジューラが、間もなくタスク スケジューラに置き換わるものと予想されます。
タスク スケジューラの機能のすべてについては、このコラムで扱いきれませんでした。ぜひ、Windows SDK (
msdn2.microsoft.com/en-us/library/aa383614.aspx) のドキュメントも参照してください。XML を直接使用するタスク作成、タスクが実行されるセキュリティ コンテキストに対する制御の強化、実行中のタスクの列挙と管理など、その他の機能について情報を得ることができます。
ご質問は、
Kenny mmwincpp.
まで英語でお送りください。
Kenny Kerr は、Windows のソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は
weblogs.asp.net/kennykerr です。