djkrose / 7DTD-ScriptingMod

Adds scripting support and other useful functionality to 7 Days To Die dedicated server
19 stars 10 forks source link

Javascript require() throws an error with absolute and relative paths. #32

Open credomane opened 6 years ago

credomane commented 6 years ago

Been trying to require another JS file only results in errors. I can't seem to find any way to make the require function work. Absolute paths seem to work but put the engine in an invalid state. While relative paths throw an error in jint for an invalid URI.

The _libcredomane.js file referenced below can be either a 100% empty file or contain valid (and working) JavaScript code. The errors below remain the same in either case.

Using an absolute path gives the error: exact JavaScript code throwing the error: require('/home/steam/7_days_to_die/Mods/ScriptingMod/scripts/_libcredomane.js');

2018-01-23T13:47:35 7218.717 ERR [SCRIPTING MOD] Script chatCommands.js failed: System.InvalidOperationException: Operation is not valid due to the current state of the object
  at System.Collections.Generic.Stack`1[Jint.Runtime.CallStackElement].Pop () [0x00000] in <filename unknown>:0 
  at Jint.Runtime.CallStack.JintCallStack.Pop () [0x00000] in <filename unknown>:0 
  at Jint.Runtime.ExpressionInterpreter.EvaluateCallExpression (Jint.Parser.Ast.CallExpression callExpression) [0x00000] in <filename unknown>:0 
  at Jint.Engine.EvaluateExpression (Jint.Parser.Ast.Expression expression) [0x00000] in <filename unknown>:0 
  at Jint.Runtime.StatementInterpreter.ExecuteExpressionStatement (Jint.Parser.Ast.ExpressionStatement expressionStatement) [0x00000] in <filename unknown>:0 
  at Jint.Engine.ExecuteStatement (Jint.Parser.Ast.Statement statement) [0x00000] in <filename unknown>:0 
  at Jint.Runtime.StatementInterpreter.ExecuteStatement (Jint.Parser.Ast.Statement statement) [0x00000] in <filename unknown>:0 
  at Jint.Runtime.StatementInterpreter.ExecuteStatementList (IEnumerable`1 statementList) [0x00000] in <filename unknown>:0 
  at Jint.Runtime.StatementInterpreter.ExecuteProgram (Jint.Parser.Ast.Program program) [0x00000] in <filename unknown>:0 
  at Jint.Engine.Execute (Jint.Parser.Ast.Program program) [0x00000] in <filename unknown>:0 
  at Jint.Engine.Execute (System.String source, Jint.Parser.ParserOptions parserOptions) [0x00000] in <filename unknown>:0 
  at ScriptingMod.ScriptEngines.JsEngine.ExecuteFile (System.String filePath) [0x00000] in <filename unknown>:0 
  at ScriptingMod.ScriptEngines.ScriptEngine.ExecuteEvent (System.String filePath, ScriptEvent eventType, System.Object eventArgs) [0x00000] in <filename unknown>:0 

Using a relative path results in the error: exact JavaScript code throwing the error: require('_libcredomane.js'); other paths tried with same error: '_libcredomane', ./_libcredomane.js', ./_libcredomane'

2018-01-23T13:48:58 7301.897 ERR [SCRIPTING MOD] JavaScript error in chatCommands.js line 3 column 0: ReferenceError: Invalid URI: The format of the URI could not be determined: _libcredomane.js
  at require(_libcredomane.js) @ chatCommands.js 0:3
2018-01-23T13:48:58 7301.898 ERR [SCRIPTING MOD] Underlying .Net exception: Jint.Runtime.JavaScriptException: Invalid URI: The format of the URI could not be determined: _libcredomane.js ---> System.UriFormatException: Invalid URI: The format of the URI could not be determined: _libcredomane.js
  at System.Uri..ctor (System.String uriString, Boolean dontEscape) [0x00000] in <filename unknown>:0 
  at System.Uri..ctor (System.String uriString) [0x00000] in <filename unknown>:0 
  at ScriptingMod.Tools.FileTools.GetRelativePath (System.String filePath, System.String folder) [0x00000] in <filename unknown>:0 
  at ScriptingMod.ScriptEngines.JsEngine.ExecuteFile (System.String filePath) [0x00000] in <filename unknown>:0 
  at ScriptingMod.ScriptEngines.JsEngine.Require (System.Object filename) [0x00000] in <filename unknown>:0 
   --- End of inner exception stack trace ---
  at Jint.Engine.Execute (Jint.Parser.Ast.Program program) [0x00000] in <filename unknown>:0 
  at Jint.Engine.Execute (System.String source, Jint.Parser.ParserOptions parserOptions) [0x00000] in <filename unknown>:0 
  at ScriptingMod.ScriptEngines.JsEngine.ExecuteFile (System.String filePath) [0x00000] in <filename unknown>:0 
