Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2024 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "scriptengine.h"
23 :
24 : #include <QJSValueIterator>
25 :
26 : #include "global/api/apiutils.h"
27 :
28 : #include "jsmoduleloader.h"
29 :
30 : #include "../api/extapi.h"
31 : #include "../api/v1/extapiv1.h"
32 :
33 : #include "log.h"
34 :
35 : using namespace muse;
36 : using namespace muse::extensions;
37 : using namespace muse::api;
38 :
39 0 : ScriptEngine::ScriptEngine(const modularity::ContextPtr& iocCtx, const muse::api::ApiContext& apiContext)
40 0 : : m_iocContext(iocCtx)
41 : {
42 0 : const int apiversion = apiContext.apiversion;
43 0 : m_engine = new QJSEngine();
44 :
45 0 : m_engine->installExtensions(QJSEngine::ConsoleExtension);
46 0 : m_engine->setProperty("apiversion", apiversion);
47 0 : m_apiengine = new JsApiEngine(m_engine, m_iocContext, apiContext);
48 :
49 0 : QJSValue globalObj = m_engine->globalObject();
50 0 : if (apiversion == 1) {
51 : //! NOTE API v1 provides not only one global `api` object,
52 : //! but also a number of others, for example `curScore`,
53 : //! this is the legacy of the Qml plugin.
54 0 : apiv1::ExtApiV1* api = new apiv1::ExtApiV1(m_apiengine, m_engine);
55 0 : api->setup(globalObj);
56 0 : m_api = api;
57 : } else {
58 0 : m_api = new api::ExtApi(m_apiengine, m_engine);
59 : }
60 :
61 0 : globalObj.setProperty("api", m_engine->newQObject(m_api));
62 :
63 0 : const std::vector<muse::api::IApiRegister::GlobalEnum>& globalEnums = apiRegister()->globalEnums();
64 0 : for (const muse::api::IApiRegister::GlobalEnum& e : globalEnums) {
65 0 : QString name = QString::fromStdString(e.name);
66 0 : QJSValue enumObj = muse::api::enumToJsValue(m_apiengine, e.meta, e.type);
67 0 : globalObj.setProperty(name, enumObj);
68 0 : }
69 :
70 0 : m_moduleLoader = new JsModuleLoader(m_iocContext, m_engine);
71 0 : m_moduleLoader->pushEngine(this);
72 0 : QJSValue loaderObj = m_engine->newQObject(m_moduleLoader);
73 0 : QJSValue requireFn = loaderObj.property("require");
74 0 : globalObj.setProperty("require", requireFn);
75 0 : globalObj.setProperty("exports", m_engine->newObject());
76 0 : globalObj.setProperty("module", loaderObj);
77 0 : }
78 :
79 0 : ScriptEngine::ScriptEngine(ScriptEngine* engine)
80 0 : : m_iocContext(engine->m_iocContext), m_engine(engine->m_engine), m_apiengine(engine->m_apiengine), m_api(engine->m_api),
81 0 : m_moduleLoader(engine->m_moduleLoader),
82 0 : m_isRequireMode(true)
83 : {
84 0 : m_moduleLoader->pushEngine(this);
85 0 : }
86 :
87 0 : ScriptEngine::~ScriptEngine()
88 : {
89 0 : m_moduleLoader->popEngine();
90 :
91 0 : if (!m_isRequireMode) {
92 0 : delete m_moduleLoader;
93 0 : delete m_api;
94 0 : delete m_apiengine;
95 0 : delete m_engine;
96 : }
97 0 : }
98 :
99 0 : void ScriptEngine::setScriptPath(const io::path_t& arg)
100 : {
101 0 : m_scriptPath = arg;
102 0 : }
103 :
104 0 : io::path_t ScriptEngine::scriptPath() const
105 : {
106 0 : return m_scriptPath;
107 : }
108 :
109 0 : QJSValue ScriptEngine::requireModule(const QString& module)
110 : {
111 0 : LOGD() << module;
112 0 : auto obj = apiRegister()->createApi(module.toStdString(), m_apiengine);
113 0 : if (!obj.first) {
114 0 : LOGE() << "failed create api for module: " << module;
115 0 : return QJSValue();
116 : }
117 0 : bool isNeedDelete = obj.second;
118 0 : QJSEngine::setObjectOwnership(obj.first, isNeedDelete ? QJSEngine::JavaScriptOwnership : QJSEngine::CppOwnership);
119 0 : return m_engine->newQObject(obj.first);
120 : }
121 :
122 0 : QJSValue ScriptEngine::requireFile(const QString& filePath)
123 : {
124 1 : TRACEFUNC;
125 :
126 0 : RetVal<ByteArray> data = readScriptContent(filePath);
127 0 : if (!data.ret) {
128 0 : LOGE() << "failed read file, err: " << data.ret.toString() << ", file: " << filePath;
129 0 : return QJSValue();
130 : }
131 :
132 0 : QByteArray content = QByteArray("(function() { \n") + data.val.toQByteArrayNoCopy() + QByteArray("}());");
133 :
134 0 : ScriptEngine requireEngine(this);
135 0 : requireEngine.setScriptPath(filePath);
136 0 : RetVal<QJSValue> val = requireEngine.evaluateContent(content, filePath);
137 0 : if (!val.ret) {
138 0 : LOGE() << "failed evaluate content ret: " << val.ret.toString();
139 0 : return QJSValue();
140 : }
141 :
142 0 : QJSValue exports = requireEngine.globalProperty("exports");
143 0 : return exports;
144 0 : }
145 :
146 0 : void ScriptEngine::setGlobalProperty(const QString& name, const QJSValue& val)
147 : {
148 0 : m_engine->globalObject().setProperty(name, val);
149 0 : }
150 :
151 0 : QJSValue ScriptEngine::globalProperty(const QString& name) const
152 : {
153 0 : return m_engine->globalObject().property(name);
154 : }
155 :
156 0 : QJSValue ScriptEngine::exports() const
157 : {
158 0 : return globalProperty("exports");
159 : }
160 :
161 0 : void ScriptEngine::setExports(const QJSValue& obj)
162 : {
163 0 : setGlobalProperty("exports", obj);
164 0 : }
165 :
166 0 : Ret ScriptEngine::evaluate()
167 : {
168 0 : io::path_t path = scriptPath();
169 :
170 0 : IF_ASSERT_FAILED(!path.empty()) {
171 0 : return make_ret(Ret::Code::InternalError);
172 : }
173 :
174 0 : RetVal<ByteArray> content = readScriptContent(path);
175 0 : if (!content.ret) {
176 0 : return content.ret;
177 : }
178 :
179 0 : const QByteArray contentData = content.val.toQByteArray();
180 :
181 0 : IF_ASSERT_FAILED(!contentData.isEmpty()) {
182 0 : return make_ret(Ret::Code::InternalError);
183 : }
184 :
185 0 : if (m_lastEvalScript != contentData) {
186 0 : RetVal<QJSValue> eval = evaluateContent(contentData, path);
187 0 : if (!eval.ret) {
188 0 : return eval.ret;
189 : }
190 0 : m_lastEvalScript = contentData;
191 0 : }
192 :
193 0 : return Ret(Ret::Code::Ok);
194 0 : }
195 :
196 0 : Ret ScriptEngine::call(const QString& funcName, const QJSValueList& args, QJSValue* retVal)
197 : {
198 0 : Ret ret = doCall(funcName, args, retVal);
199 0 : return ret;
200 : }
201 :
202 0 : Ret ScriptEngine::doCall(const QString& funcName, const QJSValueList& args, QJSValue* retVal)
203 : {
204 0 : TRACEFUNC;
205 :
206 0 : IF_ASSERT_FAILED(!funcName.isEmpty()) {
207 0 : return make_ret(Ret::Code::InternalError);
208 : }
209 :
210 0 : Ret ret = evaluate();
211 0 : if (!ret) {
212 0 : return ret;
213 : }
214 :
215 0 : if (m_lastCallFunc.funcName != funcName) {
216 0 : m_lastCallFunc.func = m_engine->globalObject().property(funcName);
217 : }
218 :
219 0 : ret = m_lastCallFunc.func.isCallable();
220 0 : if (!ret) {
221 0 : m_lastCallFunc.funcName.clear();
222 0 : ret = make_ret(Ret::Code::UnknownError, QString("not found function: `%1`").arg(funcName));
223 0 : LOGE() << "Not found function: " << funcName;
224 : //dump("globalObject", m_engine->globalObject());
225 : }
226 :
227 0 : QJSValue value;
228 0 : if (ret) {
229 0 : value = m_lastCallFunc.func.call(args);
230 0 : ret = jsValueToRet(value);
231 : }
232 :
233 0 : if (retVal) {
234 0 : *retVal = value;
235 : }
236 :
237 0 : return ret;
238 0 : }
239 :
240 0 : Ret ScriptEngine::jsValueToRet(const QJSValue& val)
241 : {
242 0 : if (val.isError()) {
243 0 : QString fileName = val.property("fileName").toString();
244 0 : int line = val.property("lineNumber").toInt();
245 0 : QString err = val.toString();
246 0 : if (err == "Error: abort") {
247 0 : return make_ret(Ret::Code::Cancel, QString("script is aborted"));
248 : }
249 0 : Ret ret = make_ret(Ret::Code::UnknownError, QString("File: %1, Exception at line: %2, %3").arg(fileName).arg(line).arg(err));
250 0 : ret.setData("file", fileName);
251 0 : ret.setData("line", line);
252 0 : ret.setData("err", err);
253 0 : return ret;
254 0 : } else if (val.isBool()) {
255 0 : return Ret(val.toBool() ? Ret::Code::Ok : Ret::Code::UnknownError);
256 0 : } else if (val.isObject() && val.hasProperty("code")) {
257 0 : Ret ret;
258 0 : ret.setCode(val.property("code").toInt());
259 0 : ret.setText(val.property("text").toString().toStdString()); // maybe not present
260 0 : return ret;
261 0 : }
262 :
263 0 : return Ret(Ret::Code::Ok);
264 : }
265 :
266 0 : void ScriptEngine::dump(const QString& name, const QJSValue& val)
267 : {
268 0 : QStringList props;
269 0 : QJSValueIterator it(val);
270 0 : while (it.next()) {
271 0 : props << it.name();
272 : }
273 :
274 0 : QString str = name + "\n";
275 0 : for (const QString& prop : props) {
276 0 : str += " " + prop + ": " + val.property(prop).toString() + "\n";
277 : }
278 :
279 0 : LOGD() << str;
280 0 : }
281 :
282 0 : void ScriptEngine::throwError(const QString& message)
283 : {
284 0 : m_engine->throwError(message);
285 0 : }
286 :
287 0 : RetVal<ByteArray> ScriptEngine::readScriptContent(const io::path_t& scriptPath) const
288 : {
289 0 : TRACEFUNC;
290 0 : return fileSystem()->readFile(scriptPath);
291 0 : }
292 :
293 0 : RetVal<QJSValue> ScriptEngine::evaluateContent(const QByteArray& fileContent, const io::path_t& filePath)
294 : {
295 0 : TRACEFUNC;
296 0 : RetVal<QJSValue> rv;
297 0 : rv.val = m_engine->evaluate(QString(fileContent), filePath.toQString());
298 0 : rv.ret = jsValueToRet(rv.val);
299 0 : return rv;
300 0 : }
|