/// SyNode - server-side JavaScript execution using the SpiderMonkey 45/52
// library with nodeJS modules support
// - this unit is a part of the freeware Synopse framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SyNode;
{
    This file is part of Synopse framework.

    Synopse framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - http://synopse.info

    SyNode for mORMot Copyright (C) 2022 Pavel Mashlyakovsky & Vadim Orel
      pavel.mash at gmail.com

    Some ideas taken from
       http://code.google.com/p/delphi-javascript
       http://delphi.mozdev.org/javascript_bridge/

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Initial Developer of the Original Code is
  Pavel Mashlyakovsky.
  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):
  - Arnaud Bouchez
  - Vadim Orel
  - Pavel Mashlyakovsky
  - win2014
  - Geogre
  - ssoftpro

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

  Version 1.18
  - initial release. Use SpiderMonkey 45
  - x64 support added
  - move a Worker classes from UnityBase to SyNode
    Allow to create and communicate with a threads from JavaScript
  - implement nodeJS Buffer
  - improved compartibility with nodeJS utils
  - add a `process.version`
  - implement a setTimeout/setInverval/setImmediate etc.
  - SpiderMonkey 52 (x32 & x64) support ( SM52 condition should be defined )
  - VSCode debugging support via [vscode-firefox-debug](https://github.com/hbenl/vscode-firefox-debug) adapter
    Thanks to George for patch
  - JS `process` now instance of EventEmitter
  - JS Engine will emit `exit` event on destroy as in node

}

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
{$I SyNode.inc}   // define SM_DEBUG CONSIDER_TIME_IN_Z
{$IFDEF CORE_MODULES_IN_RES}
  {$R 'core_modules.res' 'core_modules.rc'}
{$ENDIF}

interface

uses
  {$ifndef FPC}
  Windows,
  ShLwApi, // for older Delphi versions download this file from JEDI library
  {$else}
  LazFileUtils, dynlibs,
  {$endif}
  {$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif}
  Classes,
  {$ifndef LVCL}
  Contnrs,
  {$endif}
  {$ifdef ISDELPHIXE2}
  System.Rtti,
  {$endif}
  Variants,
  SynCommons,
  SynLog,
  SpiderMonkey,
  NSPRAPI,
  SyNodeProto,
  mORMot,
  SynCrtSock,
  SyNodeBinding_worker;

const
  /// default stack growning size
  STACK_CHUNK_SIZE: Cardinal = 8192;

