Jurassic [C# .Net Unity JavaScript运行时] 三.调试及其他说明

调试

Jurassic支持Visual Studio提供的.NET程序的集成调试。
Visual Studio调试JavaScript

但是,不支持以下功能:

  • Locals window
  • Watch window
  • Friendly names in the Call Stack window

要启用调试,请将的EnableDebugging属性设置ScriptEngine为true。这将允许在Visual Studio中进行调试,但也会带来以下负面影响:

  • EnableDebugging打开时生成的代码无法进行垃圾回收。您运行的脚本越多,程序将使用的内存就越多。
  • 生成的代码会稍微慢一些。因此,请勿为生产代码启用此选项。

提示:要以编程方式(从JavaScript内)在断点处停止,请使用以下debugger语句(有关更多详细信息,请参见MSDN)。

支持的类型

由于JavaScript具有动态特性,因此Jurassic公开了许多类型为方法参数的API System.Object。但是,Jurassic仅支持.NET类型系统的有限子集。尝试在Jurassic API中使用不受支持的类型可能会产生意想不到的影响。

支持的类型如下:

c#类型名称 .NET类型名称 JavaScript类型名称
bool System.Boolean boolean
int System.Int32 number
double System.Double number
string System.String string
Jurassic.Null Jurassic.Null null
Jurassic.Undefined Jurassic.Undefined undefined
Jurassic.Library.ObjectInstance (or a derived type) Jurassic.Library.ObjectInstance (or a derived type) object

兼容模式

ScriptEngine类具有CompatibilityMode标志,该标志可用于将某些行为恢复为ECMAScript 3的行为。

var engine = new Jurassic.ScriptEngine();
engine.CompatibilityMode = Jurassic.CompatibilityMode.ECMAScript3;

设置此标志具有以下效果:

  • parseInt()解析八进制数而无需显式基数。
  • NaNundefinedInfinity均可修改。
  • 保留关键字的列表变得更长(例如,"abstract"成为关键字)。
  • "this"在函数调用的调用位置转换为对象。

性能技巧

尽可能使用局部变量-它们比全局变量快得多。这意味着您应该始终使用var声明变量。
避免使用以下语言功能(它们倾向于禁用优化):

  • eval
  • arguments
  • with

使用严格模式-速度稍快。

非标准和不推荐使用的功能

ECMA标准化小组已弃用了许多功能,或已正式弃用这些功能。支持这些功能是出于兼容性方面的考虑,但不应在新代码中使用它们。将来可能会删除对这些功能的支持。

不推荐使用以下功能:

  • Date.prototype.getYear(改为使用getFullYear)
  • Date.prototype.setYear(改用setFullYear)
  • Date.prototype.toGMTString(改用toUTCString)
  • Date.prototype.escape(改为使用encodeURI或encodeURIComponent)
  • Date.prototype.unescape(改为使用decodeURI或decodeURIComponent)
  • RegExp.prototype.compile(不使用)
  • String.prototype.substr(改为使用slice或substring)

以下功能是非标准功能:

  • String.prototype.trimLeft
  • String.prototype.trimRight
  • String.prototype.anchor
  • String.prototype.big
  • String.prototype.blink
  • String.prototype.bold
  • String.prototype.fixed
  • String.prototype.fontcolor
  • String.prototype.fontsize
  • String.prototype.italics
  • String.prototype.link
  • String.prototype.quote(v2中的新增功能)
  • String.prototype.small
  • String.prototype.strike
  • String.prototype.sub
  • String.prototype.sup

以下属性是非标准的:

  • Function.prototype.name
  • Function.prototype.displayName
  • RegExp.$1-RegExp.$9(v2.1中的新增功能)
  • RegExp.input,RegExp.$_(v2.1中的新增功能)
  • RegExp.lastMatch(v2.1中的新增功能)
  • RegExp.lastParen(v2.1中的新增功能)
  • RegExp.leftContext(v2.1中的新增功能)
  • RegExp.rightContext(v2.1中的新增功能)

安全执行用户提供的脚本

除非您将.NET值,方法,类型等暴露给ScriptEngine或其对象,否则Jurassic会在隔离的环境中运行JavaScript,因此不受信任的脚本无法访问外部数据。但是,在以下情况下,仍然存在DoS危险,即不受信任的脚本可能导致进程终止或运行无限循环:

限制递归深度

考虑以下JavaScript代码:

(function func() {
    func();
})();

如果使用来运行此代码ScriptEngine.Execute(),则StackOverflowException由于无限递归,该过程将以a终止,并且由于StackOverflowExceptions它们被认为破坏了过程状态,因此无法捕获CLR引发的异常。

为了防止此类堆栈溢出,可以设置ScriptEngine.RecursionDepthLimit为限制允许用户定义的函数进行的最大递归次数。一旦函数超出此限制,StackOverflowException侏罗纪将抛出a (在这种情况下可以捕获)。

注意:在堆栈溢出之前可以使用的最大递归次数取决于线程的最大堆栈大小以及由编译函数创建的堆栈帧的大小。对于未指定最大堆栈大小的线程和ThreadPool线程(在其上运行异步操作的回调),将使用进程的默认堆栈大小,对于编译为EXE文件的.NET应用程序,该大小为1 MB(除非目标平台为x64,在这种情况下为4 MB)。

可以使用editbin.exe带有/STACK option的Visual Studio工具来更改进程的默认堆栈大小(保留大小)和堆栈提交大小。似乎从.NET 4.0开始(与文档相反),CLR在启动线程时不会提交完整的堆栈大小,而是使用指定的提交大小(默认为4 KB)来增量分配堆栈,除非<disableCommitThreadStack enabled="0"/>指定在应用程序配置文件中。这使您可以指定更高的默认堆栈大小,而无需CLR立即为每个线程分配完整的堆栈大小。

限制执行时间

警告:
本节使用此Thread.Abort()命令来取消脚本执行,这在.NET Core中不起作用,并且也不能防止finally子句中的无限循环,例如:

try {}
finally {
    while (true);
}

有关更多信息,请参见#85


想象一下在侏罗纪执行这样的JavaScript:

while (true); 

如果您使用进行了这样一个无限循环ScriptEngine.Execute(String code),则该方法将永远不会返回。

Jurassic没有内置方法来限制脚本的执行时间。由于JurassicJavaScript方法编译为IL代码,因此没有简单的方法来提供超时功能而不影响性能。

但是,可以在执行脚本的线程中Thread.Abort()引发ThreadAbortExceptionA。一种可能性是ScriptEngine.Execute()在新线程中运行,thread.Abort()如果新线程在特定时间后未完成,则在当前线程中调用。您还可以使用以下帮助程序类,以更简单的方式将超时应用于脚本执行。

用于限制脚本执行时间的Helper类(ScriptTimeoutHelper

下列ScriptTimeoutHelper类提供了一种简单而干净的方法,可以在调用该方法的同一线程中运行脚本时,在JavaScript执行上应用超时,而不必处理其他线程ThreadAbortExceptions(这是在后台完成的)。它还提供了一种将.NET回调标记为关键部分的方法,当应用超时时,这些部分不应中止。
这假定您以.NET 4.5或更高版本为目标,并且正在使用Visual Studio 2015或更高版本。

using System;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;

namespace JurassicTimeoutHelper
{
    /// <summary>
    /// Allows to limit execution time of a Jurassic Script by internally using the
    /// technique of aborting a thread.
    /// </summary>
    public class ScriptTimeoutHelper
    {
        private HandlerState currentState;

        public ScriptTimeoutHelper()
        {
        }

        /// <summary>
        /// Runs the specified <see cref="Action"/> in the current thread,
        /// applying the given timeout.
        /// When the handler times out, a <see cref="ThreadAbortException"/> is
        /// raised in the current thread to break. However, this is managed
        /// internally so it does not affect the caller of this method (i.e. it is
        /// ensured that a <see cref="ThreadAbortException"/> does not flow through
        /// this method or raised after this method returns).
        /// </summary>
        /// <exception cref="TimeoutException">Thrown when the handler times 
        /// out.</exception>
        /// <param name="handler"></param>
        /// <param name="timeout"></param>
        public void RunWithTimeout(Action handler, int timeout)
        {
            if (currentState != null)
                throw new InvalidOperationException(
                    $"Cannot recursively call {nameof(RunWithTimeout)}.");
            if (handler == null)
                throw new ArgumentException(nameof(handler));

            // Throw the TimeoutException immediately when the timeout is 0.
            if (timeout == 0)
                throw new TimeoutException();

            ExceptionDispatchInfo caughtException = null;

            using (var state = currentState =
                new HandlerState(Thread.CurrentThread))
            {
                /* Start a monitoring task that may abort the current thread after
                 * the specified time limit.
                 * Note that the task will start immediately. Therefore we need to
                 * ensure the task does not abort the thread until we entered the
                 * try clause; otherwise the ThreadAbortException might fly through
                 * the caller of this method. To ensure this, the monitoring task
                 * waits until we release the semaphore the first time before
                 * actually waiting for the specified time.
                */
                using (var monitoringTask = Task.Run(async () =>
                    await RunMonitoringTask(state, timeout)))
                {
                    try
                    {
                        bool waitForAbortException;
                        try
                        {
                            // Allow the monitoring task to begin by releasing the
                            // semaphore the first time.
                            // Do this in a finally block to ensure if this thread
                            // is aborted by other code, the semaphore is still
                            // released.
                            try { }
                            finally
                            {
                                state.WaitSemaphore.Release();
                            }

                            // Execute the handler.
                            handler();
                        }
                        catch (Exception ex) when (!(ex is ThreadAbortException))
                        {
                            /* Need to catch all exceptions (except our own
                             * ThreadAbortException) because we may wait for a
                             * ThreadAbortException to be thrown which is not
                             * possible in a finally handler.
                             */
                            caughtException = ExceptionDispatchInfo.Capture(ex);
                        }
                        finally
                        {
                            /* Indicate that the handler is completed, and check
                             * if we need to wait for the ThreadAbortException.
                             * This is done in a finally handler to ensure when
                             * other code wants to abort this thread, the thread
                             * actually will abort as expected but we still can
                             * notify the monitoring task that we already returned.
                             */
                            lock (state)
                            {
                                state.IsExited = true;
                                waitForAbortException =
                                    state.AbortState == AbortState.IsAborting;

                                if (state.AbortState == AbortState.None)
                                {
                                    // If the monitoring task did not do anything
                                    // yet, allow it to complete immediately.
                                    state.WaitSemaphore.Release();
                                }
                            }
                        }

                        if (waitForAbortException)
                        {
                            /* The monitoring task indicated that it will abort our
                             * thread (but the ThreadAbortException did not yet
                             * occur), so we need to wait for the
                             * ThreadAbortException.
                             * This wait is needed because otherwise we may return
                             * too early (and in the finally block we wait for the
                             * monitoring task, causing a deadlock).
                             */
                            Thread.Sleep(Timeout.Infinite);
                        }
                    }
                    catch (ThreadAbortException ex)
                        when (ex.ExceptionState == state)
                    {
                        // Reset the abort.
                        Thread.ResetAbort();

                        // Indicate that the timeout has been exceeded.
                        throw new TimeoutException();
                    }
                    finally
                    {
                        // Wait for the monitoring task to complete.
                        monitoringTask.Wait();
                        currentState = null;
                    }
                }
            }

            // Check if we need to rethrow a caught exception (preserving the
            // original stacktrace).
            if (caughtException != null)
                caughtException.Throw();
        }

        private async Task RunMonitoringTask(HandlerState state, int timeout)
        {
            // Wait until the handler thread entered the try-block.
            // Use a synchronous wait because we expect this to be a very short
            // period of time.
            state.WaitSemaphore.Wait();

            // Now asynchronously wait until the specified time has passed or the
            // semaphore has been released. In the latter case there is no need to
            // call AbortExecution().
            bool completed = await state.WaitSemaphore.WaitAsync(timeout);

            // Abort the handler thread.
            if (!completed)
                AbortExecution(state);
        }

        private void AbortExecution(HandlerState state)
        {
            bool canAbort;
            lock (state)
            {
                if (state.IsExited)
                {
                    // The handler has already exited.
                    return;
                }

                // Check if we can call Thread.Abort() or if the handler thread is
                // currently in a critical section and needs to abort himself when
                // leaving the critical section.
                canAbort = !state.IsCriticalSection;
                state.AbortState = canAbort ? AbortState.IsAborting
                    : AbortState.ShouldAbort;
            }
            if (canAbort)
            {
                /* The handler thread is not in a critical section so we can
                 * directly abort it.
                 * This needs to be done outside of the lock because Abort() could
                 * block if the  thread is currently in a finally handler (and
                 * trying to lock on the state object), which could lead to a
                 * deadlock.
                 */
                state.HandlerThread.Abort(state);
            }
        }

        /// <summary>
        /// Notifies this class that the handler thread is entering a critical
        /// section in which aborting the thread could corrupt the system's state.
        /// This means aborting the thread will be deferred until leaving the
        /// critical section.
        /// Note that you must call <see cref="ExitCriticalSection"/> in a
        /// <c>finally</c> block once the thread left the critical section.
        /// </summary>
        public void EnterCriticalSection()
        {
            if (currentState == null)
                throw new InvalidOperationException();

            bool waitForAbortException;
            lock (currentState)
            {
                if (Thread.CurrentThread != currentState.HandlerThread
                    || currentState.IsCriticalSection)
                    throw new InvalidOperationException();

                currentState.IsCriticalSection = true;
                waitForAbortException =
                    currentState.AbortState == AbortState.IsAborting;
            }
            if (waitForAbortException)
            {
                // The monitoring task indicated that it will abort our thread, so
                // we need to wait for the ThreadAbortException.
                Thread.Sleep(Timeout.Infinite);
            }
        }

        public void ExitCriticalSection()
        {
            if (currentState == null)
                throw new InvalidOperationException();

            bool shouldAbort;
            lock (currentState)
            {
                if (Thread.CurrentThread != currentState.HandlerThread
                    || !currentState.IsCriticalSection)
                    throw new InvalidOperationException();

                currentState.IsCriticalSection = false;
                shouldAbort = currentState.AbortState == AbortState.ShouldAbort;
            }
            if (shouldAbort)
            {
                // The monitoring task indicated that it wanted to abort our
                // thread while we were in a critical section, so we need to abort
                // ourselves.
                Thread.CurrentThread.Abort(currentState);
            }
        }

        private enum AbortState
        {
            /// <summary>
            /// Indicates that the monitoring task has not yet done any action.
            /// </summary>
            None = 0,

            /// <summary>
            /// Indicates that the monitoring task is about to abort the handler
            /// thread.
            /// </summary>
            IsAborting = 1,

            /// <summary>
            /// Indicates that the monitoring task wanted to abort the handler
            /// thread but the handler thread was in a critical section, and needs
            /// to abort itself when leaving the critical section.
            /// </summary>
            ShouldAbort = 2
        }

        private class HandlerState : IDisposable
        {

            public Thread HandlerThread { get; }

            public SemaphoreSlim WaitSemaphore { get; } = new SemaphoreSlim(0);

            public bool IsCriticalSection { get; set; }

            /// <summary>
            /// Indicates if the handler is already completed (so there's no need
            /// to abort the thread).
            /// This flag is set by the handler thread.
            /// </summary>
            public bool IsExited { get; set; }

            /// <summary>
            /// Indicates that the wait task wanted to abort the handler thread but
            /// the handler thread was in a critical section, and needs to abort
            /// itself when leaving the critical section.
            /// This flag is set by the wait task.
            /// </summary>
            public AbortState AbortState { get; set; }


            public HandlerState(Thread handlerThread)
            {
                this.HandlerThread = handlerThread;
            }

            ~HandlerState()
            {
                Dispose(false);
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected void Dispose(bool disposing)
            {
                if (disposing)
                {
                    WaitSemaphore.Dispose();
                }
            }
        }
    }
}

此类启动一个异步Task,该异步使用来等待超时SemaphoreSlim(这样就不会通过等待超时来阻止单独的线程),然后如果线程未在超时内完成,则中止该线程。RunWithTimeout()捕获ThreadAbortException并重置异常终止,以确保线程在处理异常终止后可以正常继续。
与可能的想法相反,Thread.Abort()在这种情况下,a允许完全取消脚本执行而没有副作用(只要正确标记了.NET Callbacks中的关键部分)。

请注意,脚本在调用该方法的同一线程上执行,而不是在新线程上执行。这样可以确保处理从Javascript调用的锁的.NET方法可以正常工作。

现在,在您的代码中,而不是编写

var engine = new ScriptEngine();
var compiledScript = engine.Compile(new StringScriptSource(
    "while (true) ;"));
compiledScript.Execute();

你可以写:

var timeoutHelper = new ScriptTimeoutHelper();
var engine = new ScriptEngine();
var compiledScript = engine.Compile(new StringScriptSource(
    "while (true) ;"));
timeoutHelper.RunWithTimeout(() => compiledScript.Execute(), 1000);

TimeoutException如果JavaScript未在1秒钟内完成,您现在将得到一个。请注意,我们使用useScriptEngine.Compile()首先编译脚本,然后将超时仅应用于实际执行脚本。

注:在以后TimeoutException发生,你不应该继续使用ScriptEngine及其关联的对象,因为它们可能处于不一致/不可用状态。而是创建一个新文件ScriptEngine以运行更多脚本。

关键部分

使用ScriptTimeoutHelper该类意味着RunWithTimeout可以通过放弃当前正在中执行的代码ThreadAbortException。如果您的脚本能够调用.NET方法,但是在执行过程中不应中止.NET方法,这可能是一个问题,例如,因为它将使系统处于不一致状态。

若要解决此问题,请在调用ScriptTimeoutHelper.EnterCriticalSection().NET方法后立即调用,并ScriptTimeoutHelper.ExitCriticalSection()finally.NET方法返回时调用(在一个块中)。这样可以确保将aThreadAbortException延迟到离开关键部分为止。

例:

假设您有一个API类,该类的方法增加了三个变量:

    class Api
    {
        public int a = 0;
        public long b = 0;
        public double c = 0;

        // Increments all three counters.
        public void Increment()
        {
            a++;
            // Loop to simulate other work
            for (int i = 0; i < 1000000; i++) ;
            b++;
            for (int i = 0; i < 1000000; i++) ;
            c++;
        }
    }

调用Api.Increment()应增加所有三个计数器的值,因此在方法返回后不会出现a = 21b = 22的情况-只要int变量不溢出,所有三个计数器的值都应相同。只要您不中止线程(或e.g. a OutOfMemoryException occurs),就可以期望它起作用。

现在,假设您要允许JavaScript调用此方法,但要限制脚本的执行时间:

class ScriptApi : ObjectInstance
    {
        private Api api;

        public ScriptApi(ScriptEngine engine, Api api)
            : base(engine)
        {
            this.api = api;
            PopulateFunctions();
        }

        [JSFunction(Name = "increment")]
        public void Increment() => api.Increment();
    }

    class Program
    {
        private static Api api = new Api();

        static void Main()
        {
            var timeoutHelper = new ScriptTimeoutHelper();
            var engine = new ScriptEngine();
            var scriptApi = new ScriptApi(engine, api);
            engine.SetGlobalValue("api", scriptApi);

            try
            {
                var compiledScript = engine.Compile(new StringScriptSource(@"
                    while (true) {
                        // Call a .NET API method.
                        api.increment();
                    }
                "));
                timeoutHelper.RunWithTimeout(() => compiledScript.Execute(), 1000);
            }
            catch (TimeoutException)
            {
                Console.WriteLine("Script timed-out.");
                // Note: The ScriptEngine object might now be in an
                // inconsistent/unable state, so you should create a new
                // ScriptEngine object to execute further scripts.
            }

            Console.WriteLine($"Result: a = {api.a}, b = {api.b}, c = {api.c}");
            Console.ReadKey();
        }
    }

如果您多次运行该程序,有时会发现a,b并且c会有不同的值。这是因为当线程中止时,它可能在脚本代码(轻量级函数)中或在Api.Increment()方法中。

为了防止线程在Api.Increment()方法中被中止,您将需要通过以下方式修改ScriptApi类:

  class ScriptApi : ObjectInstance
    {
        private Api api;
        private ScriptTimeoutHelper timeoutHelper;

        public ScriptApi(ScriptEngine engine, Api api,
            ScriptTimeoutHelper timeoutHelper)
            : base(engine)
        {
            this.api = api;
            this.timeoutHelper = timeoutHelper;
            PopulateFunctions();
        }

        [JSFunction(Name = "increment")]
        public void Increment()
        {
            timeoutHelper.EnterCriticalSection();
            try
            {
                // Call critical code which must not be aborted here.
                api.Increment();
            }
            finally
            {
                timeoutHelper.ExitCriticalSection();
            }
        }
    }

现在,您可以看到每次脚本中止时,所有三个计数器将具有相同的值。只要“关键”方法(Api.Increment())不需要花费很长时间执行,该脚本仍将在约1秒后终止。

递归调用和关键部分
如果您标记为“关键部分”的.NET回调有可能再次调用脚本函数,则您不能使用同一ScriptTimeoutHelper实例在超时的情况下运行它,因为它不允许递归调用。

相反,您可以在每次.NET代码调用JavaScript代码时创建一个Stack<ScriptTimeoutHelper>,并将一个新ScriptTimeoutHelper实例(Push)添加到堆栈中,并在调用返回后从堆栈Pop中的finally子句中删除该实例。
然后,在您的回调中,使用ScriptTimeoutHelper当前位于堆栈(Peek)顶部的调用EnterCriticalSection()ExitCriticalSection()

补充说明

同时限制最大 递归深度和最大值 执行时间可以帮助安全地执行来自不受信任来源的脚本(从而限制了DoS表面),脚本仍然可以分配任意数量的内存,例如,通过无限分配更长的字符串,这将OutOfMemoryException在启动后立即引发:

let s = 'a';
while (true)
    s += s;

当前,在侏罗纪中没有办法防止这种无限分配。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容