2013年8月30日金曜日

なつがおわりまーす

とか言いつつ今日は暑いです。

えー、8月も間もなく終わり、前回のエントリからほぼ3週間ですか。思いの外サボってしまいました…。

そんな時期もありますよね(白目)。ボチボチいきます…。

2013年8月10日土曜日

古くなったログファイルを削除する処理をC#+LINQで。

何かしら業務で利用するシステムであれば、大抵何かあったときの障害解析などに備え、ログファイルが作られることになります。で、そのログファイルは作られっぱなしだとそのうちディスクを圧迫するので、ある一定のルールの下で削除されていくことになるのが一般的だと思います。

その「削除されるルール」は、当然すべてきれいさっぱり消してしまっては、いざという時に役に立たないし、またシステムによってログの残し方の要件が違ったりで、設計や実装も結構厄介なもんです。

ここでは、いくつかログの残し方ルールをあげて、古いログファイルを削除する処理をC#+LINQを使って書いてみようと思います。

ログファイルの構成前提


前提として、ログファイルは以下のような形で残されていることとします。

  • ログフォルダにはログファイルのみが存在する。
  • ログファイルのファイル名は「log_yyyy-MM-dd_nn.log」とする。ただし、yyyyは西暦の4ケタ。MMは月の2ケタ。ddは日の2ケタ。nnはシーケンシャル番号の2ケタ。
  • ログファイルは以下のタイミングで新たに生成される。ただし、同一日のログファイルが存在しない場合はシーケンシャル番号は「0」。存在する場合はシーケンシャル番号の最大+1になるようにする。
    • 日が変わった以降最初のログ出力時。
    • 現在のログファイルが所定のサイズ以上になった時。
    • サービスの起動時。
そしてログファイル削除クラスとしてスタティックな「DeleteOldLogFiles」クラスを作ることとします。

バリエーション1:指定ファイル数のみ残して古いものを削除


指定された数のより新しいファイルを残し、それ以外のものを削除する仕様として処理を考えます。この場合、ファイル名を降順でソートして、新しいほうから所定の数は何もせず、それ以降はファイルを削除する。という処理を考えてみました。

ForEachメソッドを使いたいので、IEnumerableを一旦Listに変換しています。ま、しょうがない。

public static void ByCounts(string path, int keepCount)
{
  Directory.GetFiles(path)
    .OrderByDescending(f => f)
    .Skip(keepCount)
    .ToList()
    .ForEach(f => File.Delete(f));
}

ま、シンプルですよね。

バリエーション2:指定ファイルサイズに収まるようにして古いものを削除


バリエーション1の変形ですが、新しいほうからファイルサイズを足して行って、所定のサイズ以下のうちはスキップし、所定のサイズを超えた以降のファイルを削除する処理とするとシンプルに書けますね。

public static void BySize(string path, long size)
{
  long totalSize = 0;

  Directory.GetFiles(path)
    .OrderByDescending(f => f)
    .SkipWhile(f => (totalSize += new FileInfo(f).Length) < size)
    .ToList()
    .ForEach(f => File.Delete(f));
}


バリエーション3:日付が古いものを削除


ファイル名から日付部分を抜き出して、指定日数より以前のものは削除する仕様とします。このメソッドの引数「path」と「days」はまあいいとして、「startPos」はファイル名中の日付文字列の開始位置を、「dateForm」は日付文字列の解析フォームを渡します。

public static void ByDays(string path, int days, int startPos, string dateForm)
{
  var target = DateTime.Today.AddDays(-days);

  Directory.GetFiles(path)
    .Where(f => DateTime.ParseExact(
      Path.GetFileName(f).Substring(startPos, dateForm.Length),
      dateForm,
      System.Globalization.DateTimeFormatInfo.InvariantInfo) < target)
    .ToList()
    .ForEach(f => File.Delete(f));
}

今回のケースでログファイルの保持日数を3日とすると、ログファイル名は「log_yyyy-MM-dd_nn.log」なので、「startPos」は4、「dateForm」は「yyyy-MM-dd」となるので、以下のようにこのメソッドを呼び出すことになります。

DeleteOldLogFiles.ByDays(logFolderPath, 3, 4, "yyyy-MM-dd");


2013年8月1日木曜日

今週のマガジン