type

  {$M+}
  TSMEngineManager = class;
  {$M-}

  TSMEngine = class;

  TSMEngineClass = class of TSMEngine;

  /// implements a ThreadSafe JavaScript engine
  // - use TSMEngineManager.ThreadSafeEngine to retrieve the Engine instance
  // corresponding to the current thread, in multithread application
  // - contains JSRuntime + JSContext (to be ready for new SpiderMonkey version where
  // context and runtime is the same)
  // - contains also one "global" JavaScript object. From script it is
  // accessible via "global." (in browser, this is the "window." object)
  // - set SpiderMonkey error reporter and store last SpiderMonkey error in
  // LastError property
  TSMEngine = class
  private
    fPrivateDataForDebugger: Pointer;
    fNameForDebug: RawUTF8;
    fDoInteruptInOwnThread: TThreadMethod;
    {$IFNDEF SM52}
    fRt: PJSRuntime;
    {$ENDIF}
    fcomp: PJSCompartment;
    fStringFinalizer: JSStringFinalizer;

    fManager: TSMEngineManager;

    fGlobalObject: PJSRootedObject;
    // gloal._timerLoop function used to emulate setTimeout/setInterval
    // event loop. Implemented in WindowsTimer.js
    FGlobalTimerLoopFunc: PJSRootedValue;
    fGlobalObjectDbg: PJSRootedObject;

    fEngineContentVersion: Cardinal;
    fThreadID: TThreadID;
    fLastErrorMsg: string;
    fLastErrorNum: integer;
    fLastErrorFileName: RawUTF8;
    fLastErrorLine: integer;
    fLastErrorStackTrace: SynUnicode;
    fErrorExist: boolean;
    fThreadData: pointer;
    fDllModulesUnInitProcList: TList;
    fNeverExpire: boolean;
    fWebAppRootDir: RawUTF8;
    /// called from SpiderMonkey callback. Do not raise exception here
    // instead use CheckJSError metod after JSAPI compile/evaluate call
{$IFDEF SM52}
    procedure DoProcessJSError(report: PJSErrorReport); virtual;
{$ELSE}
    procedure DoProcessJSError(errMsg: PCChar; report: PJSErrorReport); virtual;
{$ENDIF}
    /// called from SpiderMonkey callback. It used for interrupt execution of script
    //  when it executes too long
    function DoProcessOperationCallback: Boolean; virtual;
    procedure SetPrivateDataForDebugger(const Value: Pointer);
  protected
    fCx: PJSContext;
    // used by Watchdog thread state. See js.cpp
    fTimeOutAborted: Boolean;
    fTimedOut: Boolean;
    fWatchdogLock: PRLock;
    fWatchdogWakeup: PRCondVar;
    fWatchdogThread: PRThread;
    fWatchdogHasTimeout: Boolean;
    fWatchdogTimeout: Int64;
    fSleepWakeup: PRCondVar;
    fTimeoutValue: integer;
    fCreatedAtTick: Int64;

    function ScheduleWatchdog(t: integer): Boolean;
    procedure KillWatchdog;
    function InitWatchdog: boolean;
    procedure SetTimeoutValue(const Value: Integer);
  public
    /// create one threadsafe JavaScript Engine instance
    // - initialize internal JSRuntime, JSContext, and global objects and
    // standard JavaScript classes
    // - do not create Engine directly via this constructor, but instead call
    // TSMEngineManager.ThreadSafeEngine
    constructor Create(aManager: TSMEngineManager); virtual;
    /// finalize the JavaScript engine instance
    destructor Destroy; override;
    /// reference to TSMEngineManager own this engine
    property Manager: TSMEngineManager read FManager;

    /// thread specific data - pointer to any structure, passed into ThreadSafeEngine call
    //  used to access a thread-details in the native functions
    //  as TsmEngive(cx.PrivateData).ThreadData
    property ThreadData: pointer read FThreadData;
    procedure SetThreadData(pThreadData: pointer);

    /// check if last call to JSAPI compile/eval fucntion was successful
    // - raise ESMException if any error occurred
    // - put error description to SynSMLog
    procedure CheckJSError(res: Boolean);
    /// clear last JavaScript error
    // - called before every evaluate() function call
    procedure ClearLastError;

    /// trigger Garbage Collection
    // - all unrooted things (JSString, JSObject, VSVal) will be released
    procedure GarbageCollect;
    /// Offer the JavaScript engine an opportunity to perform garbage collection if needed
    // - Tries to determine whether garbage collection in would free up enough
    // memory to be worth the amount of time it would take. If so, it performs
    // some garbage collection
    // - Frequent calls are safe and will not cause the application to spend a
    // lot of time doing redundant garbage collection work
    procedure MaybeGarbageCollect;


    /// evaluate a JavaScript script in the global scope
    // - if exception raised in script - raise Delphi ESMException
    // - on success returns last executed expression statement processed
    // in the script as a variant
    // - JavaScript equivalent to
    // ! eval(script)
    procedure Evaluate(const script: SynUnicode; const scriptName: RawUTF8;
      lineNo: Cardinal; out result: jsval); overload;

    procedure Evaluate(const script: SynUnicode; const scriptName: RawUTF8;
      lineNo: Cardinal); overload;

    /// evaluate script embadedd into application resources
    procedure EvaluateRes(const ResName: string; out result: jsval);

    /// evaluate a JavaScript script as Module
    // - if exception raised in script - raise Delphi ESMException
    // - JavaScript equivalent to import of ES6
    function EvaluateModule(const scriptName: RawUTF8): jsval;

    /// run method of object
    // - if exception raised in script - raise Delphi ESMException
    // - on success returns function result
    // - JavaScript equivalent to
    // ! obj.funcName(args[0], args[1]...)
    function CallObjectFunction(obj: PJSRootedObject; funcName: PCChar; const args: array of jsval): jsval;
    /// The same as CallObjectFunction but accept funcVal insteadof fuction name
    // a little bit faster when CallObjectFunction
    function CallObjectFunctionVal(obj: PJSRootedObject; const funcVal: PJSRootedValue; const args: array of jsval): jsval;

    /// access to the associated global object as low-level PJSRootedObject
    property GlobalObject: PJSRootedObject read FGlobalObject;
    /// access to the associated global object for debugging as low-level PJSRootedObject
    property GlobalObjectDbg: PJSRootedObject read FGlobalObjectDbg;
    /// access to the associated execution context
    property cx: PJSContext read fCx;
    {$IFNDEF SM52}
    /// access to the associated execution runtime
    property rt: PJSRuntime read frt;
    {$ENDIF}
    /// access to the associated execution compartment
    property comp: PJSCompartment read fcomp;

    /// internal version number of engine scripts
    // - used in TSMEngine.ThreadSafeEngine to determine if context is up to
    // date, in order to trigger on-the-fly reload of scripts without the need
    // if restarting the application
    // - caller must change this parameter value e.g. in case of changes in
    // the scripts folder in an HTTP server
    property EngineContentVersion: Cardinal read FEngineContentVersion;

    /// last error message triggered during JavaScript execution
    property LastErrorMsg: string read FLastErrorMsg;
    /// last error source code line number triggered during JavaScript execution
    property LastErrorLine: integer read FLastErrorLine;
    /// last error file name triggered during JavaScript execution
    property LastErrorFileName: RawUTF8 read FLastErrorFileName;
    /// TRUE if an error was triggered during JavaScript execution
    property ErrorExist: boolean read FErrorExist;
    /// notifies a WatchDog timeout
    property TimeOutAborted: boolean read FTimeOutAborted;
    /// define a WatchDog timeout interval (in seconds)
    // - is set to -1 by default, i.e. meaning no execution timeout
    property TimeOutValue: Integer read fTimeoutValue write SetTimeoutValue default -1;
    /// If `true` engine will never expire
    property NeverExpire: boolean read FNeverExpire write FNeverExpire;

    /// Define a Delphi class as JS class (prototype), so it can be created via new:
    //    engine.defineClass(TmyObject, TSMNewRTTIProtoObject);
    //  and in JS
    //    var myObj = new TmyObject();
    // - Class will be exposed in the aParent javaScript object, if aParent is omitted - in the global object
    // - AProto parameter can be a TSMCustomProtoObject descendant (TSMSimpleRTTIProtoObject, TSMNewRTTIProtoObject)
    // WARNING - possible prototypes count is limited to 255 - JSCLASS_GLOBAL_SLOT_COUNT = 76 for SM45,
    //   so it is better to use a bindings, add where a native functions and wrap it in class into the JavaScript
    //   see `fs.js` for sample.
    function defineClass(AForClass: TClass; AProto: TSMCustomProtoObjectClass; aParent: PJSRootedObject = nil): TSMCustomProtoObject;
    /// Add a set of classes with the same delphi binding implementation:
    //    engine.defineClasses([TubEntity, TubDomain, TubEntityAttribute], TSMNewRTTIProtoObject);
    procedure defineClasses(const AClasses: array of TClass; AProto: TSMCustomProtoObjectClass; aParent: PJSRootedObject = nil);
    /// define JS object for enumeratin type
    //  for TMyEnum = (etA, etB); will create in JS aObj.TMyEnum = {etA: 0, etB: 1);
    procedure defineEnum(ti: PTypeInfo; aObj: PJSRootedObject);

    /// Cancel current execution JavaScript.
    // This procedure can be called from other script
    // if AWithException paramener is thue, will raise a
    // ESMException with message LONG_SCRIPT_EXECUTION
    procedure CancelExecution(AWithException: boolean = true);

    procedure DefineProcessBinding;
    procedure DefineNodeProcess;
    procedure DefineModuleLoader;

    /// Used by debugger
    property PrivateDataForDebugger: Pointer read FPrivateDataForDebugger write SetPrivateDataForDebugger;
    /// Name of this engine. Will be shown in debugger for indentify this engine
    property nameForDebug: RawUTF8 read fnameForDebug;
    /// root path for current web app engine context
    property webAppRootDir: RawUTF8 read fWebAppRootDir;
    /// Handler hor debugger. Called from debugger thread when debugger need
    // call rt.InterruptCallback(cx) in engine''s thread
    // this method must wake up the engine's thread and thread must
    // execute rt.InterruptCallback(cx) for this engine
    property doInteruptInOwnThread: TThreadMethod read fDoInteruptInOwnThread write fDoInteruptInOwnThread;
  end;


  /// prototype of SpideMonkey notification callback method
  TEngineEvent = procedure(const Engine: TSMEngine) of object;

  /// event of getting engine name or web app root path
  TEngineNameEvent = function(const Engine: TSMEngine): RawUTF8 of object;

  /// event for loading dll module
  TDllModuleEvent = procedure(Handle: HMODULE) of object;

  /// event for adding values to process.binding
  TSMProcessBindingHandler = function(const Engine: TSMEngine; const bindingNamespaceName: SynUnicode): jsval;

  TDllModuleInitProc = function(cx: PJSContext; exports_: PJSRootedObject; require: PJSRootedObject; module: PJSRootedObject; __filename: PWideChar; __dirname: PWideChar): boolean; cdecl;
  TDllModuleUnInitProc = function: boolean; cdecl;
  TDllModuleRec = record
    init: TDllModuleInitProc;
    unInit: TDllModuleUnInitProc;
  end;
  PDllModuleRec = ^TDllModuleRec;

  /// main access point to the SpiderMonkey per-thread scripting engines
  // - allow thread-safe access to an internal per-thread TSMEngine instance list
  // - contains runtime-level properties shared between thread-safe engines
  // - you can create several TSMEngineManager instances, if you need several
  // separate scripting instances
  // - set OnNewEngine callback to initialize each TSMEngine, when a new thread
  // is accessed, and tune per-engine memory allocation via MaxPerEngineMemory
  // and MaxRecursionDepth
  // - get the current per-thread TSMEngine instance via ThreadSafeEngine method
  TSMEngineManager = class
  private
    FMaxPerEngineMemory: Cardinal;
    FMaxNurseryBytes: Cardinal;
    FMaxRecursionDepth: Cardinal;
    FEnginePool: TSynObjectListLocked;
    FRemoteDebuggerThread: TThread;
    FContentVersion: Cardinal;
    FOnNewEngine: TEngineEvent;
    FOnDebuggerInit: TEngineEvent;
    FOnGetName: TEngineNameEvent;
    FOnGetWebAppRootPath: TEngineNameEvent;
    FOnDebuggerConnected: TEngineEvent;
    FOnDllModuleLoaded: TDllModuleEvent;
    {$ifdef ISDELPHIXE2}
    FRttiCx: TRttiContext;
    {$endif}
    FEngineClass: TSMEngineClass;

    /// List of loaded dll modules
    FDllModules: TRawUTF8List;
    /// Path to core modules
    FCoreModulesPath: RawUTF8;
    FEngineExpireTimeOutTicks: Int64;
    FWorkersManager: TJSWorkersManager;

    function GetEngineExpireTimeOutMinutes: cardinal;
    procedure SetEngineExpireTimeOutMinutes(const minutes: cardinal);
    procedure SetMaxPerEngineMemory(AMaxMem: Cardinal);
    procedure SetMaxNurseryBytes(AMaxNurseryBytes: Cardinal);
  protected
    grandParent: TSMEngine;
    /// Release a javaScript engine for specified thread
    procedure ReleaseEngineForThread(aThreadID: TThreadID);
    /// returns -1 if none was defined yet
    // - this method is not protected via the global FEngineCS mutex/lock
    function ThreadEngineIndex(ThreadID: TThreadID): Integer;
    /// returns nil if none was defined yet
    function CurrentThreadEngine: TSMEngine;
    /// called when a new Engine is created
    // - this default implementation will run the OnNewEngine callback (if any)
    procedure DoOnNewEngine(const Engine: TSMEngine); virtual;

    function getPauseDebuggerOnFirstStep: boolean;
    procedure setPauseDebuggerOnFirstStep(const Value: boolean);

    function evalDllModule(cx: PJSContext; module: PJSRootedObject; const filename: RawUTF8): TDllModuleUnInitProc;
    /// Get handler of process.binding
    function GetBinding(const Name: RawUTF8): TSMProcessBindingHandler;
  public
    /// initialize the SpiderMonkey scripting engine
    // aCoreModulesPath can be empty in case core modules are embadded
    // as resources (CORE_MODULES_IN_RES is defined)
    constructor Create(const aCoreModulesPath: RawUTF8; aEngineClass: TSMEngineClass = nil); virtual;
    /// finalize the SpiderMonkey scripting engine
    destructor Destroy; override;
    /// Add a binding namespace - a set of native functions exposed to the JavaScript engine
    // - we recommend not to use it directly in the businnes logic,
    // but create a JavaScript module and wrap a binding function by the JavaScript
    // see how `fs` binding from SyNodeBinding_fs unit is wrapped by `SyNode\core_modules\node_modules\fs.js`
    class procedure RegisterBinding(const Name: RawUTF8; const handler: TSMProcessBindingHandler);
    /// get or create one Engine associated with current running thread
    // pThreadData is a pointer to any structure relative to this thread
    // accessible via TsmEngine.ThreadData
    function ThreadSafeEngine(pThreadData: pointer): TSMEngine;
    /// method to be called when a thread is about to be finished
    // - you can call this method just before a thread is finished to ensure
    // that the associated scripting Engine will be released
    // - could be used e.g. in a try...finally block inside a TThread.Execute
    // overriden method
    procedure ReleaseCurrentThreadEngine;
    /// returns nil if none was defined yet
    function EngineForThread(ThreadID: TTHreadID): TSMEngine;
    /// internal version of the script files
    // - used in TSMEngine.ThreadSafeEngine to determine if context is up to
    // date, in order to trigger on-the-fly reload of scripts without the need
    // if restarting the application
    property ContentVersion: Cardinal read FContentVersion write FContentVersion;
    /// lock/mutex used for thread-safe access to the TSMEngine list
    procedure Lock;
    procedure UnLock;
    {$ifdef ISDELPHIXE2}
    /// for defining JS object based on New RTTI we need a single TRttiContext
    // in other case we got AV if used from DLL
    property RttiCx: TRttiContext read FRttiCx;
    {$endif}
    /// Start/stop debugger listening on selected port
    // - expects the port to be specified as Ansi string, e.g. '1234'
    // - you can optionally specify a server address to bind to, e.g.
    // '1.2.3.4:1234'
    // - debugger create a dedicated thread where it listen to a requests
    // from a remote debug UI
    procedure startDebugger(const port: SockString = '6000');
    procedure stopDebugger;
    /// Write text as console log to current thread Engine's debugger (if exists)
    procedure debuggerLog(const Text: RawUTF8);
    /// when debugger connected to new engine this Engine must pause on first step
    property pauseDebuggerOnFirstStep: boolean read getPauseDebuggerOnFirstStep write setPauseDebuggerOnFirstStep;
    /// Workers manager
    property WorkersManager: TJSWorkersManager read FWorkersManager;
    /// Delete all started worker threads
    procedure ClearWorkers;
  published
    /// max amount of memory (in bytes) for a single SpiderMonkey instance
    // - this parameter will be set only at Engine start, i.e. it  must be set
    // BEFORE any call to ThreadSafeEngine
    // - default is 32 MB
    property MaxPerEngineMemory: Cardinal read FMaxPerEngineMemory write SetMaxPerEngineMemory
      default 32*1024*1024;
    // Nursery size in bytes
    // - default is 16 MB
    property MaxNurseryBytes: Cardinal read FMaxNurseryBytes write SetMaxNurseryBytes
      default 16*1024*1024;
    /// maximum expected recursion depth for JavaScript functions
    // - to avoid out of memory situation in functions like
    // ! function f(){ f() };
    // - default is 32, but you can specify some higher value
    property MaxRecursionDepth: Cardinal read FMaxRecursionDepth write FMaxRecursionDepth
      default 32;
    /// specify a maximum lifetime period after which JavaScript Engine will
    // be recreated, to avoid potential JavaScript memory leak (variables in global,
    // closures circle, etc.)
    // - 0 by default - mean no expire timeout
    // In case engine must never expire - set the NeverExpire property of engine manually
   property EngineExpireTimeOutMinutes: cardinal
      read GetEngineExpireTimeOutMinutes write SetEngineExpireTimeOutMinutes default 0;
    /// Path to synode core modules.
    // Not used in case core modules are embadded as resources (CORE_MODULES_IN_RES is defined)
    property CoreModulesPath: RawUTF8 read FCoreModulesPath;
    /// event triggered every time a new Engine is created
    // event trigered before OnDebuggerInit and OnNewEngine events
    // Result og this method is Engine's name for debug
    property OnGetName: TEngineNameEvent read FOnGetName write FOnGetName;
    /// event triggered every time a new Engine is created
    // event trigered before OnDebuggerInit and OnNewEngine events
    // Result og this method is Engine's web app root path
    property OnGetWebAppRootPath: TEngineNameEvent read FOnGetWebAppRootPath write FOnGetWebAppRootPath;

    /// event triggered every time a internal debugger process connected to Engine
    // - event trigered in debugger's compartment
    // - here your code can change the initial state of the debugger
    property OnDebuggerInit: TEngineEvent read FOnDebuggerInit write FOnDebuggerInit;

    /// event triggered every time a new Engine is created
    // - here your code can change the initial state of the Engine
    property OnNewEngine: TEngineEvent read FOnNewEngine write FOnNewEngine;

    /// event triggered every time a new remote debugger(Firefox) connect to Engine
    // - Warning!!! event trigered in debugger's communication thread(not thread of Engine)
    // so you cannot use Engine's Javascript in this method, but only Engine's properties
    property OnDebuggerConnected: TEngineEvent read FOnDebuggerConnected write FOnDebuggerConnected;

    /// event triggered every time a new dll module is loaded
    // - here you code can customize dll module or call some functions from dll
    property OnDllModuleLoaded: TDllModuleEvent read FOnDllModuleLoaded write FOnDllModuleLoaded;
  end;
  {$M-}
  function SyNodeBinding_modules(const Engine: TSMEngine; const bindingNamespaceName: SynUnicode): jsval;

