7

VBScript で記述されたレガシー システムの移行プロジェクトを開始しようとしています。さまざまなコンポーネントを "WSC" ファイルとして記述することによって、その多くが分離されているという興味深い構造を持っています。これは、VBScript コードを COM のような方法で効果的に公開する方法です。「コア」からこれらのコンポーネントへの境界インターフェイスはかなりタイトでよく知られているため、新しいコアの作成に取り組み、WSC を再利用して、書き直しを延期できることを望んでいました。

「Microsoft.VisualBasic」への参照を追加して呼び出すことで、WSC を読み込むことができます。

var component = (dynamic)Microsoft.VisualBasic.Interaction.GetObject("script:" + controlFilename, null);

ここで、「controlFilename」は完全なファイル パスです。GetObject は「System.__ComObject」型の参照を返しますが、.net の「動的」型を使用してプロパティとメソッドにアクセスできます。

これは最初は問題なく動作するように見えましたが、非常に特定の状況が組み合わされたときに問題が発生しました。私の心配は、これが他のケースで発生する可能性があること、またはさらに悪いことに、多くの場合、悪いことが起こってマスクされていることです。 、私が最も予想外のときに爆破するのを待っているだけです。

発生した例外は「System.ExecutionEngineException」タイプであり、特に恐ろしい (そしてあいまいな) ように聞こえます。

私は最小限の再現ケースであると信じているものをまとめて、問題が何であるかについて誰かが少し光を当てることができることを望んでいました. 理由は説明できませんが、それを防ぐように見えるいくつかの微調整も特定しました。

  1. 「WSCErrorExample」と呼ばれる新しい空の「ASP.NET Web アプリケーション」を作成します (VS 2013 / .net 4.5 および VS 2010 / .net 4.0 でこれを実行しましたが、違いはありません)。

  2. プロジェクトに「Microsoft.VisualBasic」への参照を追加します

  3. 「Default.aspx」という新しい「Web フォーム」を追加し、「Default.aspx.cs」の上に以下を貼り付けます。

    using System;
    using System.IO;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using Microsoft.VisualBasic;
    
    namespace WSCErrorExample
    {
        public partial class Default : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                var currentFolder = GetCurrentDirectory();
                var logFile = new FileInfo(Path.Combine(currentFolder, "Log.txt"));
                Action<string> logger = message =>
                {
                    // The try..catch is to avoid IO exceptions when reproducing by requesting the page many times
                    try { File.AppendAllText(logFile.FullName, message + Environment.NewLine); }
                    catch { }
                };
    
                var controlFilename = Path.Combine(currentFolder, "TestComponent.wsc");
                var control = (dynamic)Interaction.GetObject("script:" + controlFilename, null);
    
                logger("About to call Go");
                control.Go(new DataProvider(logger));
                logger("Completed");
            }
            private static string GetCurrentDirectory()
            {
                // This is a way to get the working path that works within ASP.Net web projects as well as Console apps
                var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
                if (path.StartsWith(@"file:\", StringComparison.InvariantCultureIgnoreCase))
                    path = path.Substring(6);
                return path;
            }
    
            [ComVisible(true)]
            public class DataProvider
            {
                private readonly Action<string> _logger;
                public DataProvider(Action<string> logger)
                {
                    _logger = logger;
                }
    
                public DataContainer GetDataContainer()
                {
                    return new DataContainer();
                }
    
                public void Log(string content)
                {
                    _logger(content);
                }
            }
    
            [ComVisible(true)]
            public class DataContainer
            {
                public object this[string fieldName]
                {
                    get { return "Item:" + fieldName; }
                }
            }
        }
    }
    
  4. 「TestComponent.wsc」という新しい「テキスト ファイル」を追加し、そのプロパティ ウィンドウを開き、「出力ディレクトリにコピー」を「新しい場合はコピー」に変更し、その内容として以下を貼り付けます。

    <?xml version="1.0" ?>
    <?component error="false" debug="false" ?>
    <package>
        <component id="TestComponent">
            <registration progid="TestComponent" description="TestComponent" version="1" />
            <public>
                <method name="Go" />
            </public>
            <script language="VBScript">
                <![CDATA[
                    Function Go(objDataProvider)
                        Dim objDataContainer: Set objDataContainer = objDataProvider.GetDataContainer()
                        If IsEmpty(objDataContainer) Then
                            mDataProvider.Log "No data provided"
                        End If
                    End Function
            ]]>
            </script>
        </component>
    </package>
    

