複数のコンソールアプリケーションを呼び出す簡易ワークフローを作ってみる


## このドキュメントの内容

業務システムでは他システム連携や実績データ集計などを夜間バッチで運用することが比較的多いです。システムの規模に比例して夜間バッチアプリケーションの数も多くなり、ジョブ管理ソフトを導入したりするわけですが、決して安いものではありません。また、ソフトがサポートしている全ての機能が必要かというとそうでもないケースが少なくありません。
そこで、シンプルなワークフローの仕組みを作ってみます。

なお、プロセス呼び出しには .NET の Process クラスではなく、先日公開されたOSSの [ProcessX](https://github.com/Cysharp/ProcessX) を使ってみます。C#8.0 の非同期ストリームを用いて標準出力を受け取ることができたり、async-await なプロセス呼び出しが可能になります。
[ProcessX - C#でProcessを C# 8.0非同期ストリームで簡単に扱うライブラリ](http://neue.cc/2020/01/30_590.html)


## 実装予定の機能

### 終了コードによる分岐

コンソールアプリケーションの終了コードによる分岐を実現します。終了コードが 0 のときは成功、0 でないときは失敗とみなします。

ワークフロー上の処理単位を「ワークフローアイテム」と考え、次のメンバを定義します。

* 処理を実行するメソッド。戻り値で終了コードを返します。
* 処理が成功したときの次処理を表すワークフローアイテムと、処理が失敗したときの次処理を表すワークフローアイテムを表すプロパティ。単方向のリンクリスト構造とします。
* 終了コードごとの次処理を表すワークフローアイテムを持たせるかどうかも検討しています。

```c#:ワークフローアイテムを表すインターフェース
/// <summary>
/// ワークフローアイテムに必要な機能を提供します。
/// </summary>
public interface IWorkflowItem
{
    /// <summary>
    /// 処理を実行します。
    /// </summary>
    /// <param name="prevExitCode">前処理の終了コード</param>
    /// <returns>終了コード</returns>
    Task<int> ExecuteAsync(int prevExitCode);

    /// <summary>
    /// 処理が成功したときの次処理を取得または設定します。
    /// </summary>
    IWorkflowItem NextOnSucceed { get; set; }

    /// <summary>
    /// 処理が失敗したときの次処理を取得または設定します。
    /// </summary>
    IWorkflowItem NextOnFailed { get; set; }
}
```

### 直列実行

一つのコンソールアプリケーションを実行する処理を「コマンド」と考え、次のメンバを定義します。

* 処理を実行するメソッド。戻り値で `ProcessX` で提供されている非同期ストリームオブジェクトを返します。このメソッドの中でプロセス呼び出しを行うように実装します。

ワークフローアイテムの中にコマンドを複数格納し、直列実行します。

```c#:コマンドを表すインターフェース
/// <summary>
/// コマンドに必要な機能を提供します。
/// </summary>
public interface IProcessCommand
{
    /// <summary>
    /// 処理を開始します。
    /// </summary>
    /// <param name="prevExitCode">前処理の終了コード</param>
    /// <returns>標準出力を返す非同期シーケンス</returns>
    ProcessAsyncEnumerable StartAsync(int prevExitCode);
}
```

### 並列実行

ワークフローアイテムの中にコマンドを複数格納するのは直列実行と同じです。
Task.WhenAll メソッドを用いて並列実行します。


### 直列／並列実行の終了コード判定

ワークフローアイテム内で複数のコマンド（＝プロセス呼び出し）を実行する場合、それぞれのコマンドの終了コードからワークフローアイテム単位の終了コードを決定できる仕組みを実装します。

```c#:終了コード決定処理を表すインターフェース
/// <summary>
/// 終了コードの制御に必要な機能を提供します。
/// </summary>
public interface IExitCodeHandler
{
    /// <summary>
    /// 指定されたコマンドの実行結果から終了コードを決定します。
    /// </summary>
    /// <param name="results">コマンドの実行結果</param>
    /// <returns>終了コード</returns>
    int HandleExitCode(ProcessCommandResult[] results);
}

/// <summary>
/// コマンドの実行結果。
/// </summary>
public readonly struct ProcessCommandResult
{
    public ProcessCommandResult(IProcessCommand command, int exitCode)
    {
        Command = command;
        ExitCode = exitCode;
    }

    /// <summary>
    /// コマンドを取得します。
    /// </summary>
    public IProcessCommand Command { get; }

    /// <summary>
    /// 終了コードを取得します。
    /// </summary>
    public int ExitCode { get; }
}
```

### 設定ファイルによるワークフローの組み立て

前述のワークフローアイテム、コマンド、終了コード制御の設定値を設定ファイルから読み込み、ワークフローを組み立てられるようにします。

* ワークフローアイテムのリンクリスト構造
* 実行するコマンドライン文字列
* 環境変数として渡すキーと値の組み合わせ
* 終了コードの制御


## サンプルコード

現時点のソースコードは [GitHub](https://github.com/mxProject/Misc/tree/master/source/ProcessWorkflow) で見ることができます。但し、検討初期段階のため、破壊的変更を含む大幅な変更を行う可能性が非常に高いです。

### ワークフロー実行アプリケーション

ワークフローアイテムのリンクリスト構造を組み立て、起点となるワークフローアイテムを実行します。
次のコードではコード上でワークフローアイテムを組み立てていますが、設定ファイル読み込みによって同等の内容を実現できるように検討しています。

```c#
class Program
{
    static async Task Main(string[] args)
    {

        string filePath = @"d:\SampleApp1.exe";

        var item = new ParallelWorkflowItem("root", "最初の処理")
        {
            // 実行するコマンド（並列実行）
            Commands = new[]{
                ProcessCommandFactory.FromCommandLine("command1", "コマンド1", filePath + " 5")
                , ProcessCommandFactory.FromCommandLine("command2", "コマンド2", filePath + " 4")
            }
            ,
            // 終了コードの制御は既定
            // 何れかのコマンドの終了コードが 0 でない場合、最初に見つかった終了コードを返す
            ExitCodeHandler = null
            ,
            // 成功時の次処理
            NextOnSucceed = new SequencialWorkflowItem("root-succeed", "成功時の後処理")
            {
                // 実行するコマンド（直列実行）
                Commands = new[]
                {
                    ProcessCommandFactory.FromFile("command3", "コマンド3", filePath, "3")
                    , ProcessCommandFactory.FromFile("command4", "コマンド4", filePath, "-2")
                }
                ,
                // 終了コードの制御
                ExitCodeHandler = ExitCodeHandlerFactory.Create(
                    // 既定の終了コード
                    -1
                    , new[]
                    {
                        // command3 の終了コード = 0 && command4 の終了コード = 0 => 0
                        (new[] { ("command3", 0), ("command4", 0) }, 0)
                        // command3 の終了コード = 1 && command4 の終了コード = 1 => 11
                        , (new[] { ("command3", 1), ("command4", 1) }, 11)
                        // command4 の終了コード = 1 => 10
                        , (new[] { ("command4", 1) }, 10)
                    }
                )
                ,
                // 成功時の次処理
                NextOnSucceed = new WorkflowItem("root-succeed-succeed", "成功時の後処理")
                {
                    Command = ProcessCommandFactory.FromFile("command5", "コマンド5", filePath, "1")
                }
                ,
                // 失敗時の次処理
                NextOnFailed = new WorkflowItem("root-succeed-failed", "失敗時の後処理")
                {
                    // 実行するコマンド
                    Command = ProcessCommandFactory.FromFile("command6", "コマンド6", filePath, "2")
                }
            }
            ,
            // 失敗時の次処理
            NextOnFailed = new WorkflowItem("root-failed", "失敗時の後処理")
            {
                // 実行するコマンド
                Command = ProcessCommandFactory.FromFile("command7", "コマンド7", filePath, "1")
            }
        };


        // ワークフローを実行する
        IWorkflowItem current = item;
        int exitCode = 0;

        try
        {
            while (current != null)
            {
                Console.WriteLine($"----- {current.ID}:{current.Name} -----");
                exitCode = await current.ExecuteAsync(exitCode);
                Console.WriteLine($"{current.ID}.ExitCode = {exitCode}");

                if (exitCode == 0)
                {
                    current = current.NextOnSucceed;
                }
                else
                {
                    current = current.NextOnFailed;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}
```

### 呼び出すコンソールアプリケーション

```c#:SampleApp1
class Program
{
    static async Task<int> Main(string[] args)
    {
        int exitCode = await MainAsync(args);
        Console.WriteLine($"ExitCode = {exitCode}");
        return exitCode;
    }

    private static async Task<int> MainAsync(string[] args)
    {

        // 環境変数から前処理の終了コードを取得して出力
        var variables = System.Environment.GetEnvironmentVariables();
        var key = ProcessCommandVariables.PrevExitCode;
        Console.WriteLine($"EnvironmentVariables[{key}] = {variables[key]}");

        // 引数を出力
        for (int i = 0; i < args.Length; ++i)
        {
            Console.WriteLine($"args[{i}] = {args[i]}");
        }

        // 先頭の引数は繰り返し回数を表すものとする
        int repeatCount = Convert.ToInt32(args[0]);

        if (repeatCount < 0) { return 1; }

        for (int i = 0; i < repeatCount; ++i)
        {
            await Task.Delay(1000);
            Console.WriteLine($"{i + 1}/{repeatCount}");
        }

        return 0;

    }

}
```

### 出力結果

```console
----- root:最初の処理 -----
[コマンド1] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド1] args[0] = 5
[コマンド2] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド2] args[0] = 4
[コマンド1] 1/5
[コマンド2] 1/4
[コマンド1] 2/5
[コマンド2] 2/4
[コマンド1] 3/5
[コマンド2] 3/4
[コマンド1] 4/5
[コマンド2] 4/4
[コマンド2] ExitCode = 0
[コマンド1] 5/5
[コマンド1] ExitCode = 0
root.ExitCode = 0
----- root-succeed:成功時の後処理 -----
[コマンド3] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド3] args[0] = 3
[コマンド3] 1/3
[コマンド3] 2/3
[コマンド3] 3/3
[コマンド3] ExitCode = 0
[コマンド4] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 0
[コマンド4] args[0] = -2
[コマンド4] ExitCode = 1
Process returns error, ExitCode:1

root-succeed.ExitCode = 10
----- root-succeed-failed:失敗時の後処理 -----
[コマンド6] EnvironmentVariables[mxProject.ProcessWorkflow.PrevExitCode] = 10
[コマンド6] args[0] = 2
[コマンド6] 1/2
[コマンド6] 2/2
[コマンド6] ExitCode = 0
root-succeed-failed.ExitCode = 0
```