var
  /// define the TSynLog class used for logging for all our SynSM related units
  // - you may override it with TSQLLog, if available from mORMot.pas
  // - since not all exceptions are handled specificaly by this unit, you
  // may better use a common TSynLog class for the whole application or module
  SynSMLog: TSynLogClass=TSynLog;

  LONG_SCRIPT_EXECUTION: string = 'Script runs for too long. Terminating';

  /// check is AFileName is relative path, and if true - transform it to absolute from ABaseDir
  //  if ACheckResultInsideBase = true the perform check of result path is under ABaseDir. If not - return '';
  //  In any case will concatenates any relative path elements and replace '/' by '\' under Windows
  function RelToAbs(const ABaseDir, AFileName: TFileName; ACheckResultInsideBase: boolean = false): TFileName;

implementation

uses
  {$IFNDEF MSWINDOWS}
  Errors, SynFPCLinux,
  {$ENDIF}
  SyNodeRemoteDebugger,
  SyNodeBinding_fs {used by other core modules},
  SyNodeBinding_const,
  SyNodeBinding_buffer,
  SyNodeBinding_util,
  SyNodeBinding_uv,
  synodebinding_os;

const
  jsglobal_class: JSClass = (name: 'global';
    flags:
      JSCLASS_GLOBAL_FLAGS or
      JSCLASS_HAS_PRIVATE or
      (255 shl JSCLASS_RESERVED_SLOTS_SHIFT);
    );

var
  GlobalSyNodeBindingHandlers: TRawUTF8List;

/// handle errors from JavaScript. Just call DoProcessJSError of corresponding TSMEngine
// to set TSMEngine error properties
{$IFDEF SM52}
procedure WarningReporter(cx: PJSContext; report: PJSErrorReport); cdecl;
begin
  TSMEngine(cx.PrivateData).DoProcessJSError(report)
end;
{$ELSE}
procedure ErrorReporter(cx: PJSContext; pErrMsg: PCChar; report: PJSErrorReport); cdecl;
begin
  TSMEngine(cx.PrivateData).DoProcessJSError(pErrMsg, report)
end;
{$ENDIF}

function OperationCallback(cx: PJSContext): Boolean; cdecl;
begin
  Result := TSMEngine(cx.PrivateData).DoProcessOperationCallback;
end;

{ TSMEngine }

// do nothing here
procedure ExternalStringFinalizer(fin: PJSStringFinalizer; chars: PCChar16); cdecl;
begin
  {}
end;

function process_binding(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
const
  USAGE = 'usage: binding(scope: String)';
var
  Eng: TSMEngine;
  in_argv: PjsvalVector;
  bindingNS: SynUnicode;
  res: jsval;
  handler: TSMProcessBindingHandler;
  FBindingObject: PJSRootedObject;
begin
  Eng := TObject(cx.PrivateData) as TSMEngine;
  try
    in_argv := vp.argv;
    if (argc <> 1) or not (in_argv[0].isString) then
      raise ESMException.Create(USAGE);
    bindingNS := in_argv[0].asJSString.ToSynUnicode(cx);
    FBindingObject := cx.NewRootedObject(vp.calleObject);
    try
      if FBindingObject.ptr <> nil then begin
        res := FBindingObject.ptr.GetPropValue(cx, bindingNS);
        if res.isVoid then begin
          handler := Eng.Manager.GetBinding(SynUnicodeToUtf8(bindingNS));
          if Assigned(handler) then begin
            res := handler(eng, bindingNS);
            if not res.isVoid then begin
              FBindingObject.ptr.DefineUCProperty(cx, bindingNS, res);
            end;
          end else
            raise ESMException.CreateFmt('Invalid binding namespace "%s"',[bindingNS]);
        end;
      end else
        res.SetNull;
    finally
      cx.FreeRootedObject(FBindingObject);
    end;

    vp.rval := res;
    Result := True;
  except
    on E: Exception do begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;

constructor TSMEngine.Create(aManager: TSMEngineManager);
const
  gMaxStackSize = 128 * sizeof(size_t) * 1024;
var
{$IFDEF SM52}
  cOpts: PJSContextOptions;
{$ELSE}
  rOpts: PJSRuntimeOptions;
{$ENDIF}
  fpu: IUnknown;
begin
  fpu := TSynFPUException.ForLibraryCode;
  if aManager = nil then
    raise ESMException.Create('No manager provided');
  FCreatedAtTick := GetTickCount64;
  FManager := aManager;
  FEngineContentVersion := FManager.ContentVersion;
{$IFDEF SM52}
// TODO - solve problem of destroying parent engine before slave and uncomment a code below
// this will save up to 20% of RAM - some internal structures of SM Engines will be reused 
// between threads
//  if aManager.grandParent <> nil then
//    fCx := PJSContext(nil).CreateNew(FManager.MaxPerEngineMemory, $01000000, aManager.grandParent.cx)
//  else
    fCx := PJSContext(nil).CreateNew(FManager.MaxPerEngineMemory, $01000000, nil);
  if fCx = nil then
    raise ESMException.Create('Create context: out of memory');
{$IFNDEF CPUX64} // This check does not always work correctly under 64-bit configurations. Need more investigation to understand the problem
  fCx.SetNativeStackQuota(gMaxStackSize);
{$ENDIF}
  fCx.GCParameter[JSGC_MAX_BYTES] := FManager.MaxPerEngineMemory;
// MPV as Mozilla recommend in https://bugzilla.mozilla.org/show_bug.cgi?id=950044
//TODO - USE JS_SetGCParametersBasedOnAvailableMemory for SM32 and override JSGC_MAX_MALLOC_BYTES
  if (FManager.MaxPerEngineMemory >= 512 * 1024 * 1024) then begin
    fCx.GCParameter[JSGC_MAX_MALLOC_BYTES] :=  96 * 1024 * 1024;
    fCx.GCParameter[JSGC_SLICE_TIME_BUDGET] :=  30;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1000;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_HIGH_LIMIT] := 500;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_LOW_LIMIT] := 100;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX] :=  300;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN] :=  150;
    fCx.GCParameter[JSGC_LOW_FREQUENCY_HEAP_GROWTH] := 150;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1500;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1500;
    fCx.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1500;
    fCx.GCParameter[JSGC_ALLOCATION_THRESHOLD] := 30;
//    fCx.GCParameter[JSGC_DECOMMIT_THRESHOLD] := 32;
//    fCx.GCParameter[JSGC_MODE] := uint32(JSGC_MODE_COMPARTMENT);
    fCx.GCParameter[JSGC_MODE] := uint32(JSGC_MODE_INCREMENTAL);
  end else begin
    fCx.GCParameter[JSGC_MAX_MALLOC_BYTES] := 6 * 1024 * 1024; //MPV in PREV realisation - FManager.MaxPerEngineMemory div 2; we got a memory overuse
    fCx.GCParameter[JSGC_MODE] := uint32(JSGC_MODE_INCREMENTAL);
  end;
{$ELSE}
  frt := PJSRuntime(nil).New(FManager.MaxPerEngineMemory, $01000000, nil);
  if frt = nil then
    raise ESMException.Create('Create runtime: out of memory');
  fRt.SetNativeStackQuota(gMaxStackSize);
  fRt.GCParameter[JSGC_MAX_BYTES] := FManager.MaxPerEngineMemory;
// MPV as Mozilla recommend in https://bugzilla.mozilla.org/show_bug.cgi?id=950044
//TODO - USE JS_SetGCParametersBasedOnAvailableMemory for SM32 and override JSGC_MAX_MALLOC_BYTES
  if (FManager.MaxPerEngineMemory >= 512 * 1024 * 1024) then begin
    fRt.GCParameter[JSGC_MAX_MALLOC_BYTES] :=  96 * 1024 * 1024;
    fRt.GCParameter[JSGC_SLICE_TIME_BUDGET] :=  30;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1000;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_HIGH_LIMIT] := 500;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_LOW_LIMIT] := 100;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX] :=  300;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN] :=  150;
    fRt.GCParameter[JSGC_LOW_FREQUENCY_HEAP_GROWTH] := 150;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1500;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1500;
    fRt.GCParameter[JSGC_HIGH_FREQUENCY_TIME_LIMIT] := 1500;
    fRt.GCParameter[JSGC_ALLOCATION_THRESHOLD] := 30;
    fRt.GCParameter[JSGC_DECOMMIT_THRESHOLD] := 32;
    fRt.GCParameter[JSGC_MODE] := uint32(JSGC_MODE_COMPARTMENT);
  end else begin
    fRt.GCParameter[JSGC_MAX_MALLOC_BYTES] := 6 * 1024 * 1024; //MPV in PREV realisation - FManager.MaxPerEngineMemory div 2; we got a memory overuse
    fRt.GCParameter[JSGC_MODE] := uint32(JSGC_MODE_INCREMENTAL);
  end;

  fCx := rt.NewContext(STACK_CHUNK_SIZE);
  if fCx = nil then
    raise ESMException.Create('JSContext create');

{$ENDIF}

{$IFDEF SM52}
  cOpts := cx.Options;
  cOpts.Baseline := True;
  cOpts.Ion := True;
  cOpts.AsmJS := True;
{$ELSE}
  // You must set jsoBaseLine,jsoTypeInference,jsoIon for the enabling ION
  // ION is disabled without these options
  rOpts := rt.Options;
  rOpts.Baseline := True;
  rOpts.Ion := True;
  rOpts.AsmJS := True;
{$ENDIF}

  fStringFinalizer.finalize := ExternalStringFinalizer;
  cx.PrivateData := self;
{$IFDEF SM52}
  // not working for errors
  // TODO: implement log warnings if needed
  // fCx.WarningReporter := WarningReporter;
{$ELSE}
  fRt.ErrorReporter := ErrorReporter;
{$ENDIF}

  FGlobalObject := cx.NewRootedObject(cx.NewGlobalObject(@jsglobal_class));
  if FGlobalObject.ptr = nil then
    raise ESMException.Create('Create global object');
  fcomp := cx.EnterCompartment(FGlobalObject.ptr);
  if not cx.InitStandardClasses(FGlobalObject.ptr) then
    raise ESMException.Create('InitStandardClasses failure');
  if not cx.InitCTypesClass(FGlobalObject.ptr) then
    raise ESMException.Create('InitCTypesClass failure');
