Sandboxie/SandboxiePlus/SandMan/Engine/BoxEngine.cpp

479 lines
14 KiB
C++

#include "stdafx.h"
#include "BoxEngine.h"
#include "../../QSbieAPI/SbieUtils.h"
#include "BoxObject.h"
#include "SbieObject.h"
#include "IniObject.h"
#include "SysObject.h"
#include "WizardObject.h"
#include "ScriptManager.h"
#include "../MiscHelpers/Common/Common.h"
#include "../SandMan.h"
#include "../Views/SbieView.h"
#include "../Windows/OptionsWindow.h"
#include "../Windows/SettingsWindow.h"
#include "../Wizards/BoxAssistant.h"
#include <private/qv4engine_p.h>
#include <private/qv4script_p.h>
#include <private/qv4qobjectwrapper_p.h>
QSet<CBoxEngine*> CBoxEngine::m_Instances;
CBoxEngine::CBoxEngine(QObject* parent) : QThread(parent)
{
m_Instances.insert(this);
m_State = eUnknown;
m_pEngine = NULL;
m_pDebuggerBackend = NULL;
//static QQmlDebuggingEnabler qQmlEnableDebuggingHelper(false);
//QQmlDebuggingEnabler::startTcpDebugServer(1234, QQmlDebuggingEnabler::WaitForClient);
}
CBoxEngine::~CBoxEngine()
{
m_Instances.remove(this);
Stop();
}
void CBoxEngine::Stop()
{
if (!isRunning())
return;
m_Mutex.lock();
if (m_State == eQuery || m_State == eReady)
Continue(true, eCanceled);
else {
if (m_State == eRunning || m_State == eRunningAsync)
m_State = eCanceled;
m_Mutex.unlock();
}
if (!wait(30 * 1000)) {
qDebug() << "Failed to stop Box Engine";
terminate();
m_State = eCanceled;
}
}
void CBoxEngine::StopAll()
{
foreach(CBoxEngine* pEngine, m_Instances)
pEngine->Stop();
}
QV4::ReturnedValue method_translate(const QV4::FunctionObject *b, const QV4::Value *v, const QV4::Value *argv, int argc)
{
QV4::Scope scope(b);
QV4::ExecutionEngine *v4 = scope.engine;
CBoxEngine* pEngine = qobject_cast<CBoxEngine*>(CJSEngineExt::getEngineByHandle(v4)->thread());
CBoxAssistant* Wizard = qobject_cast<CBoxAssistant*>(pEngine->parent()); // todo make teranslation work also for system scripts
QString Result;
for (int i = 0; i < argc; i++) {
if (i == 0) Result = theGUI->GetScripts()->Tr(argv[i].toQStringNoThrow());
else Result = Result.arg(argv[i].toQStringNoThrow());
}
return QV4::Encode(scope.engine->newString(Result));
}
QV4::ReturnedValue method_wcmp(const QV4::FunctionObject *b, const QV4::Value *v, const QV4::Value *argv, int argc)
{
QV4::Scope scope(b);
QV4::ExecutionEngine *v4 = scope.engine;
if (argc < 2)
return QV4::Encode::undefined();
return QV4::Encode(CSbieUtils::WildCompare(argv[0].toQStringNoThrow(), argv[1].toQStringNoThrow()));
}
QV4::ReturnedValue method_invoke(const QV4::FunctionObject *b, const QV4::Value *v, const QV4::Value *argv, int argc)
{
QV4::Scope scope(b);
QV4::ExecutionEngine *v4 = b->engine();
QString Name = argv[0].toQStringNoThrow();
CBoxEngine* pEngine = qobject_cast<CBoxEngine*>(CJSEngineExt::getEngineByHandle(v4)->thread());
CJSEngineExt* pJSEngine = pEngine->GetEngine();
QString Script;
QMetaObject::invokeMethod(theGUI->GetScripts(), "GetScript", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, Script), Q_ARG(QString, Name));
if (!Script.isEmpty()) {
QJSValue ret = pJSEngine->evaluateScript(Script, Name + ".js");
if (ret.isError()) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
pJSEngine->throwError(ret.toString());
#else
pJSEngine->throwError(ret);
#endif
return QV4::Encode::undefined();
} else {
QV4::ScopedValue rv(scope, scope.engine->fromVariant(ret.toVariant()));
QV4::Scoped<QV4::QObjectWrapper> qobjectWrapper(scope, rv);
if (!!qobjectWrapper) {
if (QObject *object = qobjectWrapper->object())
QQmlData::get(object, true)->setImplicitDestructible();
}
return rv->asReturnedValue();
}
}
return QV4::Encode(false);
}
void CBoxEngine::init()
{
connect(m_pEngine, SIGNAL(printTrace(const QString&)), this, SLOT(AppendLog(const QString&)));
QV4::Scope scope(m_pEngine->handle());
scope.engine->globalObject->defineDefaultProperty(QStringLiteral("tr"), method_translate);
scope.engine->globalObject->defineDefaultProperty(QStringLiteral("wildCompare"), method_wcmp);
scope.engine->globalObject->defineDefaultProperty(QStringLiteral("invoke"), method_invoke);
m_pEngine->globalObject().setProperty("system", m_pEngine->newQObject(new JSysObject(this)));
m_pEngine->globalObject().setProperty("sbie", m_pEngine->newQObject(new JSbieObject(new CSbieObject(this), this)));
}
bool CBoxEngine::RunScript(const QString& Script, const QString& Name, const QVariantMap& Params)
{
if (isRunning())
return false;
m_Script = Script;
m_Name = Name;
m_Params = Params;
m_State = eRunning;
if(!m_pEngine) m_pEngine = new CJSEngineExt(); // the engine lives in its own thread
//m_pEngine->installExtensions(QJSEngine::ConsoleExtension);
init();
m_pEngine->moveToThread(this);
//////////////////////////////////////////////////////////////////////
//
// !!! CAUTION Multi Threading !!!
//
// Note: The engine runs in its own thread but the rest of SandMan
// is mostly single threaded, also QSbieAPI is not thread safe, so
// access to it must be synchronized. We solve this by executing
// all calls to theGUI and/or theAPI in the main thread through
// the use of QT's slot system, we wrap all calls from the engine
// in blocking QMetaObject::invokeMethod calls targeting to objects
// which belong to the main thread, hence they need to be created in
// the main thread and passed to the caller from there.
//
//
start();
//return Wait();
return true; // fully async operation
}
void CBoxEngine::run()
{
//QElapsedTimer timer;
//timer.start();
for (auto I = m_Params.begin(); I != m_Params.end(); ++I)
m_pEngine->globalObject().setProperty(I.key(), m_pEngine->toScriptValue(*I));
//auto ret = m_pEngine->evaluateScript("(()=>{" + m_Script + "})()", m_Name);
auto ret = m_pEngine->evaluateScript(m_Script, m_Name);
//qDebug() << "CBoxEngine::run took" << timer.elapsed() << "ms";
if (IsRunning()) {
if (ret.isError()) {
QString Error = tr("Uncaught exception at line %1: %2").arg(ret.property("lineNumber").toInt()).arg(ret.toString());
AppendLog(Error);
SetState(eError, Error);
}
else
SetState(eCompleted);
}
m_Result = m_pEngine->globalObject().property("result").toVariant();
delete m_pEngine;
m_pEngine = NULL;
}
//bool CBoxEngine::Wait()
//{
// while (m_State == eRunning)
// QCoreApplication::processEvents(); // keep the main thread going
// return true;
//}
bool CBoxEngine::SetResult(const QVariantMap& Result)
{
m_Mutex.lock();
m_Data = Result;
return Continue(true);
}
bool CBoxEngine::Continue(bool bLocked, EState State)
{
Q_ASSERT(!IsRunning());
if (!bLocked) m_Mutex.lock();
// Note: we set the state directly and the engine thread emits set state from WaitLocked
m_State = State;
m_Wait.wakeOne();
m_Mutex.unlock();
//return Wait();
return true; // fully async operation
}
void CBoxEngine::SetState(EState state, const QString& Text)
{
m_State = state;
emit StateChanged(state, Text);
}
bool CBoxEngine::TestRunning() {
// WARNING: call this function only from the engine thread itself !!!
if (!IsRunning()) {
m_pEngine->throwError(QString("Canceled"));
return false;
}
return true;
}
bool CBoxEngine::WaitLocked() {
// WARNING: call this function only from the engine thread itself !!!
if (m_pDebuggerBackend) {
while (!m_Wait.wait(&m_Mutex, 10))
QCoreApplication::processEvents(); // for CV4DebugAgent::runJob()
} else
m_Wait.wait(&m_Mutex);
emit StateChanged(m_State);
return TestRunning();
}
void CBoxEngine::AppendLog(const QString& Line)
{
qDebug() << "BoxEngine Log:" << Line;
emit LogMessage(Line);
//QMutexLocker Locker(&m_Mutex);
//m_Log += Line + "\n";
}
QObject* CBoxEngine::GetDebuggerBackend()
{
if(!m_pEngine)
m_pEngine = new CJSEngineExt();
if (!m_pDebuggerBackend) {
m_pDebuggerBackend = newV4ScriptDebuggerBackendDynamic(m_pEngine);
if (m_pDebuggerBackend) {
QMetaObject::invokeMethod(m_pDebuggerBackend, "pause", Qt::DirectConnection);
m_pDebuggerBackend->setParent(this);
}
}
return m_pDebuggerBackend;
}
//////////////////////////////////////////////////////////////////////////////////////////
// CWizardEngine
//
int CWizardEngine::m_InstanceCount = 0;
CWizardEngine::CWizardEngine(QObject* parent)
: CBoxEngine(parent)
{
m_InstanceCount++;
}
CWizardEngine::~CWizardEngine()
{
foreach(const SBoxShadow& pShadow, m_Shadows) {
if (pShadow.pShadow) {
if (pShadow.iApplyChanges == 2)
continue; // this is a new added entry keep it
CSandBoxPtr pBox = pShadow.pShadow.objectCast<CSandBox>();
if(!pBox.isNull()) pBox->TerminateAll();
pShadow.pShadow->RemoveSection();
}
}
theAPI->ReloadBoxes(true);
m_InstanceCount--;
}
bool CWizardEngine::ApplyShadowChanges()
{
for (auto I = m_Shadows.begin(); I != m_Shadows.end(); ++I) {
if (I->iApplyChanges != 1)
continue;
if (I->pOriginal.isNull()) {
// This is a new box or template, not a copy, just clear shadow flag
I->pShadow->DelValue("IsShadow", "y");
I->iApplyChanges = 2;
continue;
}
QList<QPair<QString, QString>> New = I->pShadow->GetIniSection();
QList<QPair<QString, QString>> Old = I->pOriginal->GetIniSection();
// discard unchanged
for (auto I = New.begin(); I != New.end();) {
auto II = I++;
for (auto J = Old.begin(); J != Old.end();) {
auto JJ = J++;
if (II->first == JJ->first && II->second == JJ->second) {
I = New.erase(II);
J = Old.erase(JJ);
break;
}
}
}
// apply changed
foreach(auto & O, Old)
I->pOriginal->DelValue(O.first, O.second);
foreach(auto & N, New) {
if (N.first == "FileRootPath" || N.first == "IsShadow")
continue; // skip
if(N.first == "Template" && IsNoAppliedShadow("Template_" + N.second))
continue; // don't copy non-applied shadow templates
I->pOriginal->AppendText(N.first, N.second);
}
}
theAPI->CommitIniChanges();
return true;
}
void CWizardEngine::init()
{
CBoxEngine::init();
m_pEngine->globalObject().setProperty("wizard", m_pEngine->newQObject(new JWizardObject(this)));
}
void CWizardEngine::SetState(EState state, const QString& Text)
{
if (!Text.isEmpty()) {
if (state == eError)
m_Report["Error"] = Text;
else if (state == eFailed)
m_Report["Failure"] = Text;
}
CBoxEngine::SetState(state, Text);
}
QSharedPointer<CSbieIni> CWizardEngine::MakeShadow(const QSharedPointer<CSbieIni>& pIni)
{
SBoxShadow& pShadow = m_Shadows[pIni->GetName().toLower()];
if (!pShadow.pShadow) {
QString ShadowName = pIni->GetName();
QString Suffix = "_Shadow"; // do not translate must be a valid sandbox name suffix
ShadowName.truncate(32 - (Suffix.length() + 3)); // BOXNAME_COUNT
ShadowName = theAPI->MkNewName(ShadowName.append(Suffix));
QList<QPair<QString, QString>> Settings = pIni->GetIniSection();
for (QList<QPair<QString, QString>>::iterator I = Settings.begin(); I != Settings.end(); ++I)
theAPI->SbieIniSet(ShadowName, I->first, I->second, CSbieAPI::eIniInsert, false);
CSandBoxPtr pBox = pIni.objectCast<CSandBox>();
if(!pBox.isNull())
theAPI->SbieIniSet(ShadowName, "FileRootPath", pBox->GetFileRoot(), CSbieAPI::eIniUpdate, false);
theAPI->SbieIniSet(ShadowName, "IsShadow", "y", CSbieAPI::eIniUpdate, false);
theAPI->CommitIniChanges();
theAPI->ReloadBoxes(true);
pShadow.pOriginal = pIni;
if (pBox)
pShadow.pShadow = theAPI->GetBoxByName(ShadowName);
else
pShadow.pShadow = QSharedPointer<CSbieIni>(new CSbieIni(ShadowName, theAPI));
}
return pShadow.pShadow;
}
void CWizardEngine::AddShadow(const QSharedPointer<CSbieIni>& pIni)
{
SBoxShadow& pShadow = m_Shadows[pIni->GetName().toLower()];
if (!pShadow.pShadow) {
pIni->SetText("IsShadow", "y");
pShadow.pShadow = pIni;
}
}
void CWizardEngine::SetApplyShadow(const QString& OriginalName, bool bApply)
{
auto I = m_Shadows.find(OriginalName.toLower());
if(I != m_Shadows.end())
I->iApplyChanges = bApply ? 1 : 0;
}
bool CWizardEngine::IsNoAppliedShadow(const QString& OriginalName)
{
auto I = m_Shadows.find(OriginalName.toLower());
if (I != m_Shadows.end())
return I->iApplyChanges == 0;
return false;
}
void CWizardEngine::OpenSettings(const QString& page)
{
SetState(eReady);
CSettingsWindow* pSettingsWnd = new CSettingsWindow();
connect(pSettingsWnd, &CSettingsWindow::Closed, this, [=]() {
Continue(false);
});
//theGUI->SafeExec(pSettingsWnd);
pSettingsWnd->showTab(page);
}
void CWizardEngine::OpenOptions(const QString& box, const QString& page)
{
SetState(eReady);
CSandBoxPtr pBox = theAPI->GetBoxByName(box);
if (pBox.isNull()) {
Continue(false);
return;
}
COptionsWindow* pOptionsWnd = new COptionsWindow(pBox, pBox->GetName());
connect(pOptionsWnd, &COptionsWindow::Closed, this, [=]() {
Continue(false);
});
//theGUI->SafeExec(pOptionsWnd);
pOptionsWnd->showTab(page);
}