タグ:.Net ( 213 ) タグの人気記事
(.Net)フォルダ削除のDirectory.Delete(path, true)にやられた

.Netの System.IO.Directory.Delete(path, true) を使ってサブフォルダ・ファイルごと任意のディレクトリを削除しようとしたのですが、"アクセスが拒否されました" とIOException例外になります。


??ってなってたら、サブフォルダの一つが読み取り専用属性がついていました。
Directory.Deleteで削除しようとする場合、読み取り専用属性がついてると削除させてくれないようです。

対処法としては、一旦サブフォルダの読み取り専用属性を全てのけてから、Directory.Deleteメソッドを実行するしかなさそうですorz...
(VB.NET用のMy.Computer.FileSystem.DeleteDirectoryメソッドを使うと一気に消せるようですが。。)

読み取り専用ファイルがあるときでもフォルダを削除する: DOBON.NET Tips: C#, VB.NETに、一旦読み取り専用属性のけてから削除するサンプルコードがあるので、これを使わせてもらいました。

今まで.Netそこそこは触ってきたはずなのに、未だにこんな仕様で驚いている3流PGでした。。。
[PR]
by jehoshaphat | 2014-02-15 01:06 | .Net開発 | Trackback | Comments(0)
(.Net)任意のプロセスのCPU使用率を出すコード
任意のプロセスのCPU使用率を求めたいと思ってググっていたらVisualStudioフォーラム:複数のプロセス毎のCPU使用率同時取得にドンピシャな答えがありました。

コピペになりますが、ベンチマークソフト SUPER_PI のCPU使用率を求めたい時はこうなります。
mSystemProcessNameで、プロセスイメージ名(拡張子は無し)を指定します。

このPGでは指定したプロセスのCPU使用率しか求めれないため、全プロセスのCPU使用率を知りたい時は、一旦全プロセスイメージ名を取得して、それぞれ別スレッドでこのコードを動かすみたいな作りになろうかと思います。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
 
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
bool mStop = false;
string mSystemProcessName = "SUPER_PI";
int mMonitorInterval = 1;
 
/// <summary>パフォーマンスカウンタ・カテゴリ名(プロセス)</summary>
const string CATEGORY_NAME = "Process";
/// <summary>パフォーマンスカウンタ・カテゴリ名(プロセッサ)</summary>
const string CATEGORY_NAME_PROCESSOR = "Processor";
/// <summary>パフォーマンスカウンタ・カウンタ名(プロセッサ使用時間)</summary>
const string COUNTER_NAME_PROCESSOR_TIME = "% Processor Time";
/// <summary>ナノ秒単位での1秒の値</summary>
double NANO_SECOND_TICKS = 10000000;
/// <summary>CPUのコア数</summary>
double mCoreNumber;
 
#region 指定時間間隔内でのデータ格納領域
/// <summary>プロセッサ使用時間</summary>
double[] mDataContainer_ProcessorPercent= new double[10];
#endregion
 
#region 直前の監視タイミングでのデータ格納領域
/// <summary>プロセッサ使用時間</summary>
double mBefor_ProcesserPercent=0;
#endregion
 
PerformanceCounter mProcessTime;
 
#region CPUのコア数取得
PerformanceCounter mWorkCounter_CoreNumber;
 
mCoreNumber = 1;
if (PerformanceCounterCategory.Exists(CATEGORY_NAME_PROCESSOR))
{
if (PerformanceCounterCategory.CounterExists(COUNTER_NAME_PROCESSOR_TIME, CATEGORY_NAME_PROCESSOR))
{
for (int i = 0; i < 32; i++)
{
try
{
string wkInst = i.ToString();
mWorkCounter_CoreNumber = new PerformanceCounter(CATEGORY_NAME_PROCESSOR, COUNTER_NAME_PROCESSOR_TIME, wkInst, ".");
long buf = mWorkCounter_CoreNumber.RawValue;
mCoreNumber = double.Parse((i + 1).ToString());
}
catch
{
break;
}
}
}
}
#endregion
 
#region プロセスのパフォーマンスカウンタ定義
 
 
// プロセッサ使用時間
mProcessTime = new PerformanceCounter(
CATEGORY_NAME,
COUNTER_NAME_PROCESSOR_TIME,
mSystemProcessName,
".");
#endregion
 
// カレント時間を取得
DateTime current = DateTime.Now;
// ループ
while (true)
{
// ストップフラグが設定されたらループエンド
if (mStop) break;
// カレント時間の秒とが現在の秒と異なるか?
if (DateTime.Now.Second != current.Second)
{
// カレント時間の更新
current = DateTime.Now;
// カレント秒をログ出力間隔で割る
int div_sec = current.Second % mMonitorInterval;
// カレント秒がログ出力間隔で割り切れるか?
if (div_sec == 0)
{
//------------------------------------------------
// 監視間隔が切り替わるタイミングでログ情報出力
//------------------------------------------------
//PutLog(current); // ログファイルへデータ登録
//InitWorkArea(); // データ格納クラスの初期化
}
// CPU使用率 CPU使用時間(単位:100ナノ秒) / 1秒(10000000*100ナノ秒)/ CPUのコア数
mDataContainer_ProcessorPercent[div_sec] = (mProcessTime.RawValue - mBefor_ProcesserPercent) / NANO_SECOND_TICKS / mCoreNumber;
//CPU使用率出力
Debug.WriteLine(mDataContainer_ProcessorPercent[div_sec]);
// 直前の値として退避
mBefor_ProcesserPercent = mProcessTime.RawValue;
}
Thread.Sleep(10);
}
}
}
}