//  if not cx.InitReflectParse(FGlobalObject.ptr) then
//    raise ESMException.Create('InitReflectParse failure');
  if not cx.InitModuleClasses(FGlobalObject.ptr) then
    raise ESMException.Create('InitModuleClasses failure');

  FGlobalObject.ptr.DefineProperty(cx, 'global', FGlobalObject.ptr.ToJSValue,
    JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY, nil, nil);

  fTimeoutValue := -1;
  if not InitWatchdog then
    raise ESMException.Create('InitWatchDog failure');

{$IFDEF SM52}
  fcx.AddInterruptCallback(OperationCallback);
{$ELSE}
  fRt.InterruptCallback := OperationCallback;
{$ENDIF}
  FDllModulesUnInitProcList := TList.Create;
  DefineProcessBinding;
  DefineNodeProcess;
  DefineModuleLoader;

  EvaluateModule('synode.js');
  FGlobalTimerLoopFunc := cx.NewRootedValue(FGlobalObject.ptr.GetPropValue(cx, '_timerLoop'));

  FGlobalObjectDbg := cx.NewRootedObject(cx.NewGlobalObject(@jsglobal_class));
end;

procedure TSMEngine.defineEnum(ti: PTypeInfo; aObj: PJSRootedObject);
begin
  SyNodeProto.defineEnum(cx, ti, aObj);
end;

procedure TSMEngine.DefineModuleLoader;
var ModuleLoaderPath: TFileName;
    script: SynUnicode;
    rval: jsval;
begin
  {$IFDEF CORE_MODULES_IN_RES}
  EvaluateRes('MODULELOADER.JS', rval);
  {$ELSE}
  ModuleLoaderPath := RelToAbs(UTF8ToString(FManager.coreModulesPath) , 'ModuleLoader.js');
  script := AnyTextFileToSynUnicode(ModuleLoaderPath);
  if script = '' then
    raise ESMException.Create('File not found ' + ModuleLoaderPath);
  Evaluate(script, 'ModuleLoader.js', 1, rval);
  {$ENDIF}
end;

procedure TSMEngine.DefineNodeProcess;
var process: PJSRootedObject;
    env: PJSRootedObject;
    FStartupPath: TFileName;
    {$IFDEF FPC}
    I, Cnt: Integer;
    EnvStr: AnsiString;
    Parts: array of AnsiString;
    {$ELSE}
    L: PtrInt;
    EnvBlock, P, pEq: PChar;
    strName, strVal: SynUnicode;
    {$ENDIF}
begin
  process := cx.NewRootedObject(FGlobalObject.ptr.GetPropValue(cx, 'process').asObject);
  env := cx.NewRootedObject(cx.NewObject(nil));
  try
    process.ptr.defineProperty(cx, 'binPath', cx.NewJSString(ExeVersion.ProgramFilePath).ToJSVal,
      JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY);

    process.ptr.defineProperty(cx, 'execPath', cx.NewJSString(ExeVersion.ProgramFileName).ToJSVal,
      JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY);

    FStartupPath := GetCurrentDir + {$IFDEF FPC}DirectorySeparator{$ELSE}'\'{$ENDIF};
    if FStartupPath = '' then
      FStartupPath := ExeVersion.ProgramFilePath;

    process.ptr.defineProperty(cx, 'startupPath', cx.NewJSString(StringToSynUnicode(FStartupPath)).ToJSVal,
      JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY);
    {$IFDEF FPC}
    Cnt := GetEnvironmentVariableCount;
    for I := 1 to Cnt do begin
      EnvStr := GetEnvironmentString(I);
      Parts := EnvStr.Split('=', TStringSplitOptions.ExcludeEmpty);
      if (Length(Parts) = 2) and (Trim(Parts[0]) <> '') then begin
        env.ptr.DefineUCProperty(cx, StringToSynUnicode(Trim(Parts[0])), cx.NewJSString(Trim(Parts[1])).ToJSVal,
          JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY, nil, nil);
      end
    end;
    {$ELSE}
    EnvBlock := GetEnvironmentStrings;
    try
      P := EnvBlock;
      while P^<>#0 do begin
        {$ifdef UNICODE}
        L := StrLenW(P);
        {$else}
        L := StrLen(P);
        {$endif}
        if (L>0) and (P^<>'=') then begin
          pEq := StrScan(P, '=');
          if pEq <> nil then begin
            SetString(strName, p, pEq-P);
            SetString(strVal, pEq+1, {$ifdef UNICODE}StrLenW{$else}StrLen{$endif}(pEq+1));
            env.ptr.DefineUCProperty(cx, Pointer(strName), Length(strName), cx.NewJSString(strVal).ToJSVal,
              JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY, nil, nil);
          end;
        end;
        inc(P,L+1);
      end;
    finally
      FreeEnvironmentStrings(EnvBlock);
    end;
    {$ENDIF}
    process.ptr.defineProperty(cx, 'env', env.ptr.ToJSValue,
      JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY);
    {$IFDEF MSWINDOWS}
    process.ptr.defineProperty(cx, 'platform', cx.NewJSString('win32').ToJSVal);
    {$ELSE}
    process.ptr.defineProperty(cx, 'platform', cx.NewJSString('nix').ToJSVal);
    {$ENDIF}
    {$IFDEF CPU64}
      process.ptr.defineProperty(cx, 'arch', cx.NewJSString('x64').ToJSVal);
    {$ELSE}
      process.ptr.defineProperty(cx, 'arch', cx.NewJSString('x32').ToJSVal);
    {$ENDIF}
    with ExeVersion.Version do
      process.ptr.DefineProperty(cx, 'version',
        cx.NewJSString('v' + IntToString(Major)+'.'+IntToString(Minor) + '.' + IntToString(Release)).ToJSVal);

  finally
    cx.FreeRootedObject(env);
    cx.FreeRootedObject(process);
  end;
end;

procedure TSMEngine.DefineProcessBinding;
var
  process: PJSRootedObject;
  global: PJSRootedObject;
begin
  process := cx.NewRootedObject(cx.NewObject(nil));
  global := cx.NewRootedObject(cx.CurrentGlobalOrNull);
  try
    process.ptr.DefineFunction(cx, 'binding', process_binding, 1);
    global.ptr.DefineProperty(cx, 'process', process.ptr.ToJSValue,
      JSPROP_ENUMERATE or JSPROP_PERMANENT or JSPROP_READONLY, nil, nil);
  finally
    cx.FreeRootedObject(global);
    cx.FreeRootedObject(process);
  end;
end;

destructor TSMEngine.Destroy;
var
  unInitProc: TDllModuleUnInitProc;
  process: PJSRootedObject;
  procExitCodeVal: jsval;
begin
  try
    process := cx.NewRootedObject(GlobalObject.ptr.GetPropValue(cx,'process').asObject);
    try
      process.ptr.SetProperty(cx, '_exiting', SimpleVariantToJSval(cx, true));
      if process.ptr.HasProperty(cx, 'emit') then
        CallObjectFunction(process, 'emit', [cx.NewJSString('exit').ToJSVal])
      else
        raise Exception.Create('`process` initialized incorrectly (dont have `emit` method)');
      procExitCodeVal := process.ptr.GetPropValue(cx, 'exitCode');
      if procExitCodeVal.isInteger then
        ExitCode := procExitCodeVal.asInteger;
    finally
      cx.FreeRootedObject(process);
    end;
  except
    on E: Exception do begin
      ExitCode := 1;
    end;
  end;

  if Manager.FRemoteDebuggerThread <> nil then begin
    TSMRemoteDebuggerThread(Manager.FRemoteDebuggerThread).stopDebugCurrentThread(Self);
  end;
  while FDllModulesUnInitProcList.Count > 0 do begin
    unInitProc := FDllModulesUnInitProcList[FDllModulesUnInitProcList.Count - 1];
    FDllModulesUnInitProcList.Delete(FDllModulesUnInitProcList.Count - 1);
    unInitProc();
  end;
  FDllModulesUnInitProcList.Free;
  inherited Destroy;

  if FGlobalTimerLoopFunc <> nil then cx.FreeRootedValue(FGlobalTimerLoopFunc);
  if FGlobalObjectDbg <> nil then cx.FreeRootedObject(FGlobalObjectDbg);
  if FGlobalObject <> nil then cx.FreeRootedObject(FGlobalObject);
  with TSynFPUException.ForLibraryCode do begin
    cx.LeaveCompartment(comp);
    if FThreadID=GetCurrentThreadId then
      cx^.Destroy; // SM 45 expects the context to be released in the same thread
    KillWatchdog;
  {$IFNDEF SM52}
    if FThreadID=GetCurrentThreadId then
      rt^.Destroy;
  {$ENDIF}
  end;
end;

{$IFDEF SM52}
procedure TSMEngine.DoProcessJSError(report: PJSErrorReport);
{$ELSE}
procedure TSMEngine.DoProcessJSError(errMsg: PCChar; report: PJSErrorReport);
{$ENDIF}
var
  exc: jsval;
  exObj: PJSRootedObject;
begin
  if {$IFDEF SM52}(report = nil) or {$ENDIF}(report.flags = JSREPORT_WARNING) then
    Exit;

  FErrorExist := True;
  if report^.filename = nil then
    FLastErrorFileName := '<>' else
    FLastErrorFileName := CurrentAnsiConvert.AnsiBufferToRawUTF8(
      report^.filename,StrLen(pointer(report^.filename)));
  FLastErrorLine := report^.lineno;
  FLastErrorNum := report^.errorNumber;
  {$IFDEF SM52}
  FLastErrorMsg := Utf8ToString(FormatUTF8('%', [report^.message_]));
  {$ELSE}
  if report^.ucmessage=nil then
    FLastErrorMsg := Utf8ToString(FormatUTF8('%', [errMsg])) else
    SetString(FLastErrorMsg, PWideChar(report^.ucmessage), StrLenW(PWideChar(report^.ucmessage)));
  {$ENDIF}
  FLastErrorStackTrace := '';
  if ( cx.GetPendingException(exc)) then begin
    if exc.isObject then begin
      exObj := cx.NewRootedObject(exc.asObject);
      exObj.ptr.GetProperty(cx, 'stack', exc);
      if (not exc.isVoid) and (exc.isString) then
        FLastErrorStackTrace := exc.asJSString.ToSynUnicode(cx);
      cx.FreeRootedObject(exObj);
    end;
  end;

  (*
  // This situation is possible when application are run from the IDE
  // and stop on the breakpoint.
  // When we evaluate some js script with errors(like call JS_Stringify
  // for global object) this function will be called.
  // If breakpoint is set between ClearLastError and CheckJSError we get
  // FErrorExist value is equivalent true, but script have no error
  if DebugHook=0 then try
    CheckJSError(JS_FALSE);
  finally
    FErrorExist := false;
  end;
  *)
