C#でタスクスケジューラに登録しようとした話


# TL;DR

* 最上位権限でタスクスケジューラに登録したい
    * ログオフ（サインアウト）しておいても動いてほしいから
* エクスポートしたXMLファイルをもとに作って、登録すればなんとかいけた

## 現在作業中のユーザで登録してみる

まずは普通に登録してみます。

```C#
// タスクスケジューラの登録解除
runProcess(@"/c schtasks.exe /delete /TN SampleTask /F");

// タスクスケジューラの登録
var command = "/c schtasks.exe /create /TN SampleTask /TR " +
		Directory.GetCurrentDirectory() + @"\SampleTask.bat ";

// TODO 必要に応じて、ここでcommandに色々な指定を追加する

// 登録
Process proc = new Process();
proc.StartInfo.FileName = Environment.GetEnvironmentVariable("ComSpec"); //"cmd.exe";
 
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.RedirectStandardInput = false;
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.Arguments = command;
proc.Start();
 
var results = proc.StandardOutput.ReadToEnd();
// TODO 必要ならresultsからログ出力とか

proc.WaitForExit();
proc.Close();
 
// 完了通知
MessageBox.Show("登録しました");
```

さて。
これだと、ログインしている状態じゃないと動きません。
それでもいいなら、十分です。

## XMLを利用して、登録してみる
「いちいち色々な設定を記載して登録するのとか面倒じゃない？」
ということで、一度テンプレートとなるようなXMLを作成してからタスクを登録する方法です。

### テンプレートとなるXMLの作成

1. 普通にWindowsのタスクスケジューラを起動し、適当なタスクを作成します。
2. 作成したタスクを選択して、右クリックなり、画面右なりから「エクスポート」を選択します。
3. 作成したタスクを削除します。

### 登録

XMLをタスクスケジューラに登録します。
といっても、command部分を以下のようにするだけです。

```C#
var command = "/c schtasks.exe /create /XML " + Path.Combine(Environment.CurrentDirectory, "template.xml") + " /TN SampleTask";
```

仮に、テンプレートを読み込んで編集したい場合があれば、XMLを読み込んだ後、追加したり変更したりします。

```C#
XmlDocument document = new XmlDocument();
document.Load(inputXmlPath); // テンプレートとなるXMLをロード

var task = document.GetElementsByTagName("Task")[0];

DateTime now = DateTime.Now;

// 今日の日付
var date = document.GetElementsByTagName("Date")[0];
date.InnerText = now.ToString("yyyy-MM-ddTHH:mm:ss");

// Author
var author = document.GetElementsByTagName("Author")[0];
author.InnerText = $@"{Environment.UserDomainName}\{Environment.UserName}";

// バッチのパス
var command = document.GetElementsByTagName("Command")[0];
command.InnerText = Directory.GetCurrentDirectory() + @"\SampleTask.bat";

// todo などなど適当にXMLに追加したり、変更したりする

// 保存
document.Save(outputXmlPath); // テンプレートに上書きでもいいが、他の名前で保存しておく（つまり、登録時のパスはoutputXmlPathを使う）
```

## XMLを利用して、SYSTEMユーザで登録してみる

サインアウト時にも動くようにしたいので、登録時のユーザをSYSTEMにしてみます。

```C#
"schtasks.exe /create /XML " + Path.Combine(Environment.CurrentDirectory, "sample.xml") + " /TN SampleTask /RU "" /RP ""
```

これで実行ユーザがSYSTEM（たぶん、最上位）になる。

ただし、ツールを起動したときのアカウントの権限次第ですが、タスクが登録されているかどうかの確認でエラーになります。

```C#
ITaskService taskservice = null;
Boolean exist = false;
try
{
	taskservice = new TaskScheduler.TaskScheduler();
	taskservice.Connect(null, null, null, null);
	ITaskFolder containingFolder = taskservice.GetFolder("\\");

	// 存在確認
	containingFolder.GetTask("SampleTask");
	exist = true;
}
catch (Exception ex)
{
	// こっちに来る
	exist = false;
}
finally
{
	if (taskservice != null)
	{
		System.Runtime.InteropServices.Marshal.ReleaseComObject(taskservice);
	}
}

return exist;
```

タスクが登録されているかどうかを判定する必要がないなら、このままでもOKです。

## XMLを利用して、ユーザにIDとパスを入力させつつ登録してみる
色々こねくり回してます。

前提として、エクスポートするタスクの設定は、最低でも以下のようにしています。

<img width="500px" src="https://camo.qiitausercontent.com/3b6acfd02d5c34d622a4926b335f202374923008/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f33333334372f32663736333065302d336133652d383032662d636232642d6633373664303631343064632e706e67" alt="タスク設定例.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33347/2f7630e0-3a3e-802f-cb2d-f376d06140dc.png">


まず、バッチファイルをもう一つ作ります。
保存パスを動的に変更したいなら、バッチファイルを動的に作ります。

```C#
string str = 
		"echo off" + Environment.NewLine + 
		"set USR_INPUT_STR=" + Environment.NewLine +
		"set /P USR_INPUT_STR=\"ユーザIDを入力してください: \"" + Environment.NewLine +
		"schtasks /create /XML " + Path.Combine(Environment.CurrentDirectory, "template.xml") + " /TN SampleTask /RU %USR_INPUT_STR% /RP \"\"" + Environment.NewLine +
		"timeout 5 /nobreak"; // 最後5秒待っているのは趣味（すぐ閉じないで、結果を見たかったから）

string outputBatPath = Path.Combine(Environment.CurrentDirectory, @"Register.bat"); // 保存先のパス
StreamWriter sw = new StreamWriter(
	outputBatPath,
	false,
	Encoding.GetEncoding("shift_jis"));

// 内容を書き込む
sw.Write(str);
// 閉じる
sw.Close();
```

次に、Processも少し設定をいじります。

```C#
string outputBatPath = Path.Combine(Environment.CurrentDirectory, @"Register.bat"); // 保存先のパス

Process proc = new Process();
proc.StartInfo.Verb = "RunAs";
proc.StartInfo.FileName = Environment.GetEnvironmentVariable("ComSpec");;
proc.StartInfo.UseShellExecute = true;
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.Arguments = "/c " + outputBatPath;

try
{
	proc.Start();
	proc.WaitForExit();
	proc.Close();
}
catch (System.ComponentModel.Win32Exception)
{
	//「ユーザーアカウント制御」ダイアログでキャンセルされたなどによって
	//起動できなかった時
	return true;
}

return false;
```

この方法をとると、ユーザIDとパスワードをユーザに入力させることができます。
とはいえ、逆に管理アカウントのパスワードを知らないとダメだったりするので、他のを使った方が良い場合もあると思います。

とりあえず、私がやりたかったことはこれで実現できたということで。