概要
Revitアドインの開発中に、解消にてこずったエラーが発生しましたので、解消方法を備忘録として残します。
仕様 (期待動作)
Revitのアドイン内の処理から、外部のexeファイル (※以下、外部アプリと呼称) をProcessクラスを使い実行。
外部アプリ実行後は、アプリ終了まで待機 (処理は進まない)。
外部アプリが正常終了したら外部アプリがファイルを出力してくれるので、そのファイルを読み込み、内容を新しいウィンドウで画面に表示する。
エラーの内容
外部アプリを実行後、おおよそ8分以上プロセスを起動し続けてからアプリを終了 (プロセスを終了) すると、その後の処理である、出力されたファイルを元に結果を画面表示する部分でエラーとなる (ShowDialog()でエラーとなる)。また、エラー時はCatchに入らず、そのままRevitが強制終了してしまう。
デバッグしたときのエラーの内容は、「System.ComponentModel.Win32Exception 'このコマンドを実行するための十分なクォータがありません。'」だった。
ちなみに、外部アプリを実行後、すぐにプロセスを終了したり、5分ほどでプロセスを終了するとエラーとはならず、正常に動作していた。
public class SampleClassA
{
private AppResultWindow _appResultWindow = null;
private AppResultWindowViewModel _appResultWindowViewModel = null;
public ICommand AppExecuteCommand => _appExecuteCommand ?? (_appExecuteCommand = new DelegateCommand(() =>
{
try
{
// Revitアドイン内の、とある画面のボタンを押下するとこの処理に入る
// このコンストラクタの中でSampleClassBを呼び、外部アプリを実行したり、
// 外部アプリが終了時に出力した外部ファイルを読み込んでいる
_appResultWindowViewModel = new AppResultWindowViewModel(this);
_appResultWindow = new AppResultWindow()
{
DataContext = _appResultWindowViewModel
};
// ここで「十分なクォータがありません。」のエラー
// 外部ファイルから読み込んだ内容を画面に表示しようとしている
_appResultWindow.ShowDialog();
}
catch (Exception ex)
{
// ログ出力処理
}
}));
private DelegateCommand _appExecuteCommand;
}
public class SampleClassB
{
/// <summary>
/// 外部のexeファイル起動
/// </summary>
/// <param name="exePath">実行ファイルのパス</param>
/// <param name="parameter">起動パラメータ</param>
private void Execute(string exePath, string parameter)
{
var process = new Process();
process.StartInfo.FileName = exePath;
process.StartInfo.Arguments = parameter;
try
{
if (process.HasExited)
{
// 外部のexeファイル起動
process.Start();
// 終了まで待機
process.WaitForExit();
}
else
{
throw new Exception("既に起動されています。");
}
}
catch (InvalidOperationException)
{
// 一度もプロセスが起動していない場合
// 外部のexeファイル起動
process.Start();
// 終了まで待機
process.WaitForExit();
}
}
}
原因
恐らく、エラーになる場合ではメインスレッドがハングしており、メインスレッドがメッセージ処理をすることができない状態と思われました。
またこれも予想ですが、Revitのメインスレッドは別スレッドからメッセージが投げ込まれてそれを処理しなければならないタイミングがあるが、メインスレッドが動けないので何らかのリソースが足りなくなった、と思われます。
対応
メインスレッドを動ける状態にすればよいので、メインスレッドでWaitForExitを行わないように対応しました。ただ別スレッドで行うだけだとメインスレッドはそのまま先の処理まで進んでしまって、アプリがモードレス起動のようになってしまいます。
なので、別スレッドでWaitForExit()を行っている間はメインスレッドは見えない画面をモーダル表示して待機させておく、という対応にしました。
※モーダルダイアログは関数内で処理を止めながらもメッセージループを回せます。
これで、10分や15分以上プロセスが起動した後でも、エラーにはならず期待動作となりました。
public class SampleClassB
{
/// <summary>
/// 外部のexeファイル起動
/// </summary>
/// <param name="exePath">実行ファイルのパス</param>
/// <param name="parameter">起動パラメータ</param>
private void Execute(string exePath, string parameter)
{
FakeModal(() =>
{
var process = new Process();
process.StartInfo.FileName = exePath;
process.StartInfo.Arguments = parameter;
// 二重起動防止
try
{
if (process.HasExited)
{
// 外部のexeファイル起動
process.Start();
// 終了まで待機
process.WaitForExit();
}
else
{
throw new Exception("既に起動されています。");
}
}
catch (InvalidOperationException)
{
// 一度もプロセスが起動していない場合
// 外部のexeファイル起動
process.Start();
// 終了まで待機
process.WaitForExit();
}
});
// 内部関数として定義
void FakeModal(Action action)
{
// exeファイル起動中は透明なウィンドウを作成しておきUIスレッドのメモリ不足を防ぐ
bool cancelClose = true;
var window = new Window();
window.Closing += (_, e) => e.Cancel = cancelClose;
window.Width = 0;
window.Height = 0;
window.WindowStyle = WindowStyle.None;
window.AllowsTransparency = true;
window.Background = Brushes.Transparent;
window.ShowInTaskbar = false; // 透明なウィンドウはタスクバーにも表示させない
window.Loaded += (_, ___) =>
{
new Thread(() =>
{
// 新しいスレッドでexeファイルを実行
action();
cancelClose = false;
window.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, (Action)(() => window.Close()));
}).Start();
};
window.ShowDialog();
}
}
}