end;

procedure TSMEngine.CheckJSError(res: Boolean);
var exc: jsval;
    {$IFDEF SM52}
    excObj: PJSRootedObject;
    {$ENDIF}
    rep: PJSErrorReport;
    R: RawUTF8;
begin
{$IFDEF SM52}
  if JS_IsExceptionPending(fCx) and JS_GetPendingException(cx, exc) then begin
    if exc.isObject then begin
      excObj := cx.NewRootedObject(exc.asObject);
      try
        rep := JS_ErrorFromException(cx, excObj.ptr);
        if (rep <> nil) and (rep.flags = JSREPORT_WARNING) then
          exit;
        // Error inheritance (assert.AssertionError for example)
        // will return an nil as a rep, so we need to examine a exc object
        FErrorExist := True;

        excObj.ptr.GetProperty(cx, 'fileName', exc);
        if (not exc.isVoid) and (exc.isString) then
          FLastErrorFileName := exc.asJSString.ToUTF8(fCx);

        excObj.ptr.GetProperty(cx, 'lineNumber', exc);
        if (not exc.isVoid) then begin
          if (exc.isString) then begin // error line is in format line:char (123:16)
            R := exc.asJSString.ToUTF8(fCx);
            FLastErrorLine := GetInteger(pointer(R));
          end else if (exc.isInteger) then
            FLastErrorLine := exc.asInteger
        end;

        excObj.ptr.GetProperty(cx, 'message', exc);
        if (not exc.isVoid) and (exc.isString) then
          FLastErrorMsg := exc.asJSString.ToString(fCx);

        excObj.ptr.GetProperty(cx, 'stack', exc);
        if (not exc.isVoid) and (exc.isString) then
          FLastErrorStackTrace := exc.asJSString.ToSynUnicode(fCx);

        if (rep <> nil) then
          FLastErrorNum := rep^.errorNumber
        else begin
          excObj.ptr.GetProperty(cx, 'errorNumber', exc);
          if (not exc.isVoid) and (exc.isInteger) then
            FLastErrorNum := exc.asInteger
          else
            FLastErrorNum := 0;
        end;
      finally
        cx.FreeRootedObject(excObj);
      end;
    end else if exc.isString then begin
      FErrorExist := True;
      FLastErrorFileName := '<betterToThrowErrorInsteadOfPlainValue>';
      FLastErrorMsg := exc.asJSString.ToString(fCx);
      FLastErrorStackTrace := '';
      FLastErrorNum := 0;
    end else begin
      FErrorExist := True;
      FLastErrorFileName := '<betterToThrowError>';
      FLastErrorMsg := 'code throw a plain value instead of Error (or error like) object';
      FLastErrorStackTrace := '';
      FLastErrorNum := 0;
    end;
    raise ESMException.CreateWithTrace(FLastErrorFileName, FLastErrorNum, FLastErrorLine, FLastErrorMsg, FLastErrorStackTrace);
  end;
{$ENDIF}
  if (FTimeOutAborted and (FLastErrorMsg <> '')) or FErrorExist then begin
    raise ESMException.CreateWithTrace(FLastErrorFileName, FLastErrorNum, FLastErrorLine, FLastErrorMsg, FLastErrorStackTrace);
  end;
  if not res and not FTimeOutAborted then begin
    raise ESMException.CreateWithTrace(FLastErrorFileName, 0, FLastErrorLine, 'Error compiling script', '');
  end;
end;

procedure TSMEngine.ClearLastError;
begin
  fcx.ClearPendingException;
  FErrorExist := False;
  FTimeOutAborted := False;
  fTimedOut := False;
end;

procedure TSMEngine.GarbageCollect;
begin
{$IFDEF SM52}
  cx.GC;
{$ELSE}
  rt.GC;
{$ENDIF}
end;

procedure TSMEngine.MaybeGarbageCollect;
begin
  cx.MaybeGC;
end;

{ TSMEngineManager }

class procedure TSMEngineManager.RegisterBinding(const Name: RawUTF8;
  const handler: TSMProcessBindingHandler);
begin
  if GlobalSyNodeBindingHandlers = nil then
    GlobalSyNodeBindingHandlers := TRawUTF8List.Create(false);
  GlobalSyNodeBindingHandlers.AddObject(Name, TObject(@handler));
end;

function TSMEngineManager.GetBinding(const Name: RawUTF8): TSMProcessBindingHandler;
var
  obj: TObject;
  handler: TSMProcessBindingHandler absolute obj;
begin
  obj := GlobalSyNodeBindingHandlers.GetObjectFrom(Name);
  result := handler;
end;


function TSMEngineManager.GetEngineExpireTimeOutMinutes: cardinal;
begin
  result := fEngineExpireTimeOutTicks div 60000;
end;

procedure TSMEngineManager.ClearWorkers;
begin
  FWorkersManager.Free;
  FWorkersManager := TJSWorkersManager.Create;
end;

constructor TSMEngineManager.Create(const aCoreModulesPath: RawUTF8; aEngineClass: TSMEngineClass = nil);
begin
  FMaxPerEngineMemory := 32*1024*1024;
  FMaxRecursionDepth := 32;
  FEnginePool := TSynObjectListLocked.Create(true);
  if aEngineClass <> nil then
    FEngineClass := aEngineClass
  else
    FEngineClass := TSMEngine;
  {$ifdef ISDELPHIXE2}
  FRttiCx := TRttiContext.Create();
  {$endif}
  FDllModules := TRawUTF8List.Create();
  FCoreModulesPath := aCoreModulesPath;
  FWorkersManager := TJSWorkersManager.Create;
end;

procedure TSMEngineManager.SetMaxPerEngineMemory(AMaxMem: Cardinal);
begin
  if aMaxMem<STACK_CHUNK_SIZE*MaxRecursionDepth then
    raise ESMException.CreateFmt(
      'Per engine memory must be >= STACK_CHUNK_SIZE*%d, i.e. %d',
      [MaxRecursionDepth,STACK_CHUNK_SIZE*MaxRecursionDepth]);
  FMaxPerEngineMemory := AMaxMem;
end;

procedure TSMEngineManager.SetEngineExpireTimeOutMinutes(const minutes: cardinal);
begin
  fEngineExpireTimeOutTicks := minutes * 60000;
end;

procedure TSMEngineManager.SetMaxNurseryBytes(AMaxNurseryBytes: Cardinal);
begin
  FMaxNurseryBytes := AMaxNurseryBytes;
end;

function TSMEngineManager.ThreadEngineIndex(ThreadID: TThreadID): Integer;
begin
  if self <> nil then
    for result := 0 to FEnginePool.Count-1 do
      if TSMEngine(FEnginePool.List[result]).fThreadID=ThreadID then
        exit;
  result := -1;
end;

destructor TSMEngineManager.Destroy;
var
  dllModule: PDllModuleRec;
  i: Integer;
begin
  FWorkersManager.Free;
  FWorkersManager := nil;
  stopDebugger;
  if FEnginePool.Count>0 then
    raise ESMException.Create('There are unreleased engines');
  FEnginePool.Free;
  for I := 0 to FDllModules.Count - 1 do begin
    dllModule := PDllModuleRec(FDllModules.Objects[i]);
    Dispose(dllModule);
  end;
  FDllModules.Free;
  inherited;
  {$ifdef ISDELPHIXE2}
  FRttiCx.Free;
  {$endif}
end;

procedure TSMEngineManager.DoOnNewEngine(const Engine: TSMEngine);
begin
  if Assigned(FOnNewEngine) then begin
    Engine.cx.BeginRequest();
    try
      FOnNewEngine(Engine);
    finally
      Engine.cx.EndRequest;
    end;
  end;
end;

function TSMEngineManager.EngineForThread(ThreadID: TThreadID): TSMEngine;
var
  i: integer;
begin
  FEnginePool.Safe.Lock;
  try
    i := ThreadEngineIndex(ThreadID);
    if i < 0 then
      result := nil else
      result := FEnginePool.List[i];
  finally
    FEnginePool.Safe.UnLock;
  end;
end;

function TSMEngineManager.evalDllModule(cx: PJSContext; module: PJSRootedObject;
  const filename: RawUTF8): TDllModuleUnInitProc;
var
  dirname: TFileName;
  exports_V: jsval;
  exports_: PJSRootedObject;
  require_V: jsval;
  require: PJSRootedObject;
  __filename: PWideChar;
  __dirname: PWideChar;

  fHandle: HMODULE;
  ModuleRec: PDllModuleRec;

const
  EXPORTS_NAME: AnsiString = 'exports';
  REQUIRE_NAME: AnsiString = 'require';
  INIT_PROC_NAME = 'InitPlugin';
  UNINIT_PROC_NAME = 'UnInitPlugin';

begin
  Lock;
  try
    cx.BeginRequest;
    try
      dirname := ExtractFilePath(UTF8ToString(filename)) ;
      ModuleRec := PDllModuleRec(FDllModules.GetObjectFrom(filename));
      if ModuleRec = nil then begin
        fHandle := {$IFDEF FPC}dynlibs.{$ENDIF}SafeLoadLibrary(UTF8ToString(filename));
        if fHandle=0 then
          raise ESMException.CreateFmt('Unable to load %s (%s)',
            [filename, {$ifdef FPC}GetLoadErrorStr{$else}SysErrorMessage(GetLastError){$endif}]);
        new(ModuleRec);
        ModuleRec.init := GetProcAddress(fHandle, INIT_PROC_NAME);
        if not Assigned(ModuleRec.init) then begin
          FreeLibrary(fHandle);
          Dispose(ModuleRec);
          raise ESMException.CreateFmt('Invalid %s: missing %s',[filename, INIT_PROC_NAME]);
        end;
        if Assigned(OnDllModuleLoaded) then
          OnDllModuleLoaded(fHandle);

        ModuleRec.unInit := GetProcAddress(fHandle, UNINIT_PROC_NAME);
        FDllModules.AddObject(filename, TObject(ModuleRec));
      end;

      __filename := Pointer(filename);
      __dirname := Pointer(dirname);

      if module.ptr.GetProperty(cx, pointer(EXPORTS_NAME), exports_V) then
        exports_ := cx.NewRootedObject(exports_V.asObject)
      else
        exports_ := cx.NewRootedObject(nil);
      try

        if module.ptr.GetProperty(cx, pointer(REQUIRE_NAME), require_V) then
          require :=  cx.NewRootedObject(require_V.asObject)
        else
          require := cx.NewRootedObject(nil);
        try
          if not ModuleRec.init(cx, exports_, require, module, __filename, __dirname) then begin
            try
              RaiseLastOSError;
            except
              on E: Exception do begin
                E.Message := format('require %s failed:',[ ExtractFileName(UTF8ToString(filename))])+E.Message;
                raise;
              end;
            end;
          end;
        finally
          cx.FreeRootedObject(require);
        end;
      finally
        cx.FreeRootedObject(exports_);
      end;
        //TODO: Store all evaled libs and call UnInit on destroy engine
    finally
      cx.EndRequest;
    end;
  finally
    UnLock;
  end;

  Result := ModuleRec.unInit;
