#region License
/*
* Copyright (C) 1999-2023 Pavel Tomin.
*
* 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 2, 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; see the file COPYING. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#endregion
using Microsoft.Scripting.Hosting;
using Reko.Core;
using Reko.Core.Configuration;
using Reko.Core.IO;
using Reko.Core.Scripts;
using Reko.Core.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Reko.Scripts.Python
{
///
/// Class for execution of Python modules. Uses IronPython
/// (https://ironpython.net) engine.
///
public class PythonModule : ScriptFile
{
private readonly TextWriter outputWriter;
private readonly DecompilerEventListener eventListener;
private readonly IConfigurationService cfgSvc;
private readonly IFileSystemService fsSvc;
private RekoEventsAPI eventsAPI;
public PythonModule(IServiceProvider services, ImageLocation scriptLocation, byte[] bytes)
: base(services, scriptLocation, bytes)
{
if (scriptLocation.HasFragments)
throw new NotSupportedException("Loading scripts inside archives is not supported yet.");
var filename = scriptLocation.FilesystemPath;
this.eventListener = services.RequireService();
this.cfgSvc = services.RequireService();
this.fsSvc = services.RequireService();
var outputService = services.RequireService();
this.outputWriter = outputService.EnsureOutputSource("Scripting");
var stream = new MemoryStream(bytes);
using var rdr = new StreamReader(stream);
this.eventsAPI = Evaluate(
outputWriter, eventListener, cfgSvc, fsSvc, rdr.ReadToEnd(),
filename);
}
public override void Evaluate(string script)
{
var filename = this.Location.FilesystemPath;
this.eventsAPI = Evaluate(
outputWriter, eventListener, cfgSvc, fsSvc, script, filename);
}
public override void FireEvent(ScriptEvent @event, Program program)
{
var eventsAPI = this.eventsAPI;
var engine = eventsAPI.Engine;
try
{
var pythonAPI = new PythonAPI(cfgSvc, fsSvc, engine);
var programAPI = new RekoProgramAPI(program);
var programWrapper = pythonAPI.CreateProgramWrapper(
programAPI);
eventsAPI.FireEvent(@event, programWrapper);
}
catch (Exception ex)
{
var scriptError = CreateError(
Location.FilesystemPath,
ex,
"An error occurred while running the Python script.",
engine);
eventListener.Error(scriptError);
DumpPythonStack(outputWriter, ex, engine);
}
}
private static RekoEventsAPI Evaluate(
TextWriter outputWriter,
DecompilerEventListener eventListener,
IConfigurationService cfgSvc,
IFileSystemService fsSvc,
string script,
string filename)
{
var engine = CreateEngine(outputWriter);
outputWriter.WriteLine($"Evaluating {filename}");
var pythonAPI = new PythonAPI(cfgSvc, fsSvc, engine);
var eventsAPI = new RekoEventsAPI(engine);
var scope = CreateRekoVariable(engine, pythonAPI, eventsAPI);
var src = engine.CreateScriptSourceFromString(script, filename);
try
{
src.Execute(scope);
}
catch (Exception ex)
{
var scriptError = CreateError(
filename,
ex,
"An error occurred while evaluating the Python script.",
engine);
eventListener.Error(scriptError);
DumpPythonStack(outputWriter, ex, engine);
return new RekoEventsAPI(engine);
}
return eventsAPI;
}
private static ScriptScope CreateRekoVariable(
ScriptEngine engine,
PythonAPI pythonAPI,
RekoEventsAPI eventsAPI)
{
var scope = engine.CreateScope();
var rekoWrapper = pythonAPI.CreateRekoWrapper(eventsAPI);
scope.SetVariable("reko", rekoWrapper);
return scope;
}
private static void DumpPythonStack(
TextWriter writer, Exception ex, ScriptEngine engine)
{
var exceptionOperations = engine.GetService();
var msg = exceptionOperations.FormatException(ex);
writer.WriteLine(msg);
}
private static ScriptError CreateError(
string fileName, Exception ex, string message, ScriptEngine engine)
{
var stackFrames = GetStackFrames(ex, engine);
return new ScriptError(fileName, ex, message, stackFrames);
}
private static IEnumerable GetStackFrames(
Exception ex, ScriptEngine engine)
{
var exceptionOperations = engine.GetService();
foreach (var stackFrame in exceptionOperations.GetStackFrames(ex))
{
yield return new ScriptStackFrame(
stackFrame.GetFileName(),
stackFrame.GetFileLineNumber(),
stackFrame.GetMethodName()
);
}
}
private static ScriptEngine CreateEngine(
TextWriter outputWriter,
int recursionLimit = 100)
{
// Set recursion limit to avoid application crash if there is
// infinite recursion
var options = new Dictionary()
{
{ "RecursionLimit", recursionLimit },
};
// Redirect console output before engine was created
// See https://github.com/IronLanguages/ironpython3/issues/961
// $TODO: remove this workaround when new IronPython will be
// released
var runtime = IronPython.Hosting.Python.CreateRuntime(options);
RedirectConsoleOutput(outputWriter, runtime);
var engine = IronPython.Hosting.Python.GetEngine(runtime);
outputWriter.WriteLine(engine.Setup.DisplayName);
return engine;
}
private static void RedirectConsoleOutput(
TextWriter writer, ScriptRuntime runtime)
{
var stream = new TextWriterStream(writer);
runtime.IO.SetOutput(stream, writer.Encoding);
runtime.IO.SetErrorOutput(stream, writer.Encoding);
}
}
}