#region ================== Copyright (c) 2020 Boris Iwanski
/*
* This program is free software: you can redistribute it and/or modify
*
* it under the terms of the GNU General Public License as published by
*
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
*
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
*
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.If not, see.
*/
#endregion
#region ================== Namespaces
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Windows;
using CodeImp.DoomBuilder.UDBScript.Wrapper;
using Jint;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Esprima;
using Jint.Native;
#endregion
namespace CodeImp.DoomBuilder.UDBScript
{
class ScriptRunner
{
#region ================== Variables
private ScriptInfo scriptinfo;
Engine engine;
Stopwatch stopwatch;
int oldprocessingcount;
#endregion
#region ================== Constructor
public ScriptRunner(ScriptInfo scriptoption)
{
this.scriptinfo = scriptoption;
stopwatch = new Stopwatch();
}
#endregion
#region ================== Methods
///
/// Stops the timer, pausing the script's runtime constraint
///
public void StopTimer()
{
stopwatch.Stop();
}
///
/// Resumes the timer, resuming the script's runtime constraint
///
public void ResumeTimer()
{
stopwatch.Start();
}
///
/// Shows a message box with an "OK" button
///
/// Message to show
public void ShowMessage(object message)
{
BuilderPlug.Me.ScriptRunnerForm.InvokePaused(new Action(() => {
if (message == null)
message = string.Empty;
stopwatch.Stop();
MessageForm mf = new MessageForm("OK", null, message.ToString());
DialogResult result = mf.ShowDialog();
stopwatch.Start();
if (result == DialogResult.Abort)
throw new UserScriptAbortException();
}));
}
///
/// Shows a message box with an "Yes" and "No" button
///
/// Message to show
/// true if "Yes" was clicked, false if "No" was clicked
public bool ShowMessageYesNo(object message)
{
return (bool)BuilderPlug.Me.ScriptRunnerForm.InvokePaused(new Func(() =>
{
if (message == null)
message = string.Empty;
stopwatch.Stop();
MessageForm mf = new MessageForm("Yes", "No", message.ToString());
DialogResult result = mf.ShowDialog();
stopwatch.Start();
if (result == DialogResult.Abort)
throw new UserScriptAbortException();
return result == DialogResult.OK ? true : false;
}));
}
///
/// Exist the script prematurely without undoing its changes.
///
///
private void ExitScript(string s = null)
{
if (string.IsNullOrEmpty(s))
throw new ExitScriptException();
throw new ExitScriptException(s);
}
///
/// Exist the script prematurely with undoing its changes.
///
///
private void DieScript(string s = null)
{
if (string.IsNullOrEmpty(s))
throw new DieScriptException();
throw new DieScriptException(s);
}
public ScriptRuntimeException CreateRuntimeException(string message)
{
return new ScriptRuntimeException(message);
}
///
/// Imports the code of all script library files in a single string
///
/// Scripting engine to load the code into
/// Errors that occured while loading the library code
/// true if there were no errors, false if there were errors
private bool ImportLibraryCode(Engine engine, out string errortext)
{
string path = Path.Combine(General.AppPath, "UDBScript", "Libraries");
string[] files = Directory.GetFiles(path, "*.js", SearchOption.AllDirectories);
errortext = string.Empty;
foreach (string file in files)
{
try
{
engine.Execute(File.ReadAllText(file), file.Remove(0, General.AppPath.Length));
}
catch (ParserException e)
{
MessageBox.Show("There was an error while loading the library " + file + ":\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
catch (JavaScriptException e)
{
if (e.Error.Type != Jint.Runtime.Types.String)
{
UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.JavaScriptStackTrace, e.StackTrace);
sef.ShowDialog();
}
else
General.Interface.DisplayStatus(StatusType.Warning, e.Message); // We get here if "throw" is used in a script
return false;
}
}
return true;
}
///
/// Handles the different exceptions we're expecting, and withdraws the undo snapshot if necessary.
///
/// The exception to handle
public void HandleExceptions(Exception e)
{
bool abort = false;
if(e is UserScriptAbortException)
{
General.Interface.DisplayStatus(StatusType.Warning, "Script aborted");
abort = true;
}
else if(e is ParserException)
{
MessageBox.Show("There is an error while parsing the script:\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
abort = true;
}
else if(e is JavaScriptException jse)
{
if (jse.Error.Type != Jint.Runtime.Types.String)
{
UDBScriptErrorForm sef = new UDBScriptErrorForm(jse.Message, jse.JavaScriptStackTrace, jse.StackTrace);
sef.ShowDialog();
}
else
General.Interface.DisplayStatus(StatusType.Warning, jse.Message); // We get here if "throw" is used in a script
abort = true;
}
else if(e is ExitScriptException)
{
if (!string.IsNullOrEmpty(e.Message))
General.Interface.DisplayStatus(StatusType.Ready, e.Message);
}
else if(e is DieScriptException)
{
if (!string.IsNullOrEmpty(e.Message))
General.Interface.DisplayStatus(StatusType.Warning, e.Message);
abort = true;
}
else if(e is ExecutionCanceledException)
{
abort = true;
}
else // Catch anything else we didn't think about
{
UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, string.Empty, e.StackTrace);
sef.ShowDialog();
abort = true;
}
if (abort)
General.Map.UndoRedo.WithdrawUndo();
}
///
/// Makes sure that only properties for the currect feature version are available to scripts.
///
/// MemberInfo about the property that's being accessed
/// true if property can be accessed, false otherwise
private bool MemberFilter(MemberInfo info)
{
if (info.Name == nameof(GetType))
return false;
if (info.GetCustomAttribute(typeof(UDBScriptSettingsAttribute)) is UDBScriptSettingsAttribute sa)
return sa.MinVersion <= scriptinfo.Version;
return true;
}
/*
private JsValue GetObjectMember(Engine engine, object target, string memberName)
{
Type t = target.GetType();
MethodInfo mi = t.GetMethod(memberName);
if (mi != null)
{
var attr = mi.GetCustomAttribute(false);
if (attr != null && scriptinfo.Version < attr.MinVersion)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException($"{t.Name} requires UDBScript version {attr.MinVersion} or higher.");
}
if (t.GetCustomAttribute(typeof(UDBScriptSettingsAttribute)) is UDBScriptSettingsAttribute sa)
{
if (scriptinfo.Version < sa.MinVersion)
throw BuilderPlug.Me.ScriptRunner.CreateRuntimeException($"{t.Name} requires UDBScript version {sa.MinVersion} or higher.");
}
return null;
}
*/
///
/// Sets everything up for running the script. This has to be done on the UI thread.
///
/// Cancellation token to cancel the running script
public void PreRun(CancellationToken cancellationtoken, IProgress progress, IProgress status, IProgress log)
{
string importlibraryerrors;
// If the script requires a higher version of UDBScript than this version ask the user if they want
// to execute it anyways. Remember the choice for this session if "yes" was selected.
if (scriptinfo.Version > BuilderPlug.UDB_SCRIPT_VERSION && !scriptinfo.IgnoreVersion)
{
if (MessageBox.Show("The script requires a higher version of the feature set than this version of UDBScript supports. Executing this script might fail\n\nRequired feature version: " + scriptinfo.Version + "\nUDBScript feature version: " + BuilderPlug.UDB_SCRIPT_VERSION + "\n\nExecute anyway?", "UDBScript feature version too low", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
return;
scriptinfo.IgnoreVersion = true;
}
// Make sure the option value gets saved if an option is currently being edited
BuilderPlug.Me.EndOptionEdit();
General.Interface.Focus();
// Set engine options
Options options = new Options();
options.CancellationToken(cancellationtoken);
options.AllowOperatorOverloading();
options.SetTypeResolver(new TypeResolver
{
MemberFilter = MemberFilter// member => member.Name != nameof(GetType)
});
//options.SetMemberAccessor(GetObjectMember);
/*
options.SetWrapObjectHandler((eng, obj) =>
{
var wrapper = new ObjectWrapper(eng, obj);
if (wrapper.IsArrayLike || obj is BlockMapQueryResult)
{
wrapper.SetPrototypeOf(eng.Realm.Intrinsics.Array.PrototypeObject);
}
return wrapper;
});
*/
options.CatchClrExceptions(e => e is ScriptRuntimeException || e is CantConvertToVectorException);
// Create the script engine
engine = new Engine(options);
// Scripts with API version smaller than 4 will use the old global objects, starting from API
// version 4 the new global "UDB" object
if (scriptinfo.Version < 4)
{
engine.SetValue("showMessage", new Action