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) 2021 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 :
23 : #include "knownaudiopluginsregister.h"
24 :
25 : #include "global/serialization/json.h"
26 :
27 : #include "log.h"
28 :
29 : using namespace muse;
30 : using namespace muse::audioplugins;
31 :
32 : namespace muse::audioplugins {
33 23 : static JsonObject attributesToJson(const PluginAttributes& attributes,
34 : const PluginAttributes& runtimeOnly)
35 : {
36 23 : JsonObject result;
37 :
38 41 : for (auto it = attributes.cbegin(); it != attributes.cend(); ++it) {
39 18 : if (runtimeOnly.find(it->first) != runtimeOnly.cend()) {
40 8 : continue;
41 : }
42 :
43 10 : result.set(it->first.toStdString(), it->second.toStdString());
44 : }
45 :
46 23 : return result;
47 0 : }
48 :
49 23 : static JsonObject metaToJson(const PluginMeta& meta, const PluginAttributes& runtimeOnly)
50 : {
51 23 : JsonObject result;
52 :
53 46 : result.set("id", meta.id);
54 46 : result.set("type", meta.type);
55 :
56 23 : if (!meta.vendor.empty()) {
57 15 : result.set("vendor", meta.vendor);
58 : }
59 :
60 23 : JsonObject attributesJson = attributesToJson(meta.attributes, runtimeOnly);
61 23 : if (!attributesJson.empty()) {
62 16 : result.set("attributes", attributesJson);
63 : }
64 :
65 23 : return result;
66 23 : }
67 :
68 3 : static PluginAttributes attributesFromJson(const JsonObject& object)
69 : {
70 3 : PluginAttributes result;
71 :
72 7 : for (const std::string& key : object.keys()) {
73 4 : result.insert({ String::fromStdString(key), object.value(key).toString() });
74 3 : }
75 :
76 3 : return result;
77 0 : }
78 :
79 6 : static PluginMeta metaFromJson(const JsonObject& object)
80 : {
81 6 : PluginMeta result;
82 :
83 18 : result.id = object.value("id").toStdString();
84 18 : result.type = object.value("type").toStdString();
85 18 : result.vendor = object.value("vendor").toStdString();
86 :
87 18 : JsonValue attributes = object.value("attributes");
88 6 : if (attributes.isObject()) {
89 3 : result.attributes = attributesFromJson(attributes.toObject());
90 : }
91 :
92 6 : return result;
93 6 : }
94 : }
95 :
96 8 : Ret KnownAudioPluginsRegister::load()
97 : {
98 9 : TRACEFUNC;
99 :
100 8 : m_loaded = false;
101 8 : m_pluginInfoMap.clear();
102 8 : m_pluginPaths.clear();
103 :
104 8 : io::path_t knownAudioPluginsPath = configuration()->knownAudioPluginsFilePath();
105 8 : if (!fileSystem()->exists(knownAudioPluginsPath)) {
106 3 : m_loaded = true;
107 3 : return muse::make_ok();
108 : }
109 :
110 5 : RetVal<ByteArray> file = fileSystem()->readFile(knownAudioPluginsPath);
111 5 : if (!file.ret) {
112 0 : LOGE() << "Failed to read known-audio-plugins cache "
113 0 : << knownAudioPluginsPath << ": " << file.ret.toString();
114 0 : return file.ret;
115 : }
116 :
117 5 : std::string err;
118 5 : JsonDocument json = JsonDocument::fromJson(file.val, &err);
119 5 : if (!err.empty()) {
120 0 : LOGE() << "Failed to parse known-audio-plugins cache "
121 0 : << knownAudioPluginsPath << ": " << err;
122 0 : return Ret(static_cast<int>(Ret::Code::UnknownError), err);
123 : }
124 :
125 5 : JsonArray array;
126 5 : int fileVersion = 0;
127 :
128 5 : if (json.isArray()) {
129 : // legacy format: bare array, treated as version 0
130 1 : array = json.rootArray();
131 4 : } else if (json.isObject()) {
132 4 : JsonObject root = json.rootObject();
133 12 : const JsonValue versionVal = root.value("version");
134 12 : const JsonValue pluginsVal = root.value("plugins");
135 4 : if (!versionVal.isNumber() || !pluginsVal.isArray()) {
136 0 : LOGE() << "Malformed known-audio-plugins.json envelope at "
137 0 : << knownAudioPluginsPath << " (expected numeric \"version\" and array \"plugins\")";
138 0 : return Ret(static_cast<int>(Ret::Code::UnknownError), "Malformed known_audio_plugins.json envelope");
139 : }
140 4 : fileVersion = versionVal.toInt();
141 4 : array = pluginsVal.toArray();
142 4 : } else {
143 0 : LOGE() << "Unrecognized known-audio-plugins.json root type at "
144 0 : << knownAudioPluginsPath << " (expected array or object)";
145 0 : return Ret(static_cast<int>(Ret::Code::UnknownError), "Unrecognized known_audio_plugins.json root type");
146 : }
147 :
148 5 : if (fileVersion > CURRENT_KNOWN_AUDIO_PLUGINS_VERSION) {
149 2 : LOGW() << "known-audio-plugins cache is newer (v" << fileVersion << ") than this build (v"
150 1 : << CURRENT_KNOWN_AUDIO_PLUGINS_VERSION << "); resetting it";
151 1 : m_ignoredEntries.clear();
152 1 : m_loaded = true;
153 1 : pluginInfoListChanged().notify();
154 1 : return muse::make_ok();
155 : }
156 :
157 4 : Ret migrationRet = migrations()->migrate(fileVersion, CURRENT_KNOWN_AUDIO_PLUGINS_VERSION, array);
158 4 : if (!migrationRet) {
159 2 : LOGE() << "Failed to migrate known-audio-plugins cache from v" << fileVersion
160 1 : << " to v" << CURRENT_KNOWN_AUDIO_PLUGINS_VERSION
161 1 : << ": " << migrationRet.toString();
162 1 : return migrationRet;
163 : }
164 :
165 3 : m_ignoredEntries.clear();
166 :
167 9 : for (size_t i = 0; i < array.size(); ++i) {
168 6 : JsonObject object = array.at(i).toObject();
169 :
170 6 : AudioPluginInfo info;
171 18 : info.meta = metaFromJson(object.value("meta").toObject());
172 12 : for (const auto& kv : configuration()->runtimeAttributeDefaults()) {
173 : // assign, not emplace: defaults must override stale values from a legacy cache
174 6 : info.meta.attributes[kv.first] = kv.second;
175 : }
176 18 : const io::path_t storedPath = object.value("path").toString();
177 6 : info.path = globalConfiguration()->isBundledPath(storedPath)
178 12 : ? globalConfiguration()->fromBundledPath(storedPath)
179 6 : : storedPath;
180 18 : info.state = audioPluginStateFromName(object.value("state").toStdString());
181 18 : info.errorCode = object.value("errorCode").toInt();
182 :
183 : // id and path are the lookup keys; skip just the malformed row,
184 : // aborting the whole load would trip asserts in the next scan
185 6 : if (info.meta.id.empty() || info.path.empty()) {
186 2 : LOGW() << "Skipping malformed known-audio-plugins entry at "
187 1 : << knownAudioPluginsPath << " (missing id or path)";
188 1 : continue;
189 : }
190 :
191 : // a bundled plugin not found in this app may belong to another installed
192 : // instance; keep its record but don't expose it
193 5 : if (globalConfiguration()->isBundledPath(storedPath) && !fileSystem()->exists(info.path)) {
194 0 : m_ignoredEntries.push_back(object);
195 0 : continue;
196 : }
197 :
198 5 : m_pluginPaths.insert(info.path);
199 5 : m_pluginInfoMap.emplace(info.meta.id, std::move(info));
200 8 : }
201 :
202 3 : m_loaded = true;
203 3 : pluginInfoListChanged().notify();
204 3 : return muse::make_ok();
205 8 : }
206 :
207 0 : Ret KnownAudioPluginsRegister::clear()
208 : {
209 0 : m_pluginInfoMap.clear();
210 0 : m_pluginPaths.clear();
211 :
212 0 : Ret ret = writePluginsInfo();
213 0 : m_pluginInfoListChanged.notify();
214 :
215 0 : return ret;
216 0 : }
217 :
218 9 : AudioPluginInfoList KnownAudioPluginsRegister::pluginInfoList(PluginInfoAccepted accepted) const
219 : {
220 9 : if (!accepted) {
221 9 : return muse::values(m_pluginInfoMap);
222 : }
223 :
224 0 : AudioPluginInfoList result;
225 0 : result.reserve(m_pluginInfoMap.size());
226 :
227 0 : for (auto it = m_pluginInfoMap.cbegin(); it != m_pluginInfoMap.cend(); ++it) {
228 0 : if (accepted(it->second)) {
229 0 : result.push_back(it->second);
230 : }
231 : }
232 :
233 0 : return result;
234 0 : }
235 :
236 4 : muse::async::Notification KnownAudioPluginsRegister::pluginInfoListChanged() const
237 : {
238 4 : return m_pluginInfoListChanged;
239 : }
240 :
241 4 : const io::path_t& KnownAudioPluginsRegister::pluginPath(const PluginResourceId& resourceId) const
242 : {
243 4 : auto it = m_pluginInfoMap.find(resourceId);
244 4 : if (it == m_pluginInfoMap.end()) {
245 1 : static const io::path_t _dummy;
246 1 : return _dummy;
247 : }
248 :
249 3 : return it->second.path;
250 : }
251 :
252 10 : bool KnownAudioPluginsRegister::exists(const io::path_t& pluginPath) const
253 : {
254 10 : return muse::contains(m_pluginPaths, pluginPath);
255 : }
256 :
257 12 : bool KnownAudioPluginsRegister::exists(const PluginResourceId& resourceId) const
258 : {
259 12 : return muse::contains(m_pluginInfoMap, resourceId);
260 : }
261 :
262 7 : Ret KnownAudioPluginsRegister::registerPlugins(const AudioPluginInfoList& list)
263 : {
264 7 : IF_ASSERT_FAILED(m_loaded) {
265 0 : return false;
266 : }
267 :
268 7 : if (list.empty()) {
269 0 : return make_ok();
270 : }
271 :
272 7 : bool changed = false;
273 :
274 16 : for (const AudioPluginInfo& info : list) {
275 9 : auto it = m_pluginInfoMap.find(info.meta.id);
276 9 : if (it != m_pluginInfoMap.end()) {
277 4 : if (it->second.path == info.path) {
278 1 : LOGW() << "Plugin is already registered: " << info.path << ", ID: " << info.meta.id;
279 1 : continue;
280 : }
281 : }
282 :
283 8 : m_pluginInfoMap.emplace(info.meta.id, info);
284 8 : m_pluginPaths.insert(info.path);
285 8 : changed = true;
286 : }
287 :
288 7 : if (changed) {
289 6 : return writePluginsInfo();
290 : }
291 :
292 1 : return make_ok();
293 : }
294 :
295 1 : Ret KnownAudioPluginsRegister::setPluginsState(const io::paths_t& paths, AudioPluginState state)
296 : {
297 1 : IF_ASSERT_FAILED(m_loaded) {
298 0 : return false;
299 : }
300 :
301 1 : if (paths.empty()) {
302 0 : return make_ok();
303 : }
304 :
305 1 : bool changed = false;
306 4 : for (auto it = m_pluginInfoMap.begin(); it != m_pluginInfoMap.end(); ++it) {
307 3 : if (it->second.state != state && muse::contains(paths, it->second.path)) {
308 2 : it->second.state = state;
309 2 : changed = true;
310 : }
311 : }
312 :
313 1 : if (!changed) {
314 0 : return make_ok();
315 : }
316 :
317 1 : return writePluginsInfo();
318 : }
319 :
320 1 : Ret KnownAudioPluginsRegister::unregisterPlugins(const PluginResourceIdList& resourceIds)
321 : {
322 1 : IF_ASSERT_FAILED(m_loaded) {
323 0 : return false;
324 : }
325 :
326 1 : if (resourceIds.empty()) {
327 0 : return make_ok();
328 : }
329 :
330 1 : bool changed = false;
331 :
332 2 : for (const PluginResourceId& resourceId : resourceIds) {
333 1 : if (!exists(resourceId)) {
334 0 : continue;
335 : }
336 :
337 6 : for (const auto& pair : m_pluginInfoMap) {
338 5 : if (pair.first == resourceId) {
339 1 : muse::remove(m_pluginPaths, pair.second.path);
340 : }
341 : }
342 :
343 1 : m_pluginInfoMap.erase(resourceId);
344 1 : changed = true;
345 : }
346 :
347 1 : if (changed) {
348 1 : return writePluginsInfo();
349 : }
350 :
351 0 : return make_ok();
352 : }
353 :
354 0 : Ret KnownAudioPluginsRegister::removePluginsAtPath(const io::path_t& path)
355 : {
356 0 : IF_ASSERT_FAILED(m_loaded) {
357 0 : return false;
358 : }
359 :
360 0 : bool removed = false;
361 0 : for (auto it = m_pluginInfoMap.begin(); it != m_pluginInfoMap.end();) {
362 0 : if (it->second.path == path) {
363 0 : it = m_pluginInfoMap.erase(it);
364 0 : removed = true;
365 : } else {
366 0 : ++it;
367 : }
368 : }
369 :
370 0 : if (!removed) {
371 0 : return make_ok();
372 : }
373 :
374 0 : muse::remove(m_pluginPaths, path);
375 :
376 0 : return writePluginsInfo();
377 : }
378 :
379 8 : Ret KnownAudioPluginsRegister::writePluginsInfo()
380 : {
381 9 : TRACEFUNC;
382 :
383 8 : JsonArray array;
384 :
385 8 : const PluginAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults();
386 :
387 31 : for (const auto& pair : m_pluginInfoMap) {
388 23 : const AudioPluginInfo& info = pair.second;
389 :
390 23 : const io::path_t storedPath = globalConfiguration()->isBundledWithApp(info.path)
391 23 : ? globalConfiguration()->toBundledPath(info.path)
392 23 : : info.path;
393 :
394 23 : JsonObject obj;
395 69 : obj.set("meta", metaToJson(info.meta, runtimeOnly));
396 46 : obj.set("path", storedPath.toStdString());
397 :
398 : // load() reads a missing "state" back as Undefined, so omitting it round-trips
399 23 : if (info.state != AudioPluginState::Undefined) {
400 69 : obj.set("state", audioPluginStateName(info.state));
401 : }
402 :
403 23 : if (info.errorCode != 0) {
404 9 : obj.set("errorCode", info.errorCode);
405 : }
406 :
407 23 : array << obj;
408 23 : }
409 :
410 : // write ignored records back untouched to preserve other instances' settings
411 8 : for (const JsonObject& ignored : m_ignoredEntries) {
412 0 : array << ignored;
413 : }
414 :
415 8 : JsonObject root;
416 16 : root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION);
417 8 : root.set("plugins", array);
418 :
419 8 : io::path_t knownAudioPluginsPath = configuration()->knownAudioPluginsFilePath();
420 8 : Ret ret = fileSystem()->writeFile(knownAudioPluginsPath, JsonDocument(root).toJson());
421 :
422 8 : return ret;
423 8 : }
424 :
425 0 : Ret KnownAudioPluginsRegister::writePluginsTo(const io::path_t& file, const AudioPluginInfoList& list) const
426 : {
427 0 : TRACEFUNC;
428 :
429 0 : JsonArray array;
430 0 : const PluginAttributes& runtimeOnly = configuration()->runtimeAttributeDefaults();
431 :
432 0 : for (const AudioPluginInfo& info : list) {
433 0 : JsonObject obj;
434 0 : obj.set("meta", metaToJson(info.meta, runtimeOnly));
435 0 : obj.set("path", info.path.toStdString());
436 :
437 0 : if (info.state != AudioPluginState::Undefined) {
438 0 : obj.set("state", audioPluginStateName(info.state));
439 : }
440 :
441 0 : if (info.errorCode != 0) {
442 0 : obj.set("errorCode", info.errorCode);
443 : }
444 :
445 0 : array << obj;
446 0 : }
447 :
448 0 : JsonObject root;
449 0 : root.set("version", CURRENT_KNOWN_AUDIO_PLUGINS_VERSION);
450 0 : root.set("plugins", array);
451 :
452 0 : return fileSystem()->writeFile(file, JsonDocument(root).toJson());
453 0 : }
454 :
455 0 : RetVal<AudioPluginInfoList> KnownAudioPluginsRegister::readPluginsFrom(const io::path_t& file) const
456 : {
457 0 : TRACEFUNC;
458 :
459 0 : RetVal<ByteArray> bytes = fileSystem()->readFile(file);
460 0 : if (!bytes.ret) {
461 0 : return RetVal<AudioPluginInfoList>(bytes.ret);
462 : }
463 :
464 0 : std::string err;
465 0 : JsonDocument json = JsonDocument::fromJson(bytes.val, &err);
466 0 : if (!err.empty()) {
467 0 : return RetVal<AudioPluginInfoList>(Ret(static_cast<int>(Ret::Code::UnknownError), err));
468 : }
469 :
470 0 : JsonArray array = json.rootObject().value("plugins").toArray();
471 :
472 0 : AudioPluginInfoList result;
473 0 : result.reserve(array.size());
474 :
475 0 : for (size_t i = 0; i < array.size(); ++i) {
476 0 : JsonObject object = array.at(i).toObject();
477 :
478 0 : AudioPluginInfo info;
479 0 : info.meta = metaFromJson(object.value("meta").toObject());
480 0 : info.path = object.value("path").toString();
481 0 : info.state = audioPluginStateFromName(object.value("state").toStdString());
482 0 : info.errorCode = object.value("errorCode").toInt();
483 :
484 0 : if (info.meta.id.empty() || info.path.empty()) {
485 0 : continue;
486 : }
487 :
488 0 : result.push_back(std::move(info));
489 0 : }
490 :
491 0 : return RetVal<AudioPluginInfoList>::make_ok(result);
492 0 : }
|