end;

function TSMEngineManager.getPauseDebuggerOnFirstStep: boolean;
begin
  if FRemoteDebuggerThread<> nil then begin
    result := TSMRemoteDebuggerThread(FRemoteDebuggerThread).NeedPauseOnFirstStep;
  end else
    result := false;
end;

function TSMEngineManager.ThreadSafeEngine(pThreadData: pointer): TSMEngine;
var i: integer;
    ThreadID: TThreadID;
begin
  FEnginePool.Safe.Lock;
  try
    ThreadID := GetCurrentThreadId;
    i := ThreadEngineIndex(ThreadID); // inlined CurrentThreadEngine
    if i<0 then
      result := nil else begin
      result := FEnginePool.List[i];
      if (pThreadData) <> nil then
        result.SetThreadData(pThreadData);
    end;
    if result<>nil then begin
      // memorize a thread data for possible engine recreation
      if pThreadData = nil then
        pThreadData := result.ThreadData;
      if result.EngineContentVersion=Self.ContentVersion then begin
        if Result.NeverExpire or (FEngineExpireTimeOutTicks = 0) or (GetTickCount64 - Result.fCreatedAtTick < FEngineExpireTimeOutTicks ) then
        // return existing Engine corresponding to the current thread
        exit else begin
          // content expired -> force recreate thread Engine
          {$ifdef SM_DEBUG}
          SynSMLog.Add.Log(sllDebug,
            'Drop SpiderMonkey Engine for thread % - timeout expired', ThreadID);
          {$endif}
          ReleaseEngineForThread(ThreadID);
        end;
      end else begin
        // content version changed -> force recreate thread Engine
        {$ifdef SM_DEBUG}
        SynSMLog.Add.Log(sllDebug,
          'Drop SpiderMonkey Engine for thread % - modification found', ThreadID);
        {$endif}
        ReleaseEngineForThread(ThreadID);
      end;
    end;
    // here result=nil or to be ignored (just dropped)
    {$ifdef SM_DEBUG}
    SynSMLog.Add.Log(sllDebug, 'Create new JavaScript Engine for thread %', ThreadID);
    {$endif}

    Result := FEngineClass.Create(Self);
    if grandParent = nil then
      grandParent := Result;

    if (pThreadData <> nil) then
      Result.SetThreadData(pThreadData);

    if WorkersManager.curThreadIsWorker then
      Result.fnameForDebug := WorkersManager.getCurrentWorkerThreadName
    else if Assigned(OnGetName) then
      Result.fnameForDebug := OnGetName(Result);

    if Assigned(OnGetWebAppRootPath) then
      Result.fWebAppRootDir := OnGetWebAppRootPath(Result)
    else
      Result.fWebAppRootDir := StringToUTF8(ExeVersion.ProgramFilePath);

    result.fThreadID := ThreadID;
    FEnginePool.Add(result);
  finally
    FEnginePool.Safe.UnLock;
  end;

  if FRemoteDebuggerThread <> nil then
    TSMRemoteDebuggerThread(FRemoteDebuggerThread).startDebugCurrentThread(result);
  if WorkersManager.curThreadIsWorker then
    Result.doInteruptInOwnThread := WorkersManager.DoInteruptInOwnThreadhandlerForCurThread;

  DoOnNewEngine(Result);
end;

procedure TSMEngineManager.Lock;
begin
  FEnginePool.Safe.Lock;
end;

procedure TSMEngineManager.UnLock;
begin
  FEnginePool.Safe.UnLock;
end;

procedure TSMEngineManager.ReleaseEngineForThread(aThreadID: TThreadID);
var
  i: integer;
begin
  FEnginePool.Safe.Lock;
  try
    i := ThreadEngineIndex(aThreadID);
    if i>=0 then begin
      (TObject(FEnginePool[i]) as TSMEngine).GarbageCollect;
      FEnginePool.Delete(i);
    end;
  finally
    FEnginePool.Safe.UnLock;
  end;
end;

procedure TSMEngineManager.ReleaseCurrentThreadEngine;
begin
  ReleaseEngineForThread(GetCurrentThreadId);
end;

function TSMEngineManager.CurrentThreadEngine: TSMEngine;
var
  i: integer;
begin
  FEnginePool.Safe.Lock;
  try
    i := ThreadEngineIndex(GetCurrentThreadId);
    if i < 0 then
      result := nil else
      result := FEnginePool.List[i];
  finally
    FEnginePool.Safe.UnLock;
  end;
end;

procedure TSMEngineManager.debuggerLog(const Text: RawUTF8);
begin
  if FRemoteDebuggerThread<> nil then begin
    TSMRemoteDebuggerThread(FRemoteDebuggerThread).doLog(Text);
  end;
end;

procedure TSMEngineManager.setPauseDebuggerOnFirstStep(const Value: boolean);
begin
  if FRemoteDebuggerThread<> nil then begin
    TSMRemoteDebuggerThread(FRemoteDebuggerThread).NeedPauseOnFirstStep := Value;
  end
end;

procedure TSMEngineManager.startDebugger(const port: SockString = '6000');
begin
  FRemoteDebuggerThread := TSMRemoteDebuggerThread.Create(self, port);
  inc(FContentVersion);
end;

procedure TSMEngineManager.stopDebugger;
begin
  if FRemoteDebuggerThread <> nil then begin
    TSMRemoteDebuggerThread(FRemoteDebuggerThread).SetTerminated;
    FRemoteDebuggerThread := nil;
    end;
  end;

function StringReplaceChars(const Source: String; OldChar, NewChar: Char): String;
var i,j,n: integer;
begin
  if (OldChar<>NewChar) and (Source<>'') then begin
    n := length(Source);
    for i := 0 to n-1 do
      if PChar(pointer(Source))[i]=OldChar then begin
        SetString(result,PChar(pointer(Source)),n);
        for j := i to n-1 do
          if PChar(pointer(result))[j]=OldChar then
            PChar(pointer(result))[j] := NewChar;
        exit;
      end;
  end;
  result := Source;
end;

