C#で標準(エラー)出力をリダイレクトする
前置き。月額200円ぐらいでWindowsServer2016が使えるVPS環境が見つかったのでお遊び。バックグラウンドで動かすプログラムがあるのだが、たまに落ちてしまうことがあるので、定期的に「実行していなければ動かして、動いていれば何もしない」という操作をした。
タスクスケジューラ
これはLinuxは言う所のcronを使えば簡単に(?)できる。ワンライナーでできるらしい。Linux環境ではPythonが使えれば、モジュールのpsutil使って、プロセスを確かめていって同時起動を防止することもできる(自分はこっちのほうが好き)。Windows環境の場合、cron相当のものとしてタスクスケジューラがあり、ほぼ同様のことができる(cronと違って分単位の細かい設定をするのが難しい)。以下、設定する際のポイント。
タスクスケジューラの作業ディレクトリの設定
cronのときにありがちなミスとして、コマンドラインからは実行できたが、cronから動かすと実行できないというものがある。大方パスがらみで、cronの場合はカレントディレクトリが変わるため、気をつけないと相対パスが処理できない。cronの場合の解決法として、
- 絶対パスで記述する → 最も確実だがデバッグ環境でパスを書き換えるのがめんどい
- シェルスクリプトを噛ませて、プログラムを実行する前にcdでカレントディレクトリに移動しておく → 結構使える
- 例えばPythonの場合はプログラムレベルで設定する → これも好き
import os os.chdir("カレントディレクトリ")
タスクスケジューラの場合は、設定画面で作業ディレクトリを設定できる。これを忘れていて最初ハマったが、ここで設定したらバッチファイルが通った。
開始(オプション)に作業ディレクトリを設定する。地味にわかりづらい。
分単位での発動のさせ方
トリガーで編集する。毎日1回としておいて、トリガー→編集として繰り返し間隔で分単位で設定できる。詳細はこれ↓
ただ、5分、10分、15分、30分、1時間のような既定値でしか設定できないようで、cronのような細かい設定はできない。ここだけはcron記法で記述したかったり。
セキュリティオプションによる違い
タスクスケジューラのセキュリティオプションとして次の2つがある
- ユーザーがログオンしているときのみ実行する
- ユーザーがログオンしているかどうかにかかわらず実行する
そりゃcronの代用として使うんだから下一択でしょと思ったら、なんとこのオプションで動作の仕様が変わる。下を選ぶとバックグラウンドプロセスになり、コンソールアプリケーションの場合は黒い画面が表示されない。 なら上を選べばいいか、というと、ログオンの定義が最初「該当のユーザーでログインしていればいい、つまりリモートデスクトップを閉じてしまっても別のユーザーでログインしなければ大丈夫」という意味かと思っていたら、実行されなかった。なのでここでの、ログオンの定義=リモートデスクトップ等で該当ユーザーのセッションが維持されているという意味だと思う。
なので結局下を選ぶしかない。ところが、バックグラウンドプロセスになるので、コンソール画面が表示されず何やっているのかよくわからない。ならどうするか、解決法の1つとして、出力をファイルにリダイレクトしてしまえばいいのです。シェルなんかではおなじみのテクニック。
バッチファイルでもできるけど、せっかくWindowsServer使っているからC#で記述してみた。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; using System.IO; class Program { static string filePath = null; static Encoding utf8 = new UTF8Encoding(false); static void Main(string[] args) { var p = new Process(); //ログファイルのディレクトリを作成 var date = DateTime.Now; filePath = Path.Combine(Environment.CurrentDirectory, "log", date.ToString("yyyy"), date.ToString("MM"), date.ToString("dd"), date.ToString("s").Replace(":", "_") + ".txt"); var dir = Path.GetDirectoryName(filePath); if(!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } //出力をストリームに書き込むようにする p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardError = true; //OutputDataReceivedイベントハンドラを追加 //p.StartInfo.RedirectStandardOutput = true; //p.OutputDataReceived += P_OutputDataReceived; p.ErrorDataReceived += P_OutputDataReceived; p.StartInfo.RedirectStandardInput = false; p.StartInfo.CreateNoWindow = true; p.StartInfo.FileName = Path.Combine(Environment.CurrentDirectory, "実行するプログラム"); p.StartInfo.Arguments = @"引数1 引数2 ……"; //起動 p.Start(); //非同期で出力の読み取りを開始 p.BeginErrorReadLine(); //p.BeginOutputReadLine(); p.WaitForExit(); p.Close(); } //標準出力と標準エラー出力を同時に読むときは、標準出力読んでいるときにエラー出力が割り込む(あるいはその逆がある)ので //ファイル操作部分をロックする。どっちかだけの場合は不要 private static void P_OutputDataReceived(object sender, DataReceivedEventArgs e) { if (filePath == null) return; //追記してBOMなしUTF8 using (var sw = new StreamWriter(filePath, true, utf8)) { sw.WriteLine(e.Data); } } }
WindowsServerは何もインストールしなくても.NETのプログラムが動くのが素晴らしい(MS製品だからそれはそう)。期待通りの出力になった。実行しているプログラムが、ログを標準出力ではなく標準エラー出力に出すというちょっとよくわからないことをしているので、エラーのみを読んでいる。標準出力のみ読んでいたら何も出てこなくてびっくりした。
エラーと出力を両方読む場合は、必ずファイルをロック(排他制御)しよう。usingの部分をlockのステートメントで囲ってしまえばいい。こんなイメージ↓
static _lock = new object(); static void Method() { lock(_lock) { using(…) } }
実はこのプログラム、だいたい期待通りに動くんだけど100点ではなくて、親プロセスを終了させたときに、子のプロセスがゾンビプロセスとして生き続ける仕様がある。解決法はJob Objectを使うらしい。
ざっと読んでみたらなかなか闇が深そうなので、スルーしてしまった。自分用だしゾンビプロセスは手動でkillすればいいよね、うん(おわり)。