۱۳۹۰/۱۲/۲۳

عبارات باقاعده و نياز به Timeout


يكبار سعي كنيد مثال ساده زير را اجرا كنيد:

using System;
using System.Text.RegularExpressions;

namespace RegexLoop
{
    class Program
    {
        static void Main(string[] args)
        {
            var emailAddressRegex = new Regex(@"^[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\@[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\.[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*$|^$");
            if (emailAddressRegex.IsMatch("an.infinite.loop.sample.just_for.test"))
            {
                Console.WriteLine("Matched!");
            }

            var input = "The quick brown fox jumps";
            var pattern = @"([a-z ]+)*!";
            if (Regex.IsMatch(input, pattern))
            {
                Console.WriteLine("Matched!");
            }
        }
    }
}


پس از اجرا، برنامه هنگ خواهد كرد يا به عبارتي برنامه در يك حلقه بي‌نهايت قرار مي‌گيرد (در هر دو مثال؛ اطلاعات بيشتر و آناليز كامل در اينجا). بنابراين نياز به مكانيزمي امنيتي جهت محافظت در برابر اين نوع ورودي‌ها وجود خواهد داشت؛ مثلا يك Timeout . اگر تا 2 ثانيه به جواب نرسيديم، اجراي Regex متوقف شود. تا دات نت 4، چنين timeout ايي پيش بيني نشده؛ اما در دات نت 4 و نيم آرگوماني جهت تعريف حداكثر مدت زمان قابل قبول اجراي يك عبارت باقاعده در نظر گرفته شده است (^) و اگر در طي مدت زمان مشخص شده، كار انجام محاسبات به پايان نرسد، استثناي RegexMatchTimeoutException صادر خواهد شد.
خيلي هم خوب. به اين ترتيب كسي نمي‌تونه با يك ورودي ويژه، CPU Usage سيستم رو تا مدت زمان نامحدودي به 100 درصد برساند و عملا استفاده از سيستم رو غيرممكن كنه.
اما تا قبل از دات نت 4 و نيم چكار بايد كرد؟ روش كلي حل اين مساله به اين ترتيب است كه بايد اجراي Regex را به يك ترد ديگر منتقل كرد؛ اگر مدت اجراي عمليات، از زمان تعيين شده بيشتر گرديد، آنگاه مي‌شود ترد را Abort كرد و به عمليات خاتمه داد. روش پياده سازي و نحوه استفاده از آن‌را در ادامه ملاحظه خواهيد نمود:

using System;
using System.Text.RegularExpressions;
using System.Threading;

namespace RegexLoop
{
    public static class TimedRunner
    {
        public static R RunWithTimeout<R>(Func<R> proc, TimeSpan duration)
        {
            using (var waitHandle = new AutoResetEvent(false))
            {
                var ret = default(R);
                var thread = new Thread(() =>
                {
                    ret = proc();
                    waitHandle.Set();
                }) { IsBackground = true };
                thread.Start();

                bool timedOut = !waitHandle.WaitOne(duration, false);
                waitHandle.Close();

                if (timedOut)
                {
                    try
                    {
                        thread.Abort();
                    }
                    catch { }
                    return default(R);
                }
                return ret;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var emailAddressRegex = new Regex(@"^[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\@[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\.[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*$|^$");
            if (TimedRunner.RunWithTimeout(
                 () => emailAddressRegex.IsMatch("an.infinite.loop.sample.just_for.test"),
                 TimeSpan.FromSeconds(2)))
            {
                Console.WriteLine("Matched!");
            }

            var input = "The quick brown fox jumps";
            var pattern = @"([a-z ]+)*!";
            if (TimedRunner.RunWithTimeout(() => Regex.IsMatch(input, pattern), TimeSpan.FromSeconds(2)))
            {
                Console.WriteLine("Matched!");
            }
        }
    }
}

اينبار به هر كدام از عبارات باقاعده 2 ثانيه زمان براي اتمام كار داده شده است. در غيراينصورت مقدار پيش فرض خروجي متد فراخواني شده، بازگشت داده مي‌شود كه در اينجا false است.