『あひるの空』が凄かった。なんだこれ?数週間のモヤモヤを一気に晴らす10ページ。一瞬何が起きたのかわからないくらいの一気。いやー。スゲー。
しかし、「どこに負けるか」をバラしておいて、「敗戦までのキセキ」を描くんですか。是非描ききってほしいです。

『ベイビーステップ』も暗い話にならなくて良かった。とはいえバッドエンドの地雷は埋まったままなので、ここからどう展開するのか楽しみ。

2013年7月27日土曜日

今ハズセナイ連載中漫画 (2013年夏版)

今楽しみにしている、連載中漫画リスト。春からいくつかの作品が完結しましたね。ってことで最新版に更新。
  • 週刊少年マガジン
    • あひるの空/日向武史
    • ベイビーステップ/勝木光
    • アゲイン!!/久保ミツロウ
    • 七つの大罪/鈴木央
    • 波打際のむろみさん/名島啓二
    • 我妻さんは俺のヨメ/蔵石ユウ・西木田景志
  • 週刊少年サンデー
    • 神のみぞ知るセカイ/若木民喜
    • マギ/大高忍
    • 銀の匙 Silver Spoon/荒川弘
    • 絶対可憐チルドレン/椎名高志
    • ハヤテのごとく!/畑健二郎
    • 最後は?ストレート!!/寒川一之
  • ヤングマガジン
    • センゴク一統記/宮下英樹
    • 雪にツバサ/高橋しん
    • エイト/楠みちはる
    • 8♀1♂/咲香里
  • ビックコミックスピリッツ
    • アイアムアヒーロー/花沢健吾
    • とめはねっ!/河合克敏
    • くーねるまるた/高尾じんぐ
    • あさひなぐ/こざき亜衣
    • 王様達のヴァイキング/さだやす
  • ビックコミックスペリオール
    • 人生画力対決/西原理恵子
  • モーニング
    • GIANT KILLING/ツジトモ・網本将也
    • 宇宙兄弟/小山宙哉
    • グラゼニ/森高夕次・アダチ ケイジ
    • ひらけ駒!/南Q太
    • 鬼灯の冷徹/江口夏美
    • ピアノの森/一色まこと
    • ライスショルダー/なかいま強
  • イブニング
    • 少女ファイト/日本橋ヨヲコ
    • いとしのムーコ/みずしな孝之
    • ADAMAS/皆川亮二
    • プロチチ/逢坂えみ子
    • Eから弾きな。/佐々木拓丸
  • アフタヌーン
    • ヒストリエ/岩明均
    • ヴィンランド・サガ/幸村誠
  • ゲッサン
    • MIX/あだち充
    • アオイホノオ/島本和彦
  • グランドジャンプ
    • 瞬きのソーニャ/弓月光
    • ぼくの体はツーアウト/よしたに
  • その他
    • もやしもん/石川雅之
    • 3月のライオン/羽海野チカ
    • ましろのおと/羅川真里茂
    • 白衣のカノジョ/日坂水柯
    • kiss×sis/ぢたま某
    • よつばと!/あずまきよひこ
    • WxY/マドカマチコ
完結してリストから削除されたのは、多分この4つ。
  • ジャポニカの歩き方/西山優里子
  • アントルメティエ/早川光・きたがわ翔
  • こどものじかん/私屋カヲル
  • 純潔のマリア/石川雅之
「ジャポニカの歩き方」は、以前のエントリで最終回についてちょっと書きましたね。「純潔のマリア」は終わったらしいんですが、単行本待ちです。2巻の終わりから後1冊でどうラストに持っていくのか楽しみ。

「こどものじかん」も小学校を卒業して終わりましたね。ただ、あのラストはいーのかしら?こうこうせいだよね…。しかも見た目変わらないってのがどうもなー。ともあれ、周りにお勧めできるかどうかはともかく、楽しく読めて考えることも多かったです。イイ漫画でした。

あとは「アントルメティエ」。これはなー。ちょっと残念なラストでした。面白かったと思うんだけどなー。


今回はこんな感じです。その他、リストに挙げてないけど楽しみにしているのも多いですし、最近始まったばかりで期待しているのもいっぱいあります。なんかしら、大化けして楽しみに育ってくれるモノが出てきてくれるとさらにうれしいなー。

FxCop カスタムルールの作成(配置と実行)

「FxCop カスタムルールの作成(ビルドまで)」で作成したルールを実際に動かします。