参考:
プロセス毎のCPU使用率の取得: DOBON.NETプログラミング掲示板過去ログ
CPU使用率(C#/VB.NET) [サンプルソース] [ヨーキー景吾の逃走]
C++でCPU使用率やメモリ使用量を調べる - 小さな星がほらひとつ C++の場合はこの方法でプロセスのCPU使用率も取れるようです。
[PR]
by jehoshaphat | 2014-02-12 00:53 | .Net開発 | Trackback | Comments(0)
(.Net)ターミナルサーバでユーザのプロセスのメモリ使用量を取りたい。
WindowsServer2003上でターミナルサーバを運用してますが、どうやらメモリを使いすぎているユーザがいるようです。
それで、数日間どのユーザがどのプロセスでメモリを使いすぎているのか経過調査を行うことにしました。


当初はパフォーマンスログで取ろうかなと思っていたんですが、パフォーマンスログではそのプロセスを使っているのがどのユーザなのかがわかりません。

仕方が無いので .Net Framework で現在のプロセス情報を取得し、ユーザと紐付けてCSVに落とすアプリケーションを作ることにしました。
それをOSのタスクスケジューラーに仕込んで、一定間隔で動かす運用です。

CSVには全プロセスの情報と、ユーザ単位でメモリ使用量を集計した情報を別々に出力します。

現在動いているプロセスの情報は System.Diagnostics.Process.GetProcesses() で取れるんですが、そのプロセスがどのユーザがオーナーとなっているかがわかりません。
ココは(.Net)現在のユーザが起動した特定のプロセスを終了するで書いた WMI を使った方法を取ることにしました。


コードしては以下のような感じです。文字数制限のためハイライトはOFFです。(C# .NetFramework2.0)

try{
//ユーザとプロセスIDを取得。(WMI使用)
ManagementScope scope = new ManagementScope("\\\\.\\ROOT\\CIMV2");

//プロセス情報取得
System.Diagnostics.Process[] ps = System.Diagnostics.Process.GetProcesses();

scope.Connect();
ObjectQuery query = new ObjectQuery(@"SELECT * FROM Win32_Process");
ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
ManagementObjectCollection col = searcher.Get();

//プロセスIDとユーザ名のハッシュテーブル[プロセスID,ユーザ名]
Hashtable htPrcUsrMap = new Hashtable();
foreach (ManagementObject o in col){
string pid = o["ProcessId"].ToString();

Object[] UserInfo = new object[2];
//見つからない時があるので、try - catch
try{
o.InvokeMethod("GetOwner", UserInfo);
}catch (Exception){
UserInfo[0] = "unkwon";
}

string UserName = (string)UserInfo[0]; //実行ユーザ名称取得
//string DomainName = (string)UserInfo[1]; //実行ユーザのドメイン名称取得

//配列にプロセスIDとユーザ名格納
htPrcUsrMap.Add(pid, UserName);
}

//現在日時取得
DateTime dt = DateTime.Now;
//データを格納するためのテーブルインスタンス作成
DataSet1.tblProcessDataDataTable tbl = new DataSet1.tblProcessDataDataTable();

//出力ファイル名
string strExportFile_Process = @"C:\PerfLogs\processCounter_Process.csv";
string strExportFile_User = @"C:\PerfLogs\processCounter_User.csv";

StringBuilder strBld_Process = new StringBuilder();

foreach (System.Diagnostics.Process p in ps){
try{
DataSet1.tblProcessDataRow row = tbl.NewtblProcessDataRow();

//タイムスタンプ出力(日時分)
string timestamp = dt.ToString("yyyy-MM-dd HH:mm:00");
strBld_Process.Append(timestamp);
strBld_Process.Append(",");
row.timestamp = timestamp;

//ユーザ名出力
string user = (string)htPrcUsrMap[p.Id.ToString()];
strBld_Process.Append(user);
strBld_Process.Append(",");
row.user = user;

//プロセスID
string processId = p.Id.ToString();
strBld_Process.Append(processId);
strBld_Process.Append(",");
row.process_id = processId;

//プロセス名
string processName = p.ProcessName;
strBld_Process.Append(processName);
strBld_Process.Append(",");
row.process_name = processName;

//CPU使用率(未実装。とりあえず0にしておく)
double cpuPercent = 0;
strBld_Process.Append(cpuPercent);
strBld_Process.Append(",");
row.cpu = cpuPercent;

//CPU時間
TimeSpan cpuTime = p.TotalProcessorTime;
strBld_Process.Append(cpuTime.TotalSeconds);
strBld_Process.Append(",");
row.cpu_time = (decimal)cpuTime.TotalSeconds;

//workingset(物理メモリ使用量)
long workingset = p.WorkingSet64;
strBld_Process.Append(workingset);
strBld_Process.Append(",");
row.workingset = workingset;

//PrivateMemory(物理メモリ+スワップ使用量)
long privateMemory = p.PrivateMemorySize64;
strBld_Process.Append(privateMemory);
strBld_Process.Append(",");
row.privatememorysize = privateMemory;

//最大workingset
long perkworkingset = p.PeakWorkingSet64;
strBld_Process.Append(perkworkingset);
strBld_Process.Append(",");
row.peak_workingset = perkworkingset;

//プロセスパス
string processPath = p.MainModule.FileName;
strBld_Process.Append(processPath);
strBld_Process.Append(",");
row.process_path = processPath;

/*
//メインウィンドウキャプション(実行ユーザでしか出ない)
string windowCaption = p.MainWindowTitle;
strBld_Process.Append(windowCaption);
strBld_Process.Append(",");
*/

strBld_Process.Append(Environment.NewLine);
tbl.AddtblProcessDataRow(row);
}catch (Exception ex){
Console.WriteLine("エラー: {0}", ex.Message);
strBld_Process.Append(Environment.NewLine);
}
}

//ユーザ毎の統計データ生成
//重複を除去するため DataView を使う
DataView vw = new DataView(tbl);
//重複除去を第二引数に指定。第三引数で一意とすべき列を指定。(複数列でも可能)
DataTable tblRes = vw.ToTable("DistinctTable", true, new string[] { "user" });
//合計の列を追加
tblRes.Columns.Add("sum_cpu", Type.GetType("System.Double"));
tblRes.Columns.Add("sum_workingset", Type.GetType("System.Int64"));
tblRes.Columns.Add("sum_privatememorysize", Type.GetType("System.Int64"));
tblRes.Columns.Add("sum_processcount", Type.GetType("System.Int64"));
tblRes.Columns.Add("sum_cputime", Type.GetType("System.Decimal"));

//重複除いたDataTableをループし、元のDataTableから集計値を求める
foreach (DataRow row in tblRes.Rows) {
row["sum_cpu"] = tbl.Compute("SUM(cpu)", "user = '" + row["user"] + "'");
row["sum_cputime"] = tbl.Compute("SUM(cpu_time)", "user = '" + row["user"] + "'");
row["sum_workingset"] = tbl.Compute("SUM(workingset)", "user = '" + row["user"] + "'");
row["sum_privatememorysize"] = tbl.Compute("SUM(privatememorysize)", "user = '" + row["user"] + "'");
row["sum_processcount"] = tbl.Compute("Count(user)", "user = '" + row["user"] + "'");
}

//sum_workingset の降順でソートをかける
DataRow[] srtRows = (DataRow[])tblRes.Select("" , "sum_workingset DESC").Clone();
DataTable tblSrt = new DataTable();
tblSrt = tblRes.Clone();
foreach (DataRow row in srtRows){
tblSrt.ImportRow(row);
}

//テキスト生成
StringBuilder strBld_User = new StringBuilder();
strBld_User = new StringBuilder();
foreach (DataRow row in tblSrt.Rows){
strBld_User.Append(dt.ToString("yyyy-MM-dd HH:mm:00"));
strBld_User.Append(",");
strBld_User.Append(row["user"]);
strBld_User.Append(",");
strBld_User.Append(row["sum_cpu"]);
strBld_User.Append(",");
strBld_User.Append(row["sum_cputime"]);
strBld_User.Append(",");
strBld_User.Append(row["sum_workingset"]);
strBld_User.Append(",");
strBld_User.Append(row["sum_privatememorysize"]);
strBld_User.Append(",");
strBld_User.Append(row["sum_processcount"]);
strBld_User.Append(",");
strBld_User.Append(Environment.NewLine);
}

//ファイル出力
System.Text.Encoding enc = System.Text.Encoding.GetEncoding("shift_jis");
if (!File.Exists(strExportFile_Process)){
string strHead = "日時,ユーザ,プロセスID,プロセス名,CPU使用率(未実装)"
+ ",CPU時間,物理メモリ使用量,物理+スワップ使用量,最大物理メモリ使用量,プロセスパス";//,ウィンドウ名";
File.AppendAllText(strExportFile_Process, strHead + Environment.NewLine, enc);
}
if (!File.Exists(strExportFile_User)){
string strHead = "日時,ユーザ,CPU使用率合計(未実装)"
+ ",CPU時間合計(秒),物理メモリ使用量合計,物理+スワップ使用量合計,プロセス数";
File.AppendAllText(strExportFile_User, strHead + Environment.NewLine, enc);
}
File.AppendAllText(strExportFile_Process, strBld_Process.ToString(), enc);
File.AppendAllText(strExportFile_User, strBld_User.ToString(), enc);

}catch (Exception ex){
File.AppendAllText(@"C:\PerfLogs\err.txt" , DateTime.Now.ToString() + " " + ex.Message + " Trace:" + ex.StackTrace + Environment.NewLine);

}



ユーザの統計に使う DataTable は以下のような構成にしてます。

データセット名:DataSet
テーブル名:tblProcessData
列:timestamp :System.String
列:user :System.String
列:process_id :System.String
列:process_name :System.String
列:cpu :System.Double
列:workingset :System.Int64
列:privatememorysize :System.Int64
列:peak_workingset :System.Int64
列:process_path :System.String
列:cpu_time :System.Decimal

実際に動かすと、WMIを使ってプロセスとユーザ情報を取得する部分が非常に時間がかかります。
60ユーザくらいで800-900くらいのプロセスが動いている状態で、数分はかかります。
また、この部分、CPUも結構食うようで1コア専有しちゃうんですよね。

WMI以外にユーザとプロセスの情報を取る方法が知りたいと思う今日この頃です。

参考:
MSDN:Process メンバ (System.Diagnostics)
[VB / C#] 実行中プロセスの各種情報を取得
【C#】プロセス実行ユーザ名称の取得API - Insider.NET - @IT
[PR]
by jehoshaphat | 2014-02-11 00:46 | .Net開発 | Trackback | Comments(0)
(.NET)ClickOnceで発行後にアプリを起動すると「配置IDがサブスクリプションと一致しません」と怒られる
ClickOnceでアプリケーションを配置してるんですが、バグを修正し新バージョンを発行しなおしました。
それでクライアント側のショートカットからアプリケーションを起動しようとすると、更新チェック後にエラーが発生し起動できません。
(ちなみに、アプリケーションのショートカットからでなく、ブラウザから起動すると別物してインストールされてしまいます。何故かアプリケーション名に -1 が付きます)

エラーの詳細を見たところ内容は以下のような感じでした。

エラー
プラットフォームのバージョン情報
Windows : 6.1.7601.65536 (Win32NT)
Common Language Runtime : 4.0.30319.18408
System.Deployment.dll : 4.0.30319.18408 built by: FX451RTMGREL
clr.dll : 4.0.30319.18408 built by: FX451RTMGREL
dfdll.dll : 4.0.30319.18408 built by: FX451RTMGREL
dfshim.dll : 4.0.31106.0 (Main.031106-0000)

ソース
配置の URL: file:///C:/Users/hoge/AppData/Roaming/Microsoft/Windows/Start%20Menu/Programs/testapp/hogehogeapp.appref-ms%7C
サーバー: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
配置プロバイダの URL: http://xxx.xxx.xxx.xxx:/testapp/hogehogeapp.application

エラーの概要
以下はエラーの概要です。これらのエラーの詳細はログに一覧表示されています。
* C:\Users\hoge\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\testapp\hogehogeapp.appref-ms| のライセンス認証により例外が発生しました。 次の失敗メッセージが検出されました:
+ 配置 ID がサブスクリプションと一致しません。

コンポーネント ストア トランザクションの失敗の概要
トランザクション エラーは検出されませんでした。

警告
この操作中に警告は発生しませんでした。

操作の進行状況
* [2014/01/24 16:33:32] : C:\Users\hoge\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\testapp\hogehogeapp.appref-ms| のライセンス認証が開始されました。
* [2014/01/24 16:33:32] : 配置で指定されたように必要な更新チェックを実行しています。

エラーの詳細
この操作中に次のエラーが検出されました。
* [2014/01/24 16:33:32] System.Deployment.Application.DeploymentException (SubscriptionState)
- 配置 ID がサブスクリプションと一致しません。
- ソース: System.Deployment
- スタック トレース:
場所 System.Deployment.Application.SubscriptionStore.CheckUpdateInManifest(SubscriptionState subState, Uri updateCodebaseUri, AssemblyManifest deployment, Version currentVersion, Boolean& bUpdateInPKTGroup)
場所 System.Deployment.Application.ApplicationActivator.PerformDeploymentUpdate(SubscriptionState& subState, String& errorPageUrl)
場所 System.Deployment.Application.ApplicationActivator.ProcessOrFollowShortcut(String shortcutFile, String& errorPageUrl, TempFile& deployFile)
場所 System.Deployment.Application.ApplicationActivator.PerformDeploymentActivation(Uri activationUri, Boolean isShortcut, String textualSubId, String deploymentProviderUrlFromExtension, BrowserSettings browserSettings, String& errorPageUrl)
場所 System.Deployment.Application.ApplicationActivator.ActivateDeploymentWorker(Object state)

コンポーネント ストア トランザクションの詳細
トランザクション情報はありません。


エラーは「ライセンス認証により例外が発生しました」となり、原因が「配置 ID がサブスクリプションと一致しません。」となっています。

ClickOnce用にしている証明書が変更されたりすると、別アプリと認識され、このようなエラーが出ることは知っていましたが、今回は証明書は変更してません。

いろいろ調べてみるとMSDN:.NET Framework 2.0 ベースのアプリケーションでClickOnce の証明書を変更すると別のアプリケーションとして認識されるに気になる記述が。。。


.NET Framework 2.0 は以下の 4 つの項目を基に ClickOnce アプリケーションの一意性を識別します。.NET Framework は 各項目の 1 つでも異なる場合には別アプリケーションとして識別し、新規インストールを実施します。

アプリケーション名 <name>
公開キー トークン <publicKeyToken>
カルチャ <language>
プロセッサアーキテクチャ (x86 など) <processorArchitecture>

(.NET Framework 2.0 SP1 が当たっていると、アプリケーション名、カルチャ、プロセッサアーキテクチャの項目でアプリケーションを識別するようです。)

ここでピンと来たのがプロセッサアーキテクチャです。
そういえば、ビルドがうまくいかくて、VisutalStudio でプロセッサアーキテクチャを "x86" から "ANY CPU" に変えてました。

この問題のアプリケーションのクライアント側のClickOnceのショートカットをテキストエディタで見てみると、以下のようになっていました。

http://xxx.xxx.xxx.xxx:/testapp/hogehogeapp.application, Culture=neutral, PublicKeyToken=4fdc3b94a306e3c2, processorArchitecture=x86

確かにまともに動いていた時はプロセッサアーキテクチャはx86だったようです。

VS側で "ANY CPU" を "x86" に戻してビルドしなおし発行したところ、今度はエラーが出ず最新版に置き換わりました。

まったくヤレヤレです。
ClickOnceは確かに手軽にクライアントへの展開ができるから便利なんですが、アップデート絡みになるといろいろかゆいところに手が届かず困ります。
そもそも証明書なしでも展開できるような仕組みにして欲しかったです。(例えばクライアントが自身とClickOnce発行サーバのIP確認して同じサブネットなら証明書なしでも起動できるとか。。。)
[PR]
by jehoshaphat | 2014-01-26 23:46 | .Net開発 | Trackback | Comments(0)
VPN環境で閲覧できないWEBサイトの原因は、MTU...
YAMAHAのRTX1200でVPNを組みました。
構成としては以下のようなスター型のネットワークで、外部へのインターネットもセンター側を経由して行くようにしています。
e0091163_16361044.jpg


さて、拠点側から一部のWEBサイトが見えないという問合せがありました。
例えばMicrosoftのサイト(htt://www.microsoft.com/)とか、借りている一部のレンタルサーバ上のWEBとかです。
IEで見ると、ずっと接続に行ってるような感じですが、何も表示されず最終的にタイムアウトしてしまっているようです。
Googleとかは普通に表示されまし、センター側の端末も何も問題なく表示されます。


ということで、拠点側のPCで WireShark を使ってパケットをキャプチャして見ました。
閲覧できないレンタルサーバ上(IP:xxx.xxx.xx.xxx)に、数キロバイトのHTMLを置きそれにアクセスした時の状態です。

No. Time Source Sport Destination Dport Protocol Length Info
5 13.089742 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 62 gamegen1 > http [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM=1
6 13.116909 xxx.xxx.xx.xxx 80 192.168.100.100 1738 TCP 62 http > gamegen1 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 SACK_PERM=1
7 13.116934 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 54 gamegen1 > http [ACK] Seq=1 Ack=1 Win=65535 Len=0
8 13.117216 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 609 gamegen1 > http [PSH, ACK] Seq=1 Ack=1 Win=65535 Len=555
9 13.945675 xxx.xxx.xx.xxx 80 192.168.100.100 1712 TCP 60 http > registrar [RST, ACK] Seq=1 Ack=1 Win=65535 Len=0
10 16.269908 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 609 [TCP Retransmission] gamegen1 > http [PSH, ACK] Seq=1 Ack=1 Win=65535 Len=555
11 22.285239 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 609 [TCP Retransmission] gamegen1 > http [PSH, ACK] Seq=1 Ack=1 Win=65535 Len=555
12 34.316287 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 590 [TCP Retransmission] [TCP segment of a reassembled PDU]
13 46.456608 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 590 [TCP Retransmission] gamegen1 > http [ACK] Seq=1 Ack=1 Win=65535 Len=536
14 58.597028 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 609 [TCP Retransmission] gamegen1 > http [PSH, ACK] Seq=1 Ack=1 Win=65535 Len=555
15 59.854326 xxx.xxx.xx.xxx 80 192.168.100.100 1738 TCP 60 http > gamegen1 [RST, ACK] Seq=2829 Ack=556 Win=65535 Len=0
16 59.854345 192.168.100.100 1738 xxx.xxx.xx.xxx 80 TCP 54 [TCP Dup ACK 14#1] gamegen1 > http [ACK] Seq=556 Ack=1 Win=65535 Len=0
17 59.880261 xxx.xxx.xx.xxx 80 192.168.100.100 1738 TCP 60 http > gamegen1 [RST] Seq=1 Win=0 Len=0


最初のTCPのコネクションは上手く行っているようですが、その後クライアントからの[TCP Retransmission]が大量に出ていますね。
TCP Retransmissionは再送要求をしているようです。時間内にサーバからパケットが来なかったので要求してると思われます。
で、最終的にサーバ側から RST フラグつまり、TCP接続を中断したというメッセージが来ています。
(TCPのフラグの意味は@IT:TCPパケットの構造を参照)


で、今度は別の閲覧可能なレンタルサーバ上(IP:xxx.xx.xx.xxx)に、同じく数キロバイトのHTMLを置きそれにアクセスした時は以下のようになりました。

No. Time Source Sport Destination Dport Protocol Length Info
1 0 192.168.100.100 1661 xxx.xx.xx.xxx 80 TCP 62 netview-aix-1 > http [SYN] Seq=0 Win=65535 Len=0 MSS=1460 SACK_PERM=1
2 0.013593 xxx.xx.xx.xxx 80 192.168.100.100 1661 TCP 62 http > netview-aix-1 [SYN, ACK] Seq=0 Ack=1 Win=5632 Len=0 MSS=1408 SACK_PERM=1
3 0.01362 192.168.100.100 1661 xxx.xx.xx.xxx 80 TCP 54 netview-aix-1 > http [ACK] Seq=1 Ack=1 Win=65535 Len=0
4 0.013877 192.168.100.100 1661 xxx.xx.xx.xxx 80 HTTP 496 GET /hoge.html HTTP/1.1
5 0.026311 xxx.xx.xx.xxx 80 192.168.100.100 1661 TCP 60 http > netview-aix-1 [ACK] Seq=1 Ack=443 Win=6432 Len=0
6 0.02715 xxx.xx.xx.xxx 80 192.168.100.100 1661 TCP 1294 [TCP segment of a reassembled PDU]
7 0.027209 xxx.xx.xx.xxx 80 192.168.100.100 1661 TCP 1294 [TCP segment of a reassembled PDU]
8 0.027221 192.168.100.100 1661 xxx.xx.xx.xxx 80 TCP 54 netview-aix-1 > http [ACK] Seq=443 Ack=2481 Win=65103 Len=0
9 0.027235 192.168.100.100 1661 xxx.xx.xx.xxx 80 TCP 54 [TCP Dup ACK 8#1] netview-aix-1 > http [ACK] Seq=443 Ack=2481 Win=65103 Len=0
10 0.04047 xxx.xx.xx.xxx 80 192.168.100.100 1661 TCP 1294 [TCP segment of a reassembled PDU]
11 0.040535 xxx.xx.xx.xxx 80 192.168.100.100 1661 TCP 1294 [TCP segment of a reassembled PDU]
12 0.040549 xxx.xx.xx.xxx 80 192.168.100.100 1661 HTTP 652 HTTP/1.1 200 OK (text/html)
13 0.040581 192.168.100.100 1661 xxx.xx.xx.xxx 80 TCP 54 netview-aix-1 > http [ACK] Seq=443 Ack=5559 Win=64505 Len=0
14 0.04062 192.168.100.100 1661 xxx.xx.xx.xxx 80 TCP 54 [TCP Dup ACK 13#1] netview-aix-1 > http [ACK] Seq=443 Ack=5559 Win=64505 Len=0


データ受信時に[TCP segment of a reassembled PDU]が発生してますが、これはセグメントが分割されて送ってきたよというメッセージのようです。
(分割には、IPレベルで分割というかフラグメントする方法がありますが、これはそれではありません。IPレベルでの分割の場合が起こっている場合、Wiresharkでは [Fragmented IP protocol] となります。IPレベルで分割されると、ルータがパケットを分割するためスループットが非常に悪くなります。よって、大抵の通信はIPプロトコルで分割しないフラグ Don't Fragment をONにしてるようです)

この[TCP segment of a reassembled PDU]は大きなデータを送受信するときは必ず発生します。
なぜなら、通信では1回の転送で送信できるデータの最大値(MTU)が決まっていますが、アプリケーション側からみた1回のデータの送信はMTU(IPパケット全体の長さ)やMSS(IPパケット内のデータ部分の長さ)を考えないからです。
送信時に、大きいデータはお互いの通信路で適切なMTU値に分割されて、受信側で、[TCP segment of a reassembled PDU]で送られてきたパケットを結合してアプリケーション側に渡していると思われます。
このことから、この分割は普通、アプリケーションから送るデータがMSSを超えている場合に起こることがわかります。

さて、サーバとクライアントはどのように分割するサイズ、つまりMSSを知るのでしょうか。
TCPではコネクションを確立する3wayハンドシェイクで、お互いのMSSを通知し合って、値が小さい方を採用するようです。
例えば、上記の閲覧できたレンタルサーバのパケットキャプチャを見ると、以下のようになっていることがわかります。
e0091163_16362489.jpg


で、今回問題となっていたのはVPN内のMTUです。
@IT:pingでMTUサイズを調査するを参考にして、センター側と拠点間のMTUサイズを測って見ました。
最終的に以下のパケットサイズより大きい値にすると、"Packet needs to be fragmented but DF set."エラーが出てしまいました。つまり、1253以上のパケットになるとIPフラグメントするわけですね。

ping xxx.xxx.xxx.xxx -l 1252 -f


なのでこのVPNルータ間のMTUは以下の計算によると、1280Byteのようです。
1252+8(ICMPヘッダ)+20(IPヘッダ)=1280(MTU)
(1280なのはヤマハのRTXルータでVPN組んだ時のデフォルト値のようです。本当はもっと大きい値でも問題なさそうな気がするのですが。。。)

IP,TCPのヘッダはそれぞれ20Byteなので、このVPNルータ間のMSSは以下のように1240Byteになります。

1280-20(IP)-20(TCP)=1240(MSS)


さて、送られてきたデータのサイズが大きすぎる場合でDFビットON(分割不可)の場合、ルータはパケットを破棄し、ICMP Type3 Coede4を送信元ホストに送信し、ホストはそれによりパケットサイズを調整して再送という段取りをとるようです。
上記のような仕組みを、Path MTU Discovery(経路MTU検索)と言うようです。
DFビットが設定されていないと、ルータはフラグメントを行なってパケットを送信するようです。(スループットは落ちます)
e0091163_16363449.jpg


しかし、送信元に「パケットでかいから小さくしてよ」というICMPパケットがFWなどによって遮断されるとどうなるでしょうか。
WEBサーバ側はクライアントからACKパケットが帰ってこないため、RST パケットを送り、パケットが届かねぇから通信辞めると捨て台詞を吐いてるようです。
おそらく、MSのサイトを始め幾つかのサイトが見れなかったのはこれが原因だと思います。
試しに、拠点側PCのMTUを強制的に 1280 に書き換えてみたところ、閲覧出来なかったサイトも見れるようになりました。
MTUのサイズを変更するには、MSサポート:ブラック ホール ルーターの問題をトラブルシュートする方法の方法3に有るように、以下の手順を行います。(以下引用)
(ネットワークのMTUサイズを変更する - @ITにも書かれてます)

1.[スタート] ボタン、[コントロール パネル] の順にクリックします。

2.[ネットワークとインターネット接続]をクリックし、[ネットワーク接続] をクリックして開きます。

3.複数のネットワーク接続が表示された場合は、各接続をダブルクリックし、表示された [状態] インターフェイスの [サポート] タブをクリックします。[デフォルト ゲートウェイ] エントリが表示された場合、その接続がインターネット接続に使用されているネットワーク接続であると考えられます。接続名 ("ローカル エリア接続 2" など) をメモします。

4.レジストリ エディタ (Regedit.exe) を起動します。

5.HKEY_LOCAL_MACHINE ツリーの下にある次のレジストリ キーに移動します。
SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}\

6.このキーの下に、数字の識別子を持ついくつかのキーがあります。各キーには Connection サブキーが設定されています。次のようなキーをすべて調べます。
アダプタの ID\Connection
Connection サブキーの [Name] 値には、ネットワーク接続フォルダに使用されるネットワーク接続名が設定されています。ステップ 3 でメモした名前と一致するものが見つかったら、そのネットワーク接続名の アダプタの ID をメモします。

7.HKEY_LOCAL_MACHINE に戻り、次のレジストリ キーに移動します。
SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\アダプタの ID
アダプタの ID は、ステップ 6 でメモした番号です。このキーを強調表示すると、画面の右側にいくつかの値が表示され、その中に [DefaultGateway] および [EnableDHCP] が含まれています。

8.画面の右側を右クリックし、[新規] をクリックして、[DWORD 値] をクリックします。この値に [MTU] という名前を付けます。

9.その値をダブルクリックして編集します。[表記] を [10 進数] に変更し、Ping テストで確認した MTU の最大許容サイズを入力します。

10.レジストリ エディタを終了します。


以下のような感じです。
e0091163_16364499.jpg


このあたりの話は、Path MTU Discovery 1で詳しく図入りでまとめられています。


この現象を再現するために、センター側にxpでWEBサーバを立てて検証して見ました。
全パケット素通しにした場合は、VPNの向こう側の拠点からページの閲覧できました。(MTUは1280になっており、ルータからICMPパケットが届いているのが確認できました。)
しかし、WEBサーバ側にFW(今回はフリーのKerio使いました)を入れ、ICMP全遮断すると、拠点からページの閲覧ができない状態になりました。


さて、この場合の解決方法としてはどうすればいいのでしょうか。
ICMPパケットがブロックされるのは向こう側の問題なので、どうしようもありません。つまり、経路MTU検索は使えないというわけです。
拠点側のクライアント全部のMTUを、VPNのMTUより小さくすればOKですが、端末数が多い場合設定に手間取ります。
幸い今回VPNルータに使ったYAMAH RTX1200には、TCPコネクション開始時にMSSを調整してくれる機能があるようです。
コマンドどしては以下になります。

ip tunnel tcp mss limit auto


上記のコマンドによって以下のようになります。
e0091163_1636534.jpg


これで、なんとかまともに通信ができるようになりました。
補足ですが、強制的にMTUを指定したい場合は以下のように出来ます。
(設定する値はMSS(MTUから40バイト引いた値)なので、MTUと間違えないように注意)

ip tunnel tcp mss limit 1240


LAN3のインターフェイスでMSS指定したい場合は以下のようにします。

ip lan3 tcp mss limit 1240


今回のように、経路MTU検索が使えず、VPN内でMTUが小さくなる故に通信できなくなるという現象は、VPN組んでいると結構あるようです。
ルータによっては経路MTUより検索に対応しておらず、ICMP type3 code4パケットを返さないものもあるようで、このようなルータはブラックホールルータと呼ばれているようです。

まぁ理屈がわかれば非常に簡単な話ですが、TCP/IPの基礎を忘れかけてた3流PGは、原因突き止めるのに結構時間がかかってしまいました。


参考:
ペンギンの覚書: ネットワーク更改時のトラブル "YAMAHAのルータではVPN等のMTUがデフォルトで1280バイトになっている。対応策は、ip tunnel tcp mss limit auto"
◎IPの分割化と再構築 ここの"3.MTUとMSSとの違い。"に、ヤマハルータでのMSS自動調整"ip tunnel tcp mss limit auto"に関する記述があります。
あんじーのテクニカルブログ: MTUが小さいVPN間でファイル共有通信をすると通信できなくなる問題 "VPN通信では1280Byte、それ以外は1500Byteになっていて、ICMPパケットを破棄する設定になっていると通信できなくなる問題を引き起こすようです。"
@IT:VPNの実力を知る(後編)"(4)MTUに関する確認"あたりにMTUの話があります。
PPPoE ルータ同志で IPsec を構築するときの問題 - nabeの雑記帳
忘れっぽいエンジニアのメモ MTUとMRUとMSSについて
経路MTU探索 - Networkキーワード:ITpro
[PR]
by jehoshaphat | 2014-01-15 00:34 | ネットワーク | Trackback(1) | Comments(2)
(C#)ハンドルされない例外を捕まえる方法
以前に(VB.Net)ハンドルされない例外を捕まえる方法という記事を書きましたが、この内容のC#版のコードをメモ代わりに載せておきます。

まず、メインフォームのコンストラクタに以下のイベントハンドラを登録します。
//キャッチされない例外をとらえるイベント
Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
System.Threading.Thread.GetDomain().UnhandledException += new UnhandledExceptionEventHandler(Application_UnhandledException);


イベントハンドラは以下のようになります。

/// <summary>
/// 未処理例外をキャッチするイベントハンドラ。メインスレッド用。(WindowsForm専用)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e){
string sourceName = Properties.Settings.Default.AppName; //ソフト名が入っていることとする
//イベントログ出力
OutPutLogErr(e.Exception);
//メッセージボックス表示
MessageBox.Show("エラーが発生しました。処理を中断します。\nエラー情報:" + e.Exception.Message, sourceName, MessageBoxButtons.OK, MessageBoxIcon.Error);
//予期せぬ例外時は強制終了
Environment.Exit(-1);
}
 
/// <summary>
/// 未処理例外をキャッチするイベントハンドラ。別スレッドorコンソールアプリ用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void Application_UnhandledException(object sender, UnhandledExceptionEventArgs e){
string sourceName = Properties.Settings.Default.AppName;
Exception ex = (Exception)e.ExceptionObject;
if (ex != null) {
//イベントログ出力
OutPutLogErr(ex);
//メッセージボックス表示
MessageBox.Show("エラーが発生しました。処理を中断します。\nエラー情報:" + ex.Message, sourceName, MessageBoxButtons.OK, MessageBoxIcon.Error);
//予期せぬ例外時は強制終了
Environment.Exit(-1);
}
}
 
 
/// <summary>
/// エラーをイベントログに出力。
/// </summary>
/// <param name="e"></param>
public static void OutPutLogErr(Exception e){
try{
string sourceName = Properties.Settings.Default.AppName;
if (!System.Diagnostics.EventLog.SourceExists(sourceName))
{
//ログ名を空白にすると、"Application"となる
System.Diagnostics.EventLog.CreateEventSource(sourceName, "");
}
byte[] myData = { };
string msg = "例外\n:" + e.Message + "\n例外スタックトレース:\n" + e.StackTrace + "\n";
if (e.InnerException != null)
{
msg = msg + "InnerException:\n" + e.InnerException.Message + "\nInnerExceptionスタックトレース:\n" + e.InnerException.StackTrace;
}
//イベントログにエントリを書き込む
//ここではエントリの種類をエラー、イベントIDを1、分類を1000とする
System.Diagnostics.EventLog.WriteEntry(sourceName, msg, System.Diagnostics.EventLogEntryType.Error, 1, 1000, myData);
} catch (Exception){
return;
}
}

[PR]
by jehoshaphat | 2014-01-14 00:07 | .Net開発 | Trackback | Comments(0)
(.Net)小数点第一位まで入力可という入力チェック
DataGridViewで、ある列は小数点第一位までの入力を許可させ、それ以外の値は認めないという要件を満たす方法です。

ちょっと悩んだんですが、まずfloatにパースできるかどうか判断します。
floatにパースできるようなら、10を掛け、それがintにパースできるかどうか判断します。
パースできないなら、小数点第二位以上の値が入っていることになるので、セルを移動させません。
というのが、以下のコードになります。(C#)


// 不正な場合、入力フォーカスを移動させない(小数点第一位まで可)
float f;
int i;
e.Cancel = (!float.TryParse(val, out f) || !int.TryParse((f * 10).ToString(), out i));


もっといい方法があるのかもしれませんが、これくらいしかスマートなのは思いつきませんでした。
[PR]
by jehoshaphat | 2014-01-13 00:56 | .Net開発 | Trackback | Comments(0)
(.Net)DataGridViewで右クリックしたときに行選択したい
DataGridViewでコンテキストメニューを割り当てた時、右クリックをしても、クリックしたセルに対してイベント走るのではなく、その時に選択されているセルに対してイベントが走ってしまいます。

Excelみたいに、右クリックしたら、マウスポインタの位置の行が選択されて、その後コンテキストメニューが出るようにしたい場合、以下のようにMouseDownイベントで行選択してしまえばいいようです。
参考先そのままですがコードを載せておきます。(C#)

/// <summary>
/// セルでマウスダウンイベントあった時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void dgvParent_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
dgvParent.ClearSelection();
dgvParent.Rows[e.RowIndex].Selected = true;
}
 
}



参考:
DataGridView で右クリックして行を選択
[PR]
by Jehoshaphat | 2013-03-31 22:59 | .Net開発 | Trackback | Comments(0)
(.Net)文字列で桁を揃えるため0埋めしたい

下記のように文字列型で数値をいれているケースが有るとします。
string no = "1234";


これを7桁にし、先頭に0で埋めたいというケースの場合、String.PadLeft メソッドを使えばいいようです。
string no = "1234";
no = no.PadLeft(7,'0');


こうすると、0001234 という文字列になります。


ちなみに、数値型を0埋め文字列にしたい場合は、String.Formatが使えますね。
int num = 123;
string str = String.Format("{0:0000}", num);



参考:
指定の文字数になるまで先頭を文字で埋める
@IT:数値を右詰めや0埋めで文字列化するには?[C#、VB]
[PR]
by Jehoshaphat | 2012-05-16 00:32 | .Net開発 | Trackback | Comments(0)
(.Net)DataTable.Selectの条件は引用符でくくらないとおかしなことになる
DataTable.Selectメソッドを使って、データの抽出を行ってました。
下記のように検索対象の列は文字列型ですが、中身は数値が入っています。

string strNo = "10";
 
DataTable tbl = new DataTable();
tbl.Columns.Add("no");
tbl.Columns.Add("name");
 
tbl.Rows.Add(new object[] { "1", "hoge" });
tbl.Rows.Add(new object[] { "10", "hoge" });
tbl.Rows.Add(new object[] { "2", "hoge" });
tbl.Rows.Add(new object[] { "20", "hoge" });
 
DataRow[] rows = tbl.Select("no=" + strNo);


検索値を 10 とすると以下のような例外が select メッソド実行時に発生します。

System.ArgumentException がキャッチされました
Message="Range オブジェクトの Min (1) は、 max (-1) 以下でなければなりません。"
Source="System.Data"
StackTrace:
場所 System.Data.Select.GetBinaryFilteredRecords()
場所 System.Data.Select.SelectRows()
場所 System.Data.DataTable.Select(String filterExpression)
場所 Hoge.hoge() 場所 D:\mydoc\devlop\hoge.cs:行 12
InnerException:


ちなみに、検索値を他の値に設定するとこのエラーが出ません。

で、原因を調べた結果、MSDNフォーラム: VB2005 .NET2.0 DataTable.Select()メソッドに関してに答えが。。

ほぼコピペですが、詳しい話を。。
DataTable.Selectメソッドの内部ではバイナリサーチ(二分探索)を使っているようです。
バイナリサーチは、一旦リストをソートしてから出ないと実行できません。
検索列は文字列型なので、ソートすると下記のようになります。(文字列上の昇順になるわけです)

1,10,2,20

バイナリサーチはソートしたリストから中央の値を検索値と比較して、検索したい値が中央の値の右にあるか、左にあるかを判断するので、この場合、2番目以降(値10)に値があると判断されます。
そして、10の値がどこまで続いているかを調べるためにバイナリサーチではもう一回検索されるわけですが、その時検索値の 10 とリスト内の 2 が比較された結果、10 > 2となり、DataTable内の "2" と "20" の間に 10 があるはずだとなって、エラーになるようですね。

対策として、下記のようにSelectメソッドの検索値に引用符(シングルクォーテーション)を付けて明示的に文字列だとすればこのエラーは回避できます。

DataRow[] rows = tbl.Select("no='" + strNo + "'");


文字列検索するときは引用符の付け忘れに注意しましょう。
引用符つけなくても一応動いてしまうので厄介です。

参考:
DataTable.Select の条件式での型推論による弊害
[PR]
by Jehoshaphat | 2012-05-15 23:39 | .Net開発 | Trackback | Comments(1)