#include #include #include #include #include #include #include #include #include #include #include #include #include namespace rack { namespace library { static std::mutex appUpdateMutex; static std::mutex updateMutex; void init() { if (settings::autoCheckUpdates && !settings::devMode) { std::thread t([&]() { system::setThreadName("Library"); // checkAppUpdate(); checkUpdates(); }); t.detach(); } } void destroy() { // Wait until all library threads are finished { std::lock_guard lock(appUpdateMutex); } { std::lock_guard lock(updateMutex); } } void checkAppUpdate() { if (!appUpdateMutex.try_lock()) return; DEFER({appUpdateMutex.unlock();}); std::string versionUrl = API_URL + "/version"; json_t* reqJ = json_object(); json_object_set(reqJ, "edition", json_string(APP_EDITION.c_str())); DEFER({json_decref(reqJ);}); json_t* resJ = network::requestJson(network::METHOD_GET, versionUrl, reqJ); if (!resJ) { WARN("Request for version failed"); return; } DEFER({json_decref(resJ);}); json_t* versionJ = json_object_get(resJ, "version"); if (versionJ) appVersion = json_string_value(versionJ); json_t* changelogUrlJ = json_object_get(resJ, "changelogUrl"); if (changelogUrlJ) appChangelogUrl = json_string_value(changelogUrlJ); json_t* downloadUrlsJ = json_object_get(resJ, "downloadUrls"); if (downloadUrlsJ) { json_t* downloadUrlJ = json_object_get(downloadUrlsJ, APP_ARCH.c_str()); if (downloadUrlJ) appDownloadUrl = json_string_value(downloadUrlJ); } } bool isAppUpdateAvailable() { return (appVersion != "") && (appVersion != APP_VERSION); } bool isLoggedIn() { return settings::token != ""; } void logIn(std::string email, std::string password) { if (!updateMutex.try_lock()) return; DEFER({updateMutex.unlock();}); loginStatus = "Logging in..."; json_t* reqJ = json_object(); json_object_set(reqJ, "email", json_string(email.c_str())); json_object_set(reqJ, "password", json_string(password.c_str())); std::string url = API_URL + "/token"; json_t* resJ = network::requestJson(network::METHOD_POST, url, reqJ); json_decref(reqJ); if (!resJ) { loginStatus = "No response from server"; return; } DEFER({json_decref(resJ);}); json_t* errorJ = json_object_get(resJ, "error"); if (errorJ) { const char* errorStr = json_string_value(errorJ); loginStatus = errorStr; return; } json_t* tokenJ = json_object_get(resJ, "token"); if (!tokenJ) { loginStatus = "No token in response"; return; } const char* tokenStr = json_string_value(tokenJ); settings::token = tokenStr; loginStatus = ""; refreshRequested = true; } void logOut() { settings::token = ""; updateInfos.clear(); } static network::CookieMap getTokenCookies() { network::CookieMap cookies; cookies["token"] = settings::token; return cookies; } void checkUpdates() { if (!updateMutex.try_lock()) return; DEFER({updateMutex.unlock();}); if (settings::token.empty()) return; // Refuse to check for updates while updating plugins if (isSyncing) return; updateStatus = "Querying for updates..."; // Check user token std::string userUrl = API_URL + "/user"; json_t* userResJ = network::requestJson(network::METHOD_GET, userUrl, NULL, getTokenCookies()); if (!userResJ) { DEBUG("User failed"); return; } DEFER({json_decref(userResJ);}); // Get user's plugins list std::string pluginsUrl = API_URL + "/plugins"; json_t* pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, NULL, getTokenCookies()); if (!pluginsResJ) { WARN("Request for user's plugins failed"); updateStatus = "Could not query plugins"; return; } DEFER({json_decref(pluginsResJ);}); json_t* errorJ = json_object_get(pluginsResJ, "error"); if (errorJ) { WARN("Request for user's plugins returned an error: %s", json_string_value(errorJ)); updateStatus = "Could not query plugins"; return; } // Get library manifests std::string manifestsUrl = API_URL + "/library/manifests"; json_t* manifestsReq = json_object(); json_object_set(manifestsReq, "version", json_string(APP_VERSION_MAJOR.c_str())); json_t* manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, manifestsReq); json_decref(manifestsReq); if (!manifestsResJ) { WARN("Request for library manifests failed"); updateStatus = "Could not query updates"; return; } DEFER({json_decref(manifestsResJ);}); json_t* manifestsJ = json_object_get(manifestsResJ, "manifests"); json_t* pluginsJ = json_object_get(pluginsResJ, "plugins"); size_t pluginIndex; json_t* pluginJ; json_array_foreach(pluginsJ, pluginIndex, pluginJ) { // Get plugin manifest std::string slug = json_string_value(pluginJ); json_t* manifestJ = json_object_get(manifestsJ, slug.c_str()); if (!manifestJ) { WARN("VCV account has plugin %s but no manifest was found", slug.c_str()); continue; } // Don't replace existing UpdateInfo, even if version is newer. // This keeps things sane and ensures that only one version of each plugin is downloaded to `plugins/` at a time. auto it = updateInfos.find(slug); if (it != updateInfos.end()) { continue; } UpdateInfo update; // Get plugin name json_t* nameJ = json_object_get(manifestJ, "name"); if (nameJ) update.name = json_string_value(nameJ); // Get version json_t* versionJ = json_object_get(manifestJ, "version"); if (!versionJ) { // WARN("Plugin %s has no version in manifest", slug.c_str()); continue; } update.version = json_string_value(versionJ); // Reject plugins with ABI mismatch if (!string::startsWith(update.version, APP_VERSION_MAJOR + ".")) { continue; } // Check if update is needed plugin::Plugin* p = plugin::getPlugin(slug); if (p && p->version == update.version) continue; // Require that plugin is available json_t* availableJ = json_object_get(manifestJ, "available"); if (!json_boolean_value(availableJ)) continue; // Get changelog URL json_t* changelogUrlJ = json_object_get(manifestJ, "changelogUrl"); if (changelogUrlJ) update.changelogUrl = json_string_value(changelogUrlJ); // Add update to updates map updateInfos[slug] = update; } // Get module whitelist // TODO // { // std::string whitelistUrl = API_URL + "/modules"; // json_t* whitelistResJ = network::requestJson(network::METHOD_GET, whitelistUrl, NULL, getTokenCookies()); // if (!whitelistResJ) { // WARN("Request for module whitelist failed"); // updateStatus = "Could not query updates"; // return; // } // DEFER({json_decref(whitelistResJ);}); // std::map> moduleWhitelist; // json_t* pluginsJ = json_object_get(whitelistResJ, "plugins"); // // Iterate plugins // const char* pluginSlug; // json_t* modulesJ; // json_object_foreach(pluginsJ, pluginSlug, modulesJ) { // // Iterate modules in plugin // size_t moduleIndex; // json_t* moduleSlugJ; // json_array_foreach(modulesJ, moduleIndex, moduleSlugJ) { // std::string moduleSlug = json_string_value(moduleSlugJ); // // Insert module in whitelist // moduleWhitelist[pluginSlug].insert(moduleSlug); // } // } // settings::moduleWhitelist = moduleWhitelist; // } updateStatus = ""; refreshRequested = true; } bool hasUpdates() { for (auto& pair : updateInfos) { if (!pair.second.downloaded) return true; } return false; } void syncUpdate(std::string slug) { if (!updateMutex.try_lock()) return; DEFER({updateMutex.unlock();}); if (settings::token.empty()) return; isSyncing = true; DEFER({isSyncing = false;}); // Get the UpdateInfo object auto it = updateInfos.find(slug); if (it == updateInfos.end()) return; UpdateInfo update = it->second; updateSlug = slug; DEFER({updateSlug = "";}); // Set progress to 0% updateProgress = 0.f; DEFER({updateProgress = 0.f;}); INFO("Downloading plugin %s v%s for %s", slug.c_str(), update.version.c_str(), APP_ARCH.c_str()); // Get download URL std::string downloadUrl = API_URL + "/download"; downloadUrl += "?slug=" + network::encodeUrl(slug); downloadUrl += "&version=" + network::encodeUrl(update.version); downloadUrl += "&arch=" + network::encodeUrl(APP_ARCH); // Get file path std::string packageFilename = slug + "-" + update.version + "-" + APP_ARCH + ".vcvplugin"; std::string packagePath = system::join(plugin::pluginsPath, packageFilename); // Download plugin package if (!network::requestDownload(downloadUrl, packagePath, &updateProgress, getTokenCookies())) { WARN("Plugin %s download was unsuccessful", slug.c_str()); return; } // updateInfos could possibly change in the checkUpdates() thread, so re-get the UpdateInfo to modify it. it = updateInfos.find(slug); if (it == updateInfos.end()) return; it->second.downloaded = true; } void syncUpdates() { if (settings::token.empty()) return; // updateInfos could possibly change in the checkUpdates() thread, but checkUpdates() will not execute if syncUpdate() is running, so the chance of the updateInfos map being modified while iterating is rare. auto updateInfosClone = updateInfos; for (auto& pair : updateInfosClone) { syncUpdate(pair.first); } restartRequested = true; } std::string appVersion; std::string appDownloadUrl; std::string appChangelogUrl; std::string loginStatus; std::map updateInfos; std::string updateStatus; std::string updateSlug; float updateProgress = 0.f; bool isSyncing = false; bool restartRequested = false; bool refreshRequested = false; } // namespace library } // namespace rack