本当はデバッガで動作を追っかけたいところですが、VS2012Expressではデバッガで外部ツール(プログラム)を実行させるのは難しいんですかね。残念ですがデバッガの使用はあきらめます。

ちなみに、Express以外の有償のエディションであれば、プロジェクトのプロパティで「デバッグ」を開き、「開始動作」を「外部プログラムの開始」に、「外部プログラムの開始」に「FxCopCmd.exe」へのパスを、「コマンドライン引数」に「/f:」に続いて静的分析したいアセンブリのフルパスを、さらに「/c」のスイッチを付けてコンソールに結果を出力させるようにしておくことで、作成したルールをデバッグできる「はず」です。

デバッグ時の注意点としては、VisualStudioもデバッグしたいルールアセンブリを読み込んでしまうので、修正して再配置するときは、VisualStudioを一度終了させる必要があります。面倒ですが仕方ない。

さて、ビルドしたルールアセンブリは、Program FilesのVSのフォルダの、「Team Tools\Static Analysis Tools\FxCop\Rules」のフォルダにコピーします。デバッグする場合は"pdb"ファイルも一緒にコピーします。

作成したルールアセンブリが、正しく動作するかどうかを確認するテスト用アセンブリも作っておきます。VS2012Expressで新規プロジェクトを作成し、クラスライブラリを作ります。名前は「DataTableRuleDummy」とでもします。

テスト用アセンブリには一つクラスを作ります。ルールがすべて引っかかるように、以下のようにしてみました。

public class Dummy
{
  private DataTable table;

  public Dummy(DataTable table)
  {
    this.table = table;
  }

  public int SumOfQuantity
  {
    get
    {
      int sum = 0;
      foreach (DataRow row in table.Rows)
      {
        sum += (int)row["qty"];
      }
      return sum;
    }
  }

  public int FirstOrder
  {
    get { return (int)table.Rows[0]["order"]; }
  }

  public void SetDone()
  {
    foreach (var row in table.Rows.Cast<DataRow>().Where(r => (int)r["f"] == 0))
    {
      row["f"] = 1;
      row["doneAt"] = DateTime.Now;
    }
  }
}

で、テスト用のアセンブリもビルドしておきます。

実行はここではFxCopのコマンド版で試してみます。GUI版やVisualStudioビルド統合版も多分動くはず。

コマンドプロンプトを起動し、以下のように入力します。(もちろん、実際は改行なしです。)

>"C:\Program Files\Microsoft Visual Studio 11.0\Team Tools\
Static Analysis Tools\FxCop\FxCopCmd.exe" /f:D:\Projects\Da
taTableRuleDummy\bin\Debug\DataTableRuleDummy.dll /c

これで実行してみると以下の結果となりました。



4つのルールがすべて出力されています。続いて、テスト用アセンブリを以下のように修正してビルド。その後もう一度FxCopを実行させてみます。

public class Dummy
{
  private DataTable table;

  public Dummy(DataTable table)
  {
    this.table = table;
  }

  public int SumOfQuantity
  {
    get { return table.AsEnumerable().Sum(r => r.Field<int>("qty")); }
  }

  public int FirstOrder
  {
    get { return table.AsEnumerable().First().Field<int>("order"); }
  }

  public void SetDone()
  {
    foreach (var row in table.Select("flag=0"))
    {
      row.SetField("flag", 1);
      row.SetField("doneAt", DateTime.Now);
    }
  }
}


DataTable関連の警告が出なくなりましたね。

と、とてもとても時間がかかりましたが、古式ゆかしきDataSetも、工夫次第でそれなりに使えるということを言いたかったのです。

あと、会社やプロジェクト単位でコーディングスタンダードを用意している場合、コードレビューでスタンダードに適合しているかをチェックするなんてのはやってらんないので、このようにFxCopのカスタムルールを一度作ってしまえば、スタンダードに適合しているか否かのチェックをFxCopに任せられるので、あとがずいぶん楽になります。このために参考になる方もいるのではないかなぁ。と思いました。

2013年7月26日金曜日

FxCop カスタムルールの作成(ビルドまで)

「ADO.netのDataSetをLINQableに書くために (本題) ~ FxCopカスタムルールを作成する。」の続きです。今度こそカスタムルールを作ります。ただ、手元にVS2012Expressしかないので、これで作ります。

前エントリで書いたとおり、アセンブリを静的分析し、以下のパターンに適合する場合は警告を出すように作ります。