function RelToAbs(const ABaseDir, AFileName: TFileName; ACheckResultInsideBase: boolean = false): TFileName;
var
{$IFNDEF FPC}
  aBase, aTail: PChar;
  localBase, localTail: TFileName;
{$ELSE}
  aBase: TFileName;
{$ENDIF}
begin
{$IFNDEF FPC}
  if AFileName <> '' then begin
    if PathIsRelative(PChar(AFileName)) then begin
      localTail := AFileName;
      localBase := ABaseDir;
    end else begin
      localTail := '';
      localBase := AFileName;
    end;
  end else begin
    localTail := '';
    localBase := ABaseDir;
  end;
  localBase := StringReplaceChars(localBase, '/', '\');;
  localTail := StringReplaceChars(localTail, '/', '\');
  aBase := PChar(LocalBase);
  aTail := PChar(localTail);

  SetLength(Result, MAX_PATH);
  // PathCombine do not understand '/', so we raplace '/' -> '\' above
  if PathCombine(@Result[1], aBase, aTail) = nil then
    Result := ''
  else
    SetLength(Result, {$ifdef UNICODE}StrLenW{$else}StrLen{$endif}(@Result[1]));

  if ACheckResultInsideBase and
     ((length(Result) < length(ABaseDir)) or (length(ABaseDir)=0) or
      (StrLIComp(PChar(@Result[1]), @localBase[1], length(localBase)) <> 0)) then
    Result := ''
{$ELSE}
{
  aBase := GetForcedPathDelims(ABaseDir);
  if AFileName <> '' then begin
    aTail := GetForcedPathDelims(AFileName);
    if FilenameIsAbsolute(aTail) then begin
      localTail := '';
      localBase := aBase;
    end else begin
      localTail := aTail;
      localBase := aBase;
    end;
  end else begin
    localTail := '';
    localBase := aBase;
  end;
  Result := CreateAbsolutePath(localTail, localBase);
}
  aBase := ExpandFileName(ABaseDir);
  Result := ExpandFileNameUTF8(AFileName, aBase);

  if ACheckResultInsideBase and
     ((Length(Result) < Length(aBase)) or (Length(aBase)=0) or
      (StrCompL(PUtf8Char(Result), PUtf8Char(aBase), length(aBase), 0) <> 0)) then
      //{ not Result.StartsWith(aBase, false))} not compiled in fpc 3.1.1
    Result := ''
{$ENDIF}
end;

function TSMEngine.DoProcessOperationCallback: Boolean;
begin
  Result := not fTimedOut;
end;

// Remove #13 characters from script(change it to #32)
// Spidermonkey debugger crashes when `...`(new ES6 strings) contains #13#10
procedure remChar13FromScript(const Script: SynUnicode); {$IFDEF HASINLINE}inline;{$ENDIF}
var
    c: PWideChar;
    i: Integer;
begin
  c := pointer(script);
  for I := 1 to Length(script) do begin
    if (c^ = #13) then
        c^ := ' ';
    Inc(c);
  end;
end;

// get a pointer to a file embadded as a UNICODE resource and it length in chars
function getResCharsAndLength(const ResName: string; out pRes: pointer;
  out resLength: LongWord): boolean;
var HResInfo: THandle;
    HGlobal: THandle;
    Instance: THandle;
begin
  Instance := HInstance;
  HResInfo := FindResource(Instance,PChar(ResName),PChar(10)); //RC_DATA
  if HResInfo=0 then
    exit(false);
  HGlobal := LoadResource(Instance,HResInfo);
  if HGlobal=0 then
    exit(false);
  pRes := LockResource(HGlobal);
  resLength := SizeofResource(Instance,HResInfo) div 2;
  Result := resLength > 0;
end;

procedure TSMEngine.EvaluateRes(const ResName: string; out result: jsval);
var r: Boolean;
    opts: PJSCompileOptions;
    isFirst: Boolean;
    pScript: pointer;
    scriptLength: LongWord;
    rval: jsval;
begin
  with TSynFPUException.ForLibraryCode do begin
    ClearLastError;
    ScheduleWatchdog(fTimeoutValue);
    isFirst := not cx.IsRunning;
    opts := cx.NewCompileOptions;
    opts.filename := Pointer(ResName);

    if not getResCharsAndLength(ResName, pScript, scriptLength) then
      raise ESMException.CreateUTF8('Resource "%" not found', [ResName]);

    r := cx.EvaluateUCScript(opts, pScript, scriptLength, result);
    cx.FreeCompileOptions(opts);
    if r and isFirst and GlobalObject.ptr.HasProperty(cx, '_timerLoop') then
      r := GlobalObject.ptr.CallFunctionName(cx, '_timerLoop', 0, nil, rval);
    if not r then
      r := false;
    ScheduleWatchdog(-1);
    CheckJSError(r);
  end;
end;

procedure TSMEngine.Evaluate(const script: SynUnicode;
  const scriptName: RawUTF8; lineNo: Cardinal; out result: jsval);
var r: Boolean;
    opts: PJSCompileOptions;
    isFirst: Boolean;
    rval: jsval;
begin
  with TSynFPUException.ForLibraryCode do begin
    ClearLastError;
    ScheduleWatchdog(fTimeoutValue);
    isFirst := not cx.IsRunning;
    opts := cx.NewCompileOptions;
    opts.filename := Pointer(scriptName);

    remChar13FromScript(script);
    r := cx.EvaluateUCScript(
        opts, pointer(script), length(script), result);
    cx.FreeCompileOptions(opts);
    if r and isFirst and GlobalObject.ptr.HasProperty(cx, '_timerLoop') then
      r := GlobalObject.ptr.CallFunctionName(cx, '_timerLoop', 0, nil, rval);
    if not r then
      r := false;
    ScheduleWatchdog(-1);
    CheckJSError(r);
  end;
end;

procedure TSMEngine.Evaluate(const script: SynUnicode; const scriptName: RawUTF8; lineNo: Cardinal);
var
  jsvalue: jsval;
begin
  Evaluate(script, scriptName, lineNo, jsvalue);
end;

function TSMEngine.EvaluateModule(const scriptName: RawUTF8): jsval;
var
  global: PJSRootedObject;
  reflect: jsval;
  moduleLoader: PJSRootedObject;
begin
  global := cx.NewRootedObject(cx.CurrentGlobalOrNull);
  reflect := global.ptr.GetPropValue(cx, 'Reflect');
  moduleLoader := cx.NewRootedObject(reflect.asObject.GetPropValue(cx, 'Loader').asObject);
  try
    result := CallObjectFunction(moduleLoader, 'import', [cx.NewJSString(scriptName).ToJSVal])
  finally
    cx.FreeRootedObject(moduleLoader);
    cx.FreeRootedObject(global);
  end;
end;

function TSMEngine.CallObjectFunctionVal(obj: PJSRootedObject; const funcVal: PJSRootedValue; const args: array of jsval): jsval;
var r: Boolean;
    isFirst: Boolean;
    rval: jsval;
    global: PJSRootedObject;
begin
  with TSynFPUException.ForLibraryCode do begin
    ClearLastError;
    ScheduleWatchdog(fTimeoutValue);
    isFirst := not cx.IsRunning;
    r := obj.ptr.CallFunctionValue(cx, funcVal.ptr, high(args) + 1, @args[0], Result);
    if r and isFirst then begin
      global := cx.NewRootedObject(cx.CurrentGlobalOrNull);
      try
        if FGlobalTimerLoopFunc <> nil then
          r := global.ptr.CallFunctionValue(cx, FGlobalTimerLoopFunc.ptr, 0, nil, rval);
      finally
        cx.FreeRootedObject(global);
      end;
    end;
    ScheduleWatchdog(-1);
    CheckJSError(r);
  end;
end;

function TSMEngine.CallObjectFunction(obj: PJSRootedObject; funcName: PCChar;
  const args: array of jsval): jsval;
var r: Boolean;
    isFirst: Boolean;
    rval: jsval;
    global: PJSRootedObject;
begin
  with TSynFPUException.ForLibraryCode do begin
    ClearLastError;
    ScheduleWatchdog(fTimeoutValue);
    isFirst := not cx.IsRunning;
    r := obj.ptr.CallFunctionName(cx, funcName, high(args) + 1, @args[0], Result);
    if r and isFirst then begin
      global := cx.NewRootedObject(cx.CurrentGlobalOrNull);
      try
        if FGlobalTimerLoopFunc <> nil then
          r := global.ptr.CallFunctionValue(cx, FGlobalTimerLoopFunc.ptr, 0, nil, rval);
      finally
        cx.FreeRootedObject(global);
      end;
    end;
    ScheduleWatchdog(-1);
    CheckJSError(r);
  end;
end;

procedure TSMEngine.CancelExecution(AWithException: boolean = true);
begin
  fTimedOut := True;
  FTimeOutAborted := True;
  if AWithException then begin
    FErrorExist := True;
    FLastErrorFileName := '<>';
    FLastErrorLine := 0;
    FLastErrorNum := 0;
    FLastErrorMsg := LONG_SCRIPT_EXECUTION;
  end;
{$IFDEF SM52}
  cx.RequestInterruptCallback;
{$ELSE}
  rt.RequestInterruptCallback;
{$ENDIF}
end;

function TSMEngine.InitWatchdog: boolean;
begin
  Assert(not Assigned(fWatchdogThread));
  fWatchdogLock := PR_NewLock;
  if Assigned(fWatchdogLock) then begin
    fWatchdogWakeup := PR_NewCondVar(fWatchdogLock);
    if Assigned(fWatchdogWakeup) then begin
      fSleepWakeup := PR_NewCondVar(fWatchdogLock);
      if Assigned(fSleepWakeup) then begin
        result := True;
        exit;
      end;
      PR_DestroyCondVar(fWatchdogWakeup);
    end;
  end;
  result := False;
end;

procedure TSMEngine.KillWatchdog;
var thread: PRThread;
begin
  PR_Lock(fWatchdogLock);
  thread := fWatchdogThread;
  if Assigned(thread) then begin
    // The watchdog thread is running, tell it to terminate waking it up
    // if necessary.
    fWatchdogThread := nil;
    PR_NotifyCondVar(fWatchdogWakeup);
  end;
  PR_Unlock(fWatchdogLock);
  if Assigned(thread) then
    PR_JoinThread(thread);
  PR_DestroyCondVar(fSleepWakeup);
  PR_DestroyCondVar(fWatchdogWakeup);
  PR_DestroyLock(fWatchdogLock);
end;

function IsBefore( t1, t2: int64): Boolean;
begin
  Result := int32(t1 - t2) < 0;
end;

procedure WatchdogMain(arg: pointer); cdecl;
var eng: TSMEngine;
{$IFDEF SM52}
    cx: PJSContext;
{$ELSE}
    rt: PJSRuntime;
{$ENDIF}
    now_: int64;
    sleepDuration: PRIntervalTime;
    status: PRStatus;
begin
  PR_SetCurrentThreadName('JS Watchdog');
  eng := TSMEngine(arg);
{$IFDEF SM52}
  cx := eng.cx;
{$ELSE}
  rt := eng.rt;
{$ENDIF}
  PR_Lock(eng.fWatchdogLock);
  while Assigned(eng.fWatchdogThread) do begin
{$IFDEF SM52}
    now_ := cx.NowMs;
{$ELSE}
    now_ := rt.NowMs;
{$ENDIF}
    if (eng.fWatchdogHasTimeout and not IsBefore(now_, eng.fWatchdogTimeout)) then begin
      // The timeout has just expired. Trigger the operation callback outside the lock
      eng.fWatchdogHasTimeout := false;
      PR_Unlock(eng.fWatchdogLock);
      eng.CancelExecution;
      PR_Lock(eng.fWatchdogLock);
      // Wake up any threads doing sleep
      PR_NotifyAllCondVar(eng.fSleepWakeup);
    end else begin
      if (eng.fWatchdogHasTimeout) then begin
        // Time hasn't expired yet. Simulate an operation callback
        // which doesn't abort execution.
{$IFDEF SM52}
        cx.RequestInterruptCallback;
{$ELSE}
        rt.RequestInterruptCallback;
{$ENDIF}
      end;
      sleepDuration := PR_INTERVAL_NO_TIMEOUT;
      if (eng.fWatchdogHasTimeout) then
        sleepDuration := PR_TicksPerSecond() div 10;
      status := PR_WaitCondVar(eng.fWatchdogWakeup, sleepDuration);
      Assert(status = PR_SUCCESS);
    end
  end;
  PR_Unlock(eng.fWatchdogLock);
end;

function TSMEngine.ScheduleWatchdog(t: integer): Boolean;
var interval: Int64;
    timeout: Int64;
begin
  if (t <= 0) then begin
    PR_Lock(fWatchdogLock);
    fWatchdogHasTimeout := false;
    PR_Unlock(fWatchdogLock);
    result := true;
    exit;
  end;
  interval := int64(t * PRMJ_USEC_PER_SEC);
{$IFDEF SM52}
  timeout := cx.NowMs + interval;
{$ELSE}
  timeout := rt.NowMs + interval;
{$ENDIF}
  PR_Lock(fWatchdogLock);
  if not Assigned(fWatchdogThread) then begin
    Assert(not fWatchdogHasTimeout);
    fWatchdogThread := PR_CreateThread(PR_USER_THREAD,
                                       @WatchdogMain,
                                       Self,
                                       PR_PRIORITY_NORMAL,
                                       PR_LOCAL_THREAD,
                                       PR_JOINABLE_THREAD,
                                       0);
    if not Assigned(fWatchdogThread) then begin
      PR_Unlock(fWatchdogLock);
      Result := false;
      Exit;
   end
  end else if (not fWatchdogHasTimeout or IsBefore(timeout, fWatchdogTimeout)) then
    PR_NotifyCondVar(fWatchdogWakeup);

  fWatchdogHasTimeout := true;
  fWatchdogTimeout := timeout;
  PR_Unlock(fWatchdogLock);
  Result := true;
end;

procedure TSMEngine.SetPrivateDataForDebugger(const Value: Pointer);
begin
  FPrivateDataForDebugger := Value;
end;

function TSMEngine.defineClass(AForClass: TClass; AProto: TSMCustomProtoObjectClass; aParent: PJSRootedObject=nil): TSMCustomProtoObject;
begin
  if aParent = nil then
    aParent := GlobalObject;
  result := SyNodeProto.defineClass(cx, AForClass, AProto, aParent);
end;

procedure TSMEngine.defineClasses(const AClasses: array of TClass; AProto: TSMCustomProtoObjectClass; aParent: PJSRootedObject=nil);
var i: Integer;
begin
  for i := 0 to high(AClasses) do
    defineClass(AClasses[i], AProto);
end;

procedure TSMEngine.SetThreadData(pThreadData: pointer);
begin
  FThreadData := pThreadData;
end;

procedure TSMEngine.SetTimeoutValue(const Value: Integer);
begin
  if fTimeoutValue = Value then
    exit;
  fTimeoutValue := Value;
  ScheduleWatchdog(fTimeoutValue);
end;

// Bindings
// synode
function synode_parseModule(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
var
  in_argv: PjsvalVector;
  Script: SynUnicode;
  FileName: RawUTF8;
  global: PJSRootedObject;
  options: PJSCompileOptions;
  res: PJSRootedObject;
const
  USAGE = 'usage parseModule(source, [path]: String): ModuleObject';
begin
  try
    in_argv := vp.argv;
    if (argc=0) or not in_argv[0].IsString or ((argc > 1) and not in_argv[1].IsString) then
      raise ESMException.Create(USAGE);

    Script := in_argv[0].asJSString.ToSynUnicode(cx);

    FileName := '';
    if argc > 1 then
      FileName := in_argv[1].asJSString.ToUTF8(cx);
    if FileName = '' then
      FileName := 'no file';

    global := cx.NewRootedObject(cx.CurrentGlobalOrNull);
    try
      options := cx.NewCompileOptions;
      options.filename := Pointer(FileName);
      res := cx.NewRootedObject(cx.CompileModule(global.ptr, options, Pointer(Script), Length(Script)));
      result := res <> nil;
      if result then
        vp.rval := res.ptr.ToJSValue;
      cx.FreeRootedObject(res);
      cx.FreeCompileOptions(options);
    finally
      cx.FreeRootedObject(global);
    end;
  except
    on E: Exception do
    begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;

{$IFDEF CORE_MODULES_IN_RES}
/// Parse and evaluate module stored in resources
function synode_parseModuleRes(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
var
  in_argv: PjsvalVector;
  ResName: string;
  FileName: RawUTF8;
  global: PJSRootedObject;
  options: PJSCompileOptions;
  res: PJSRootedObject;
  pScript: pointer;
  scriptLength: LongWord;
const
  USAGE = 'usage parseModuleRes(resourceName, [path]: String): ModuleObject';
begin
  try
    in_argv := vp.argv;
    if (argc=0) or not in_argv[0].IsString or ((argc > 1) and not in_argv[1].IsString) then
      raise ESMException.Create(USAGE);

    ResName := in_argv[0].asJSString.ToString(cx);
    if not getResCharsAndLength(ResName, pScript, scriptLength) then
      raise ESMException.CreateUTF8('Resource "%" not found', [ResName]);

    FileName := '';
    if argc > 1 then
      FileName := in_argv[1].asJSString.ToUTF8(cx);
    if FileName = '' then
      FileName := ResName;

    global := cx.NewRootedObject(cx.CurrentGlobalOrNull);
    try
      options := cx.NewCompileOptions;
      options.filename := Pointer(FileName);
      res := cx.NewRootedObject(cx.CompileModule(global.ptr, options, pScript, scriptLength));
      result := res <> nil;
      if result then
        vp.rval := res.ptr.ToJSValue;
      cx.FreeRootedObject(res);
      cx.FreeCompileOptions(options);
    finally
      cx.FreeRootedObject(global);
    end;
  except
    on E: Exception do
    begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;
{$endif}

function synode_setModuleResolveHook(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
var
  in_argv: PjsvalVector;
  ro: PJSRootedObject;
begin
  try
    in_argv := vp.argv;

    result := (argc > 0) and (in_argv[0].isObject);
    if Result then begin
      ro := cx.NewRootedObject(in_argv[0].asObject);
      try
        Result := ro.ptr.isFunction(cx);
        if Result then
          cx.SetModuleResolveHook(ro.ptr);
      finally
        cx.FreeRootedObject(ro);
      end;
    end;

  except
    on E: Exception do begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;

function synode_runInThisContext(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
const
  USAGE = 'usage: runInThisContext(script, [fileName]: string)';
var
  in_argv: PjsvalVector;
  res: jsval;
  Script: SynUnicode;
  FileName: RawUTF8;
  opts: PJSCompileOptions;
begin
  try
    in_argv := vp.argv;
    if (argc < 1) or not in_argv[0].isString or ((argc >1) and not in_argv[1].isString) then
      raise ESMException.Create(USAGE);
    Script := in_argv[0].asJSString.ToSynUnicode(cx);
    FileName := '';
    if argc > 1 then
      FileName := in_argv[1].asJSString.ToUTF8(cx);
    if FileName = '' then
      FileName := 'no file';
    cx.BeginRequest;
    try
      opts := cx.NewCompileOptions;
      opts.filename := Pointer(FileName);

      remChar13FromScript(Script);
      Result := cx.EvaluateUCScript(opts, Pointer(Script), Length(Script), res);
      cx.FreeCompileOptions(opts);
      if Result then
        vp.rval := res;
    finally
      cx.EndRequest;
    end;
    vp.rval := res;
  except
    on E: Exception do
    begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;

{$IFDEF CORE_MODULES_IN_RES}
/// Compile and execute a script from named resource
function synode_runInThisContextRes(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
const
  USAGE = 'usage: runResInThisContextRes(resourceName, [fileName]: string)';
var
  in_argv: PjsvalVector;
  res: jsval;
  ResName: string;
  pScript: pointer;
  scriptLength: LongWord;
  FileName: RawUTF8;
  opts: PJSCompileOptions;
begin
  try
    in_argv := vp.argv;
    if (argc < 1) or not in_argv[0].isString or ((argc >1) and not in_argv[1].isString) then
      raise ESMException.Create(USAGE);
    ResName := in_argv[0].asJSString.ToString(cx);
    if not getResCharsAndLength(ResName, pScript, scriptLength) then
      raise ESMException.CreateUTF8('Resource "%" not found', [ResName]);

    FileName := '';
    if argc > 1 then
      FileName := in_argv[1].asJSString.ToUTF8(cx);
    if FileName = '' then
      FileName := ResName;
    cx.BeginRequest;
    try
      opts := cx.NewCompileOptions;
      opts.filename := Pointer(FileName);
      Result := cx.EvaluateUCScript(opts, pScript, scriptLength, res);
      cx.FreeCompileOptions(opts);
      if Result then
        vp.rval := res;
    finally
      cx.EndRequest;
    end;
    vp.rval := res;
  except
    on E: Exception do
    begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;
{$ENDIF}

function synode_loadDll(cx: PJSContext; argc: uintN; var vp: jsargRec): Boolean; cdecl;
const
  USAGE = 'usage: loadDll(module:Object , filename: String)';
var
  Eng: TSMEngine;
  in_argv: PjsvalVector;
  res: jsval;
  filename: RawUTF8;
  module: PJSRootedObject;
  uninitProc: TDllModuleUnInitProc;
begin
  Eng := TObject(cx.PrivateData) as TSMEngine;
  try
    in_argv := vp.argv;
    if (argc < 2) or not (in_argv[0].isObject) or not (in_argv[1].isString) then
      raise ESMException.Create(USAGE);
    filename := in_argv[1].asJSString.ToUTF8(cx);
    module := cx.NewRootedObject(in_argv[0].asObject);
    try
      uninitProc := Eng.FManager.evalDllModule(cx, module, filename);
      if Assigned(uninitProc) then
        Eng.FDllModulesUnInitProcList.Add(@uninitProc);
    finally
      cx.FreeRootedObject(module);
    end;

    res := JSVAL_NULL;
    vp.rval := res;
    Result := True;
  except
    on E: Exception do
    begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;

end;

function SyNodeBinding_modules(const Engine: TSMEngine; const bindingNamespaceName: SynUnicode): jsval;
var
  obj: PJSRootedObject;
  cx: PJSContext;
  {$IFDEF CORE_MODULES_IN_RES}
  val: jsval;
  {$endif}
begin
  cx := Engine.cx;
  obj := cx.NewRootedObject(cx.NewObject(nil));
  try
    obj.ptr.DefineFunction(cx, 'parseModule', synode_parseModule, 2, JSPROP_READONLY or JSPROP_PERMANENT);
    obj.ptr.DefineFunction(cx, 'setModuleResolveHook', synode_setModuleResolveHook, 1, JSPROP_READONLY or JSPROP_PERMANENT);
    obj.ptr.DefineFunction(cx, 'runInThisContext', synode_runInThisContext, 2, JSPROP_READONLY or JSPROP_PERMANENT);
    obj.ptr.DefineFunction(cx, 'loadDll', synode_loadDll, 2, JSPROP_READONLY or JSPROP_PERMANENT);
    {$IFDEF CORE_MODULES_IN_RES}
    val.asBoolean := true;
    obj.ptr.DefineProperty(cx, '_coreModulesInRes', val, JSPROP_READONLY or JSPROP_PERMANENT);
    obj.ptr.DefineFunction(cx, 'parseModuleRes', synode_parseModuleRes, 2, JSPROP_READONLY or JSPROP_PERMANENT);
    obj.ptr.DefineFunction(cx, 'runInThisContextRes', synode_runInThisContextRes, 2, JSPROP_READONLY or JSPROP_PERMANENT);
    {$ENDIF}
    obj.ptr.DefineProperty(cx, 'coreModulesPath', cx.NewJSString(Engine.Manager.CoreModulesPath).ToJSVal, JSPROP_READONLY or JSPROP_PERMANENT);
    Result := obj.ptr.ToJSValue;
  finally
    cx.FreeRootedObject(obj);
  end;
end;

/// sleep X ms
function synode_sleep(cx: PJSContext; argc: uintN; var vp: JSArgRec): Boolean; cdecl;
var
  in_argv: PjsvalVector;
  interval: int64;
  engine: TSMEngine;
const
  USAGE = 'usage: sleep(module: Number)';
begin
  try
    in_argv := vp.argv;
    if (argc < 1) or not in_argv[0].isNumber then
      raise ESMException.Create(USAGE);
    engine := TSMEngine(cx.PrivateData);
    if in_argv[0].isInteger then
      interval := in_argv[0].asInteger
    else
      interval := Trunc(in_argv[0].asDouble);
    while not engine.TimeOutAborted and (interval > 0) do
    begin
      Sleep(100);
      interval := interval - 100;
    end;
    Result := True;
  except
    on E: Exception do
    begin
      Result := False;
      vp.rval := JSVAL_VOID;
      JSError(cx, E);
    end;
  end;
end;

function SyNodeBinding_syNode(const Engine: TSMEngine; const bindingNamespaceName: SynUnicode): jsval;
var
  obj: PJSRootedObject;
  cx: PJSContext;
begin
  cx := Engine.cx;
  obj := cx.NewRootedObject(cx.NewObject(nil));
  try
    obj.ptr.DefineFunction(cx, 'sleep', synode_sleep, 1, JSPROP_READONLY or JSPROP_PERMANENT);
    Result := obj.ptr.ToJSValue;
  finally
    cx.FreeRootedObject(obj);
  end;
end;

initialization
  TSMEngineManager.RegisterBinding('modules', SyNodeBinding_modules);
  TSMEngineManager.RegisterBinding('syNode', SyNodeBinding_syNode);
finalization
  FreeAndNil(GlobalSyNodeBindingHandlers);

end.