これを 1 回実行すると、明らかな問題は発生しません。「Log.txt」ファイルが「bin」フォルダーに書き込まれます。ただし、ページを更新すると、通常は例外が発生します

マネージ デバッグ アシスタント 'FatalExecutionEngineError' は、'C:\Program Files (x86)\IIS Express\iisexpress.exe' で問題を検出しました。

追加情報: ランタイムで致命的なエラーが発生しました。エラーのアドレスは、スレッド 0x1e10 の 0x733c3512 でした。エラー コードは 0xc0000005 です。このエラーは、CLR のバグ、またはユーザー コードの安全でない部分または検証不可能な部分のバグである可能性があります。このバグの一般的な原因には、COM-> 相互運用機能または PInvoke のユーザー マーシャリング エラーが含まれ、スタックが破損する可能性があります。

場合によっては、2 番目の要求でこの例外が発生しないことがありますが、ブラウザー ウィンドウで F5 キーを数秒間押し続けると、その醜い頭が浮かび上がることが保証されます。私が知る限り、例外は「If IsEmpty」チェックで発生します(この再現ケースの他のバージョンには、より多くのロギング呼び出しがあり、その行が問題の原因であることを示していました)。

この問題を解決するためにさまざまなことを試しました。コンソール アプリで再作成しようとしましたが、何百ものスレッドをスピンアップして上記の作業を処理しても問題は発生しません。Web フォームを使用するのではなく、ASP.Net MVC Web アプリケーションを試しましたが、同じ問題が発生します。アパートメントの状態をデフォルトの MTA から STA に変更しようとしましたが (その時点で少しストローを握りしめていました!)、動作に変化はありませんでした。Microsoft のOWIN 実装を使用する Web プロジェクトを構築しようとしましたが、そのシナリオでも問題が発生します。

私が気付いた 2 つの興味深い点 - 「DataContainer」クラスにインデックス付きプロパティ (または [DispId(0)] 属性で装飾されたデフォルトのメソッド/プロパティ - この例には示されていません) がない場合、エラーは発生しません。発生する。「ロガー」クロージャに「FileInfo」参照が含まれていない場合 (FileInfo インスタンス「logFile」ではなく文字列「logFilePath」が保持されている場合)、エラーは発生しません。これらのことを避けるのが一つのアプローチのように聞こえると思います! しかし、私が現在知らないこのシナリオをトリガーする他の方法がある可能性があり、コードベースが成長するにつれて、これらのことを行わないというルールを強制しようとすることが複雑になる可能性があることを懸念しています。これは想像できます理由がすぐに明らかになることなく、エラーが忍び寄ります。

(Katana を使用して) 1 回実行すると、追加のコール スタック情報が得られました。

このスレッドは、コール スタック上の外部コード フレームのみで停止されます。通常、外部コード フレームはフレームワーク コードからのものですが、ターゲット プロセスに読み込まれる他の最適化されたモジュールを含めることもできます。

外部コードを使用したコール スタック