イマイチ好みじゃないずいぶんマシ
foeach構文でDataTable.Rowsプロパティを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。
LINQを使うために、DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。
DataRowCollectionクラスのインデクサを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。(インデクサは使わない。)
DataRowクラスのインデクサを使う。DataRow.Field拡張メソッドとDataRow.SetField拡張メソッドを使う。

VS2012Express(今回はDesktopを使いました。)を開き、新規プロジェクトを作ります。「クラスライブラリ」を選択し、プロジェクト名は「DataTableRules」とでもしておきましょう。


作成したプロジェクトに、FxCopのアセンブリを参照するようにします。プロジェクトの「参照設定」を右クリックし、「参照の追加」を選択します。

プロジェクト外の、GACにもない外部アセンブリを参照するので、「参照」ボタンをクリックします。
Program FilesのVSのフォルダの、「Team Tools\Static Analysis Tools\FxCop」にある2つのアセンブリファイル、「FxCopSdk.dll」と「Microsoft.Cci.dll」を選択して追加します。


作成したプロジェクトには、警告メッセージなどを管理するためのXMLリソースが必要になります。
ここでは「CustomRules.xml」というファイルをプロジェクトに追加します。プロジェクトを右クリックして「新規作成」-「追加」-「新しい項目」を選び、XMLファイルをプロジェクトに追加します。


  追加したXMLファイルはリソースとして扱います。ソリューションエクスプローラーで追加したXMLファイルを選び、プロパティで「ビルドアクション」を「埋め込まれたリソース」に変更します。


XMLの中身の編集は後でやります。

さて、前述の警告出力パターンですが、どうすれば検知できるかを考えてみます。

まず、「foeach構文でDataTable.Rowsプロパティを使う。」ですが、これは「DataTable.Rows.GetEnumerable」メソッドが使われたときに警告を出す。と考えればよさそう。

「DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。」は、そのまんま「Enumerable.Cast<DataRow>」静的メソッドが使われたときに警告を出すでオケ。

「DataRowCollectionクラスのインデクサを使う。」はインデクサの呼び出しですが、CLI的には「DataRowCollection.get_Item」メソッドが使われた時。という扱いになるようです。

「DataRowクラスのインデクサを使う。」も同様に、「DataRow.get_Item」または「DataRow.set_Item」メソッドが使われた時。となるようです。

これら4つのルールすべてにおいて、メソッドの呼び出しを確認することで検知可能ということになります。そこで、まずは基本となる抽象クラスを作ります。

プロジェクト作成時に作られるプレースホルダのクラス「Class1.cs」を使いましょう。まずはファイル名を「BaseDataTableRule.cs」に修正、クラス名もそれに合わせて変更します。内容は「すべてのメソッドを確認する」的な内容になっている…と思われます。なにせドキュメントが少なく、いろいろ試してうまくいっているだけなので、定かじゃないところも残ってます。

using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public abstract class BaseDataTableRule : BaseIntrospectionRule
  {
    protected BaseDataTableRule(string name) : base(
      name, "DataTableRules.CustomRules", typeof(BaseDataTableRule).Assembly) {}

    public override ProblemCollection Check(Member member)
    {
      var m = member as Method;
      if (m != null)
        Visit(m);

      return Problems;
    }
  }
}