2018-01-23T13:48:58 7301.898 INF Chat: 'Server': a
kylesmyrk commented 6 years ago

Unfortunately, standard JavaScript does not natively support require(). Your use case would work in something like Node.js, where require() can be used to import modules.

credomane commented 6 years ago

ScriptingMod has require() built into. This is the line where it is added to jint https://github.com/djkrose/7DTD-ScriptingMod/blob/a11f6df5229132bd47e7b456175c38512c0be5be/ScriptingMod/ScriptEngines/JsEngine.cs#L38 Then this is the .net code for it https://github.com/djkrose/7DTD-ScriptingMod/blob/a11f6df5229132bd47e7b456175c38512c0be5be/ScriptingMod/ScriptEngines/JsEngine.cs#L121

The importAssembly() function is done in the same fashion.

_js_variables.js in the scripts directory has commented code showing all the base features provided by ScriptingMod inside the jsEngine.

kylesmyrk commented 6 years ago

You're absolutely right - wasn't aware that existed.

Operation is not valid due to the current state of the object is expected to me, the engine resets itself each script invocation, and I'm guessing it's null (or otherwise) at the point of trying to require another file. Seems to be the most reasonable assumption, however, I haven't extensively looked into it and could be wrong.

What's actually needed is the ability to modularise code and pull required files into your calling scope without executing the scripts through Jint (as I personally don't see why you would need to do so).

A very basic approach is to parse the filename parameter passed to the require function, and then append the entire script ahead of the event/command code before Jint handles its execution.

Easily done inside the ExecuteFile method.

var script = File.ReadAllText(filePath);
var match = Regex.Match(script, @"require(?:\()(.+)+(?:\))", RegexOptions.IgnoreCase);

while (match.Success)
{
    var filename = match.Groups[1].Value;

    if (filename.Contains("'") || filename.Contains('"'))
    {
        var path = Path.Combine(Constants.ScriptsFolder, filename.Trim('\"').Trim('\''));

        if (File.Exists(path))
        {
            script = File.ReadAllText(path) + Environment.NewLine + script;
        }
    }

    match = match.NextMatch();
}

_jint.Execute(script, new ParserOptions { Source = fileRelativePath });

Although very basic (and not entirely foolproof), it does the job well and gives a degree of modularity to scripts. See below.

// _say-hello.js
function hello() {
    return 'Hello!';
}
// _say-goodbye.js
function goodbye() {
    return 'Goodbye!';
}
// event-chatMessage.js
require('_say-hello.js');
require('_say-goodbye.js');

if (event.message != null) {
    console.log('Module #1 - ' + hello());
    console.log('Module #2 - ' + goodbye());
}
[SCRIPTING MOD] [DEBUG] [CONSOLE] Module #1 - Hello!
[SCRIPTING MOD] [DEBUG] [CONSOLE] Module #2 - Goodbye!
credomane commented 6 years ago

The ExecuteFile() doesn't reset the engine. The real issue is jint callstack corruption due to an issue in Jint. I've been playing around with this all weekend. Not sure how to fixed it but this problem comes down to Jint itself or atleast the custom jint variant used by ScriptingMod.

Basically calling _jint.Execute() twice before the first one completes results in the System.InvalidOperationException. Jint's Execute is never expecting that to be possible and errors out because it resets the callstack and a few other things. https://github.com/djkrose/jint-unity/blob/8953e847b9abb7042f322114427ea4b60759d55e/Jint/Engine.cs#L324

I do love your solution but it doesn't look like it would work with nested require statements.

Was hoping that require could be patched up to work mush like the lua counterpart or node.js style.

[edit]
About the only idea I have is to have Require() create a separate JsEngine and run the require()'d file there then port over the value of module.exports or something back into the original script. I wasn't able to make pulling the variable out of the require and shoving it into the original script work. Had everything else working far as i can tell though.

credomane commented 6 years ago

Well I've managed to make require work almost exactly like node.js! Even added an optional passthrough boolean that will copy event, eventType, sender, params, player from the originating script to the required script.

Doing a few last tests then going to upload it for feedback! Only issue I see is require() loops. Such as this and I have absolutely no clue how to even go about breaking out of such loops with an error and not taking out the 7dtd server:

test1.js

//@commands test
var test2 = require("test2");

test2.js

var test1 = require("test1");

I'm still playing around to see how I can make require("file") work too.