mscorlib.dll!System.Variant.Variant(オブジェクト obj) mscorlib.dll!System.OleAutBinder.ChangeType(オブジェクト値、System.Type タイプ、System.Globalization.CultureInfo cultureInfo) mscorlib.dll!System.RuntimeType.TryChangeType(オブジェクト値) 、System.Reflection.Binder バインダー、System.Globalization.CultureInfo カルチャ、bool needsSpecialCast) mscorlib.dll!System.RuntimeType.CheckValue(オブジェクト値、System.Reflection.Binder バインダー、System.Globalization.CultureInfo カルチャ、System.Reflection.BindingFlags invokeAttr) mscorlib.dll!System.Reflection.MethodBase.CheckArguments(object[] パラメータ、System.Reflection.Binder バインダー、System.Reflection.BindingFlags、invokeAttr、System.Globalization.CultureInfo カルチャ、System.Signature sig) [マネージド トランジションのネイティブ]

最後の注意: IReflectを使用して「DataProvider」クラスのラッパーを作成し、IDispatch を介した呼び出しを基になる「DataProvider」インスタンスへの呼び出しにマップすると、問題は解決します。しかし、繰り返しになりますが、これがどういうわけか答えであると判断するのは危険に思えます.コンポーネントに渡される参照にそのようなラッパーがあることを確認することに細心の注意を払う必要がある場合、エラーが入り込んで追跡が困難になる可能性があります. IReflect を実装するラッパーに含まれる参照が、同じ方法でラップされていないメソッドまたはプロパティ呼び出しからの参照を返す場合はどうなりますか? ラッパーは、「安全な」参照のみを返すようにするようなことを試みることができると思います(つまり.さらに IReflect ラッパーで..しかし、それはすべて少しハッキーに思えます。

この問題で次にどこに行くべきか本当にわかりません。誰にもわかりませんか?

4

1 に答える 1

2

私の推測では、表示されているエラーは、WSC スクリプト コンポーネントが本質的に COM STA オブジェクトであるという事実が原因であると思われます。それらは、それ自体が STA COM オブジェクトである、基礎となる VBScript アクティブ スクリプト エンジンによって実装されます。そのため、STA スレッドを作成してアクセスする必要があり、そのようなスレッドは、特定の WSC オブジェクトの存続期間中同じままにする必要があります (オブジェクトにはスレッド アフィニティが必要です)。

ASP.NET スレッドは STA ではありません。これらはThreadPoolスレッドであり、COM オブジェクトの使用を開始すると暗黙的に COM MTA スレッドになります (STA と MTA の違いについては、INFO: Descriptions and Workings of OLE Threading Models を参照してください)。次に COM は、WSC オブジェクト用に別の暗黙的な STA アパートメントを作成し、ASP.NET 要求スレッドからの呼び出しをマーシャリングします。ASP.NET 環境では、すべてがうまくいく場合とうまくいかない場合があります。

理想的には、WSC スクリプト コンポーネントを取り除き、.NET アセンブリに置き換える必要があります。それが短期的に実現できない場合は、明示的に制御された独自の STA スレッドを実行して、WSC コンポーネントをホストすることをお勧めします。以下が役立つ場合があります。

更新しました。これを試してみませんか? コードは次のようになります。

// create a global instance of ThreadAffinityTaskScheduler - per web app
public static class GlobalState 
{
    public static ThreadAffinityTaskScheduler TaScheduler { get; private set; }

    public static GlobalState() 
    {
        GlobalState.TaScheduler = new ThreadAffinityTaskScheduler(
            numberOfThreads: 10,
            staThreads: true, 
            waitHelper: WaitHelpers.WaitWithMessageLoop);
    }
}

// ... inside Page_Load

GlobalState.TaScheduler.Run(() => 
{
    var control = (dynamic)Interaction.GetObject("script:" + controlFilename, null);

    logger("About to call Go");
    control.Go(new DataProvider(logger));
    logger("Completed");

}, CancellationToken.None).Wait();

それが機能する場合は、ブロッキングの代わりにPageAsyncTaskandを使用することで、Web アプリのスケーラビリティをいくらか改善できます。async/awaitWait()

于 2014-07-15T14:42:38.193 に答える