抽象クラスを作ったら、1ルールにつき1クラスとなるように、4つのクラスを作り抽象クラスから派生させます。まず「foeach構文でDataTable.Rowsプロパティを使う。」を検出するルール。「NoUseGetEnumeratorMethodToDataTableRows」としました。長いけどこのクラスを誰か人に使ってもらうわけではないので、今回は気にしない。名前がわかりやすいほうが重要。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseGetEnumeratorMethodToDataTableRows : BaseDataTableRule
  {
    public NoUseGetEnumeratorMethodToDataTableRows()
      : base("NoUseGetEnumeratorMethodToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var target = binding.TargetObject;
      var member = binding.BoundMember;

      if (member.Name.Name == "GetEnumerator" && 
          target.Type.FullName == "System.Data.DataRowCollection")
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

2つめのクラスは「DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。」これを「NoUseCastMethodToDataTableRows」クラスとして実装します。(18行目と19行目に分けているのは、Web上で表示させるのに横長になりすぎるためで、本来分ける必要はありません。)

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseCastMethodToDataTableRows : BaseDataTableRule
  {
    public NoUseCastMethodToDataTableRows()
      : base("NoUseCastMethodToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName == 
        "System.Linq.Enumerable.Cast<System.Data.DataRow>"
        + "(System.Collections.IEnumerable)")
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

そして3つめのクラスは「DataRowCollectionクラスのインデクサを使う。」を「NoUseIndexerToDataTableRows」クラスとして実装します。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseIndexerToDataTableRows : BaseDataTableRule
  {
    public NoUseIndexerToDataTableRows()
      : base("NoUseIndexerToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName.StartsWith(
         "System.Data.DataRowCollection.get_Item(", StringComparison.Ordinal))
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

そして4つめ。「DataRowクラスのインデクサを使う。」を「NoUseIndexerToDataRow」として実装します。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseIndexerToDataRow : BaseDataTableRule
  {
    public NoUseIndexerToDataRow() : base("NoUseIndexerToDataRow") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName.StartsWith(
            "System.Data.DataRow.get_Item(", StringComparison.Ordinal) ||
          member.FullName.StartsWith(
            "System.Data.DataRow.set_Item(", StringComparison.Ordinal))
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

あとはXMLリソースの中身の編集です。長いですが、こんな感じ。(適宜改行してます。)
<?xml version="1.0" encoding="utf-8" ?>
<Rules FriendlyName="データテーブルの使い方の規則">
<Rule TypeName="NoUseCastMethodToDataTableRows"
      Category="My.DataTable" CheckId="DT0001">
<Name>DataTable.RowsにCast&lt;DataRow&gt;()拡張メソッドを使用しません</Name>
<Description>可読性の向上のため、DataTable.RowsにCast&lt;DataRow&gt;拡張メソッド
を使用しません。</Description>
<Url />
<Resolution>DataTable.RowsにCast&lt;DataRow&gt;拡張メソッドが使用されています。
さらなる条件の絞り込みや、ソートが必要であればDataTable.Select()メソッドを、
そうでなければDataTable.AsEnumerable()拡張メソッドを使用してください。
</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseGetEnumeratorMethodToDataTableRows"
      Category="My.DataTable" CheckId="DT0002">
<Name>foreach構文でDataTable.Rowsを使用した列挙を行いません</Name>
<Description>LINQとの親和性向上のため、DataTable.Rows.GetEnumerable()メソッドを
使用しません。</Description>
<Url />
<Resolution>DataTable.Rows.GetEnumerable()が使用されています。さらなる条件の
絞り込みや、ソートが必要であればDataTable.Select()メソッドを、そうでなければ
DataTable.AsEnumerable()拡張メソッドを使用してください。</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseIndexerToDataTableRows"
      Category="My.DataTable" CheckId="DT0003">
<Name>DataTable.Rowsのインデクサを使用しません</Name>
<Description>可読性の向上のため、DataTable.Rowsのインデクサを使用しません。
</Description>
<Url />
<Resolution>DataTable.Rowsのインデクサが使用されています。テーブル内の各行を列挙
する場合には、DataTable.Select()メソッド、またはDataTable.AsEnumerable()拡張
メソッドを使用してforeach構文で列挙してください。 先頭行のみ取り出す場合には、
DataTable.AsEnumerable()拡張メソッドと、Enumerable.First()を使用して一時変数に
代入後、使用してください。
      例:var firstRow = dt.AsEnumerable().First();</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseIndexerToDataRow"
      Category="My.DataTable" CheckId="DT0004">
<Name>DataRowのインデクサを使用しません</Name>
<Description>可読性の向上のため、DataRowのインデクサを使用しません。
</Description>
<Url />
<Resolution>DataRowのインデクサが使用されています。テーブル内の各行のカラムに値
を設定、または取得する場合は、DataRow.SetField&lt;T&gt;()拡張メソッド、および
DataRow.Field&lt;T&gt;()拡張メソッドを使用してください。</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
</Rules>

ふぅ。これで準備完了。のはず。後はビルド~配置~デバッグ。です。

が、もう限界。眠い。またまた続きにします。しくしく。いい加減終わらせたかったけどなぁ。

2013年7月18日木曜日

今週のマガジン

『ベイビーステップ』の完全に予想外な展開にびっくり。今後どう展開していくのか…。
あとは『我妻さんは俺のヨメ』がどんどん面白くなってきてます。今後さらに期待したい。