バックグラウンドで C# の DLL を実行する
1.5.8.1 のプリザンターでスケジュール実行できるのは、JavaScript のバックグラウンドサーバースクリプトだけです。このページは、C# で書いた DLL や EXE を定期実行する方法を比べた設計メモです。1 つ目の方法は本体の改修が要りませんが、残りは本体の標準機能ではありません。調査は 1.5.1.0 を対象に行いました。
前提にした現行実装(Quartz.NET のジョブ、Context の作り方、安全のための制御)は バックグラウンドサーバースクリプトの仕組み にまとめています。
流用できる既存の仕組み
Quartz.NET のジョブ
バックグラウンドサーバースクリプトは ClusterExecutionTimerBase([DisallowConcurrentExecution] 付き)を継承した BackgroundServerScriptJob として動き、クラスタリング時は DB のジョブストアで複数台に分散されます。同じ基底クラスを継承したジョブを足せば、C# の処理も同じスケジューラに載せられます。
DLL を読み込む 2 つの仕組み
| 項目 | ExtendedLibrary | PDF プラグイン |
|---|---|---|
| 置き場所 | 実行ファイルと同じフォルダの ExtendedLibraries/(直下と 1 階層下のサブフォルダ) | App_Data/UserPlugins/ |
| 読み込み | 起動時。AddApplicationPart() で Controller を登録(Startup.cs#L201-L207、#L469-L479) | 初回使用時に Assembly.LoadFrom() し、IPdfPlugin を実装した型を Activator.CreateInstance()(PdfPluginCache.cs#L12-L30) |
| キャッシュ | ApplicationPart として常駐 | ConcurrentDictionary(プロセス内) |
| 差し替え | 再起動が必要 | キャッシュ済みなら再起動が必要 |
| 設定 | ファイルを置くだけ | Extensions テーブル(ExtendedPlugin) |
ExtendedLibrary の使い方は 拡張ライブラリ を参照してください。
4 つの方法
方法 1:httpClient で別プロセスの Web API を呼ぶ(改修不要)
C# の処理を ASP.NET Core の Web API として別プロセスで動かし、バックグラウンドサーバースクリプトから httpClient で呼びます。
図を読み込み中…
httpClient.RequestUri = 'http://localhost:5001/api/my-task';
httpClient.Content = JSON.stringify({ tenantId: context.TenantId });
httpClient.MediaType = 'application/json';
httpClient.Post();
if (!httpClient.IsSuccess) {
logs.LogSystemError('Task failed: ' + httpClient.StatusCode);
}- 本体を変えずにすぐ使え、プロセスも完全に分かれます。
- Web API の配置と、API キーなどの認証は自前で用意します。
Script.jsonのDisableServerScriptHttpClientがfalse(既定)である必要があります。
方法 2:Quartz のジョブとして組み込む
ClusterExecutionTimerBase を継承した BackgroundAssemblyJob を作り、ジョブのデータ(テナント ID・DLL のパス・型名)から Assembly.LoadFrom() で読み込んで、IBackgroundTask を実装した型を実行します。例外は SysLogModel に記録します。
- プリザンターの
Contextをそのまま使え、デバッグしやすい方法です。 - 新しいジョブクラスに加え、テナント設定と画面の改修が要ります。
- 同じプロセスで動くので、DLL の依存が本体と衝突する可能性があります。
方法 3:サーバースクリプトから Process.Start で EXE を起動する
process.start({ fileName, arguments, waitForExit, timeoutMs }) のようなホストオブジェクトを追加して EXE を起動します。子プロセスに分かれますが、任意のコマンド実行につながるため本番では勧めません。
方法 4:PDF プラグインの方式を汎用化する
PdfPluginCache と同じ方式で、汎用のタスク用プラグインを読み込みます。
public interface IBackgroundTaskPlugin
{
string Name { get; }
BackgroundTaskResult Execute(BackgroundTaskContext context);
}
// BackgroundTaskContext: TenantId・UserId・Parameters(JSON)
// BackgroundTaskResult: Success・Message| 追加するもの | 変更するもの |
|---|---|
Implem.Plugins/IBackgroundTaskPlugin.cs | TenantSettings に BackgroundTaskPlugins(SettingList)を追加 |
Libraries/BackgroundServices/BackgroundTaskPluginCache.cs | TenantUtilities にプラグインの設定画面を追加 |
Libraries/BackgroundServices/BackgroundTaskPluginJob.cs | Startup.cs でプラグインのジョブを初期化 |
ExtendedPlugin.cs の種類に BackgroundTask を追加 |
結果の型をインターフェースでそろえられ、プラグインは Implem.Plugins だけを参照すればよくなります。
比較
| 観点 | 1. httpClient | 2. Quartz のジョブ | 3. Process.Start | 4. プラグイン |
|---|---|---|---|---|
| 本体の改修 | 不要 | 中 | 小〜中 | 中〜大 |
| プロセスの分離 | あり | なし | あり | なし |
| 既存の仕組みの流用 | 高 | 高 | 低 | 高 |
| セキュリティリスク | 低 | 高 | 極めて高 | 高 |
| デバッグのしやすさ | 中 | 高 | 低 | 高 |
| クラスタリング | 対応済み | 対応できる | ノードに依存 | 対応できる |
| タイムアウト | httpClient.TimeOut | CancellationToken | Process.WaitForExit(timeout) | CancellationToken をプラグインに渡す |
短期的には方法 1、組み込むなら方法 4 を勧めます。依存の衝突が心配なら方法 1 が安全です。
実装上の注意
依存の衝突
同じプロセスで読み込む方法 2・4 では、同じライブラリの別バージョンで FileLoadException や誤動作が起きることがあります。プラグインごとに AssemblyLoadContext(isCollectible: true と AssemblyDependencyResolver)で読み込むと軽減でき、本体の DLL は Implem.Plugins だけを参照させます。ネイティブライブラリの衝突はプロセスを分けないと避けられません。
ログ
既存のジョブと同じく、開始時に SysLogModel を作り、終わりに Finish し、例外も SysLogModel に記録します。
セキュリティ
| 方法 | 主な脅威 | 対策 |
|---|---|---|
| 1. httpClient | SSRF(httpClient に接続先の制限が無い)、認証情報や応答の外部への送信 | Web API 側のファイアウォール、API キーを環境変数などで管理 |
| 2・4. DLL | 同じプロセス・同じ権限での任意コード実行、プロセスの停止、接続文字列の参照、DLL の差し替え、依存ライブラリの脆弱性 | 配置フォルダの書き込み権限をデプロイ手順だけに限定、DLL の署名の検証、AssemblyLoadContext での分離、パスの検証 |
| 3. Process.Start | コマンドインジェクション、OS レベルの権限、子プロセスの資源消費 | 実行ファイルの許可リスト、UseShellExecute = false と引数のパラメータ化、cgroup などの資源制限、実行ログ、設定をテナント管理者ではなくサービス管理者に限定 |
PdfPluginCache は libraryPath を App_Data/UserPlugins と結合するだけで、../ を含むパスを検証していません(PdfPluginCache.cs#L18-L23)。$ps.file がセクション・パスを [0-9a-zA-Z_.\-](パスは / も可)の正規表現で制限しているのとは対照的です(ServerScriptFile.cs#L353-L355)。汎用化するときは、同様の正規表現チェックと、Path.GetFullPath した結果が基準フォルダの下にあることの確認を入れます。
マルチテナント
| 懸念 | 現行の制御 | 追加で要ること |
|---|---|---|
| テナント間のデータ | Context のテナント ID で SQL を絞る | DLL が DB に直接つなぐと制御できない |
| 共有資源の取り合い | 同じジョブの多重実行は防ぐ | テナントごとの資源の割り当て |
| エラーの波及 | ジョブごとに例外を捕捉 | DLL の読み込み方式ではプロセス全体に影響する |
| ログ | SysLogs にテナント ID を記録 | テナントごとのログの分離 |
方法 2・4 は、DLL の出どころと中身を信頼できることが前提です。信頼できない DLL を読み込む運用はしないでください。